Article technique

CCITTFaxDecode en Delphi : mode uncompressed, lignes cassées

HotPDF, le composant PDF natif pour Delphi, décode les images CCITTFaxDecode avec prise en charge complète de l'extension uncompressed mode T.4/T.6 et de la récupération de lignes endommagées pilotée par DamagedRowsBeforeError. Le mode uncompressed ne s'ouvre que par le code d'extension finissant en 111, et les lignes endommagées ne sont réparées que pour des données Group 3 avec EndOfLine posé. Tout le reste échoue proprement au lieu de produire une page de travers

Les images encodées fax sont la longue traîne du décodage d'images PDF. La plupart des flux CCITT que vous croisez sortent d'un pilote de scanner ou d'une chaîne de capture documentaire qui n'émet que du Group 4 simple, et un décodeur qui gère le cas courant passe tous les tests qu'on lui lance. Puis arrive une archive d'un serveur de fax vieux de quinze ans, une page se rend en bruit diagonal à partir de la ligne 400, et vous découvrez que les deux parties des recommandations ITU-T que personne n'implémente sont exactement les deux parties que ce fichier utilise. Cet article parcourt ces deux parties telles que HotPDF les traite, et les endroits où un décodeur peut se tromper tout en ayant l'air correct

Quels paramètres CCITTFaxDecode changent réellement le flux de bits ?

Quatre entrées du dictionnaire DecodeParms changent la façon dont le décodeur cadre le flux de bits : K, EndOfLine, EncodedByteAlign et DamagedRowsBeforeError. ISO 32000-1 §7.4.6 (Table 11) les définit, et se tromper sur une seule décale chaque ligne après la première incohérence. BlackIs1, Columns, Rows et EndOfBlock comptent aussi, mais ils touchent la polarité, la géométrie et l'endroit où le décodage s'arrête plutôt que la manière dont une frontière de ligne est trouvée

  • K choisit le schéma de codage : négatif signifie du Group 4 purement bidimensionnel (T.6), zéro signifie du Modified Huffman unidimensionnel (Group 3 1D), et positif signifie du Group 3 2D mixte, où un bit de balise après chaque EOL dit si la ligne suivante est 1D ou 2D
  • EndOfLine dit que chaque ligne Group 3 est précédée d'un code EOL, onze bits à zéro suivis d'un un
  • EncodedByteAlign dit que chaque ligne encodée démarre sur une frontière d'octet, donc le décodeur doit jeter les bits de remplissage avant de la lire
  • DamagedRowsBeforeError est le nombre de lignes corrompues que le décodeur peut tolérer avant d'abandonner, avec un défaut de 0

HotPDF lit tout cela dans le dictionnaire d'image quand vous appelez THotPDF.ExtractLoadedImage ou THotPDF.DecodeLoadedImageStream. Un écart délibéré mérite d'être connu : quand Columns manque, HotPDF retombe sur la Width de l'image plutôt que sur le défaut de la spécification, 1728, et quand Rows manque, il utilise Height. Un producteur qui omet Columns sur un scan A4 de 2480 pixels a tort, mais c'est assez courant pour que se fier à la géométrie de l'image donne une page utilisable là où le défaut littéral donnerait de la bouillie

Pourquoi 000000 ne peut-il pas être traité comme la fin du bloc ?

Dans les lignes bidimensionnelles, six bits à zéro de tête sont ambigus, et un décodeur qui les lit comme le début d'un EOFB prendra une entrée valide en mode uncompressed pour la fin de l'image. Le Group 4 n'a pas d'EOL entre les lignes ; les seuls codes chargés en zéros sont l'EOFB, qui est deux EOL bout à bout, et les codes d'extension d'ITU-T T.4 et T.6. Les deux commencent par 000000, et le septième bit est là où ils divergent

Le décodeur CCITT de HotPDF résout le préfixe bit par bit. Un septième bit à 1 signifie que les trois bits suivants sont un code d'extension, le motif complet étant 0000001xxx. Seul xxx = 111 ouvre le mode uncompressed ; toute autre valeur d'extension est traitée comme malformée, puisqu'un filtre fax PDF n'a pas d'usage pour les autres. Un septième bit à 0 ne peut que continuer un EOL, donc le décodeur exige alors quatre bits à zéro de plus et un 1 de fermeture, onze zéros et un un en tout, avant de rapporter un EOFB. Tout ce qui est plus court ou plus long est une erreur, pas une fin d'image tolérante

La version tolérante est tentante parce qu'elle fait passer pour sains toute une catégorie de fichiers cassés : le décodage s'arrête simplement tôt et vous obtenez une image courte sans exception. C'est aussi exactement le bug qui tronque des fax légitimes au premier passage de données uncompressed, qui sur un formulaire scanné est en général une signature dense ou un tampon. HotPDF choisit la lecture stricte, donc une image courte signifie que l'encodeur a vraiment terminé là

Comment le mode uncompressed encode-t-il les pixels ?

Le mode uncompressed remplace le codage par longueur de course par des motifs de pixels littéraux, chaque mot de code portant au plus cinq pixels, jusqu'à ce qu'un code de sortie remette le codage normal. Les lignes unidimensionnelles y entrent avec le code 12 bits 000000001111 ; les lignes bidimensionnelles utilisent l'extension 10 bits 0000001111 décrite plus haut. Les encodeurs y basculent pour les régions de type trame où des courses d'un ou deux pixels coûteraient plus cher que les pixels eux-mêmes

Dans le mode, les mots de code se comptent par leurs zéros de tête. De zéro à quatre zéros suivis d'un 1 signifient autant de pixels blancs puis un pixel noir. Cinq zéros suivis d'un 1 signifient cinq pixels blancs et rien d'autre. Six à dix zéros suivis d'un 1 sont des codes de sortie : les zéros au-delà de six émettent jusqu'à quatre pixels blancs de plus, et l'unique bit après le code de sortie, le bit de balise, fixe la couleur de la prochaine course quand le codage ordinaire reprend

{ Mots de code du mode uncompressed (ITU-T T.4 / T.6)
  W = pixel blanc, B = pixel noir, T = bit de balise

  1            B
  01           W B
  001          W W B
  0001         W W W B
  00001        W W W W B
  000001       W W W W W
  0000001T     exit, then resume runs with color T
  00000001T    W, exit
  000000001T   W W, exit
  0000000001T  W W W, exit
  00000000001T W W W W, exit
  plus de dix zéros : malformé }

Le bit de balise est la partie subtile. Le codage par course est à état : une ligne 1D alterne courses blanches et noires, et une ligne 2D suit la couleur de l'élément changeant courant a0. Après le code de sortie, le décodeur doit reprendre avec la couleur que nomme la balise, qui peut ou non correspondre au dernier pixel littéral. Le décodeur HotPDF enregistre un changement de couleur à la position courante quand la balise diffère de la couleur qu'il suivait, pour que la prochaine course démarre de la bonne couleur. Ignorez la balise et le reste de la ligne sort en couleurs inversées, et en mode 2D chaque ligne suivante hérite du dommage via sa ligne de référence

Que récupère DamagedRowsBeforeError, et que ne récupère-t-il pas ?

DamagedRowsBeforeError permet à HotPDF de remplacer une ligne corrompue et de continuer, mais seulement quand K vaut 0 ou plus et que EndOfLine est vrai. Ces deux conditions ne sont pas arbitraires. La récupération a besoin d'un moyen de trouver où démarre la prochaine ligne après que les bits ont cessé d'avoir du sens, et le seul repère fiable dans un flux CCITT est le code EOL. Les données Group 4 n'ont pas d'EOL, et les données Group 3 sans EOL obligatoires ne garantissent pas qu'il en existe un ; dans les deux cas, une ligne endommagée met fin au décodage quelle que soit la valeur du paramètre

Quand une ligne échoue, le décodeur HotPDF scanne en avant jusqu'au prochain EOL, une course d'au moins onze bits à zéro suivie d'un un, et substitue une ligne de remplacement. La première ligne endommagée après une bonne est remplacée par une copie de la ligne précédente, ce qui sur une page de texte scannée est en général assez proche pour être invisible. Une ligne endommagée qui suit directement une autre ligne endommagée est remplacée par une ligne tout blanc à la place, parce que copier une copie étalerait une ligne de texte en une barre noire haute. Le substitut devient ensuite la ligne de référence pour la prochaine ligne 2D, et c'est ce qui garde un flux en mode mixte décodable après la réparation

Deux frontières sont faciles à manquer. Le budget est cumulatif sur toute l'image, pas par salve de mauvaises lignes, donc DamagedRowsBeforeError 3 tolère trois lignes endommagées en tout et la quatrième fait échouer le décodage. Et un décodage en échec dans HotPDF est tout ou rien : THotPDF.ExtractLoadedImage renvoie nil plutôt qu'un bitmap au bord inférieur déchiqueté. THPDFLoadedImageInfo.Decodable signale que le filtre est pris en charge, pas que ce flux particulier est intact ; vérifiez donc toujours le bitmap contre nil

Pourquoi le scan doit s'arrêter au début de l'EOL

Le scan de resynchronisation laisse le lecteur au premier bit à zéro de l'EOL, pas après. Le décodeur a une boucle de cadrage unique qui consomme les EOL, vérifie le double EOL qui termine le bloc, et lit le bit de balise du mode mixte. Si le scan de récupération avale l'EOL lui-même, cette boucle voit une ligne sans EOL, la traite comme endommagée elle aussi, et brûle une deuxième unité du budget en jetant une bonne ligne. Pire, un bit de balise sauté fait qu'une ligne 2D est décodée comme 1D ou l'inverse. La même règle vaut quand un décodeur de courses 1D bute sur un EOL inattendu en pleine ligne : il rembobine à l'endroit où le code de course a démarré, si bien que le marqueur est toujours là pour le scan de récupération

Faire l'inventaire des images CCITT dans un PDF chargé

Avant de faire confiance à un lot de fax archivés, listez quelles images utilisent CCITTFaxDecode et lesquelles échouent au décodage. THotPDF.GetLoadedImageInfo vous donne la géométrie et le nom du filtre sans rien décoder, et THotPDF.ExtractLoadedImage vous dit si le flux a survécu. Pour la vue d'ensemble du routage de chaque filtre par le répartiteur d'images, voir le guide d'extraction des images PDF chargées et de leurs filtres de décodage

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('fax-archive.pdf', '') <= 0 then
      Exit;
    for I := 0 to Pdf.GetLoadedImageCount - 1 do
    begin
      if not Pdf.GetLoadedImageInfo(I, Info) then
        Continue;
      if Pos('CCITT', UpperCase(String(Info.Filter))) = 0 then
        Continue;
      Bmp := Pdf.ExtractLoadedImage(I);   // nil quand le flux est trop endommagé
      try
        if Bmp = nil then
          Writeln(Format('obj %d: %dx%d CCITT failed to decode',
            [Info.ObjectNumber, Info.Width, Info.Height]))
        else
          Bmp.SaveToFile(Format('fax_%d.bmp', [I]));
      finally
        Bmp.Free;                         // l'appelant est propriétaire du bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Pour les échecs, gardez la preuve. THotPDF.ExtractLoadedImageToStream avec le mode par défaut lismEncoded écrit les octets du flux stocké intacts, ce que vous voulez remettre à un second décodeur, envelopper dans un conteneur TIFF pour inspection, ou joindre à un rapport de bug. Réencoder un fax endommagé ne fait que cacher où était le dommage

procedure SaveRawFax(Pdf: THotPDF; ImageIndex: Integer; const FileName: string);
var
  Fs: TFileStream;
begin
  Fs := TFileStream.Create(FileName, fmCreate);
  try
    if not Pdf.ExtractLoadedImageToStream(ImageIndex, Fs, lismEncoded) then
      raise Exception.CreateFmt('Image %d has no stream data', [ImageIndex]);
  finally
    Fs.Free;
  end;
end;

Si vous parcourez vous-même les flux d'images via THotPDF.CollectLoadedImageStreams, passez chaque THPDFStreamObject à THotPDF.DecodeLoadedImageStream et vous obtenez le même chemin CCITT, les mêmes paramètres et les mêmes règles de récupération. Libérez la liste renvoyée mais pas les objets de flux qu'elle contient, qui restent la propriété du document

Faut-il augmenter DamagedRowsBeforeError sur des fichiers que vous ne contrôlez pas ?

Non, pas en éditant le PDF, et en général pas du tout. Le paramètre appartient au producteur, qui est la seule partie à savoir si son canal de transmission perd des bits. Un fichier qui déclare DamagedRowsBeforeError 0 puis contient une ligne endommagée vous dit que la copie d'archive est corrompue, et le résultat honnête est un échec de décodage que vous pouvez journaliser. Recoudre silencieusement des lignes sur un contrat ou un dossier médical transforme un défaut détectable en image plausible à laquelle il manque une ligne, ce qui est une issue pire pour quiconque s'y fie ensuite

Le schéma plus sûr pour des archives non fiables est de garder le décodage strict, d'enregistrer quels numéros d'objet ont échoué, et de router ceux-là vers un humain ou un décodeur séparé. Si l'archive est grosse et vient de nombreuses sources, faire tourner le décodage d'images hors processus limite ce qu'un flux hostile peut faire à l'application hôte ; l'article sur l'isolation des codecs d'images quand on traite des PDF non fiables couvre ce montage, et le guide de la compression bilevel JBIG2 compare l'autre filtre bilevel que vous trouverez dans les mêmes archives scannées

FAQ

HotPDF prend-il en charge le mode uncompressed CCITT dans les images Group 4 ?

Oui. HotPDF reconnaît l'extension 10 bits 0000001111 dans les lignes bidimensionnelles et le code d'entrée 12 bits 000000001111 dans les lignes unidimensionnelles, décode les mots de code littéraux, et honore le bit de balise de sortie quand le codage par course reprend

Pourquoi DamagedRowsBeforeError n'a-t-il aucun effet sur mon fax Group 4 ?

La récupération exige des marqueurs EOL pour se resynchroniser. HotPDF applique DamagedRowsBeforeError seulement quand K vaut 0 ou plus et que EndOfLine est vrai ; les données Group 4 (K négatif) n'ont pas d'EOL, donc toute ligne endommagée met fin au décodage

Que renvoie ExtractLoadedImage pour une image CCITT endommagée ?

THotPDF.ExtractLoadedImage renvoie nil quand le dommage dépasse ce que permet DamagedRowsBeforeError ou ne peut pas être récupéré du tout. Il ne renvoie jamais un bitmap partiel ; testez donc contre nil et utilisez ExtractLoadedImageToStream pour garder les octets bruts

HotPDF traite les images CCITT, JBIG2, JPEG, JPEG 2000 et Flate dans des PDF chargés depuis du code Delphi et C++Builder ordinaire, sans DLL externe pour le chemin fax. Détails et éditions sont sur la page du composant HotPDF Delphi PDF