Technisch artikel

JBIG2-encoderbackends en de Free Pascal-linker

PDFlibPas kan bilevelafbeeldingen als JBIG2 coderen via twee verschillende backends. De ene is een native Object Pascal MMR-encoder die er altijd is. De andere is een externe symbol-dictionary-encoder die op gescande tekst aanzienlijk kleinere uitvoer produceert, en die is optioneel: een project moet de backend-unit linken om die überhaupt beschikbaar te hebben. Dat onderscheid is de bron van de meest voorkomende verrassing bij deze functie, dus het is het vermelden waard om voorop te stellen: DefaultJBIG2EncodeOptions vraagt standaard om de externe encoder, en wanneer de backend-unit niet gelinkt is valt dat verzoek geruisloos terug op het Pascal MMR-pad

Onder Delphi en C++Builder is de externe backend een set voorgebouwde statische objecten. Onder Free Pascal moest die een DLL worden, en de weg naar die conclusie is een linker-verhaal dat nuttig is voor iedereen die ooit C++-objecten in een Free Pascal-programma heeft willen linken

Registratie is het contract

De backend-unit registreert zichzelf vanuit zijn initialisatiesectie door RegisterJBIG2EncoderBackend aan te roepen. Aanroepers vragen die aan via de optiebit PDF_JBIG2_OPTION_EXTERNAL_ENCODER, met de waarde 4, of via de parameter UseExternalEncoder van de uitgebreide beeldtoegangspunten. De paraplu-unit van de bibliotheek trekt de backend-unit bewust niet mee, want het meedragen van een grote objectenset is een beslissing voor elk project; in de C++Builder-boom wordt die bijvoorbeeld expliciet opgenomen door de projecten die die willen

Het gevolg voor aanroepers is dat het vragen om de externe encoder een voorkeur is en geen garantie, en dat een build die de unit vergeet grotere bestanden oplevert in plaats van een fout. Als de uitvoergrootte er genoeg toe doet om om de betere encoder te vragen, doet die er genoeg toe om te controleren dat u hem ook heeft gekregen

JBIG2-coderingsverzoek van PDFlibPas waarbij de voorkeur voor de externe encoder zonder backend-unit geruisloos terugvalt op het native Pascal MMR-pad
De externe symbol-dictionary-encoder vragen is een voorkeur: gelinkt krimpt de uitvoer; niet gelinkt draait het Pascal MMR-pad geruisloos met grotere bestanden
uses
  PDFlibrary,
{$IFDEF FPC}
  PDFlibJBIG2EncDLL;    // dynamische backend voor Free Pascal
{$ELSE}
  PDFlibJBIG2EncC;      // statische objectenset voor Delphi / C++Builder
{$ENDIF}

var
  Pdf: TPDFlib;
  ImageId: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.NewPage;
    // Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
    // BlackDotSize, LossyLevel
    ImageId := Pdf.AddImageJBIG2FromFileEx('scan-page-1.tif',
      0, 1, 1, 0, 0, 0);
    if ImageId = 0 then
      raise Exception.Create('JBIG2 encoding failed');
    Pdf.SaveToFile('archive.pdf');
  finally
    Pdf.Free;
  end;
end;

De unit compileren was twee regels. Symbolen waren het werk

De backend-unit zelf aan de praat krijgen onder Free Pascal kostte precies twee wijzigingen: het instellen van het assembler-dialect, en het vervangen van een op records gebaseerde constructor voor formaatinstellingen door de globale standaardvariabele. Dat is een goede weerspiegeling van hoe draagbaar rechttoe-rechtaan-Pascal tussen de twee compilers is

De symbolenkant was het echte werk. De objectenset verwijst naar 176 C-symbolen. Daarvan hadden er 128 al Pascal-implementaties binnen de unit en hoefden alleen exportnamen te krijgen, omdat Delphi de functienaam als symboolnaam gebruikt terwijl Free Pascal een expliciete public-name-declaratie vereist. Zevenentwintig werden gedeeld met de JPEG 2000-codec en moesten vanaf precies één plaats geëxporteerd worden, omdat ze dubbel definiëren ieder programma dat beide linkt breekt. De overige 21 waren platform- en C-runtime-ingangen, zestien Win32-bestandsfuncties plus een handjevol aanroepen uit de standaardbibliotheek, en die gingen naar een nieuwe compatibiliteitsunit

Geen daarvan is conceptueel moeilijk, en alles is nodig voordat de linker het zelfs maar probeert. Bij de linker stopte het

Drie linkroutes, drie doodlopende wegen

De interne Free Pascal-linker kan de objectbestanden niet lezen, omdat die door een compiler zijn geproduceerd die associatieve COMDAT-secties uitzendt en de interne linker meldt dat die ze niet ondersteunt. Dat is een botte weigering, geen waarschuwing

Een externe linker gebruiken leek het antwoord. De bij Free Pascal meegeleverde binutils-linker crasht ronduit tijdens het toepassen van section garbage collection op dit archief, en die vlag is deel van de vaste parameterset die Free Pascal voor het 64-bit Windows-doel doorgeeft, dus die kan niet van de commandoregel af; de gedocumenteerde schakelaars om die te onderdrukken worden op dit pad genegeerd. Een veel nieuwere binutils gebruiken faalt anders: die kan het Free Pascal-linkscript helemaal niet verwerken, wat zonder script een lege uitvoer oplevert en met het script een muur van relocatiefouten

Een grens die onderweg werd ontdekt is het kennen waard, zelfs als u het linkerprobleem nooit tegenkomt. De externe linker lost objectbestandspaden op relatief aan de uitvoermap van het uitvoerbare bestand in plaats van de bronboom, dus een relatieve include-object-directief werkt alleen wanneer de uitvoermap toevallig gelijk is aan de werkmap tijdens het compileren. Een bibliotheek kan daar niet van uitgaan bij het project van een gebruiker, wat op zichzelf al een reden is om een gelinkte bibliotheek te verkiezen boven losse objecten

Drie mislukte linkroutes voor C++ JBIG2-encoderobjecten onder Free Pascal en de DLL met twee platte C-toegangspunten die het oplosten
COMDAT-secties verslaan de interne linker en beide externe linkers falen, dus de C++-encoder verschijnt als één DLL die de backend-unit dynamisch bindt

Waarom een andere C++-compiler niet helpt

De voor de hand liggende volgende gedachte is de C++-kant opnieuw te bouwen met een compiler wiens objecten Free Pascal kan lezen. Ook dat werkt niet, en de reden is fundamenteel en geen kwestie van schakelaars. Een minimale C++-translation unit met een template, gecompileerd met elke codegeneratiefunctie uitgeschakeld, zendt nog steeds weak external symbols uit, omdat template- en inline-instantiatie die per definitie produceert. Free Pascal verwerpt die symboolklasse ronduit. De omgekeerde richting faalt ook: een gangbare C++-linker kan objecten van de andere compiler niet verwerken vanwege dezelfde COMDAT-sectie-afhandeling

De C++-code kan dus via geen enkele beschikbare route als objecten bij Free Pascal worden afgeleverd. Als DLL kan dat wel, en dat is wat er gebeurd is: de encoder en zijn beeldverwerkingsafhankelijkheid zijn in één bibliotheek gebouwd met twee platte C-toegangspunten, en de Free Pascal-backend-unit bindt die dynamisch en registreert zichzelf precies zoals de statische backend dat doet. Het Delphi- en C++Builder-pad is helemaal niet aangeraakt, en dat is de juiste uitkomst; een portabiliteitsprobleem op één toolchain mag de toolchain die al werkte niet verstoren

Polariteit is het enige dat u zal bijten

Tussen een Windows-bilevelbitmap en een JBIG2-encoder zit een conventiemismatch die geen typesysteem vangt. Een scanline van een device-independent bitmap met één bit per pixel behandelt een gezette bit als wit. De encoder behandelt een gezette bit als zwart. Geef de scanlines onveranderd door en u krijgt een volkomen geldige JBIG2-stream van de fotografische negatief van uw pagina

Polariteitconventies van one-bit DIB en JBIG2 waarbij een gezette bit wit is in de scanline en zwart in de encoder, opgelost door elke byte te inverteren
Dezelfde bytes, tegengestelde betekenis: zonder elke byte te inverteren levert de encoder een geldige JBIG2-stream van de negatief op
// One-bit DIB: gezette bit betekent wit. JBIG2-encoder: gezette bit
// betekent zwart. Inverteer elke byte op de weg naar binnen
for I := 0 to RowBytes - 1 do
  Row[I] := Row[I] xor $FF;

De verificatiemethode doet er evenveel toe als de oplossing. Gecomprimeerde streamlengtes vergelijken vertelt u niets, want een negatieve afbeelding comprimeert tot een vergelijkbare grootte. Naar de pagina kijken bewijst alleen dat die niet openlijk geïnverteerd is. De betrouwbare controle is de uitvoer van beide coderingspaden, native Pascal en extern, naar PNG renderen en byte voor byte vergelijken: beide encoders zijn verliesvrij op dezelfde bronafbeelding, dus alles behalve een exacte match is een bug in een van beide. Die vergelijking is nu een permanente regressiontest, en het is het soort assertie dat het bouwen waard is telkens wanneer twee implementaties exact zouden moeten overeenkomen

Welke backend u kunt gebruiken

Voor algemene bilevelinhoud, geditherde halftonen, lijntekeningen, gemengde grafieken, is de native Pascal MMR-encoder toereikend en heeft die geen uitrolkosten. Voor gescande tekst, het geval waarvoor JBIG2 is ontworpen, zit de groottereductie in de externe symbol-dictionary-encoder, omdat die herhaalde glyph-vormen in een woordenboek onderbrengt in plaats van elke voorkomende vorm opnieuw te coderen. Als u archieven van gescande documenten produceert, is dat verschil groot genoeg om opslagplanning te veranderen

De upstreamvraag, hoe de bilevelafbeelding in de eerste plaats wordt geproduceerd, doet voor de uitvoergrootte evenveel af; region-based monochrome rendering wordt behandeld in het artikel over monochrome region rendering, en documentbrede maatstrategie in PDF-bestandsgrootte optimaliseren en font subsetting. Voor scansets met herhaalde pagina's wint deduplicatie het vaak van betere compressie, wat het onderwerp is van perceptuele beelddeduplicatie. De beschikbaarheid per platform van toolchain en backend staat op de productpagina van de losLab PDF Developer Library