PDFlibPas décode le TIFF avec un analyseur Object Pascal écrit à la main plutôt qu’une liaison libtiff, et la version 3.534.1 a précisé exactement l’endroit où cet analyseur refuse les entrées. Le magic 43 de BigTIFF est désormais rejeté nommément, TileOffsets et TileByteCounts sont refusés dès l’analyse des balises, et chaque tampon est dimensionné en arithmétique Int64 sous un plafond de décodage de 256 MiB
Le défaut corrigé ici ne se manifeste jamais en laboratoire. Il se manifeste sur une passerelle de scan qui tourne tranquillement depuis trois ans, jusqu’au jour où un client y fait passer une archive géospatiale ou une image médicale whole-slide. Le fichier possède un en-tête TIFF légitime. Il se parse. Ce qui en sort, c’est une page de bruit strié, ou une allocation de plusieurs gigaoctets qui fait tomber le service, et rien en chemin n’a jamais déclaré l’entrée invalide. Voilà la forme de défaillance contre laquelle il faut concevoir : pas un plantage, mais une mauvaise réponse livrée avec assurance
Pourquoi II ou MM ne prouve-t-il pas que vous avez un TIFF classique ?
Parce que le marqueur d’ordre des octets est partagé par les deux dialectes. Le TIFF classique et le BigTIFF ouvrent chacun avec II ou MM, et le champ qui les distingue réellement est le magic 16 bits qui suit immédiatement : 42 pour le TIFF classique tel que défini dans la spécification TIFF 6.0, 43 pour le BigTIFF avec ses décalages 64 bits. Un chargeur écrit sous la forme FValidTIFF := PopWord = 42 ne se trompe pas sur le TIFF classique, mais il fusionne deux rejets très différents en un seul booléen silencieux, si bien qu’un BigTIFF devient indiscernable d’un JPEG tronqué que quelqu’un a renommé. PDFlibPas sépare désormais les cas et enregistre chacun dans TPDFTIFF.LastError : un en-tête plus court que quatre octets, un marqueur d’ordre des octets invalide, le magic 43, et toute autre valeur de magic produisent des textes distincts. La bibliothèque ne décode toujours pas le BigTIFF, et le dire clairement est précisément l’objectif. L’appelant obtient la différence entre « ceci n’est pas un TIFF » et « ceci est un TIFF dont la mise en page à décalages 64 bits n’est pas prise en charge par le décodeur intégré », ce qui est la différence entre un ticket de support auquel on répond en un message et celui qui se transforme en une semaine de devinettes
var
Tiff: TPDFTIFF;
Page: Integer;
begin
Tiff := TPDFTIFF.Create;
try
Tiff.LoadFromFile('inbox\scan-0417.tif');
if not Tiff.ValidTIFF then
raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
if Tiff.PageCount < 1 then
raise Exception.Create('TIFF carries no decodable page');
for Page := 1 to Tiff.PageCount do
Writeln(Format('page %d: %dx%d, %d spp',
[Page,
Tiff.PageInfo[Page].Width,
Tiff.PageInfo[Page].Height,
Tiff.PageInfo[Page].SamplesPerPixel]));
finally
Tiff.Free;
end;
end;
Les tuiles sont une autre géométrie, pas un simple tableau de décalages
PDFlibPas refuse le TIFF pavé pendant l’analyse des balises, avant que la moindre donnée de pixel ne soit touchée. Le raccourci qui invite le bogue est facile à voir : les balises 324 (TileOffsets) et 325 (TileByteCounts) sont des tableaux de décalages de fichier et de comptes d’octets, structurellement identiques aux tableaux de bandes, si bien que pointer les champs de bandes existants vers elles coûte deux lignes et compile proprement. C’est aussi faux. Les tuiles forment une grille bidimensionnelle avec des blocs de bord rembourrés, leur propre pas de ligne à l’intérieur de chaque tuile, et aucune sémantique RowsPerStrip, comme le précise la section images pavées de TIFF 6.0. Alimenter un décodeur de bandes avec des données de tuiles ne provoque donc pas d’échec bruyant. SimpleExtract et CompDecode parcourent les données avec le mauvais pas et produisent une image aux bonnes dimensions et aux mauvais pixels. L’ancien code aggravait cela en conservant StripsAreTiles, ColumnsPerTile et RowsPerTile dans TTIFFPage : une géométrie de tuiles enregistrée par un décodeur sans assembleur de tuiles derrière. En 3.534.1, les gestionnaires des balises 324 et 325 déclenchent l’erreur de tuile et abandonnent immédiatement l’IFD, si bien que le refus porte le mot « tiled » au lieu de se manifester des semaines plus tard comme une plainte de rendu
Un plafond de dimension n’est pas un budget mémoire
Plafonner la largeur et la hauteur à 65,535 chacune est nécessaire et loin d’être suffisant, car la quantité qui pilote l’allocation est un produit. RowsPerStrip * Width * SamplesPerPixel peut déborder d’une arithmétique 32 bits bien avant que l’un des termes n’atteigne sa propre limite, et même sans débordement il peut nommer une allocation qu’aucun service ne devrait tenter. PDFlibPas calcule les octets de ligne en Int64 et applique trois plafonds ensemble : 65,535 par dimension, 32 composantes couleur, et 256 MiB d’octets décodés
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// dans TPDFTIFF.ValidatePageForDecode
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
(RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
Exit(False);
DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
(DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(DecodedBytes > MaxInt) then
Exit(False);
Trois détails comptent ici plus que les constantes elles-mêmes. Le test de hauteur est écrit comme une division plutôt qu’une multiplication, si bien que le produit surdimensionné n’est jamais formé du tout. Un RowsPerStrip inférieur à 1 ou supérieur à la hauteur de l’image est d’abord normalisé à la hauteur, ce que la lecture à bande unique de TIFF 6.0 implique déjà et qui empêche une balise hostile de gonfler le tampon de bande. Et la routine est partagée : ValidatePageForDecode s’exécute à la fin de l’analyse des balises puis à l’entrée de SimpleExtract comme de CompDecode, si bien qu’un code qui atteint directement un décodeur ne peut pas contourner le budget. C’est la même règle que PDFlibPas applique lors du parsing de graphes d’objets PDF non fiables, car une limite appliquée à une porte sur trois n’est pas une limite
Que doit vérifier un appelant avant de lire PageInfo ?
Vérifiez d’abord ValidTIFF, puis PageCount, et seulement ensuite indexez PageInfo. Un fichier rejeté peut laisser PageCount à zéro, et GetPageInfo répond à un index hors plage avec un enregistrement TTIFFPage non initialisé, si bien qu’un chemin d’erreur qui lit la résolution ou les comptes d’échantillons en route pour signaler l’échec finit par lire du bruit. La version 3.534.1 a corrigé les deux appelants à l’intérieur de la bibliothèque : le chemin d’import d’image ne lit XRes et YRes que dans la branche valide, et TPDFlib.GetImagePageCount exige ValidTIFF au lieu de se fier seul à un compte de pages non nul. En aval, l’argument Options de AddImageFromFile est le numéro de page à base 1 d’un TIFF multipage, si bien que GetImagePageCount doit être fiable avant que la boucle ne commence plutôt qu’après. Zéro page est désormais une vraie réponse signifiant « rien ici n’est décodable », et non l’accident d’un retour prématuré, ce qui compte surtout lorsque vous collez et entrelacez des lots de scans recto verso et qu’une feuille mal décodée en silence se retrouverait au mauvais endroit
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // en-tête invalide, BigTIFF, mise en page pavée ou hors budget
Pdf.NewDocument;
for I := 1 to Pages do
begin
Pdf.NewPage;
ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
if ImageID > 0 then
begin
Pdf.SelectImage(ImageID);
Pdf.DrawImage(0, 0, 595, 842);
end;
end;
Pdf.SaveToFile('scan-0417.pdf');
finally
Pdf.Free;
end;
end;
Construire le décodeur ou lier libtiff ?
PDFlibPas conserve le décodeur intégré, et le facteur décisif est la portée des plateformes plutôt que la paternité du code. Environ 1,873 lignes d’Object Pascal compilent partout où le compilateur va : Win32, Win64, macOS, iOS, Android, et FPC sous Linux. libtiff 4.7.1 représente environ 30,000 lignes de C réparties sur 34 unités de traduction tif_*.c, et les fichiers objets précompilés qui existent aujourd’hui ne couvrent que Windows. L’adopter échangerait une couverture TIFF complète contre une liste de plateformes prises en charge qui se réduit à toute machine capable d’exécuter la chaîne d’outils C, plus une passe d’édition de liens que personne n’a encore parcourue
Ce que cela coûte mérite d’être énoncé sans enjolivement. Le décodeur intégré traite ce que le travail sur documents scannés produit réellement : CCITT Group 3 unidimensionnel et bidimensionnel, Group 4, LZW, Deflate, PackBits et JPEG-in-TIFF, sur les photométries WhiteIsZero, BlackIsZero, RGB, palette et CMYK avec Predictor 1 et 2. Ces charges utiles correspondent aux filtres PDF de l’ISO 32000-1 §7.4.4 et §7.4.6, ce qui explique pourquoi le frontal TIFF pèse autant dans un pipeline de scan. Ce qu’il ne traite pas : BigTIFF, les tuiles, le Predictor 3 en virgule flottante, PixarLog et SGILog, la vieille compression JPEG de style 6, et les pyramides de sous-IFD. Depuis 3.534.1, chacun de ces éléments est un refus nommé plutôt qu’une image erronée, et la bibliothèque conserve une liste écrite de déclencheurs pour rouvrir la décision libtiff :
- un client signale un fichier BigTIFF et a besoin d’une prise en charge native plutôt que d’une étape de conversion
- un client signale un TIFF pavé issu de sources médicales, SIG ou industrielles et a besoin qu’il soit décodé sur place
- un client signale un TIFF Predictor 3 en virgule flottante
- une vulnérabilité publiée touche les chemins de décodage CCITT ou LZW intégrés
- l’argument multiplateforme cesse de s’appliquer, soit parce que la prise en charge de macOS, iOS et Android est abandonnée, soit parce qu’une intégration libtiff réutilisable couvre déjà macOS et Linux
La migration elle-même est délimitée plutôt qu’hypothétique : un conditionnel USE_LIBTIFF conserverait la surface publique de TPDFTIFF intacte, acheminerait LoadFromStream via TIFFClientOpen avec des rappels de flux, et laisserait l’analyseur Pascal comme solution de repli hors Windows. Tant qu’aucun de ces déclencheurs ne se réalise réellement, maintenir deux décodeurs et une matrice de tests doublée n’achète rien qu’un client puisse ressentir. Différer un coût en ayant déjà noté la porte de sortie est une tout autre chose que l’ignorer
Où cela laisse un pipeline de documents scannés
Traitez TPDFTIFF comme une porte plutôt qu’un convertisseur. Chargez le fichier, lisez ValidTIFF, et journalisez LastError verbatim chaque fois qu’il est faux, car cette chaîne est désormais le chemin le plus court d’un rapport de terrain à un diagnostic. Les fichiers qui échouent à la porte restent récupérables en les convertissant en amont, ce qui est la réponse pratique pour les sources BigTIFF et pavées aujourd’hui. Pour une entrée hors TIFF, PDFlibPas emprunte une voie séparée via son chemin d’entrée d’images AVIF, HEIF et JPEG XL, si bien que la question de savoir quel décodeur possède quel format reste explicite plutôt qu’émergente
Tout cela se cache derrière l’API d’image ordinaire, si bien qu’un pipeline de documents gagne la frontière plus étanche sans changer une seule ligne de code appelant, hormis la vérification du compte de pages qu’il aurait déjà dû faire. Si vous évaluez un chemin TIFF vers PDF natif pour Delphi ou C++Builder, le composant complet et sa gestion d’images sont documentés sur la page PDF Library for Delphi