Le tutoriel sur les images construit directement des pixels. Celui-ci part de fichiers encodés : il sépare leur import de leur représentation PDF, puis de leur placement. L'exemple utilise le core et le module d'images aux versions publiées sélectionnées par le manifeste de sources.
La lecture de ces fichiers appartient au module optionnel
pdftoolkit/pdf-images. Les
échantillons, les images DCT et les flux XObject appartiennent au core.
composer install dans le checkout du core installe le module pour les exemples ;
une application l'installe depuis Packagist avec
composer require "pdftoolkit/pdf-images:^0.1.6". Le
guide des packages explique comment sélectionner aussi
la série du core.
- Exemple complet : image-files.php.
- Résultat : quatre images sur une page Letter, avec un damier révélant la transparence.
- Données : motifs synthétiques MIT, distribués avec le module dans
vendor/pdftoolkit/pdf-images/examples/assets/images/. - Prérequis : premier document, dessin et images.
1. Donner des octets, sans déléguer l'accès au fichier
ImageFileBytes représente la suite d'octets du fichier. Les importeurs ne
reçoivent aucun chemin et ne chargent aucune URL. Dans l'exemple, l'application
lit quatre fichiers connus dans ses propres ressources, contrôle l'échec de
lecture, puis construit cet objet. Une application web décide elle-même comment
authentifier et limiter un téléversement avant d'en fournir les octets.
use PDF\Images\ImageFileBytes;
use PDF\Images\ImageImportLimits;
use PDF\Images\JpegImageReader;
use PDF\Images\PngImageDecoder;
$limits = new ImageImportLimits(maxEncodedBytes: 1_048_576, maxPixels: 262_144);
$jpeg = (new JpegImageReader($limits))->read(new ImageFileBytes($jpegBytes));
$png = (new PngImageDecoder($limits))->decode(new ImageFileBytes($pngBytes));
Les limites sont des objets propres à chaque importeur. Par défaut : 16 MiB encodés, 16 MiB de données décodées, 4 194 304 pixels, 32 768 pixels par dimension et 65 536 chunks ou marqueurs. Le PNG compte aussi les octets de filtre et le développement des indices de palette. Ces plafonds bornent les données du codec ; le processus PHP doit également disposer de mémoire pour les chaînes temporaires et le document final. Les produits de dimensions sont vérifiés avant allocation.
2. JPEG : préserver la représentation DCT
JpegImageReader vérifie le début et la fin du fichier, les longueurs de segments,
la trame, les composants, les tables et les en-têtes de scan. Il accepte le JPEG
DCT Huffman 8 bits de base et progressif. Il ne décode pas les coefficients DCT
et ne prétend pas vérifier chaque bit du codage entropique : cette étape appartient
au lecteur PDF. Le fichier encodé reste exactement le même.
Le résultat DCTImage du core expose les dimensions, le SampleLayout, la transformation de couleur
et les octets. Une image en niveaux de gris utilise /DeviceGray. Le JPEG RGB
utilise /DeviceRGB et un /ColorTransform explicite : YCbCr pour JFIF, ou la
valeur du marqueur Adobe. Sans marqueur, seuls les identifiants de composants
connus 1,2,3 ou R,G,B sont acceptés.
Le CMYK nécessite un marqueur Adobe avec transformation 0. Les composantes
Adobe sont inversées ; le dictionnaire contient donc /Decode [1 0 1 0 1 0 1 0].
Le YCCK, les profondeurs autres que 8 bits, les codages arithmétiques, les trames
sans perte et les profils ICC embarqués sont explicitement rejetés. Les métadonnées
EXIF, dont l'orientation, ne sont pas appliquées automatiquement.
3. PNG : reconstruire les échantillons
PngImageDecoder vérifie signature, framing, CRC, chunks critiques, ordre de
IHDR, PLTE, tRNS, IDAT et IEND, ainsi que l'absence de données après
la fin du fichier. Les chunks IDAT consécutifs forment un flux zlib unique.
La décompression est bornée par la longueur exacte des scanlines attendues.
Une fin zlib manquante, des octets supplémentaires ou un filtre inconnu sont rejetés.
PngScanlineDecoder reconstruit les filtres None, Sub, Up, Average et Paeth.
Le résultat est un SampledImage : des ImageSamples couleur et, si nécessaire,
des ImageSamples d'opacité. Chaque objet décrit dimensions, espace de couleur,
profondeur et octets intercalés. Une ligne apparaît dans l'ordre du fichier, depuis
le haut de l'image. Les couleurs ne sont pas prémultipliées par l'alpha.
Le décodeur prend en charge les PNG statiques 8 bits non entrelacés : niveaux
de gris, RGB, palette, gris avec alpha et RGBA. Les indices de palette deviennent
des échantillons RGB ; tRNS devient un canal alpha, y compris les clés de couleur
pour les PNG gris et RGB. Un alpha 0 est transparent, 255 opaque.
Les PNG entrelacés, les profondeurs 1, 2, 4 ou 16 bits, APNG et profils ICC sont rejetés. Les indications gamma, chromaticités et sRGB ne sont pas transformées en espaces PDF calibrés : les échantillons sont représentés dans des espaces Device. Les autres métadonnées auxiliaires ne sont pas importées dans le PDF.
4. Composer l'image XObject et son masque
use PDF\Document\Images\ImageXObjectFactory;
$factory = new ImageXObjectFactory();
$jpegStream = $factory->create($jpeg);
$pngStream = $factory->create($png);
Le service compose un StreamObject avec les objets d'entrée de la table 89 :
/Type /XObject, /Subtype /Image, dimensions, espace de couleur et profondeur.
Le JPEG utilise EncodedData(DCTDecode) ; les échantillons PNG sont compressés
avec FlateDecode. On peut injecter un autre niveau de compression Flate par objet.
ImageXObjectFactory est un composant du core. Il reçoit des DCTImage ou
SampledImage explicites ; il ne parse aucun fichier PNG ou JPEG. Une autre
bibliothèque ou l'application peut construire ces objets sans installer le module.
L'alpha devient un autre flux image, huit bits et /DeviceGray. Une
IndirectReference sous /SMask le relie à l'image principale. Le writer le
découvre dans le graphe et attribue son numéro, sans registre global. Le masque
doit avoir les mêmes dimensions ; le constructeur le vérifie.
Les entrées qui décrivent les échantillons et les masques sont gérées par cette
composition : une entrée supplémentaire ne peut pas remplacer /Width,
/ColorSpace, /Decode ou /SMask. D'autres choix, par exemple /Interpolate,
restent des objets d'entrée explicites. Comme pour tout flux, StreamObject
gère /Length, /Filter et /DecodeParms.
5. Référencer et peindre les images
Le service n'ajoute ni page, ni ressource, ni opérateur. L'exemple construit
/XObject << /JpegRGB … /PngRGBA … >> dans le ResourceDictionary, puis quatre
groupes q, cm, Do, Q. Les matrices fixent 216 × 162 points et la position.
La résolution de 48 × 36 pixels ne détermine donc pas la taille physique du PDF.
Le damier est construit avec des rectangles et des remplissages avant l'image.
Il reste visible sous les PNG partiellement transparents. Les deux JPEG montrent
le même motif en RGB et CMYK : ils permettent de vérifier le sens de /Decode.
Exécuter
php examples/image-files.php /tmp/image-files.pdf
qpdf --check /tmp/image-files.pdf
Les tests comparent les échantillons reconstruits aux motifs originaux et exercent les cinq filtres, les masques, les CRC, les longueurs tronquées, les indices de palette invalides et les données zlib excessives. Le résultat PDF se vérifie aussi avec un lecteur indépendant ; une validation syntaxique ne suffit pas à confirmer les couleurs et l'opacité.
Références : PNG, W3C, JPEG, ITU T.81 et ISO 32000-1, sections 7.4.8 et 8.9.
Voir le résultat PDF
Ce fichier a été généré avec le code de cette documentation. Toutes ses pages sont reproduites ci-dessous ; ouvrir le PDF pour utiliser ses liens et ses signets.
Utiliser le lecteur PDF du navigateur
Ce lecteur dépend du navigateur. Si le document ne s’affiche pas, utiliser « Ouvrir le PDF » ou « Télécharger ».
Code source complet
Code de roukmoute/pdf · Consulter la source au commit documenté ↗
Lire le script commenté : image-files.php
<?php
declare(strict_types=1);
use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Images\ImageFileBytes;
use PDF\Images\ImageImportLimits;
use PDF\Document\Images\ImageXObjectFactory;
use PDF\Images\JpegImageReader;
use PDF\Images\PngImageDecoder;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\Document\Resources\XObject;
use PDF\FileStructure;
use PDF\Graphics\CoordinateSystems\Matrix;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators\AppendRectangle;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\ConcatenateMatrix;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\FillPathNonzero;
use PDF\Syntax\ContentStreamsResources\Operators\InvokeXObject;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
use PDF\Syntax\ContentStreamsResources\Operators\RestoreGraphicsState;
use PDF\Syntax\ContentStreamsResources\Operators\SaveGraphicsState;
use PDF\Syntax\ContentStreamsResources\Operators\SetNonstrokingGray;
use PDF\Syntax\ContentStreamsResources\Operators\SetNonstrokingRGBColor;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DocumentStructure\DocumentCatalog;
use PDF\Syntax\DocumentStructure\PageTree\PageObject;
use PDF\Syntax\DocumentStructure\PageTree\PageTreeNode;
use PDF\Syntax\FileStructure\FileBody;
use PDF\Syntax\FileStructure\FileHeader;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType;
use PDF\Syntax\Object\StreamObject;
use PDF\Syntax\Object\StringObject\LiteralString;
require dirname(__DIR__) . '/vendor/autoload.php';
// 1. The application chooses four known local files. Importers
// receive only byte objects: no path or URL allows them to
// open a resource. Repository patterns are MIT-licensed.
$files = [];
foreach (['rgb.jpg', 'rgba.png', 'cmyk.jpg', 'palette.png'] as $filename) {
$bytes = file_get_contents(dirname(__DIR__) . '/vendor/pdftoolkit/pdf-images/examples/assets/images/' . $filename);
if ($bytes === false) {
throw new RuntimeException('The example image fixture could not be read.');
}
$files[$filename] = new ImageFileBytes($bytes);
}
// 2. These limits bound encoded data, pixels and decoded scanlines.
// They are configured per object, for example for an upload endpoint.
$limits = new ImageImportLimits(
maxEncodedBytes: 1_048_576,
maxDecodedBytes: 1_048_576,
maxPixels: 262_144,
maxDimension: 1024,
);
$jpegReader = new JpegImageReader($limits);
$pngDecoder = new PngImageDecoder($limits);
$rgb = $jpegReader->read($files['rgb.jpg']);
$rgba = $pngDecoder->decode($files['rgba.png']);
$cmyk = $jpegReader->read($files['cmyk.jpg']);
$palette = $pngDecoder->decode($files['palette.png']);
// 3. Composition creates the /Subtype /Image streams from Table 89.
// JPEG remains DCTDecode byte for byte. PNG is reconstructed, then
// compressed with FlateDecode; its alpha becomes another /SMask stream.
// Mask references are discovered through the writer's graph.
$factory = new ImageXObjectFactory();
$images = [
'JpegRGB' => $factory->create($rgb),
'PngRGBA' => $factory->create($rgba),
'JpegCMYK' => $factory->create($cmyk),
'PngPalette' => $factory->create($palette),
];
$imageEntries = [];
foreach ($images as $name => $image) {
$imageEntries[] = new Entry(new NameType($name), new IndirectReference($image));
}
$helvetica = new Type1Font(StandardFont::Helvetica);
$resources = new ResourceDictionary(
new Font(new DictionaryType(new Entry(new NameType('F1'), new IndirectReference($helvetica)))),
new XObject(new DictionaryType(...$imageEntries)),
);
$operators = [
new SetNonstrokingRGBColor(new NumericType(0.08), new NumericType(0.18), new NumericType(0.28)),
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(24)),
new MoveTextPosition(new NumericType(52), new NumericType(732)),
new ShowText(new LiteralString('JPEG et PNG : des octets aux objets PDF')),
new SetTextFontAndSize(new NameType('F1'), new NumericType(11)),
new MoveTextPosition(new NumericType(0), new NumericType(-26)),
new ShowText(new LiteralString('Quatre images originales de 48 x 36 pixels. Taille sur la page : 216 x 162 points.')),
new EndTextObject(),
];
// 4. Each PDF image occupies the unit square. The explicit matrix produces
// a 216 × 162 rectangle, then translates it; there is no automatic placement.
// A checkerboard behind images reveals both PNGs' alpha channels.
$panels = [
['JpegRGB', 52, 452, 'JPEG RGB', 'DCTDecode : les octets JPEG restent intacts.', 'Trois composantes, transformation YCbCr.'],
['PngRGBA', 328, 452, 'PNG avec alpha', 'FlateDecode + /SMask en niveaux de gris.', 'L\'opacite augmente de gauche a droite.'],
['JpegCMYK', 52, 190, 'JPEG Adobe CMYK', 'Quatre composantes et /Decode inverse.', 'Le marqueur Adobe explicite les couleurs.'],
['PngPalette', 328, 190, 'PNG palette et tRNS', 'Les indices sont developpes en RGB.', 'Quatre opacites : 0, 85, 170 et 255.'],
];
foreach ($panels as [$name, $x, $y, $title, $line1, $line2]) {
$operators[] = new BeginTextObject();
$operators[] = new SetTextFontAndSize(new NameType('F1'), new NumericType(16));
$operators[] = new MoveTextPosition(new NumericType($x), new NumericType($y + 184));
$operators[] = new ShowText(new LiteralString($title));
$operators[] = new EndTextObject();
for ($row = 0; $row < 6; ++$row) {
for ($column = 0; $column < 8; ++$column) {
$operators[] = new SetNonstrokingGray(new NumericType(($row + $column) % 2 === 0 ? 0.85 : 0.98));
$operators[] = new AppendRectangle(new NumericType($x + $column * 27), new NumericType($y + $row * 27), new NumericType(27), new NumericType(27));
$operators[] = new FillPathNonzero();
}
}
$operators[] = new SaveGraphicsState();
$operators[] = new ConcatenateMatrix(new Matrix(
new NumericType(216),
new NumericType(0),
new NumericType(0),
new NumericType(162),
new NumericType($x),
new NumericType($y),
));
$operators[] = new InvokeXObject(new NameType($name));
$operators[] = new RestoreGraphicsState();
$operators[] = new SetNonstrokingRGBColor(new NumericType(0.08), new NumericType(0.18), new NumericType(0.28));
$operators[] = new BeginTextObject();
$operators[] = new SetTextFontAndSize(new NameType('F1'), new NumericType(10));
$operators[] = new MoveTextPosition(new NumericType($x), new NumericType($y - 21));
$operators[] = new ShowText(new LiteralString($line1));
$operators[] = new MoveTextPosition(new NumericType(0), new NumericType(-17));
$operators[] = new ShowText(new LiteralString($line2));
$operators[] = new EndTextObject();
}
// 5. The document graph follows previous tutorials: resources, content,
// page, page tree and catalog, all as explicit objects.
$contents = new StreamObject(new ContentStreams(...$operators));
$page = new PageObject(
new Rectangle(new NumericType(0), new NumericType(0), new NumericType(612), new NumericType(792)),
$resources,
$contents,
);
$pages = (new PageTreeNode())->addKid($page);
$catalog = new DocumentCatalog($pages);
$file = new FileStructure(new FileHeader('1.7'), new FileBody($catalog), new IndirectReference($catalog));
$path = $argv[1] ?? __DIR__ . '/image-files.pdf';
$file->writeTo($path);
echo $path . PHP_EOL;
