Teknisk artikel

CCITTFaxDecode i Delphi: uncompressed mode og skadede rækker

HotPDF, den native Delphi PDF-komponent, afkoder CCITTFaxDecode-billeder med fuld understøttelse af T.4/T.6 uncompressed mode-udvidelsen og af gendannelse af skadede rækker styret af DamagedRowsBeforeError. Uncompressed mode indtrædes kun via udvidelseskoden sluttende på 111, og skadede rækker repareres kun for Group 3-data med EndOfLine sat. Alt andet fejler rent i stedet for at frembringe en skæv side

Fax-kodede billeder er den lange hale af PDF-billedafkodning. De fleste CCITT-strømme, man møder, kommer fra en scannerdriver eller en dokument-capture-pipeline, der kun udsender almindelig Group 4, og en afkoder, der håndterer det almindelige tilfælde, består alle de tests, man kaster efter den. Så ankommer et arkiv fra en femten år gammel faxserver, en side renderes som diagonalt støj fra række 400 og ned, og man opdager, at de to dele af ITU-T-anbefalingerne, som ingen implementerer, er præcis de to dele, den fil bruger. Denne artikel gennemgår de to dele, som HotPDF håndterer dem, og de steder, hvor en afkoder kan tage fejl og stadig se korrekt ud

Hvilke CCITTFaxDecode-parametre ændrer rent faktisk bitstrømmen?

Fire poster i DecodeParms-dictionaryen ændrer, hvordan afkoderen indrammer bitstrømmen: K, EndOfLine, EncodedByteAlign og DamagedRowsBeforeError. ISO 32000-1 §7.4.6 (Table 11) definerer dem, og at få én af dem forkert forskubber hver række efter det første mismatch. BlackIs1, Columns, Rows og EndOfBlock betyder også noget, men de påvirker polaritet, geometri og hvor afkodningen stopper snarere end hvordan en rækkegrænse findes

  • K vælger kodningsskemaet: negativ betyder ren todimensional Group 4 (T.6), nul betyder endimensional Modified Huffman (Group 3 1D), og positiv betyder blandet Group 3 2D, hvor en tag-bit efter hver EOL siger, om næste række er 1D eller 2D
  • EndOfLine siger, at hver Group 3-række indledes med en EOL-kode, elleve nul-bits efterfulgt af en ener
  • EncodedByteAlign siger, at hver kodet række starter på en bytegrænse, så afkoderen skal kassere padding-bits, før den læser den
  • DamagedRowsBeforeError er, hvor mange korrupte rækker afkoderen må tolerere, før den giver op, med standard 0

HotPDF læser alle disse fra billed-dictionaryen, når man kalder THotPDF.ExtractLoadedImage eller THotPDF.DecodeLoadedImageStream. Én bevidst afvigelse er værd at kende: mangler Columns, falder HotPDF tilbage på billedets Width frem for spec-standardens 1728, og mangler Rows, bruger den Height. En producer, der udelader Columns på et 2480-pixel A4-scan, tager fejl, men det er almindeligt nok til, at det at stole på billedets geometri giver en brugbar side, hvor den bogstavelige standard ville give skrald

Hvorfor kan 000000 ikke behandles som slutningen på blokken?

I todimensionale rækker er seks indledende nul-bits tvetydige, og en afkoder, der læser dem som begyndelsen på EOFB, vil misfortolke en gyldig uncompressed mode-indgang som slutningen på billedet. Group 4 har ingen EOL'er mellem rækker; de eneste noltunge koder er EOFB, som er to EOL'er ryg mod ryg, og udvidelseskoderne fra ITU-T T.4 og T.6. Begge begynder med 000000, og den syvende bit er, hvor de adskiller sig

HotPDF CCITT-afkoderen opløser præfikset bit for bit. En syvende bit på 1 betyder, at de næste tre bits er en udvidelseskode, hele mønsteret værende 0000001xxx. Kun xxx = 111 fører ind i uncompressed mode; enhver anden udvidelsesværdi behandles som misdannet, da et PDF-faxfilter ikke har brug for de andre. En syvende bit på 0 kan kun fortsætte en EOL, så afkoderen kræver derefter fire flere nul-bits og en afsluttende 1, i alt elleve nuller og en ener, før den rapporterer EOFB. Alt kortere eller længere er en fejl, ikke en efterladende slut-på-billede

Den efterladende version er fristende, for den får en hel klasse af ødelagte filer til at se fine ud: afkodningen stopper blot tidligt, og man får et kort billede uden exception. Den er også præcis den bug, der afkorter legitime faxer ved den første strækning af uncompressed data, hvilket på et scannet formular typisk er en tæt underskrift eller et stempel. HotPDF vælger den strikte læsning, så et kort billede betyder, at encoderen virkelig sluttede dér

Hvordan koder uncompressed mode pixels?

Uncompressed mode erstatter run-length-kodning med literale pixelmønstre, hvor hvert kodeword bærer højst fem pixels, indtil en exit-kode skifter tilbage. Endimensionale rækker træder ind i den med 12-bit-koden 000000001111; todimensionale rækker bruger 10-bit-udvidelsen 0000001111 beskrevet ovenfor. Encodere skifter til den for halvtone-agtige regioner, hvor run-længder på én eller to pixels ville koste mere end pixelsne selv

Inde i moden tælles kodewordene efter deres indledende nuller. Nul til fire nuller efterfulgt af en 1 betyder så mange hvide pixels og derefter én sort pixel. Fem nuller efterfulgt af en 1 betyder fem hvide pixels og intet andet. Seks til ti nuller efterfulgt af en 1 er exit-koder: nullerne ud over seks udsender op til fire flere hvide pixels, og den enkelte bit efter exit-koden, tag-bitten, sætter farven på det næste run, når almindelig run-kodning genoptages

{ Uncompressed mode-kodewords (ITU-T T.4 / T.6)
  W = white pixel, B = black 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, then resume runs with color T
  00000001T    W, exit
  000000001T   W W, exit
  0000000001T  W W W, exit
  00000000001T W W W W, exit
  more than ten zeros: malformed }

Tag-bitten er den subtile del. Run-kodning har tilstand: en 1D-række skifter mellem hvide og sorte runs, og en 2D-række sporer farven på det aktuelle skiftende element a0. Efter exit-koden skal afkoderen genoptage med den farve, tagen nævner, hvilket måske matcher den sidste literale pixel, måske ikke. HotPDF-afkoderen registrerer en farveændring på den aktuelle position, når tagen afviger fra den farve, den sporede, så næste run starter med den rigtige farve. Ignorerer man tagen, kommer resten af rækken ud farve-inverteret, og i 2D-mode arver hver efterfølgende række skaden gennem sin referencelinje

Hvad gendanner DamagedRowsBeforeError, og hvad gør den ikke?

DamagedRowsBeforeError lader HotPDF erstatte en korrupt række og fortsætte, men kun når K er 0 eller positiv, og EndOfLine er true. De to betingelser er ikke vilkårlige. Gendannelse har brug for en måde at finde ud af, hvor næste række starter, efter bitsene holder op med at give mening, og det eneste pålidelige vartegn i en CCITT-strøm er EOL-koden. Group 4-data har ingen EOL'er, og Group 3-data uden obligatoriske EOL'er giver ingen garanti for, at der findes én, så i begge tilfælde afslutter en skadet række afkodningen uanset parameterværdien

Når en række fejler, scanner HotPDF-afkoderen frem til næste EOL, et run på mindst elleve nul-bits efterfulgt af en ener, og indsætter en erstatningsrække. Den første skadede række efter en god erstattes af en kopi af den forrige række, hvilket på en scannet tekstside normalt er tæt nok på til at være usynligt. En skadet række, der direkte følger efter en anden skadet række, erstattes i stedet med en helhvid række, for at kopiere en kopi ville smøre én tekstlinje ud i en høj sort bjælke. Erstatningen bliver så referencelinjen for næste 2D-række, hvilket er det, der holder en mixed mode-strøm afkodbar efter reparationen

To grænser er lette at overse. Budgettet er kumulativt over hele billedet, ikke pr. række af dårlige rækker, så DamagedRowsBeforeError 3 tolererer tre skadede rækker i alt, og den fjerde fikser afkodningen. Og en fejlet afkodning i HotPDF er alt eller intet: THotPDF.ExtractLoadedImage returnerer nil frem for en bitmap med en laset bundkant. THPDFLoadedImageInfo.Decodable rapporterer, at filteret er understøttet, ikke at netop denne strøm er intakt, så tjek altid bitmapen for nil

Hvorfor scanningen skal stoppe ved EOL'ens begyndelse

Resynkroniseringsscanningen efterlader læseren ved den første nul-bit i EOL'en, ikke efter den. Afkoderen har én framing-løkke, der indtager EOL'er, tjekker den dobbelte EOL, der afslutter blokken, og læser mixed mode-tag-bitten. Sluger gendannelsesscanningen EOL'en selv, ser den løkke en række uden EOL, behandler den også som skadet og brænder en ekstra budgetenhed af, mens den smider en god række væk. Værre er det, at en sprunget tag-bit betyder, at en 2D-række afkodes som 1D eller omvendt. Samme regel gælder, når en 1D run-afkoder rammer en uventet EOL midt i en række: den spoler tilbage til, hvor run-koden startede, så markøren stadig er der til gendannelsesscanningen

Inventar af CCITT-billeder i et indlæst PDF

Før man stoler på en bunke arkiverede faxer, skal man liste, hvilke billeder bruger CCITTFaxDecode, og hvilke der fejler i afkodning. THotPDF.GetLoadedImageInfo giver geometri og filternavnet uden at afkode noget, og THotPDF.ExtractLoadedImage fortæller, om strømmen overlevede. For det bredere billede af, hvordan image dispatcheren ruter hvert filter, se guiden til udtræk af indlæste PDF-billeder og deres decode-filtre

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, når strømmen er for skadet
      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;                         // kalderen ejer bitmapen
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Til fejlerne: gem beviset. THotPDF.ExtractLoadedImageToStream med standard lismEncoded-mode skriver de lagrede strøm-bytes urørt, hvilket er det, man vil give til en anden afkoder, pakke ind i en TIFF-container til inspektion eller vedhæfte til en bugrapport. At re-encode en skadet fax skjuler blot, hvor skaden var

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;

Gennemgår man billedstrømme selv gennem THotPDF.CollectLoadedImageStreams, så giv hver THPDFStreamObject til THotPDF.DecodeLoadedImageStream, og man får samme CCITT-vej, parametre og gendannelsesregler. Frigiv den returnerede liste, men ikke strøm-objekterne i den, som forbliver ejet af dokumentet

Bør man hæve DamagedRowsBeforeError på filer, man ikke kontrollerer?

Nej, ikke ved at redigere PDF'en, og som regel slet ikke. Parameteren tilhører producenten, som er den eneste part, der ved, om dens transmissionskanal taber bits. En fil, der erklærer DamagedRowsBeforeError 0 og derefter indeholder en skadet række, fortæller en, at arkivkopien er korrupt, og det ærlige resultat er en afkodningsfejl, man kan logge. At lappe rækker i stilhed på en kontrakt eller en journal gør en detekterbar defekt til et plausibelt udseende billede med en manglende linje, hvilket er et værre udfald for enhver, der senere stoler på det

Det sikrere mønster til utroværdige arkiver er at holde afkodningen strikt, notere, hvilke objektnumre der fejlede, og sende dem videre til et menneske eller en separat afkoder. Er arkivet stort og kommer fra mange kilder, begrænser det at køre billedafkodning out of process, hvad en fjendtlig strøm kan gøre mod værtsprogrammet; artiklen om at isolere image codecs ved behandling af utroværdige PDF'er dækker den opsætning, og JBIG2 bilevel compression-guiden sammenligner det andet bilevel-filter, man finder i de samme scannede arkiver

FAQ

Understøtter HotPDF CCITT uncompressed mode i Group 4-billeder?

Ja. HotPDF genkender 10-bit-udvidelsen 0000001111 i todimensionale rækker og 12-bit-indgangskoden 000000001111 i endimensionale rækker, afkoder de literale kodewords og respekterer exit-tag-bitten, når run-kodning genoptages

Hvorfor har DamagedRowsBeforeError ingen effekt på min Group 4-fax?

Gendannelse kræver EOL-markører for at resynkronisere. HotPDF anvender DamagedRowsBeforeError kun, når K er 0 eller positiv, og EndOfLine er true; Group 4-data (K negativ) har ingen EOL'er, så enhver skadet række afslutter afkodningen

Hvad returnerer ExtractLoadedImage for et skadet CCITT-billede?

THotPDF.ExtractLoadedImage returnerer nil, når skaden overstiger, hvad DamagedRowsBeforeError tillader, eller slet ikke kan gendannes. Den returnerer aldrig en delvis bitmap, så test for nil og brug ExtractLoadedImageToStream til at gemme de rå bytes

HotPDF håndterer CCITT-, JBIG2-, JPEG-, JPEG 2000- og Flate-billeder i indlæste PDF'er fra almindelig Delphi- og C++Builder-kode, uden ekstern DLL til fax-stien. Detaljer og udgaver finder man på HotPDF Delphi PDF component-siden