Technischer Artikel

CCITTFaxDecode in Delphi: Uncompressed-Mode, defekte Zeilen

HotPDF, die native Delphi-PDF-Komponente, dekodiert CCITTFaxDecode-Bilder mit voller Unterstützung für die T.4/T.6-Uncompressed-Mode-Erweiterung und für die Reparatur beschädigter Zeilen, gesteuert über DamagedRowsBeforeError. In den Uncompressed-Mode führt nur der Extension-Code mit der Endung 111, und beschädigte Zeilen werden nur bei Group-3-Daten mit gesetztem EndOfLine repariert. Alles andere failt sauber, statt eine schräge Seite zu produzieren

Fax-kodierte Bilder sind der Long Tail der PDF-Bilddekodierung. Die meisten CCITT-Streams, denen Sie begegnen, stammen aus einem Scanner-Treiber oder einer Document-Capture-Pipeline, die nur schlicht Group 4 emittiert, und ein Decoder, der den Normalfall beherrscht, besteht jeden Test, den Sie ihm vorwerfen. Dann kommt ein Archiv von einem fünfzehn Jahre alten Faxserver an, eine Seite rendert ab Zeile 400 als diagonales Rauschen, und Sie stellen fest, dass genau die zwei Teile der ITU-T-Empfehlungen, die niemand implementiert, die zwei Teile sind, die diese Datei benutzt. Dieser Artikel geht die beiden Teile durch, so wie HotPDF sie handhabt, und die Stellen, an denen ein Decoder sich verrechnen kann und dabei trotzdem korrekt aussieht

Welche CCITTFaxDecode-Parameter verändern den Bit-Stream tatsächlich?

Vier Einträge im DecodeParms-Dictionary verändern, wie der Decoder den Bit-Stream rahmt: K, EndOfLine, EncodedByteAlign und DamagedRowsBeforeError. ISO 32000-1 §7.4.6 (Tabelle 11) definiert sie, und einen davon falsch zu lesen verschiebt jede Zeile nach der ersten Abweichung. BlackIs1, Columns, Rows und EndOfBlock zählen auch, aber sie betreffen Polarität, Geometrie und wo die Dekodierung endet, nicht wie eine Zeilengrenze gefunden wird

  • K wählt das Codierungsschema: negativ heißt rein zweidimensionales Group 4 (T.6), null heißt ein-dimensionales Modified Huffman (Group 3 1D), und positiv heißt gemischtes Group 3 2D, wobei ein Tag-Bit nach jedem EOL sagt, ob die nächste Zeile 1D oder 2D ist
  • EndOfLine sagt, dass jeder Group-3-Zeile ein EOL-Code vorangeht, elf Null-Bits gefolgt von einer Eins
  • EncodedByteAlign sagt, dass jede kodierte Zeile an einer Byte-Grenze beginnt, also muss der Decoder die Padding-Bits verwerfen, bevor er sie liest
  • DamagedRowsBeforeError ist, wie viele korrupte Zeilen der Decoder tolerieren darf, bevor er aufgibt, mit einem Default von 0

HotPDF liest all diese Werte aus dem Image-Dictionary, wenn Sie THotPDF.ExtractLoadedImage oder THotPDF.DecodeLoadedImageStream aufrufen. Eine bewusste Abweichung ist wissenwert: Fehlt Columns, fällt HotPDF auf die Width des Bildes zurück statt auf den Spec-Default 1728, und fehlt Rows, nimmt es Height. Ein Producer, der bei einem 2480-Pixel-A4-Scan Columns weglässt, macht es falsch, aber es ist häufig genug, dass dem Bild-Geometrie zu vertrauen eine brauchbare Seite liefert, wo der wörtliche Default Müll ergäbe

Warum darf 000000 nicht als Blockende behandelt werden?

In zweidimensionalen Zeilen sind sechs führende Null-Bits mehrdeutig, und ein Decoder, der sie als EOFB-Anfang liest, verwechselt einen gültigen Uncompressed-Mode-Einstieg mit dem Ende des Bildes. Group 4 hat keine EOLs zwischen Zeilen; die einzigen Null-lastigen Codes sind die EOFB, die zwei EOLs hintereinander ist, und die Extension-Codes aus ITU-T T.4 und T.6. Beide beginnen mit 000000, und das siebte Bit ist die Stelle, an der sie sich trennen

Der HotPDF-CCITT-Decoder löst das Präfix bitweise auf. Ein siebtes Bit von 1 heißt, die nächsten drei Bits sind ein Extension-Code, das vollständige Muster ist 0000001xxx. Nur xxx = 111 führt in den Uncompressed-Mode; jeder andere Extension-Wert gilt als malformed, denn ein PDF-Fax-Filter hat für die anderen keine Verwendung. Ein siebtes Bit von 0 kann nur ein EOL fortsetzen, also verlangt der Decoder danach vier weitere Null-Bits und eine abschließende 1, insgesamt elf Nullen und eine Eins, bevor er EOFB meldet. Alles Kürzere oder Längere ist ein Fehler, kein kulantes Bildende

Die kulante Variante verführt, weil sie eine ganze Klasse kaputter Dateien gesund aussehen lässt: Die Dekodierung stoppt einfach früh, und Sie bekommen ein kurzes Bild ohne Exception. Sie ist auch exakt der Bug, der legitime Faxe an der ersten Strecke unkomprimierter Daten abschneidet, was auf einem gescannten Formular meist eine dichte Unterschrift oder ein Stempel ist. HotPDF wählt die strikte Lesart, ein kurzes Bild heißt also, der Encoder hat wirklich dort geendet

Wie kodiert der Uncompressed-Mode Pixel?

Der Uncompressed-Mode ersetzt die Lauflängenkodierung durch literale Pixel-Muster, jedes Codewort trägt höchstens fünf Pixel, bis ein Exit-Code zurückschaltet. Ein-dimensionale Zeilen betreten ihn mit dem 12-Bit-Code 000000001111; zweidimensionale Zeilen benutzen die oben beschriebene 10-Bit-Erweiterung 0000001111. Encoder schalten für Raster-artige Regionen um, wo Lauflängen von ein oder zwei Pixeln mehr kosten würden als die Pixel selbst

Innerhalb des Modes zählen die Codewörter nach ihren führenden Nullen. Null bis vier Nullen gefolgt von einer 1 bedeuten so viele weiße Pixel und dann ein schwarzes Pixel. Fünf Nullen gefolgt von einer 1 bedeuten fünf weiße Pixel und nichts sonst. Sechs bis zehn Nullen gefolgt von einer 1 sind Exit-Codes: Die Nullen über sechs emittieren bis zu vier weitere weiße Pixel, und das einzelne Bit nach dem Exit-Code, das Tag-Bit, setzt die Farbe des nächsten Runs, wenn die gewöhnliche Lauflängenkodierung fortsetzt

{ Uncompressed-Mode-Codewörter (ITU-T T.4 / T.6)
  W = weißes Pixel, B = schwarzes Pixel, T = Tag-Bit

  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, danach Runs mit Farbe T fortsetzen
  00000001T    W, Exit
  000000001T   W W, Exit
  0000000001T  W W W, Exit
  00000000001T W W W W, Exit
  mehr als zehn Nullen: fehlerhaft }

Das Tag-Bit ist der subtile Teil. Lauflängenkodierung ist zustandsbehaftet: Eine 1D-Zeile wechselt zwischen weißen und schwarzen Runs, und eine 2D-Zeile verfolgt die Farbe des aktuellen Wechselpunkts a0. Nach dem Exit-Code muss der Decoder mit der Farbe fortsetzen, die das Tag nennt, die zur letzten literalen Pixel-Farbe passt oder nicht. Der HotPDF-Decoder vermerkt einen Farbwechsel an der aktuellen Position, wenn das Tag von der verfolgten Farbe abweicht, sodass der nächste Run mit der richtigen Farbe startet. Ignorieren Sie das Tag, kommt der Rest der Zeile farbinvertiert heraus, und im 2D-Mode erbt jede folgende Zeile den Schaden über ihre Referenzzeile

Was erholt DamagedRowsBeforeError, und was nicht?

DamagedRowsBeforeError erlaubt HotPDF, eine korrupte Zeile zu ersetzen und weiterzumachen, aber nur wenn K 0 oder positiv ist und EndOfLine true. Diese zwei Bedingungen sind nicht beliebig. Die Wiederherstellung braucht einen Weg zu finden, wo die nächste Zeile beginnt, nachdem die Bits aufhören Sinn zu ergeben, und das einzige verlässliche Landmark in einem CCITT-Stream ist der EOL-Code. Group-4-Daten haben keine EOLs, und Group-3-Daten ohne Pflicht-EOLs garantieren nicht, dass einer existiert, in beiden Fällen beendet also eine beschädigte Zeile die Dekodierung, egal was der Parameter sagt

Wenn eine Zeile failt, scannt der HotPDF-Decoder vorwärts bis zum nächsten EOL, einer Folge von mindestens elf Null-Bits gefolgt von einer Eins, und ersetzt eine Ersatzzeile. Die erste beschädigte Zeile nach einer guten wird durch eine Kopie der vorherigen Zeile ersetzt, was auf einer gescannten Textseite meist unauffällig nah dran ist. Eine beschädigte Zeile, die direkt auf eine beschädigte folgt, wird stattdessen durch eine komplett weiße Zeile ersetzt, denn eine Kopie der Kopie würde eine Textzeile in einen hohen schwarzen Balken verschmieren. Der Ersatz wird dann zur Referenzzeile für die nächste 2D-Zeile, was einen Mixed-Mode-Stream nach der Reparatur dekodierbar hält

Zwei Grenzen übersieht man leicht. Das Budget ist kumulativ über das ganze Bild, nicht pro Reihe schlechter Zeilen, DamagedRowsBeforeError 3 toleriert also drei beschädigte Zeilen insgesamt, und die vierte lässt die Dekodierung failen. Und eine fehlgeschlagene Dekodierung ist in HotPDF alles oder nichts: THotPDF.ExtractLoadedImage gibt nil zurück statt einer Bitmap mit ausgefranstem unterem Rand. THPDFLoadedImageInfo.Decodable meldet, dass der Filter unterstützt wird, nicht dass dieser bestimmte Stream intakt ist, prüfen Sie die Bitmap also immer auf nil

Warum der Scan am Anfang des EOL stoppen muss

Der Resynchronisations-Scan lässt den Reader am ersten Null-Bit des EOL stehen, nicht danach. Der Decoder hat eine Framing-Schleife, die EOLs konsumiert, das doppelte EOL prüft, das den Block beendet, und das Mixed-Mode-Tag-Bit liest. Schluckt der Recovery-Scan das EOL selbst, sieht diese Schleife eine Zeile ohne EOL, hält sie ebenfalls für beschädigt und verbrennt eine zweite Budget-Einheit, während sie eine gute Zeile fallen lässt. Schlimmer noch: Ein übersprungenes Tag-Bit heißt, eine 2D-Zeile wird als 1D dekodiert oder umgekehrt. Dieselbe Regel greift, wenn ein 1D-Run-Decoder mitten in der Zeile auf ein unerwartetes EOL trifft: Er spult dorthin zurück, wo der Run-Code begann, damit die Markierung für den Recovery-Scan noch da ist

CCITT-Bilder in einem geladenen PDF inventarisieren

Bevor Sie einem Batch archivierter Faxe vertrauen, listen Sie, welche Bilder CCITTFaxDecode benutzen und welche nicht dekodieren. THotPDF.GetLoadedImageInfo liefert Geometrie und Filternamen, ohne etwas zu dekodieren, und THotPDF.ExtractLoadedImage sagt Ihnen, ob der Stream überlebt hat. Für das größere Bild, wie der Image-Dispatcher jeden Filter route, siehe den Guide zu extrahierten geladenen PDF-Bildern und ihren Decode-Filtern

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, wenn der Stream zu stark beschädigt ist
      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;                         // der Aufrufer besitzt die Bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Für die Fehler: Bewahren Sie die Beweise auf. THotPDF.ExtractLoadedImageToStream im Default-Mode lismEncoded schreibt die gespeicherten Stream-Bytes unangetastet, genau das, was Sie einem zweiten Decoder geben, in einen TIFF-Container zur Inspektion packen oder an einen Bug-Report anhängen wollen. Ein beschädigtes Fax neu zu kodieren versteckt nur, wo der Schaden war

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;

Wenn Sie Image-Streams selbst über THotPDF.CollectLoadedImageStreams durchlaufen, übergeben Sie jeden THPDFStreamObject an THotPDF.DecodeLoadedImageStream, und Sie bekommen denselben CCITT-Pfad, dieselben Parameter und Recovery-Regeln. Geben Sie die zurückgegebene Liste frei, aber nicht die Stream-Objekte darin; die bleiben im Besitz des Dokuments

Sollten Sie DamagedRowsBeforeError bei Dateien setzen, die Sie nicht kontrollieren?

Nein, nicht durch Bearbeiten des PDFs, und meist gar nicht. Der Parameter gehört zum Producer, der als Einziger weiß, ob sein Übertragungskanal Bits verliert. Eine Datei, die DamagedRowsBeforeError 0 deklariert und dann eine beschädigte Zeile enthält, sagt Ihnen, dass die Archivkopie korrupt ist, und das ehrliche Ergebnis ist ein Dekodierfehler, den Sie loggen können. Zeilen stillschweigend in einem Vertrag oder einer Krankenakte zu flicken, verwandelt einen erkennbaren Defekt in ein plausibel aussehendes Bild mit fehlender Zeile, was für jeden, der sich später darauf verlässt, ein schlechteres Ergebnis ist

Das sicherere Muster für nicht vertrauenswürdige Archive: Dekodierung strikt lassen, festhalten, welche Objektnummern failen, und die an einen Menschen oder einen separaten Decoder weiterreichen. Ist das Archiv groß und kommt aus vielen Quellen, begrenzt Out-of-Process-Bilddekodierung, was ein feindlicher Stream der Host-Anwendung antun kann; der Artikel zu isolierten Image-Codecs bei der Verarbeitung nicht vertrauenswürdiger PDFs behandelt dieses Setup, und der JBIG2-Bilevel-Compression-Guide vergleicht den anderen Bilevel-Filter, denselben gescannten Archiven begegnen wird

FAQ

Unterstützt HotPDF den CCITT-Uncompressed-Mode in Group-4-Bildern?

Ja. HotPDF erkennt die 10-Bit-Erweiterung 0000001111 in zweidimensionalen Zeilen und den 12-Bit-Eingangscode 000000001111 in ein-dimensionalen Zeilen, dekodiert die literalen Codewörter und beachtet das Exit-Tag-Bit, wenn die Lauflängenkodierung fortsetzt

Warum hat DamagedRowsBeforeError keine Wirkung auf mein Group-4-Fax?

Die Wiederherstellung braucht EOL-Marker zur Resynchronisation. HotPDF wendet DamagedRowsBeforeError nur an, wenn K 0 oder positiv ist und EndOfLine true; Group-4-Daten (negatives K) haben keine EOLs, jede beschädigte Zeile beendet also die Dekodierung

Was gibt ExtractLoadedImage für ein beschädigtes CCITT-Bild zurück?

THotPDF.ExtractLoadedImage gibt nil zurück, wenn der Schaden übersteigt, was DamagedRowsBeforeError erlaubt, oder sich gar nicht beheben lässt. Er gibt nie eine teilweise Bitmap zurück, testen Sie also auf nil und benutzen ExtractLoadedImageToStream, um die rohen Bytes zu behalten

HotPDF handhabt CCITT-, JBIG2-, JPEG-, JPEG-2000- und Flate-Bilder in geladenen PDFs aus reinem Delphi- und C++Builder-Code, ohne externe DLL für den Fax-Pfad. Details und Editionen finden Sie auf der HotPDF Delphi PDF component page