Det første bånd indeholdt hele tegningen mast sammen i én stribe, og de fem efterfølgende bånd kom tilbage som blanke. Det var den gamle banded export, og PDFiumPas rettede den i v3.66.0: RenderPageBanded sender nu målbredde og målhøjde for hele siden til FPDF_RenderPageBitmap på hvert eneste bånd sammen med en negativ vertikal offset, så den native clip kun skriver rækkerne fra det aktuelle bånd, mens siden bevarer sin fulde koordinatgeometri. Use casen bag alt dette er kedelig og uundgåelig. Nogen giver dig et E-size-plot eller en stitched panoramaside og vil have et raster af den ved 600 DPI. Et ISO A0-ark ved 600 DPI er 19866 x 28086 pixels, og et 32-bit-destinationsbitmap i den størrelse er lidt over 2 GB sammenhængende hukommelse. På 32-bit Delphi fejler allokeringen ganske enkelt. På 64-bit lykkes den ofte nok til at gøre fejlen til et kundeproblem og ikke et testproblem. Banded rendering findes, så peak-allokeringen er én stribe og ikke én side
Hvorfor indeholdt hvert bånd hele siden?
Den gamle kode forvekslede to forskellige par argumenter i PDFiums siderenderingskald. FPDF_RenderPageBitmap tager start_x, start_y, size_x og size_y, hvor størrelsesparret siger, hvor stor hele siden skal skaleres til, og startparret siger, hvor den skalerede side lander inde i destinationsbitmapet. Båndløkken før v3.66.0 kaldte bibliotekets RenderPage-helper med båndets top som destinationsoffset og båndhøjden som sidehøjden. De to tal gik direkte videre til det native kald, så PDFium skalerede hele siden ind i et rektangel, der kun var BandHeight rækker højt, og tegnede den derefter ved y = BandTop inde i et bitmap, der selv kun var BandHeight rækker højt. Resultatet er præcis, hvad man kan forudsige, når man først ser det. Bånd nul modtog hele siden lodret mast sammen til båndhøjden. Alle senere bånd modtog den samme sammenmastede side skubbet under bitmapets nederste kant, så de kom tilbage som baggrundsfill. Fejlen skjuler sig i det ene tilfælde, de fleste smoke tests bruger, nemlig en side, hvis renderhøjde er mindre end båndhøjden, fordi der så kun er ét bånd, og den forkerte geometri tilfældigvis falder sammen med den rigtige. Alt, der er højere end ét bånd, afslører den straks
Hvad garanterer den negative offset?
Den rettede implementering sender hvert bånd gennem RenderTile, som er det ene sted i komponenten, der allerede forstod forskellen. RenderTile tager en tile-origo i fuldside-pixelkoordinater plus en separat PageWidth og PageHeight, og den giver PDFium -Left og -Top, mens sidestørrelsen forbliver urørt. Når offsetten negeres, skubbes den fuldstørrelse side opad, indtil det ønskede bånd ligger på række nul i destinationsbitmapet; PDFium clipper derefter native mod bitmapets bounds, så intet uden for båndet nogensinde rasteriseres. Side-til-device-mappingen, der beskrives i ISO 32000-1 afsnit 8.3.2, forbliver identisk fra første bånd til sidste, og det er hele pointen: Bånd N er bitidentisk med rækkerne BandTop til BandTop + h i en enkelt full-page-rendering, og regressionspakken hævder præcis det pixel for pixel mod RenderPage-output ved samme dimensioner
// Ét bånd i hånden. Destinationsbitmapet er kun BandHeight højt,
// men sidens target-størrelse forbliver den fulde Width x Height
Band := Pdf.RenderTile(0, BandTop, // tile-origo i sidepixels
Width, BandHeight, // destinationsbitmapets størrelse
Width, Height); // full-page target-størrelse
try
// Band indeholder nu rækkerne BandTop .. BandTop + BandHeight - 1 fra siden
finally
Band.Free;
end;
Den offentlige band-API er en callback-løkke. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) returnerer antallet af bånd, den faktisk renderede, eller 0 når argumenterne afvises, og den holder komponentens render-lock i hele passet. Callback-signaturen er TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmapet er pf32bit, Width pixels bredt og ikke højere end BandHeight, og det frigives, så snart din handler returnerer, så kopiér alt, du vil beholde. Hvis du returnerer False, stopper passet efter det aktuelle bånd, hvilket giver dig den samme kooperative annulleringsmodel, som bruges af annullerbar progressiv PDF-rendering i Delphi, blot på stribeniveau i stedet for PDFium-continuation-granularitet
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
// Bitmapet dør, når denne metode returnerer - konsumér det her
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
Streaming af PNG og TIFF uden et fuldside-bitmap
Rendering i bånd hjælper kun, hvis encoderen også er sekventiel, så v3.66.0 tilføjede RenderPageBandedToStream, som skriver PNG eller TIFF direkte til en stream, kaldere ejer. TPdfBandedImageStreamOptions.Default vælger en båndhøjde på 256 rækker, PNG-kompressionsniveau 6 og en MaxOutputBytes på 0, hvilket betyder ubegrænset. Den returnerede TPdfBandedImageReport indeholder Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes og Completed. PeakBandBytes er det tal, du faktisk interesserer dig for, når du dimensionerer et job: Det er Width * BandHeight * 4, så A0-arket ovenfor topper ved cirka 19 MB båndbuffer i stedet for 2 GB sidebuffer
PNG-encoderen er bevidst snæver. Den udsender fast RGB8, skriver en IHDR med bitdybde 8 og farvetype 2, bygger derefter hver scanline med filtertype 0 (ISO/IEC 15948 filtermetode 0, filtertype None) og sender den gennem platformens zlib-kompressionsstream. De komprimerede bytes kommer ud igen som CRC-bærende IDAT-chunks skrevet i rækkefølge. Den interessante begrænsning er streamen under deflate-laget: Den besvarer positionsforespørgsler, fordi kompressionsstreamen spørger efter dem, men ethvert forsøg på en ægte seek rejser en fejl. Det er med vilje. Når en IDAT-chunk og dens CRC er på ledningen, er der ingen vej tilbage for at rette dem, og en lydløs seek ville korrumpere output, der stadig ser strukturelt gyldigt ud
TIFF-encoderen skriver little-endian classic TIFF, byte-order-markeringen II efterfulgt af magic 42, med én strip pr. bånd. Pixels streames først ud, og den ti-poster lange IFD genereres til sidst, når strip-offsets og byteantal er kendt. Kompressionen er tag 259 værdi 1, så der er slet ingen entropy coding: Payloaden er præcis Width * Height * 3 bytes, PhotometricInterpretation er RGB, PlanarConfiguration er chunky, og RowsPerStrip registrerer båndhøjden, mens den sidste korte strip beskrives af sin egen StripByteCounts-post. Båndhøjden ændrer derfor peak-hukommelse og antal strips, men ikke outputstørrelse, hvilket er værd at vide, før du tuner den. Hvis du vil have små filer i stedet for lossless-filer, er per-page-stien i konvertering af PDF-sider til JPEG-billeder med PDFium VCL-komponenten stadig det bedre værktøj
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 banded export?
To lofter begrænser outputtet, og de fejler forskellige steder med vilje. Det første er kalderens budget: MaxOutputBytes håndhæves af en begrænset write-stream, der rejser EPdfError før enhver skrivning, der ville krydse grænsen, så budgettet er et hårdt loft og ikke en rapport bagefter. Det andet er strukturelt. Classic TIFF gemmer strip-offsets som 32-bit-værdier, så BeginImage validerer Width * Height * 3 plus header og directory mod den grænse og afviser jobbet, før en eneste pixel er skrevet; samme kontrol køres mod MaxOutputBytes på forhånd, fordi en TIFF, hvis budget ikke kan dække sin egen pixelpayload, ikke er værd at starte. PNG har ingen tilsvarende grænse, eftersom IDAT-chunks er rent sekventielle, og der ikke er nogen 32-bit-offsettabel, der kan løbe over
Vær klar over, hvad en stoppet export efterlader. Når passet ikke når den sidste række, forbliver Completed False, og encoderen lukkes ned med EndImage(False), som med vilje hverken skriver PNG IEND-chunken eller TIFF-IFD'en. Den delvise fil er derfor ugyldig, og enhver decoder vil sige det, i stedet for at være et plausibelt billede med manglende rækker. Oprydningen er pakket ind, så en sekundær fejl inde i EndImage ikke kan erstatte den oprindelige exception, hvilket er forskellen på en stack trace, der nævner den reelle årsag, og en, der nævner pedellen. Hvis du har brug for progress, der overlever, så checkpoint pr. bånd i din egen callback; caching-teknikkerne på strip-niveau i PDFium Delphis render-cache- og zoom-guide gælder også her
Tilslutning af din egen codec
Når PNG og TIFF ikke er målet, tager RenderPageBandedToEncoder en descendant af TPdfBandedImageEncoder og driver den samme løkke. Livscyklussen er eksplicit og kort: BeginImage(Width, Height), derefter WriteBand(BandIndex, BandTopY, Bitmap) én gang pr. strip i strengt stigende rækkefølge, og til sidst EndImage(Completed), mens GetBytesWritten føder Report.OutputBytes. De indbyggede encodere afviser et bånd i forkert rækkefølge direkte i stedet for at forsøge at buffere det, og enhver encoder, du skriver, bør gøre det samme, fordi en codec, der lydløst omordner strips, producerer en fil, der åbner og lyver. Dette er samlingspunktet til JPEG 2000-tiles, en JPEG-writer, der får én MCU-række-bånd ad gangen, eller et direkte feed til en printspooler
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;
// Giv Bitmap.ScanLine[0 .. Bitmap.Height - 1] til codecen her
Inc(FNextBand);
Result := True;
end;
Én cross-compiler-fælde, der er værd at kende
Zlib-enheden staves forskelligt på hver understøttet toolchain: Delphi XE5 og senere bruger System.ZLib, FPC bruger zstream, og ældre Delphi bruger almindelig ZLib. Det er almindelig conditional compilation. Fælden er, at alle tre eksporterer kompressionsniveaukonstanter med navnet clNone og clDefault, som kolliderer frontalt med TColor-medlemmerne med samme navn i graphics-enheden. Så snart zlib-enheden optræder i implementationens uses-klausul, kan en ukvalificeret clNone i renderkode opløses til et kompressionsniveau i stedet for en farve uden diagnostik. PDFiumPas fastlåser dette med eksplicitte farvesentinel-aliaser, PdfGraphicsColorNone og PdfGraphicsColorDefault, bundet én gang til de fuldt kvalificerede graphics-konstanter og brugt overalt, hvor en renderbaggrund eller farveskema-sentinel sammenlignes. Tre linjer kode, og symbolopløsningen holder op med at drive mellem compilere
Banded rendering ligner en bekvemmelighedsfunktion, lige indtil du møder den side, der ikke kan være i RAM, og så er det den eneste vej, der virker. Den korrigerede band-geometri, de sekventielle PNG- og TIFF-encodere og samlingspunktet for en custom encoder leveres alle som en del af PDFium Delphi-komponenten, med den fulde band-versus-page-pixelsammenligning kørende i regressionspakken på tværs af Delphi, Lazarus og C++Builder