Technisch artikel

CCITTFaxDecode in Delphi: uncompressed mode en kapotte rijen

HotPDF, de native Delphi PDF-component, decodeert CCITTFaxDecode-beelden met volledige ondersteuning voor de uncompressed-mode-extensie van T.4/T.6 en voor herstel van beschadigde rijen aangestuurd door DamagedRowsBeforeError. Uncompressed mode wordt alleen betreden via de extensiecode die eindigt op 111, en beschadigde rijen worden alleen gerepareerd bij Group 3-data met EndOfLine gezet. Alles daarnaast faalt netjes in plaats van een scheve pagina te produceren

Faxgecodeerde beelden zijn de lange staart van PDF-beelddecoding. De meeste CCITT-streams die u tegenkomt komen uit een scannerdriver of een documentcapturepijplijn die nooit anders dan gewone Group 4 uitstuurt, en een decoder die het gewone geval aankan slaagt voor elke test die u hem voorschotelt. Tot er een archief van een vijftien jaar oude faxserver binnenkomt, een pagina vanaf rij 400 als diagonale ruis rendert, en u ontdekt dat de twee delen van de ITU-T-aanbevelingen die niemand implementeert precies de twee delen zijn die dat bestand gebruikt. Dit artikel loopt door die twee delen zoals HotPDF ze behandelt, en langs de plekken waar een decoder mis kan gaan en er toch correct uitziet

Welke CCITTFaxDecode-parameters veranderen de bitstroom echt?

Vier entries in het DecodeParms-woordenboek veranderen hoe de decoder de bitstroom kadert: K, EndOfLine, EncodedByteAlign en DamagedRowsBeforeError. ISO 32000-1 §7.4.6 (Table 11) definieert ze, en er een van verkeerd hebben verschuift elke rij na de eerste mismatch. Ook BlackIs1, Columns, Rows en EndOfBlock tellen mee, maar die raken polariteit, geometrie en waar het decoderen stopt, niet hoe een rijgrens gevonden wordt

  • K kiest het codeschema: negatief betekent puur tweedimensionale Group 4 (T.6), nul betekent ééndimensionale Modified Huffman (Group 3 1D), en positief betekent gemengde Group 3 2D, waarbij een tag-bit na elke EOL zegt of de volgende rij 1D of 2D is
  • EndOfLine zegt dat elke Group 3-rij voorafgegaan wordt door een EOL-code, elf nulbits gevolgd door een één
  • EncodedByteAlign zegt dat elke gecodeerde rij op een bytegrens begint, dus de decoder moet de opvulbits weggooien voor hij hem leest
  • DamagedRowsBeforeError is hoeveel corrupte rijen de decoder mag tolereren voordat hij opgeeft, met 0 als default

HotPDF leest ze allemaal uit het beeldwoordenboek wanneer u THotPDF.ExtractLoadedImage of THotPDF.DecodeLoadedImageStream aanroept. Eén bewuste afwijking is goed om te weten: ontbreekt Columns, dan wijkt HotPDF uit naar de Width van het beeld in plaats van naar de spec-default van 1728, en ontbreekt Rows, dan gebruikt hij Height. Een producent die Columns weglaat bij een A4-scan van 2480 pixels zit fout, maar het komt zo vaak voor dat vertrouwen op de beeldgeometrie een bruikbare pagina oplevert waar de letterlijke default rommel geeft

Waarom mag 000000 niet als het einde van het blok worden behandeld?

In tweedimensionale rijen zijn zes voorloopnulbits dubbelzinnig, en een decoder die ze als het begin van EOFB leest, misleest een geldige uncompressed-mode-ingang als het einde van het beeld. Group 4 heeft geen EOL's tussen rijen; de enige nulrijke codes zijn de EOFB — twee EOL's rug aan rug — en de extensiecodes uit ITU-T T.4 en T.6. Beide beginnen met 000000, en de zevende bit is waar ze uiteenlopen

De CCITT-decoder van HotPDF herleidt het voorvoegsel bit voor bit. Een zevende bit van 1 betekent dat de volgende drie bits een extensiecode zijn, met als volledig patroon 0000001xxx. Alleen xxx = 111 betreedt uncompressed mode; elke andere extensiewaarde wordt als misvormd behandeld, want een PDF-faxfilter heeft de anderen niet nodig. Een zevende bit van 0 kan alleen een EOL voortzetten, dus de decoder eist daarna vier extra nulbits en een afsluitende één, in totaal elf nullen en een één, voordat hij EOFB meldt. Alles wat korter of langer is, is een fout, geen soepel einde-van-beeld

De soepele versie is verleidelijk, want die laat een hele klasse kapotte bestanden er goed uitzien: het decoderen stopt gewoon vroeg en u krijgt een kort beeld zonder exception. Het is ook precies de bug die legitieme faxen afkapt bij de eerste lap ongecomprimeerde data, wat op een gescand formulier meestal een dichte handtekening of een stempel is. HotPDF kiest de strenge lezing, dus een kort beeld betekent dat de encoder daar echt eindigde

Hoe codeert uncompressed mode pixels?

Uncompressed mode vervangt run-length-codering door letterlijke pixelpatronen, waarbij elk codewoord hooguit vijf pixels voert, totdat een exit-code terugschakelt. Eéndimensionale rijen betreden hem met de 12-bits code 000000001111; tweedimensionale rijen gebruiken de 10-bits extensie 0000001111 van hierboven. Encoders schakelen ernaar toe voor rasterachtige gebieden waar runlengtes van een of twee pixels meer zouden kosten dan de pixels zelf

Binnen de modus worden de codewoorden geteld aan hun voorloopnullen. Nul tot vier nullen gevolgd door een 1 betekenen zoveel witte pixels en daarna één zwarte pixel. Vijf nullen gevolgd door een 1 betekent vijf witte pixels en niets anders. Zes tot tien nullen gevolgd door een 1 zijn exit-codes: de nullen voorbij zes sturen tot vier extra witte pixels mee, en de enkele bit na de exit-code, de tag-bit, stelt de kleur van de volgende run in wanneer de gewone runcodering hervat

{ Ongecomprimeerde-modus codewoorden (ITU-T T.4 / T.6)
  W = witte pixel, B = zwarte 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, daarna runs hervatten met kleur T
  00000001T    W, exit
  000000001T   W W, exit
  0000000001T  W W W, exit
  00000000001T W W W W, exit
  meer dan tien nullen: misvormd }

De tag-bit is het subtiele deel. Runcodering heeft staat: een 1D-rij wisselt witte en zwarte runs af, en een 2D-rij volgt de kleur van het huidige veranderende element a0. Na de exit-code moet de decoder hervatten met welke kleur de tag ook noemt, en die hoeft niet te matchen met de laatste letterlijke pixel. De decoder van HotPDF registreert een kleurwissel op de huidige positie wanneer de tag afwijkt van de kleur die hij volgde, zodat de volgende run vanuit de juiste kleur start. Negeert u de tag, dan komt de rest van de rij kleurgeïnverteerd uit de bus, en in 2D-modus erft elke volgende rij de schade via zijn referentielijn

Wat herstelt DamagedRowsBeforeError, en wat niet?

DamagedRowsBeforeError laat HotPDF een corrupte rij vervangen en doorgaan, maar alleen wanneer K 0 of positief is en EndOfLine true is. Die twee voorwaarden zijn niet willekeurig. Herstel heeft een manier nodig om te vinden waar de volgende rij begint zodra de bits ophouden te kloppen, en het enige betrouwbare herkenningspunt in een CCITT-stream is de EOL-code. Group 4-data heeft geen EOL's, en Group 3-data zonder verplichte EOL's garandeert niet dat er een bestaat, dus in beide gevallen eindigt een beschadigde rij de decoding, ongeacht de parameterwaarde

Faalt een rij, dan scant de HotPDF-decoder vooruit naar de volgende EOL — een reeks van minstens elf nulbits gevolgd door een één — en substitueert een vervangende rij. De eerste beschadigde rij na een goede wordt vervangen door een kopie van de vorige rij, wat op een gescande tekstpagina meestal nauwelijks opvalt. Een beschadigde rij die direct op een andere beschadigde rij volgt, wordt in plaats daarvan vervangen door een volledig witte rij, want een kopie van een kopie zou één regel tekst uitwassen tot een hoge zwarte balk. De vervanger wordt daarna de referentielijn voor de volgende 2D-rij, en dat houdt een mixed-mode-stream na de reparatie decodeerbaar

Twee grenzen zijn makkelijk te missen. Het budget is cumulatief over het hele beeld, niet per reeks slechte rijen, dus DamagedRowsBeforeError 3 verdraagt in totaal drie beschadigde rijen en de vierde laat de decoding falen. En een mislukte decoding is bij HotPDF alles of niets: THotPDF.ExtractLoadedImage geeft nil terug in plaats van een bitmap met een rafelige onderrand. THPDFLoadedImageInfo.Decodable meldt dat het filter wordt ondersteund, niet dat deze specifieke stream intact is, dus controleer de bitmap altijd op nil

Waarom de scan moet stoppen aan het begin van de EOL

De resynchronisatiescan laat de lezer achter op de eerste nulbit van de EOL, niet erachter. De decoder heeft één framing-lus die EOL's verbruikt, controleert op de dubbele EOL die het blok beëindigt en leest de mixed-mode-tag-bit. Slikt de herstelscan de EOL zelf in, dan ziet die lus een rij zonder EOL, behandelt haar ook als beschadigd en verbrandt een tweede eenheid van het budget terwijl hij een goede rij laat vallen. Erger nog: een overgeslagen tag-bit betekent dat een 2D-rij als 1D wordt gedecodeerd of andersom. Dezelfde regel geldt wanneer een 1D-rundecoder midden in een rij een onverwachte EOL raakt: hij spoelt terug naar waar de runcode begon, dus de marker is er nog voor de herstelscan

CCITT-beelden in een geladen PDF inventariseren

Voordat u een batch gearchiveerde faxen vertrouwt, lijst dan op welke beelden CCITTFaxDecode gebruiken en welke niet decoderen. THotPDF.GetLoadedImageInfo geeft geometrie en de filternaam zonder iets te decoderen, en THotPDF.ExtractLoadedImage vertelt of de stream het overleefd heeft. Voor het bredere plaatje van hoe de beelddispatcher elk filter routeert, zie de gids over geladen PDF-beelden en hun decode-filters extraheren

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 wanneer de stream te zwaar beschadigd is
      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;                         // de aanroeper is eigenaar van de bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Bewaar bij de foutgevallen het bewijs. THotPDF.ExtractLoadedImageToStream schrijft met de default-modus lismEncoded de opgeslagen streambytes onaangetast weg — precies wat u wilt overhandigen aan een tweede decoder, in een TIFF-container verpakken voor inspectie of aan een bugrapport toevoegen. Een beschadigde fax opnieuw encoderen verbergt alleen maar waar de schade zat

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;

Loopt u zelf door beeldstreams via THotPDF.CollectLoadedImageStreams, geef dan elk THPDFStreamObject door aan THotPDF.DecodeLoadedImageStream en u krijgt dezelfde CCITT-route, dezelfde parameters en dezelfde herstelregels. Geef de teruggegeven lijst vrij, maar niet de streamobjecten erin; die blijven eigendom van het document

Moet u DamagedRowsBeforeError verhogen bij bestanden die u niet beheert?

Nee, niet door de PDF te bewerken, en meestal helemaal niet. De parameter hoort bij de producent, de enige partij die weet of het transmissiekanaal bits laat vallen. Een bestand dat DamagedRowsBeforeError 0 declareert en dan een beschadigde rij bevat, vertelt u dat de archiefkopie corrupt is, en het eerlijke resultaat is een decodeerfout die u kunt loggen. Stilletjes rijen plakken op een contract of een medisch dossier maakt van een detecteerbaar defect een ogenschijnlijk plausibel beeld met een ontbrekende regel, en dat is een slechtere uitkomst voor iedereen die er later op vertrouwt

Het veiligere patroon voor onvertrouwde archieven is de decoding streng houden, bijhouden welke objectnummers faalden en die naar een mens of een aparte decoder sturen. Is het archief groot en komt het uit veel bronnen, dan beperkt beelddecodering buiten het proces de schade die een vijandige stream de hosttoepassing kan toebrengen; het artikel over image codecs isoleren bij het verwerken van onvertrouwde PDF's behandelt die opzet, en de JBIG2-bilevelcompressiegids vergelijkt het andere bilevel-filter dat u in dezelfde gescande archieven tegenkomt

FAQ

Ondersteunt HotPDF CCITT-uncompressed mode in Group 4-beelden?

Ja. HotPDF herkent de 10-bits extensie 0000001111 in tweedimensionale rijen en de 12-bits ingangscode 000000001111 in ééndimensionale rijen, decodeert de letterlijke codewoorden en respecteert de exit-tag-bit wanneer de runcodering hervat

Waarom heeft DamagedRowsBeforeError geen effect op mijn Group 4-fax?

Herstel heeft EOL-markers nodig om te resynchroniseren. HotPDF past DamagedRowsBeforeError alleen toe wanneer K 0 of positief is en EndOfLine true is; Group 4-data (K negatief) heeft geen EOL's, dus elke beschadigde rij beëindigt de decoding

Wat geeft ExtractLoadedImage terug bij een beschadigd CCITT-beeld?

THotPDF.ExtractLoadedImage geeft nil terug wanneer de schade boven gaat wat DamagedRowsBeforeError toestaat of helemaal niet te herstellen is. Hij geeft nooit een gedeeltelijke bitmap terug, dus test op nil en gebruik ExtractLoadedImageToStream om de ruwe bytes te bewaren

HotPDF verwerkt CCITT-, JBIG2-, JPEG-, JPEG 2000- en Flate-beelden in geladen PDF's vanuit gewone Delphi- en C++Builder-code, zonder externe DLL voor het faxpad. Details en edities staan op de HotPDF Delphi PDF-componentpagina