PDFlibPas heeft twee onafhankelijke gebreken in zijn native JBIG2-halftoneregiondecoder verholpen: in v3.539.37 wordt het HSKIP-skipmask geïndexeerd als HSKIP[ng, mg], zoals ITU-T T.88 §6.6.5.1 het definieert, en in v3.539.38 worden grids die negatieve coördinaten bereiken, via een negatieve HGX of HGY of via rotatie, geplaatst met een echte floor-shift. Vóór die releases kwamen getroffen halftoneregions verhaspeld of verschoven uit de bus, zonder enige foutmelding. Beide bugs zaten verscholen achter testdata die toevallig symmetrisch of niet-negatief was, en de tweede draait om een eigenschap van Delphi en Free Pascal die ver buiten JBIG2 toehapt: shr op een signed integer is een logical shift, niet de arithmetic >> die de standaard veronderstelt
Halftoneregions zijn het minst voorkomende JBIG2-regiontype, dus een decoder kan duizenden gescande documenten verwerken voordat hij een gerasterde foto tegenkomt die als zodanig is gecodeerd. Als het zover is, is de falering gemene: het bestand parst netjes, de segmentlengten kloppen op, de pagina heeft de juiste grootte, en de region is rommel
Wat decodeert een JBIG2-halftoneregion eigenlijk?
Een JBIG2-halftoneregion is een grid van kleine bitmaps uit een pattern dictionary, en het echte werk van de decoder is voor elke gridcel een index en de pixelpositie berekenen waar die cel neerkomt. De pattern dictionary bevat HNUMPATS patterns van HPW × HPH pixels. Het halftoneregionsegment beschrijft vervolgens een grid van HGW kolommen bij HGH rijen en een grijswaardenafbeelding van dezelfde grootte, gecodeerd als Gray-gecodeerde bitplanes. Elke bitplane wordt met de generic region-procedure over een HGW × HGH-bitmap gedecodeerd, meest significante plane eerst, en samen geven de planes elke cel zijn patternindex
Celplaatsing gebruikt fixed-point rekenkunde met een breuk van 8 bits. De gridorigine HGX, HGY is een paar 32-bit waarden, en de gridvector HRX, HRY beschrijft de stap tussen naburige cellen, wat een gedraaid grid toestaat. Voor gridrij mg en gridkolom ng berekent T.88 §6.6.5 de pixelpositie als:
x = (HGX + mg × HRY + ng × HRX) >> 8y = (HGY + mg × HRX − ng × HRY) >> 8
Het skipmask komt binnen via de optionele vlag HENABLESKIP. Als de vlag staat, bouwt §6.6.5.1 een HGW × HGH-bitmap HSKIP en zet HSKIP[ng, mg] op 1 voor elke cel waarvan het patroon volledig buiten de region ligt: x + HPW <= 0, x >= HBW, y + HPH <= 0 of y >= HBH. De grijswaarden-bitplanes worden daarna met dat mask als skip-bitmap van de generic region gedecodeerd, dus de arithmetic decoder leest voor een overgeslagen cel geen context en werkt die niet bij. Decoder en encoder moeten op elke bit van HSKIP overeenkomen, anders lopen de twee arithmetic coders uit de pas
Waarom brak een getransponeerd HSKIP-mask alleen niet-vierkante grids?
Het skipmask werd met verwisselde coördinaten weggeschreven, en alleen een niet-vierkant grid legde het bloot, want een vierkant grid houdt elke verwisselde coördinaat binnen het mask. PDFlibPas bewaart bitmaps met een pixel-accessor in de vorm (column, row), en de code die het mask bouwde gaf (mg, ng) door, rij eerst. De grijswaarden-bitplanedecoder leest het mask correct als (ng, mg). De patroonplaatsingslus las het terug in de verwisselde volgorde van de bouwer, dus de twee waren het eens, en een review van alleen de plaatsingslogica zou hem laten passeren. Een naamvalkuil maakte het erger: in de plaatsingslus itereert de variabele col over gridrijen en Row over gridkolommen
Neem het grid van 5 × 3 met 4 × 4-patronen op een region van 16 × 8 dat v3.539.37 als regressiegeval gebruikt. Met HRX = 1024 en HRY = 0 landt gridkolom 4 op x = 16 en gridrij 2 op y = 8, allebei buiten de region. Het correcte mask markeert zeven cellen: heel kolom 4 en heel rij 2. De verwisselde schrijfbewerkingen probeerden pixels op rijindexen 3 en 4 te zetten in een mask van maar drie rijen hoog, en de bitmapsetter negeerde die buiten-bereik-schrijfacties stilletjes. Wat overbleef was kolom 2, rijen 0 tot 2. De decoder sloeg dus twee cellen over die de encoder had gecodeerd, en decodeerde zes cellen die de encoder had overgeslagen
De arithmetic decoder faalt niet zodra dat gebeurt. Hij decodeert extra pixels uit bits die aan latere cellen toebehoren, zijn contexts lezen verkeerde buren, en elke patternindex na de eerste mismatch is ruis, en daarom was het symptoom een verhaspelde region in plaats van een paar verkeerd geplaatste cellen. Op een vierkant grid is dezelfde bug vaak onzichtbaar: geen enkele verwisselde coördinaat verlaat het mask, en als de buiten-de-region-cellen symmetrisch rond de diagonaal liggen, bijvoorbeeld een grid dat rechts en onder met evenveel cellen uitsteekt, is het getransponeerde mask bit voor bit het correcte. HENABLESKIP is bovendien optioneel, moet 0 zijn wanneer de grijswaardenafbeelding MMR-gecodeerd is, en wordt zelden door encoders gezet, dus de bug had heel weinig manieren om naar boven te komen. Sinds v3.539.37 schrijft de bouwer HSKIP[ng, mg] en leest de plaatsingslus dezelfde volgorde
Waarom falen negatieve halftone-gridoffsets in drie lagen?
Een halftonegrid dat links van of boven zijn region begint brak PDFlibPas op drie aparte plekken, en elk gebrek hield het volgende verborgen. T.88 staat deze geometrie met opzet toe. Een encoder die zijn raster op de pagina uitlijnt in plaats van op de region, of een gedraaid grid gebruikt, produceert van nature negatieve celhoeken die de region bijsnijdt. v3.539.38 verholpen alle drie de lagen tegelijk, want er één alleen fixen veranderde hooguit het symptoom
Laag 1: een signed veld als unsigned gelezen
T.88 §7.4.5.1.2 definieert HGX en HGY als signed 32-bit waarden, maar de decoder las ze met dezelfde 32-bit helper die hij voor unsigned velden gebruikte, en die helper dwong elke negatieve uitkomst naar 0. Een grid dat op HGX = -900 had moeten beginnen, werd stilletjes op de regionorigine neergezet. In het regressiegeval van v3.539.38 kwam het hele beeld twee rijen te laag uit de bus. De clamp verklaart ook waarom de andere twee gebreken zo lang konden overleven: met een geforceerd niet-negatieve origine kon een negatieve coördinaat alleen via een gedraaid grid met HRY > 0 verschijnen, waar y = HGY + mg × HRX − ng × HRY bij latere gridkolommen onder nul zakt
Laag 2: shr is niet >> 8
T.88 schrijft >> 8 en bedoelt een arithmetic shift, die richting min oneindig afrondt. De decoder vertaalde dat als shr 8. In Delphi en Free Pascal is shr op een signed integer een logical shift: het tekenbit wordt als nul ingeschoven. Voor een Integer met -512 geeft shr 8 16777214 in plaats van -2. Een patroon dat op y = -2 getekend en tot zijn onderhelft bijgesneden had moeten worden, werd zestien miljoen rijen naar beneden gestuurd en als buiten de region weggegooid. Niets crashte; de bovenste rij van de halftone verdween simpelweg
Laag 3: fixed point vergelijken in plaats van pixels
De skiptest vergeleek fixed-point waarden, geen pixelposities, en de twee zijn niet equivalent zodra de breuk ongelijk nul is. De originele code ontliep de logical shift door xx + HPW × 256 <= 0 te testen op de ongeschoven waarde, een vermeend equivalent van de T.88-test. Met HGX = -900 en een patroon van 4 pixels geeft dat -900 + 1024 = 124, wat positief is, dus de cel wordt niet overgeslagen. De standaard schuift eerst: floor(-900 / 256) = -4, en -4 + 4 = 0 voldoet aan x + HPW <= 0, dus de cel ligt volledig buiten en moet worden overgeslagen. De encoder sloeg hem over, de decoder decodeerde hem, en de grijswaardenafbeelding dreef precies af zoals in het getransponeerde-mask-geval
Het regressiegeval uit v3.539.38 gebruikt een grid van 4 × 3 met 4 × 4-patronen op HGX = -900, HGY = -512, HRX = 1024 op een region van 12 × 10. Gridkolommen landen op x = -4, 0, 4 en 8, dus kolom 0 ligt volledig buiten en hoort in HSKIP; gridrijen landen op y = -2, 2 en 6, dus rij 0 moet tot zijn onderste twee pixelrijen worden bijgesneden in plaats van te worden weggegooid. De lagen één voor één fixen reproduceert de stapel:
| Verholpen gebreken | Gedecodeerde region |
|---|---|
| Geen (vóór v3.539.38) | Grid naar de origine getrokken, hele beeld twee rijen te laag |
| Alleen signed HGX / HGY lezen | Eerste gridrij ontbreekt, de rest verhaspeld door de skiptest-drift |
| Signed lezen, floor-shift en skiptest in pixelruimte | Identiek, pixel voor pixel, aan de pagina berekend uit T.88 §6.6.5 en aan twee onafhankelijke referentiedecoders |
De fix is één helper, HalftoneGridPixel, gedeeld door de skipmask-bouwer en de plaatsingslus. Hij accumuleert de coördinaat in Int64 zodat een groot product mg × HRX niet kan omslaan, deelt door 256 met afronding richting min oneindig, en klemt op ±MaxInt div 2 zodat een corrupt grid de bitmaprekenkunde later niet kan laten overlopen. De skiptest vergelijkt die pixelwaarden nu tegen HPW, HPH, HBW en HBH, precies zoals §6.6.5.1 het voorschrijft
Hoe schrijft u een arithmetic right shift in Delphi?
Delphi heeft geen arithmetic shift-operator, dus een correcte signed right shift moet als floordivisie worden geschreven, en gewone div is die deling niet. div kapt richting nul af. Voor niet-negatieve waarden komen afkappen en afronden naar beneden overeen, en ook voor negatieve waarden die exacte veelvouden van de deler zijn, en daarom ziet -512 div 256 = -2 er in een snelle test prima uit. Ze verschillen overal elders: -900 div 256 is -3 terwijl de floor -4 is, en -1 div 256 is 0 terwijl de floor -1 is. Een JBIG2-coördinaat met een ongelijk-nul-breuk is precies het geval waarin div de verkeerde pixel geeft
Op de Delphi-compilers Win32 en Win64 geeft een Integer-variabele met -512 acht posities naar rechts geschoven 16777214, en een Int64 met -512 geeft 72057594037927934. Free Pascal definieert shr eveneens als logical shift en levert SarLongint en SarInt64 mee in zijn System-unit voor de arithmetic-variant, maar die functies bestaan niet in Delphi, dus code die tussen de twee compilers deelt heeft een eigen helper nodig:
// Floordivisie: rondt richting min oneindig af bij elk teken van A en B.
// B mag niet 0 zijn, en FloorDiv(Low(Integer), -1) overloopt net als 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;
// Arithmetic right shift (de C- en T.88-">>" op signed waarden).
// Bij negatieve Value is not Value = -Value - 1 niet-negatief, dus de
// logical shr is daar veilig, en de buitenste not zet het resultaat terug
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;
De not-truc schuift nooit een negatief getal, dus ze hangt niet af van hoe een compiler het tekenbit behandelt, en ze loopt nooit over, ook niet voor Low(Integer). Beide helpers kwamen over enkele miljoenen waarden, elke shift van 0 tot 31 en de randen Low(Integer) en High(Integer) overeen met een Int64-floorreferentie op Delphi Win32, Delphi Win64 en Free Pascal x86_64. Een sanity check die het bewaren waard is in elke unittest die coördinaten aanraakt:
var
V: Integer;
begin
V := -900;
Writeln(V shr 8); // 16777212 logical shift, de oude bug
Writeln(V div 256); // -3 afkappen richting nul
Writeln(FloorDiv(V, 256)); // -4 wat T.88 met >> 8 bedoelt
Writeln(SarInt32(V, 8)); // -4
end;
Math.Floor(V / 256) geeft ook -4 terug, maar zijn omweg via Double verliest precisie voor Int64-waarden boven 253, dus integer-geometrie hoort in integers te blijven
Welke PDFlibPas-aanroepen laten de halftonedecoder draaien?
De JBIG2-halftonedecoder draait zodra PDFlibPas een pagina met de ingebouwde renderer rendert, want renderen heeft pixels nodig. RenderPageToFile en RenderPageToStream bereiken hem allebei via de JBIG2Decode-beeldstreams van de pagina, dus een halftonepagina opnieuw renderen is de directe manier om te bevestigen dat v3.539.38 uw uitvoer verandert. Dezelfde decoder behandelt de andere JBIG2-regiontypes, behandeld in JBIG2 custom Huffman tables in de pure Pascal-decoder en random-access JBIG2-bestanden decoderen in Delphi, en de gerenderde bitmap voedt conversies zoals PDF-pagina's renderen naar 1-bit monochrome
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
// Renderen decodeert elke JBIG2-region, halftones inbegrepen
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.
Beeldextractie volgt normaal een ander pad. GetPageImageList geeft JBIG2-afbeeldingen in native vorm terug, en SaveImageListItemDataToFile of GetImageListItemDataToString overhandigt u een standalone JBIG2-bestand opgebouwd uit de streambytes: de file header, de JBIG2Globals-data en een end-of-file-segment om de paginadata. Property 400 van GetImageListItemIntProperty meldt 6 voor zo'n item. Op dat pad wordt niets gedecodeerd, dus een geëxtraheerde .jb2 die er in een andere viewer prima uitzag terwijl de gerenderde pagina ruis toonde, was een typisch teken van deze twee halftonebugs:
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;
Wanneer masks of kleurconversie een gerenderde fallback afdwingen, komt het item terug als gedecodeerde bitmap en draait de halftonedecoder wel. Meer over image lists staat in Delphi PDF-tekst-, afbeeldings- en fontextractie
Snelnaslag: JBIG2-halftonegridregels
- Indexeer het skipmask als
HSKIP[ng, mg], gridkolom eerst, en lees het terug in dezelfde volgorde waar cellen ook worden geplaatst (T.88 §6.6.5.1, opgelost in PDFlibPas v3.539.37) - Test elke halftone- of gridcode met een niet-vierkant grid en een asymmetrische set buiten-de-region-cellen, want een vierkant grid kan een getransponeerde index volledig verbergen
- Lees
HGXenHGYals signed 32-bit waarden (T.88 §7.4.5.1.2), nooit via een unsigned helper die negatieven klemt - Vertaal de
>> 8van de standaard als een floordivisie door 256, niet alsshr 8en niet alsdiv 256 - Draai de skiptest op geschoven pixelposities; de fixed-point-vorm wijkt af zodra de breuk ongelijk nul is, zoals
HGX = -900met een patroon van 4 pixels laat zien - Accumuleer gridcoördinaten in
Int64en klem ze af voordat u ze aan bitmapcode doorgeeft, zodat een corrupt grid niet kan overlopen - Upgrade naar v3.539.38 of later als uw documenten halftoneregions bevatten met
HENABLESKIP, negatieve gridoriginen of gedraaide grids
PDFlibPas rendert, extraheert en bewerkt PDF-documenten vanuit Delphi en C++Builder met een native Pascal JBIG2-decoder die halftone-skipmasks, negatieve gridoriginen en gedraaide grids nu verwerkt zoals T.88 het voorschrijft. Zie de PDFlibPas Delphi PDF-library voor functies, edities en een proefdownload