Technický článek

CCITTFaxDecode v Delphi: nekomprimovaný mód, poškozené řádky

HotPDF, nativní PDF komponenta pro Delphi, dekóduje obrázky CCITTFaxDecode s plnou podporou rozšíření nekomprimovaného módu T.4/T.6 a s obnovou poškozených řádků řízenou přes DamagedRowsBeforeError. Do nekomprimovaného módu se vstupuje jen rozšiřujícím kódem končícím na 111 a poškozené řádky se opravují jen u dat Group 3 s nastaveným EndOfLine. Všechno ostatní selže čistě místo vyrobit zkosenou stránku

Faxem kódované obrázky jsou dlouhý ocas dekódování PDF obrázků. Většina CCITT toků, které potkáte, vyšla ze skenerového ovladače nebo z dokumentové capture pipeline, která emituje jen obyčejné Group 4, a dekodér zvládající běžný případ projde každým testem, který na něj hodíte. Pak přijde archiv z patnáct let starého faxového serveru, stránka se vykreslí jako šikmý šum od řádku 400 dolů a zjistíte, že právě ta dvě místa doporučení ITU-T, která nikdo neimplementuje, ten soubor používá. Tenhle článek oběma místy projde tak, jak je HotPDF řeší, a podívá se i na místa, kde se dekodér může splést, aniž by přestal vypadat správně

Které parametry CCITTFaxDecode doopravdy mění bitový tok?

Čtyři položky ve slovníku DecodeParms mění to, jak dekodér rámuje bitový tok: K, EndOfLine, EncodedByteAlign a DamagedRowsBeforeError. Definuje je ISO 32000-1 §7.4.6 (tabulka 11) a chyba v kteréhokoli posune každý řádek za první neshodou. BlackIs1, Columns, Rows a EndOfBlock mají taky význam, ale dotýkají se polarity, geometrie a místa, kde dekódování skončí, spíš než způsobu, jakým se najde hranice řádku

  • K vybírá kódovací schéma: negativní znamená čistě dvourozměrné Group 4 (T.6), nula jednorozměrné Modified Huffman (Group 3 1D) a kladné smíšené Group 3 2D, kde tag bit za každým EOL říká, zda další řádek je 1D nebo 2D
  • EndOfLine říká, že každému řádku Group 3 předchází kód EOL, jedenáct nulových bitů následovaných jedničkou
  • EncodedByteAlign říká, že každý zakódovaný řádek začíná na hranici bajtu, takže dekodér musí výplňové bity vyhodit, než ho přečte
  • DamagedRowsBeforeError říká, kolik poškozených řádků dekodér snese, než to vzdá, s výchozí hodnotou 0

HotPDF čte tohle všechno ze slovníku obrázku, když zavoláte THotPDF.ExtractLoadedImage nebo THotPDF.DecodeLoadedImageStream. Jedna záměrná odchylka stojí za znalost: když Columns chybí, HotPDF sáhne po Width obrázku místo výchozích 1728 z normy a když chybí Rows, použije Height. Producent, který vynechá Columns u skenu A4 o 2480 pixelech, se plete, ale děje se to dost často na to, aby důvěra v geometrii obrázku dala použitelnou stránku tam, kde doslovná výchozí hodnota dá nesmysl

Proč nelze 000000 brát jako konec bloku?

Ve dvourozměrných řádcích je šest úvodních nulových bitů dvojznačných a dekodér, který je čte jako začátek EOFB, si platný vstup do nekomprimovaného módu přečte jako konec obrázku. Group 4 nemá mezi řádky žádné EOLy; jediné kódy plné nul jsou EOFB, což jsou dvě EOLy za sebou, a rozšiřující kódy z ITU-T T.4 a T.6. Oba začínají 000000 a sedmý bit je místo, kde se rozejdou

CCITT dekodér HotPDF rozluští prefix bit po bitu. Sedmý bit 1 znamená, že další tři bity jsou rozšiřující kód, celý vzorek je 0000001xxx. Do nekomprimovaného módu vstupuje jen xxx = 111; jakákoli jiná hodnota rozšíření se bere jako poškozená, protože faxový filtr PDF ostatní nepotřebuje. Sedmý bit 0 může jen pokračovat v EOLu, takže dekodér pak vyžaduje čtyři další nulové bity a závěrečnou jedničku, celkem jedenáct nul a jedna jednička, než ohlásí EOFB. Cokoli kratšího nebo delšího je chyba, ne shovívavý konec obrázku

Shovívavá verze láká, protože celé třídě rozbitých souborů dá vypadat v pořádku: dekódování prostě skončí dřív a dostanete krátký obrázek bez výjimky. Je to ale přesně ta chyba, která usekne legitimní faxy na prvním úseku nekomprimovaných dat, což je u skenovaného formuláře obvykle hustý podpis nebo razítko. HotPDF volí přísné čtení, takže krátký obrázek znamená, že encoder tam skutečně skončil

Jak nekomprimovaný mód kóduje pixely?

Nekomprimovaný mód vystřídá kódování běhů (run-length) doslovnými pixelovými vzorky, každé kódové slovo nese nejvýš pět pixelů, dokud výstupní kód nepřepne zpátky. Jednorozměrné řádky do něj vstupují 12bitovým kódem 000000001111; dvourozměrné řádky používají výše popsané 10bitové rozšíření 0000001111. Encodery na něj přecházejí u polotónově podobných oblastí, kde by běhy jednoho či dvou pixelů stály víc než samotné pixely

Uvnitř módu se kódová slova počítají podle úvodních nul. Nula až čtyři nuly následované jedničkou znamenají tolik bílých pixelů a pak jeden černý. Pět nul následovaných jedničkou znamená pět bílých pixelů a nic víc. Šest až deset nul následovaných jedničkou jsou výstupní kódy: nuly nad šest vypustí až čtyři další bílé pixely a jediný bit za výstupním kódem, tag bit, nastaví barvu dalšího běhu, když se obyčejné kódování běhů obnoví

{ Kódová slova nekomprimovaného módu (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 bit je ten jemný detail. Kódování běhů je stavové: 1D řádek se střídá bílé a černé běhy a 2D řádek sleduje barvu aktuálního měnícího se elementu a0. Po výstupním kódu musí dekodér pokračovat barvou, kterou tag jmenuje, a ta může, ale nemusí, odpovídat poslednímu doslovnému pixelu. Dekodér HotPDF zaznamená změnu barvy na aktuální pozici, když se tag liší od barvy, kterou sledoval, takže další běh začíná se správnou barvou. Ignorujte tag a zbytek řádku vyleze s přehozenými barvami a ve 2D módu každý následující řádek zdědí škodu přes svou referenční linku

Co DamagedRowsBeforeError obnoví a co ne?

DamagedRowsBeforeError dovolí HotPDF vyměnit poškozený řádek a pokračovat, ale jen když je K 0 nebo kladné a EndOfLine je true. Ty dvě podmínky nejsou libovolné. Obnova potřebuje způsob, jak najít, kde další řádek začíná, jakmile bity přestávají dávat smysl, a jediným spolehlivým orientačním bodem v CCITT toku je kód EOL. Data Group 4 nemají žádné EOLy a data Group 3 bez povinných EOLů negarantují, že nějaký existuje, takže v obou případech poškozený řádek ukončí dekódování, jakoukoli hodnotu parametr má

Když řádek selže, dekodér HotPDF skenuje dopředu k dalšímu EOLu, běhu nejméně jedenácti nulových bitů následovaných jedničkou, a dosadí náhradní řádek. První poškozený řádek po dobrém se nahradí kopií předchozího řádku, což je na skenované textové stránce obvykle tak blízko, že je to neviditelné. Poškozený řádek hned za jiným poškozeným řádkem se místo toho nahradí celobílým řádkem, protože kopie kopie by rozmázla jednu řádku textu do vysokého černého pruhu. Náhradník se pak stane referenční linkou pro další 2D řádek, což udrží smíšený tok po opravě dekódovatelný

Dvě hranice se dají snadno přehlédnout. Rozpočet je kumulativní přes celý obrázek, ne na běh špatných řádků, takže DamagedRowsBeforeError 3 snese tři poškozené řádky celkem a čtvrtý dekódování zruší. A neúspěšné dekódování je v HotPDF všechno nebo nic: THotPDF.ExtractLoadedImage vrátí nil místo bitmapy s roztřepeným spodním okrajem. THPDFLoadedImageInfo.Decodable říká, že filtr je podporovaný, ne že tento konkrétní tok je neporušený, takže bitmapu vždy kontrolujte na nil

Proč sken musí skončit na začátku EOLu

Resynchronizační sken nechá čtečku na prvním nulovém bitu EOLu, ne za ním. Dekodér má jednu rámovací smyčku, která konzumuje EOLy, kontroluje dvojitý EOL ukončující blok a čte tag bit smíšeného módu. Pokud obnovovací sken EOL spolkne, ta smyčka uvidí řádek bez EOLu, označí ho taky za poškozený a spálí druhou jednotku rozpočtu, zatímco zahodí dobrý řádek. Hůř, přeskočený tag bit znamená, že se 2D řádek dekóduje jako 1D nebo naopak. Táž pravidla platí, když 1D dekodér běhů narazí na nečekaný EOL uprostřed řádku: přetočí se tam, kde kód běhu začínal, takže značka pro obnovovací sken zůstane na místě

Inventura CCITT obrázků v načteném PDF

Než důvěřujete dávce archivovaných faxů, vyliste, které obrázky používají CCITTFaxDecode a u kterých se dekódování nepovedlo. THotPDF.GetLoadedImageInfo vám dá geometrii a název filtru bez dekódování čehokoli a THotPDF.ExtractLoadedImage prozradí, jestli tok přežil. O širším obrazu toho, jak dispečer obrázků směruje každý filtr, píše průvodce extrakcí načtených PDF obrázků a jejich dekódovacích filtrů

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, když je tok moc poškozený
      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;                         // bitmapu vlastní volající
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

U neúspěchů si schovejte důkazy. THotPDF.ExtractLoadedImageToStream ve výchozím módu lismEncoded zapíše uložené bajty toku netknuté, což je to, co chcete podat druhému dekodéru, zabalit do kontejneru TIFF k prohlídce nebo přiložit k bug reportu. Překódování poškozeného faxu jen zakryje, kde škoda byla

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;

Pokud si obrázkovými toky projdete sami přes THotPDF.CollectLoadedImageStreams, předejte každý THPDFStreamObject do THotPDF.DecodeLoadedImageStream a dostanete tutéž CCITT cestu, parametry a pravidla obnovy. Uvolněte vrácený seznam, ale ne streamové objekty v něm — ty zůstávají ve vlastnictví dokumentu

Máte u souborů, které nekontrolujete, zvyšovat DamagedRowsBeforeError?

Ne, ne úpravou PDF a obvykle vůbec ne. Parametr patří producentovi, kterému je jedinému známo, jestli jeho přenosový kanál ztrácí bity. Soubor, který deklaruje DamagedRowsBeforeError 0 a pak obsahuje poškozený řádek, vám říká, že archivní kopie je poškozená, a čestným výsledkem je selhání dekódování, které můžete zalogovat. Potichu záplatované řádky ve smlouvě nebo zdravotním záznamu promění detekovatelnou vadu v věrohodně vypadající obrázek s chybějící řádkou, což je horší výsledek pro každého, kdo se na něj později spolehne

Bezpečnější vzorec pro nedůvěryhodné archivy je nechat dekódování přísné, zapsat si, která čísla objektů selhala, a ty poslat člověku nebo samostatnému dekodéru. Je-li archiv velký a přichází z mnoha zdrojů, omezuje běh dekódování obrázků v samostatném procesu, co nepřátelský tok dokáže hostitelské aplikaci; článek o izolaci obrazových kodeků při zpracování nedůvěryhodných PDF rozebírá tohle uspořádání a průvodce kompresí JBIG2 bilevel srovnává druhý bilevel filtr, který ve stejných skenovaných archivech potkáte

FAQ

Podporuje HotPDF nekomprimovaný mód CCITT u obrázků Group 4?

Ano. HotPDF pozná 10bitové rozšíření 0000001111 ve dvourozměrných řádcích a 12bitový vstupní kód 000000001111 v jednorozměrných řádcích, dekóduje doslovná kódová slova a respektuje výstupní tag bit, když se kódování běhů obnoví

Proč nemá DamagedRowsBeforeError u mého faxu Group 4 žádný účinek?

Obnova potřebuje k resynchronizaci značky EOL. HotPDF aplikuje DamagedRowsBeforeError jen tehdy, když je K 0 nebo kladné a EndOfLine je true; data Group 4 (záporné K) nemají žádné EOLy, takže jakýkoli poškozený řádek dekódování ukončí

Co vrátí ExtractLoadedImage pro poškozený CCITT obrázek?

THotPDF.ExtractLoadedImage vrátí nil, když škoda přesahuje to, co dovoluje DamagedRowsBeforeError, nebo se nedá obnovit vůbec. Nikdy nevrací částečnou bitmapu, takže testujte na nil a pro uchování syrových bajtů použijte ExtractLoadedImageToStream

HotPDF zpracovává obrázky CCITT, JBIG2, JPEG, JPEG 2000 a Flate v načtených PDF z obyčejného kódu Delphi a C++Builder, bez externí DLL pro faxovou cestu. Detaily a edice najdete na stránce komponenty HotPDF Delphi PDF