Articolo tecnico

CCITTFaxDecode in Delphi: uncompressed e righe danneggiate

HotPDF, il componente PDF nativo per Delphi, decodifica le immagini CCITTFaxDecode con supporto completo per l'estensione uncompressed-mode T.4/T.6 e per il recupero delle righe danneggiate guidato da DamagedRowsBeforeError. La modalità non compressa si entra solo con il codice di estensione che termina in 111, e le righe danneggiate vengono riparate solo per dati Group 3 con EndOfLine attivo. Tutto il resto fallisce in modo pulito invece di produrre una pagina inclinata

Le immagini codificate fax sono la coda lunga della decodifica delle immagini PDF. La maggior parte degli stream CCITT che incontrate esce da un driver di scanner o da una pipeline di acquisizione documenti che emette solo Group 4 semplice, e un decoder che gestisce il caso comune supera qualunque test gli lanciate contro. Poi arriva un archivio da un fax server di quindici anni fa, una pagina si renderizza come rumore diagonale dalla riga 400 in giù, e scoprite che le due parti delle raccomandazioni ITU-T che nessuno implementa sono esattamente le due parti che quel file usa. Questo articolo percorre quelle due parti così come le gestisce HotPDF, e i punti in cui un decoder può andare storto pur sembrando corretto

Quali parametri CCITTFaxDecode cambiano davvero il bit stream?

Quattro voci del dizionario DecodeParms cambiano il modo in cui il decoder incornicia il bit stream: K, EndOfLine, EncodedByteAlign e DamagedRowsBeforeError. ISO 32000-1 §7.4.6 (Tabella 11) le definisce, e sbagliarne una sola sposta ogni riga dopo la prima discrepanza. BlackIs1, Columns, Rows e EndOfBlock contano anch'esse, ma agiscono su polarità, geometria e punto di arresto della decodifica, non su come si trova un confine di riga

  • K sceglie lo schema di codifica: negativo significa Group 4 puramente bidimensionale (T.6), zero significa Modified Huffman unidimensionale (Group 3 1D), e positivo significa Group 3 2D misto, dove un bit tag dopo ogni EOL dice se la riga successiva è 1D o 2D
  • EndOfLine dice che ogni riga Group 3 è preceduta da un codice EOL, undici bit a zero seguiti da un uno
  • EncodedByteAlign dice che ogni riga codificata inizia su un confine di byte, quindi il decoder deve scartare i bit di riempimento prima di leggerla
  • DamagedRowsBeforeError è quante righe corrotte il decoder può tollerare prima di arrendersi, con un predefinito di 0

HotPDF legge tutti questi valori dal dizionario immagine quando chiamate THotPDF.ExtractLoadedImage o THotPDF.DecodeLoadedImageStream. Una deviazione deliberata merita di essere conosciuta: quando Columns manca, HotPDF ripiega sulla Width dell'immagine invece che sul predefinito di specifica di 1728, e quando Rows manca usa Height. Un produttore che omette Columns su una scansione A4 da 2480 pixel sbaglia, ma è abbastanza comune che fidarsi della geometria dell'immagine dia una pagina usabile dove il predefinito letterale darebbe spazzatura

Perché 000000 non può essere trattato come fine del blocco?

Nelle righe bidimensionali sei bit a zero iniziali sono ambigui, e un decoder che li legge come inizio di EOFB scambierà un valido ingresso in modalità non compressa per la fine dell'immagine. Group 4 non ha EOL fra le righe; gli unici codici ricchi di zeri sono l'EOFB, che sono due EOL di fila, e i codici di estensione di ITU-T T.4 e T.6. Entrambi iniziano con 000000, e il settimo bit è dove divergono

Il decoder CCITT di HotPDF risolve il prefisso bit per bit. Un settimo bit pari a 1 significa che i tre bit successivi sono un codice di estensione, il pattern completo essendo 0000001xxx. Solo xxx = 111 entra in modalità non compressa; qualunque altro valore di estensione è trattato come malformato, dato che un filtro fax PDF non ha uso per gli altri. Un settimo bit pari a 0 può solo continuare un EOL, quindi il decoder richiede allora altri quattro bit a zero e un 1 di chiusura, undici zeri e un uno in totale, prima di riportare EOFB. Qualcosa di più corto o più lungo è un errore, non una fine immagine indulgente

La versione indulgente è tentante perché fa sembrare a posto un'intera classe di file rotti: la decodifica si ferma semplicemente presto e vi ritrovate un'immagine corta senza eccezioni. È anche esattamente il bug che tronca fax legittimi al primo tratto di dati non compressi, che su un modulo scansionato di solito è una firma fitta o un timbro. HotPDF sceglie la lettura severa, quindi un'immagine corta significa che l'encoder ha davvero finito lì

Come codifica i pixel la modalità non compressa?

La modalità non compressa sostituisce la codifica a run-length con pattern di pixel letterali, ogni parola codice portando al massimo cinque pixel, finché un codice di uscita non torna indietro. Le righe unidimensionali vi entrano con il codice a 12 bit 000000001111; le righe bidimensionali usano l'estensione a 10 bit 0000001111 descritta sopra. Gli encoder vi passano per le regioni tipo halftone dove run di uno o due pixel costerebbero più dei pixel stessi

Dentro la modalità, le parole codice si contano dagli zeri iniziali. Da zero a quattro zeri seguiti da un 1 significano altrettanti pixel bianchi e poi un pixel nero. Cinque zeri seguiti da un 1 significano cinque pixel bianchi e nient'altro. Da sei a dieci zeri seguiti da un 1 sono codici di uscita: gli zeri oltre sei emettono fino ad altri quattro pixel bianchi, e il bit singolo dopo il codice di uscita, il bit tag, imposta il colore del prossimo run quando riprende la codifica a run ordinaria

{ Parole codice della modalità non compressa (ITU-T T.4 / T.6)
  W = pixel bianco, B = pixel nero, T = bit tag

  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     uscita, poi i run riprendono col colore T
  00000001T    W, uscita
  000000001T   W W, uscita
  0000000001T  W W W, uscita
  00000000001T W W W W, uscita
  più di dieci zeri: malformato }

Il bit tag è la parte sottile. La codifica a run mantiene stato: una riga 1D alterna run bianchi e neri, e una riga 2D traccia il colore dell'elemento di cambiamento corrente a0. Dopo il codice di uscita, il decoder deve riprendere con il colore che il tag nomina, che può corrispondere o no all'ultimo pixel letterale. Il decoder di HotPDF registra un cambio di colore alla posizione corrente quando il tag differisce dal colore che stava tracciando, così il prossimo run parte dal colore giusto. Ignorate il tag e il resto della riga esce con i colori invertiti, e in modalità 2D ogni riga successiva eredita il danno attraverso la sua riga di riferimento

Cosa recupera DamagedRowsBeforeError, e cosa no?

DamagedRowsBeforeError permette a HotPDF di sostituire una riga corrotta e continuare, ma solo quando K è 0 o positivo e EndOfLine è true. Quelle due condizioni non sono arbitrarie. Il recupero ha bisogno di un modo per trovare dove inizia la riga successiva dopo che i bit hanno smesso di avere senso, e l'unico punto di riferimento affidabile in uno stream CCITT è il codice EOL. I dati Group 4 non hanno EOL, e i dati Group 3 senza EOL obbligatori non danno garanzia che esista, quindi in entrambi i casi una riga danneggiata termina la decodifica a prescindere dal valore del parametro

Quando una riga fallisce, il decoder di HotPDF scansiona in avanti fino al prossimo EOL, una sequenza di almeno undici bit a zero seguiti da un uno, e sostituisce una riga di ricambio. La prima riga danneggiata dopo una buona viene sostituita con una copia della riga precedente, che su una pagina di testo scansionato di solito è abbastanza vicina da risultare invisibile. Una riga danneggiata che segue direttamente un'altra riga danneggiata viene invece sostituita con una riga tutta bianca, perché copiare una copia spalmerebbe una riga di testo in una barra nera alta. Il sostituto diventa poi la riga di riferimento per la prossima riga 2D, che è ciò che mantiene decodificabile uno stream in modalità mista dopo la riparazione

Due confini sono facili da perdere. Il budget è cumulativo su tutta l'immagine, non per sequenza di righe cattive, quindi DamagedRowsBeforeError 3 tollera tre righe danneggiate in totale e la quarta fa fallire la decodifica. E una decodifica fallita in HotPDF è tutto o niente: THotPDF.ExtractLoadedImage restituisce nil invece di una bitmap con il bordo inferiore a strappi. THPDFLoadedImageInfo.Decodable riporta che il filtro è supportato, non che questo stream particolare è intatto, quindi controllate sempre la bitmap per nil

Perché la scansione deve fermarsi all'inizio dell'EOL

La scansione di risincronizzazione lascia il reader al primo bit a zero dell'EOL, non dopo. Il decoder ha un unico loop di incorniciatura che consuma gli EOL, controlla il doppio EOL che chiude il blocco e legge il bit tag della modalità mista. Se la scansione di recupero ingoia l'EOL stesso, quel loop vede una riga senza EOL, la tratta come danneggiata a sua volta e brucia una seconda unità di budget scartando una riga buona. Peggio, un bit tag saltato significa che una riga 2D viene decodificata come 1D o viceversa. La stessa regola vale quando un decoder di run 1D incontra un EOL inatteso a metà riga: riavvolge al punto in cui il codice di run era iniziato, così il marcatore resta lì per la scansione di recupero

Fare l'inventario delle immagini CCITT in un PDF caricato

Prima di fidarvi di un lotto di fax archiviati, elencate quali immagini usano CCITTFaxDecode e quali non riescono a decodificarsi. THotPDF.GetLoadedImageInfo vi dà geometria e nome del filtro senza decodificare nulla, e THotPDF.ExtractLoadedImage vi dice se lo stream è sopravvissuto. Per il quadro più ampio di come il dispatcher delle immagini instrada ogni filtro, vedere la guida a estrazione delle immagini PDF caricate e dei loro filtri di decodifica

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 quando lo stream è troppo danneggiato
      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;                         // la bitmap appartiene a chi chiama
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Per i fallimenti, conservate la prova. THotPDF.ExtractLoadedImageToStream con la modalità predefinita lismEncoded scrive i byte dello stream memorizzato intatti, che è ciò che volete passare a un secondo decoder, avvolgere in un contenitore TIFF per ispezione o allegare a un bug report. Ricodificare un fax danneggiato non fa che nascondere dov'era il danno

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;

Se percorrete voi gli stream immagine tramite THotPDF.CollectLoadedImageStreams, passate ogni THPDFStreamObject a THotPDF.DecodeLoadedImageStream e otterrete lo stesso percorso CCITT, gli stessi parametri e le stesse regole di recupero. Liberate l'elenco restituito ma non gli oggetti stream dentro, che restano di proprietà del documento

Conviene alzare DamagedRowsBeforeError su file che non controllate?

No, non modificando il PDF, e di solito per niente. Il parametro appartiene al produttore, che è l'unica parte che sa se il suo canale di trasmissione perde bit. Un file che dichiara DamagedRowsBeforeError 0 e poi contiene una riga danneggiata vi sta dicendo che la copia d'archivio è corrotta, e il risultato onesto è un fallimento di decodifica che potete registrare. Rattoppare in silenzio le righe di un contratto o di una cartella clinica trasforma un difetto rilevabile in un'immagine plausibile con una riga mancante, che è un esito peggiore per chiunque vi farà affidamento più tardi

Il pattern più sicuro per archivi non fidati è tenere la decodifica severa, registrare quali numeri di oggetto hanno fallito e instradarli a un essere umano o a un decoder separato. Se l'archivio è grande e arriva da molte fonti, far girare la decodifica delle immagini fuori processo limita ciò che uno stream ostile può fare all'applicazione ospite; l'articolo su isolamento dei codec immagine quando si elaborano PDF non fidati copre quel setup, e la guida alla compressione bilevel JBIG2 confronta l'altro filtro bilevel che troverete negli stessi archivi scansionati

FAQ

HotPDF supporta la modalità CCITT non compressa nelle immagini Group 4?

Sì. HotPDF riconosce l'estensione a 10 bit 0000001111 nelle righe bidimensionali e il codice d'ingresso a 12 bit 000000001111 nelle righe unidimensionali, decodifica le parole codice letterali e onora il bit tag di uscita quando riprende la codifica a run

Perché DamagedRowsBeforeError non ha effetto sul mio fax Group 4?

Il recupero richiede marcatori EOL per risincronizzarsi. HotPDF applica DamagedRowsBeforeError solo quando K è 0 o positivo e EndOfLine è true; i dati Group 4 (K negativo) non hanno EOL, quindi qualunque riga danneggiata termina la decodifica

Cosa restituisce ExtractLoadedImage per un'immagine CCITT danneggiata?

THotPDF.ExtractLoadedImage restituisce nil quando il danno supera ciò che DamagedRowsBeforeError consente o non è recuperabile per niente. Non restituisce mai una bitmap parziale, quindi testate per nil e usate ExtractLoadedImageToStream per conservare i byte grezzi

HotPDF gestisce immagini CCITT, JBIG2, JPEG, JPEG 2000 e Flate in PDF caricati da semplice codice Delphi e C++Builder, senza DLL esterne per il percorso fax. Dettagli ed edizioni sono sulla pagina del componente HotPDF Delphi PDF