Teknisk artikkel

Båndvis PDF-rendering i Delphi: det negative Y-offsetet

Det første båndet holdt hele tegningen klemt sammen i én stripe, og de fem båndene etter det kom tilbake blanke. Det var den gamle båndvise eksporten, og PDFiumPas rettet det i v3.66.0: RenderPageBanded sender nå full målbredd og målhøyde for siden til FPDF_RenderPageBitmap på hvert eneste bånd, sammen med et negativt vertikalt offset, slik at den native klippingen bare skriver radene til det gjeldende båndet mens siden beholder sin fulle koordinatgeometri. Bruksområdet bak alt dette er kjedelig og uunngåelig. Noen gir deg en E-størrelsesplott eller en sammensydd panoramaside og vil ha en raster på 600 DPI. Et ISO A0-ark ved 600 DPI er 19866 x 28086 piksler, og en 32-biters målbitmap av den størrelsen er litt over 2 GB sammenhengende minne. På 32-biters Delphi feiler allokeringen ganske enkelt. På 64-biters lykkes den ofte nok til at feilen blir et kundeproblem i stedet for et testproblem. Båndvis rendering finnes slik at toppallokeringen er én stripe, ikke én side

Hvorfor inneholdt hvert bånd hele siden?

Den gamle koden forvekslet to forskjellige par med argumenter i PDFium-kallet for siderendering. FPDF_RenderPageBitmap tar start_x, start_y, size_x og size_y, der størrelsesparet sier hvor stor hele siden skal skaleres til, og startparet sier hvor den skalerte siden havner inne i målbitmapen. Båndløkken før v3.66.0 kalte bibliotekets RenderPage-hjelper med toppen av båndet som mål-offset og båndhøyden som sidehøyde. Disse to tallene gikk rett gjennom til det native kallet, så PDFium skalerte hele siden inn i et rektangel som bare var BandHeight rader høyt, og tegnet den deretter ved y = BandTop inne i en bitmap som selv bare var BandHeight rader høy. Resultatet var nøyaktig det du kan forutsi når du først ser det. Bånd null fikk hele siden vertikalt klemt til båndhøyden. Hvert senere bånd fikk den samme sammenklemte siden skjøvet under bitmapens nedre kant, så det kom tilbake som bakgrunnsfyll. Feilen skjuler seg i det ene tilfellet de fleste smoke-tester bruker, en side hvis renderingshøyde er mindre enn båndhøyden, fordi det da bare er ett bånd og feil geometri tilfeldigvis faller sammen med riktig. Alt som er høyere enn ett bånd, avslører den umiddelbart

Hva garanterer det negative offsetet?

Den rettede implementasjonen sender hvert bånd gjennom RenderTile, som er det ene stedet i komponenten som allerede forsto forskjellen. RenderTile tar et flisopphav i fulle sidepikselkoordinater pluss en separat PageWidth og PageHeight, og gir PDFium -Left og -Top med sidestørrelsen urørt. Å negere offsetet skyver siden i full størrelse oppover til det forespurte båndet ligger på rad null i målbitmapen; PDFium klipper deretter native mot bitmapgrensene, så ingenting utenfor båndet rasteriseres. Side-til-enhets-mappingen beskrevet i ISO 32000-1 punkt 8.3.2 forblir identisk fra første bånd til siste, og det er hele poenget: Bånd N er bitidentisk med radene BandTop til BandTop + h fra én full siderendering, og regresjonstesten hevder nøyaktig dette, piksel for piksel, mot RenderPage-output med samme dimensjoner

// Ett bånd for hånd. Målbitmapen er bare BandHeight høy,
// men målestørrelsen for siden beholder full Width x Height
Band := Pdf.RenderTile(0, BandTop,          // flisopphav i sidepiksler
                       Width, BandHeight,   // målbitmapens størrelse
                       Width, Height);      // mål i full sidestørrelse
try
  // Band inneholder nå radene BandTop .. BandTop + BandHeight - 1 fra siden
finally
  Band.Free;
end;

Det offentlige bånd-API-et er en callback-løkke. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) returnerer antallet bånd den faktisk rendret, eller 0 når argumentene avvises, og holder komponentens render-lås gjennom hele passet. Callback-signaturen er TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmap-en er pf32bit, Width piksler bred og ikke høyere enn BandHeight, og den frigjøres så snart handleren din returnerer, så kopier alt du vil beholde. Returnerer du False, stopper passet etter det gjeldende båndet, noe som gir deg den samme samarbeidende kanselleringsmodellen som brukes av kansellerbar progressiv PDF-rendering i Delphi, bare på stripenivå i stedet for PDFium-fortsettelsesnivå

type
  TBandSink = class
  private
    FCancelled: Boolean;
    FRows: Integer;
  public
    function HandleBand(BandIndex, BandTopY: Integer;
      Bitmap: TBitmap): Boolean;
    property Rows: Integer read FRows;
  end;

function TBandSink.HandleBand(BandIndex, BandTopY: Integer;
  Bitmap: TBitmap): Boolean;
begin
  // Bitmap-en dør når denne metoden returnerer - bruk den her
  Inc(FRows, Bitmap.Height);
  Result := not FCancelled;
end;

// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);

Strøm PNG og TIFF uten en fullsidebitmap

Rendering i bånd hjelper bare hvis koderen også er sekvensiell, så v3.66.0 la til RenderPageBandedToStream, som skriver PNG eller TIFF rett til en caller-strøm. TPdfBandedImageStreamOptions.Default setter en båndhøyde på 256 rader, PNG-komprimeringsnivå 6 og MaxOutputBytes på 0, som betyr ubegrenset. Den returnerte TPdfBandedImageReport har Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes og Completed. PeakBandBytes er tallet du faktisk bryr deg om når en jobb dimensjoneres: Det er Width * BandHeight * 4, så A0-arket over topper rundt 19 MB med båndbuffer i stedet for 2 GB med sidebuffer

PNG-koderen er med vilje smal. Den skriver fast RGB8, med en IHDR med bitdybde 8 og fargetype 2, bygger deretter hver skannelinje med filtertype 0 (ISO/IEC 15948 filtermetode 0, filtertype None) og sender den gjennom plattformens zlib-komprimeringsstrøm. De komprimerte byte-ene kommer ut som CRC-bærende IDAT-chunker skrevet i rekkefølge. Den interessante begrensningen er strømmen under deflate-laget: Den svarer på posisjonsspørringer fordi komprimeringsstrømmen ber om dem, men ethvert forsøk på en reell seek kaster en feil. Det er med hensikt. Når en IDAT-chunk og CRC-en er på ledningen, finnes det ingen vei tilbake for å rette dem, og en stille seek ville korrumpert output som fortsatt ser strukturelt gyldig ut

TIFF-koderen skriver little-endian klassisk TIFF, byteordremarkøren II fulgt av magic 42, med én stripe per bånd. Piksler strømmer ut først, og den tidobbeltoppførte IFD-en genereres til slutt, når stripe-offset-er og byteantall er kjent. Komprimeringen er tag 259 verdi 1, så det finnes ingen entropikoding i det hele tatt: Payloaden er nøyaktig Width * Height * 3 byte, PhotometricInterpretation er RGB, PlanarConfiguration er chunky, og RowsPerStrip registrerer båndhøyden mens den siste korte stripen beskrives av sin egen StripByteCounts-oppføring. Båndhøyden endrer derfor toppminnet og antallet striper, men ikke output-størrelsen, noe som er verdt å vite før du justerer den. Hvis du vil ha små filer i stedet for tapsfrie, er sidebanen i konvertering av PDF-sider til JPEG-bilder med PDFium VCL-komponenten fortsatt det bedre verktøyet

var
  StreamOptions: TPdfBandedImageStreamOptions;
  Report: TPdfBandedImageReport;
  Output: TFileStream;
begin
  StreamOptions := TPdfBandedImageStreamOptions.Default(pbifPng);
  StreamOptions.BandHeight := 512;
  StreamOptions.CompressionLevel := 6;
  StreamOptions.MaxOutputBytes := Int64(256) * 1024 * 1024;

  Output := TFileStream.Create('sheet-a0-600dpi.png', fmCreate);
  try
    Report := Pdf.RenderPageBandedToStream(Output, 19866, 28086,
      StreamOptions);
  finally
    Output.Free;
  end;

  if not Report.Completed then
    raise Exception.Create('Banded export stopped before the last row');
  // Report.PeakBandBytes = 19866 * 512 * 4, ikke 19866 * 28086 * 4
end;

Hvor stopper en båndvis eksport?

To tak begrenser output-en, og de feiler med vilje på forskjellige steder. Det første er caller-budsjettet: MaxOutputBytes håndheves av en avgrenset skrivestrøm som kaster EPdfError før enhver skriving som ville gått over grensen, så budsjettet er et hardt tak i stedet for en rapport i etterkant. Det andre er strukturelt. Klassisk TIFF lagrer stripe-offset-er som 32-biters verdier, så BeginImage validerer Width * Height * 3 pluss header og katalog mot den grensen og avviser jobben før én eneste piksel skrives; den samme kontrollen kjøres mot MaxOutputBytes på forhånd, fordi en TIFF hvis budsjett ikke kan dekke sin egen pixel-payload, ikke er verdt å starte. PNG har ingen tilsvarende grense, siden IDAT-chunker er rent sekvensielle og det ikke finnes noen 32-biters offset-tabell som kan flyte over

Vær tydelig på hva en stoppet eksport etterlater seg. Når passet ikke når siste rad, forblir Completed False, og koderen rives ned med EndImage(False), som med vilje hverken skriver PNG IEND-chunken eller TIFF-IFD-en. Den delvise filen er derfor ugyldig, og hver dekoder vil si det, i stedet for å være et plausibelt bilde med manglende rader. Denne oppryddingen er pakket slik at en sekundær feil inne i EndImage ikke kan erstatte det opprinnelige unntaket, og det er forskjellen mellom en stack trace som navngir den egentlige årsaken, og en som navngir renholderen. Hvis du trenger fremdrift som overlever, lagre et checkpoint per bånd i din egen callback; cache-teknikkene på stripenivå i PDFium Delphi-renderingscachen og zoom-veiledningen gjelder her også

Koble inn din egen codec

Når PNG og TIFF ikke er målet, tar RenderPageBandedToEncoder imot en avleder av TPdfBandedImageEncoder og driver den samme løkken. Livssyklusen er eksplisitt og kort: BeginImage(Width, Height), deretter WriteBand(BandIndex, BandTopY, Bitmap) én gang per stripe i strengt stigende rekkefølge, så EndImage(Completed), med GetBytesWritten som mater Report.OutputBytes. De innebygde koderne avviser et bånd ute av rekkefølge direkte i stedet for å bufre det, og enhver encoder du skriver bør gjøre det samme, fordi en codec som stille omorganiserer striper, produserer en fil som åpnes og lyver. Dette er skjøten å bruke for JPEG 2000-tiles, en JPEG-skriver som får én MCU-radstripe om gangen eller en direkte mating inn i en utskriftskø

type
  TCodecBandEncoder = class(TPdfBandedImageEncoder)
  private
    FNextBand: Integer;
    FWritten: Int64;
  public
    procedure BeginImage(Width, Height: Integer); override;
    function WriteBand(BandIndex, BandTopY: Integer;
      Bitmap: TBitmap): Boolean; override;
    procedure EndImage(Completed: Boolean); override;
    function GetBytesWritten: Int64; override;
  end;

function TCodecBandEncoder.WriteBand(BandIndex, BandTopY: Integer;
  Bitmap: TBitmap): Boolean;
begin
  if BandIndex <> FNextBand then
    raise EPdfError.Create('Bands must arrive in order');
  Bitmap.PixelFormat := pf32bit;
  // Mat Bitmap.ScanLine[0 .. Bitmap.Height - 1] til kodeken her
  Inc(FNextBand);
  Result := True;
end;

Én krysskompilatorfelle som er verdt å kjenne

Zlib-enheten skrives forskjellig i hver støttet toolchain: Delphi XE5 og senere bruker System.ZLib, FPC bruker zstream og eldre Delphi bruker vanlig ZLib. Det er rutinemessig conditional compilation. Fellen er at alle tre eksporterer komprimeringsnivåkonstanter med navnene clNone og clDefault, som kolliderer direkte med TColor-medlemmene med samme navn i graphics-enheten. Når zlib-enheten dukker opp i implementation uses-klausulen, kan en ukvalifisert clNone i renderingskode løses til et komprimeringsnivå i stedet for en farge, uten diagnose. PDFiumPas låser dette ned med eksplisitte aliaser for fargesentineler, PdfGraphicsColorNone og PdfGraphicsColorDefault, bundet én gang til de fullt kvalifiserte graphics-konstantene og brukt overalt der en renderingsbakgrunn eller et fargeordnings-sentinel sammenlignes. Tre kodelinjer, og symboloppløsningen slutter å drive mellom kompilatorene

Båndvis rendering ser ut som en bekvemmelighetsfunksjon helt til du møter siden som ikke får plass i RAM, og da er det den eneste stien som fungerer. Den korrigerte båndgeometrien, de sekvensielle PNG- og TIFF-koderne og skjøten for egne koder leveres som del av PDFium Delphi-komponenten, med den fulle bånd-mot-side-pikselkontrollen kjørt i regresjonspakken på tvers av Delphi, Lazarus og C++Builder