PDFlibPas ha corretto due guasti indipendenti nel suo decoder nativo delle regioni halftone JBIG2: nella v3.539.37 la maschera skip HSKIP è indicizzata come HSKIP[ng, mg] come la definisce ITU-T T.88 §6.6.5.1, e nella v3.539.38 le griglie che raggiungono coordinate negative, tramite un HGX o HGY negativo o tramite rotazione, vengono posizionate con uno shift floor vero. Prima di quelle release, le regioni halftone colpite uscivano conficcate o spostate, senza che alcun errore venisse sollevato. Entrambi i bug si nascondevano dietro dati di prova che per caso erano simmetrici o non negativi, e il secondo è innescato da una proprietà di Delphi e Free Pascal che morde ben oltre JBIG2: shr su un intero con segno è uno shift logico, non la >> aritmetica che lo standard presume
Le regioni halftone sono il tipo di regione JBIG2 meno comune, così un decoder può processare migliaia di documenti scansionati prima di incontrare una fotografia a retino codificata come tale. Quando ci arriva, il guasto è cattivo: il file si analizza, le lunghezze dei segmenti tornano, la pagina ha la dimensione giusta, e la regione è spazzatura
Che cosa decodifica davvero una regione halftone JBIG2?
Una regione halftone JBIG2 è una griglia di piccole bitmap scelte da un dizionario di pattern, e il lavoro vero del decoder è calcolare un indice per ogni cella della griglia e la posizione in pixel dove quella cella atterra. Il dizionario di pattern contiene HNUMPATS pattern di HPW × HPH pixel. Il segmento di regione halftone descrive poi una griglia di HGW colonne per HGH righe e un'immagine a scala di grigi della stessa dimensione, codificata come bitplane Gray-coded. Ogni bitplane viene decodificato con la procedura di regione generica su una bitmap HGW × HGH, piano più significativo prima, e i piani insieme danno a ogni cella il suo indice di pattern
Il posizionamento delle celle usa aritmetica fixed point con una frazione a 8 bit. L'origine della griglia HGX, HGY è una coppia di valori a 32 bit, e il vettore griglia HRX, HRY descrive il passo tra celle vicine, il che consente una griglia ruotata. Per la riga griglia mg e la colonna griglia ng, T.88 §6.6.5 calcola la posizione in pixel come:
x = (HGX + mg × HRY + ng × HRX) >> 8y = (HGY + mg × HRX − ng × HRY) >> 8
La maschera skip entra attraverso il flag opzionale HENABLESKIP. Quando il flag è impostato, §6.6.5.1 costruisce una bitmap HGW × HGH HSKIP e imposta HSKIP[ng, mg] a 1 per ogni cella il cui pattern giace interamente fuori dalla regione: x + HPW <= 0, x >= HBW, y + HPH <= 0 oppure y >= HBH. I bitplane a scala di grigi vengono poi decodificati con quella maschera come bitmap skip della regione generica, così il decoder aritmetico né legge né aggiorna contesto per una cella saltata. Decoder ed encoder devono concordare su ogni bit di HSKIP, o i due codificatori aritmetici perdono il passo
Perché una maschera HSKIP trasposta rompeva solo le griglie non quadrate?
La maschera skip veniva scritta con le coordinate scambiate, e solo una griglia non quadrata la esponeva, perché una griglia quadrata tiene ogni coordinata scambiata dentro la maschera. PDFlibPas memorizza le bitmap con un accessore pixel (column, row), e il codice che costruiva la maschera passava (mg, ng), riga prima. Il decoder dei bitplane a scala di grigi legge la maschera correttamente come (ng, mg). Il ciclo di posizionamento dei pattern la rileggeva nell'ordine scambiato del costruttore, così i due erano d'accordo, e una revisione della sola logica di posizionamento la sarebbe passata. Una trappola di nomi peggiorava le cose: nel ciclo di posizionamento la variabile chiamata col itera le righe della griglia e Row itera le colonne
Prendi la griglia 5 × 3 di pattern 4 × 4 su una regione 16 × 8 che la v3.539.37 usa come caso di regressione. Con HRX = 1024 e HRY = 0, la colonna griglia 4 atterra a x = 16 e la riga griglia 2 a y = 8, entrambe fuori dalla regione. La maschera corretta marca sette celle: tutta la colonna 4 e tutta la riga 2. Le scritture scambiate provavano a impostare pixel agli indici di riga 3 e 4 in una maschera alta solo tre righe, e il setter della bitmap ignorava silenziosamente quelle scritture fuori range. Ciò che sopravviveva era la colonna 2, righe da 0 a 2. Il decoder saltava quindi due celle che l'encoder aveva codificato, e decodificava sei celle che l'encoder aveva saltato
Il decoder aritmetico non fallisce quando succede. Decodifica pixel extra da bit che appartengono a celle successive, i suoi contesti leggono vicini sbagliati, e ogni indice di pattern dopo il primo disaccordo è rumore, ecco perché il sintomo era una regione conficcata invece di qualche cella fuori posto. Su una griglia quadrata lo stesso bug è spesso invisibile: nessuna coordinata scambiata lascia la maschera, e quando le celle fuori regione sono simmetriche rispetto alla diagonale, per esempio una griglia che sporge dai bordi destro e inferiore dello stesso numero di celle, la maschera trasposta è bit per bit quella corretta. HENABLESKIP è anche opzionale, deve valere 0 quando l'immagine a scala di grigi è codificata MMR, e raramente viene impostato dagli encoder, così il bug aveva pochissimi modi di emergere. Dalla v3.539.37 il costruttore scrive HSKIP[ng, mg] e il ciclo di posizionamento legge lo stesso ordine
Perché gli offset negativi della griglia halftone falliscono su tre livelli?
Una griglia halftone che parte a sinistra o sopra la propria regione rompeva PDFlibPas in tre punti distinti, e ogni guasto nascondeva il successivo. T.88 consente questa geometria deliberatamente. Un encoder che allinea il proprio retino alla pagina invece che alla regione, o usa una griglia ruotata, produce naturalmente angoli di cella negativi che la regione ritaglia. La v3.539.38 ha corretto tutti e tre i livelli insieme, perché correggerne uno solo cambiava solo il sintomo
Livello 1: un campo con segno letto come senza segno
T.88 §7.4.5.1.2 definisce HGX e HGY come valori a 32 bit con segno, ma il decoder li leggeva con lo stesso helper a 32 bit usato per i campi senza segno, e quel helper forzava ogni risultato negativo a 0. Una griglia destinata a partire a HGX = -900 veniva spostata in silenzio sull'origine della regione. Nel caso di regressione della v3.539.38 l'intera immagine usciva due righe più in basso. Il clamp spiega anche perché gli altri due guasti sono sopravvissuti così a lungo: con l'origine forzata a essere non negativa, una coordinata negativa poteva comparire solo attraverso una griglia ruotata con HRY > 0, dove y = HGY + mg × HRX − ng × HRY scende sotto zero per le colonne griglia successive
Livello 2: shr non è >> 8
T.88 scrive >> 8 e intende uno shift aritmetico, che arrotonda verso meno infinito. Il decoder lo traduceva come shr 8. In Delphi e Free Pascal, shr su un intero con segno è uno shift logico: il bit di segno entra come zero. Per una Integer che tiene -512, shr 8 dà 16777214 invece di -2. Un pattern che avrebbe dovuto essere disegnato a y = -2 e ritagliato alla sua metà inferiore veniva mandato 16 milioni di righe più giù e scartato come fuori regione. Niente crash; semplicemente la riga in alto dell'halftone spariva
Livello 3: confrontare fixed point invece che pixel
Il test skip confrontava valori fixed point, non posizioni in pixel, e le due cose non sono equivalenti appena la frazione è diversa da zero. Il codice originale scansava lo shift logico testando xx + HPW × 256 <= 0 sul valore non shiftato, un presunto equivalente del test T.88. Con HGX = -900 e un pattern di 4 pixel, dà -900 + 1024 = 124, che è positivo, quindi la cella non viene saltata. Lo standard shifta prima: floor(-900 / 256) = -4, e -4 + 4 = 0 soddisfa x + HPW <= 0, così la cella giace interamente fuori e va saltata. L'encoder la saltava, il decoder la decodificava, e l'immagine a scala di grigi derivava esattamente come nel caso della maschera trasposta
Il caso di regressione della v3.539.38 usa una griglia 4 × 3 di pattern 4 × 4 a HGX = -900, HGY = -512, HRX = 1024 su una regione 12 × 10. Le colonne griglia atterrano a x = -4, 0, 4 e 8, quindi la colonna 0 è interamente fuori e appartiene a HSKIP; le righe griglia atterrano a y = -2, 2 e 6, quindi la riga 0 va ritagliata alle sue ultime due righe di pixel invece che scartata. Correggere i livelli uno alla volta riproduce la pila:
| Guasti corretti | Regione decodificata |
|---|---|
| Nessuno (prima della v3.539.38) | Griglia tirata all'origine, intera immagine due righe più in basso |
| Solo lettura con segno di HGX / HGY | Prima riga griglia mancante, il resto conficcato dalla deriva del test skip |
| Lettura con segno, shift floor e test skip in spazio pixel | Identica, pixel per pixel, alla pagina calcolata da T.88 §6.6.5 e a due decoder di riferimento indipendenti |
La correzione è un solo helper, HalftoneGridPixel, condiviso dal costruttore della maschera skip e dal ciclo di posizionamento. Accumula la coordinata in Int64 così un grande prodotto mg × HRX non può fare wrap, divide per 256 arrotondando verso meno infinito, e limita a ±MaxInt div 2 così una griglia corrotta non può far traboccare la successiva aritmetica bitmap. Il test skip ora confronta quei valori in pixel con HPW, HPH, HBW e HBH, esattamente come lo enuncia §6.6.5.1
Come si scrive uno shift right aritmetico in Delphi?
Delphi non ha un operatore di shift aritmetico, quindi uno shift right con segno corretto va scritto come divisione floor, e il semplice div non è quella divisione. div tronca verso zero. Per valori non negativi troncamento e floor concordano, e concordano anche per valori negativi multipli esatti del divisore, ecco perché -512 div 256 = -2 sembra a posto in un test veloce. Discordano ovunque altro: -900 div 256 è -3, mentre il floor è -4, e -1 div 256 è 0, mentre il floor è -1. Una coordinata JBIG2 con frazione diversa da zero è esattamente il caso in cui div dà il pixel sbagliato
Sui compilatori Delphi Win32 e Win64, una variabile Integer che tiene -512 shiftata a destra di 8 dà 16777214, e una Int64 che tiene -512 dà 72057594037927934. Free Pascal definisce pure shr come shift logico e spedisce SarLongint e SarInt64 nella propria unità System per la versione aritmetica, ma quelle funzioni non esistono in Delphi, quindi il codice condiviso tra i due compilatori ha bisogno di un helper proprio:
// Divisione floor: arrotonda verso meno infinito per qualunque segno di A e B.
// B non deve essere 0, e FloorDiv(Low(Integer), -1) fa overflow proprio come div
function FloorDiv(A, B: Integer): Integer;
begin
Result := A div B;
if (A mod B <> 0) and ((A < 0) <> (B < 0)) then
Dec(Result);
end;
// Shift right aritmetico (la ">>" di C e T.88 su valori con segno).
// Per Value negativo, not Value = -Value - 1 è non negativo, così lo
// shr logico è sicuro lì, e il not esterno riporta il risultato indietro
function SarInt32(Value: Integer; Shift: Integer): Integer; // Shift 0..31
begin
if Value >= 0 then
Result := Value shr Shift
else
Result := not ((not Value) shr Shift);
end;
function SarInt64(Value: Int64; Shift: Integer): Int64; // Shift 0..63
begin
if Value >= 0 then
Result := Value shr Shift
else
Result := not ((not Value) shr Shift);
end;
Il trucco not non shifta mai un numero negativo, quindi non dipende da come un compilatore tratta il bit di segno, e non fa mai overflow, incluso per Low(Integer). Entrambi gli helper hanno riscontrato un riferimento floor Int64 su diversi milioni di valori, ogni shift da 0 a 31 e i bordi Low(Integer) e High(Integer) su Delphi Win32, Delphi Win64 e Free Pascal x86_64. Un controllo di sanità da conservare in qualsiasi unit test che tocca coordinate:
var
V: Integer;
begin
V := -900;
Writeln(V shr 8); // 16777212 shift logico, il vecchio bug
Writeln(V div 256); // -3 troncamento verso zero
Writeln(FloorDiv(V, 256)); // -4 ciò che T.88 intende per >> 8
Writeln(SarInt32(V, 8)); // -4
end;
Math.Floor(V / 256) restituisce pure -4, ma la sua deviazione attraverso Double perde precisione per valori Int64 sopra 253, quindi la geometria intera dovrebbe restare negli interi
Quali chiamate PDFlibPas fanno girare il decoder halftone?
Il decoder halftone JBIG2 gira quando PDFlibPas renderizza una pagina con il renderer integrato, perché il rendering ha bisogno di pixel. RenderPageToFile e RenderPageToStream entrambi lo raggiungono attraverso gli stream immagine JBIG2Decode della pagina, quindi ri-renderizzare una pagina halftone è la via diretta per confermare che la v3.539.38 cambia il tuo output. Lo stesso decoder gestisce gli altri tipi di regione JBIG2, coperti in tabelle Huffman personalizzate JBIG2 nel decoder Pascal puro e decodificare file JBIG2 ad accesso casuale in Delphi, e la bitmap renderizzata alimenta conversioni come renderizzare pagine PDF in bianco e nero a 1 bit
uses
SysUtils, PDFlibrary;
var
Lib: TPDFlib;
Page: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('scanned-halftone.pdf', '') <> 1 then
raise Exception.CreateFmt('Load failed, error %d', [Lib.LastErrorCode]);
for Page := 1 to Lib.PageCount do
// Il rendering decodifica ogni regione JBIG2, halftone compresi
if Lib.RenderPageToFile(150, Page, PDF_RENDER_PNG,
Format('page-%.3d.png', [Page])) <> 1 then
Writeln('Page ', Page, ' was not rendered');
finally
Lib.Free;
end;
end.
L'estrazione delle immagini prende normalmente una via diversa. GetPageImageList restituisce le immagini JBIG2 in forma nativa, e SaveImageListItemDataToFile o GetImageListItemDataToString ti passa un file JBIG2 standalone costruito dai byte dello stream: l'header del file, i dati JBIG2Globals e un segmento end-of-file attorno ai dati della pagina. La proprietà 400 di GetImageListItemIntProperty riporta 6 per un tale elemento. Nulla viene decodificato su quella via, quindi un .jb2 estratto che sembra corretto in un altro viewer mentre la pagina renderizzata mostra rumore era un segno tipico di questi due bug halftone:
var
ListID, I: Integer;
begin
Lib.SelectPage(1);
ListID := Lib.GetPageImageList(0);
if ListID = 0 then
Exit;
try
for I := 1 to Lib.GetImageListCount(ListID) do
if Lib.GetImageListItemIntProperty(ListID, I, 400) = 6 then // standalone JBIG2
Lib.SaveImageListItemDataToFile(ListID, I, 0,
Format('page1-image%d.jb2', [I]));
finally
Lib.ReleaseImageList(ListID);
end;
end;
Quando maschere o conversioni di colore forzano un fallback renderizzato, l'elemento torna come bitmap decodificata e il decoder halftone gira davvero. Di più sulle image list in estrazione di testo, immagini e font PDF in Delphi
Riferimento rapido: regole della griglia halftone JBIG2
- Indicizza la maschera skip come
HSKIP[ng, mg], colonna griglia prima, e rileggila nello stesso ordine ovunque si posizionino celle (T.88 §6.6.5.1, corretto in PDFlibPas v3.539.37) - Prova qualsiasi codice halftone o griglia con una griglia non quadrata e un insieme asimmetrico di celle fuori regione, perché una griglia quadrata può nascondere completamente un indice trasposto
- Leggi
HGXeHGYcome valori a 32 bit con segno (T.88 §7.4.5.1.2), mai attraverso un helper senza segno che forzi i negativi - Traduci la
>> 8dello standard come divisione floor per 256, non comeshr 8e non comediv 256 - Esegui il test skip su posizioni in pixel shiftate; la forma fixed point differisce ogni volta che la frazione è diversa da zero, come mostra
HGX = -900con un pattern di 4 pixel - Accumula le coordinate griglia in
Int64e limitale prima di passarle al codice bitmap, così una griglia corrotta non può fare overflow - Passa alla v3.539.38 o successiva se i tuoi documenti contengono regioni halftone con
HENABLESKIP, origini griglia negative o griglie ruotate
PDFlibPas renderizza, estrae e modifica documenti PDF da Delphi e C++Builder con un decoder JBIG2 Pascal nativo che ora gestisce maschere skip halftone, origini griglia negative e griglie ruotate come specifica T.88. Vedi la PDFlibPas Delphi PDF library per funzionalità, edizioni e un download di prova