Prva traka sadržala je ceo crtež zbijen u jednu prugu, a pet traka posle nje vraćale su se prazne. To je bio stari banded export, a PDFiumPas ga je popravio u v3.66.0: RenderPageBanded sada svakom pozivu FPDF_RenderPageBitmap prosleđuje punu širinu i visinu ciljne stranice, zajedno sa negativnim vertikalnim offset-om, pa native clip upisuje samo redove trenutne trake, dok stranica zadržava svoju punu koordinatnu geometriju. Slučaj upotrebe je dosadan i neizbežan. Neko vam preda E-size crtež ili spojenu panoramsku stranicu i želi raster od 600 DPI. ISO A0 list na 600 DPI ima 19866 x 28086 piksela, a 32-bitna odredišna bitmapа te veličine zauzima nešto više od 2 GB susedne memorije. Na 32-bitnom Delphi-ju ta alokacija jednostavno ne uspeva. Na 64-bitnom uspeva dovoljno često da problem postane problem klijenta, a ne testa. Banded rendering postoji da bi vršna alokacija bila jedna traka, a ne jedna stranica
Zašto je svaka traka sadržala celu stranicu?
Stari kod pomešao je dva različita para argumenata u pozivu za renderovanje PDFium stranice. FPDF_RenderPageBitmap prima start_x, start_y, size_x i size_y, pri čemu par size govori na koju veličinu treba skalirati celu stranicu, a par start govori gde ta skalirana stranica pada unutar odredišne bitmapе. Pre-v3.66.0 band petlja pozivala je library RenderPage helper sa vrhom trake kao destination offset-om i visinom trake kao visinom stranice. Ta dva broja prolazila su pravo do native poziva, pa je PDFium celu stranicu skalirao u pravougaonik visok samo BandHeight redova, a zatim je crtao na y = BandTop unutar bitmapе koja je i sama bila visoka samo BandHeight redova. Rezultat je tačno ono što biste predvideli kada ga jednom vidite. Traka nula dobila je celu stranicu vertikalno zbijenu na visinu trake. Svaka kasnija traka dobila je istu zbijenu stranicu pomerenu ispod donje ivice svoje bitmapе, pa se vratila kao boja pozadine. Bag se skriva u jedinom slučaju koji većina smoke testova koristi, stranici čija je render visina manja od visine trake, jer tada postoji jedna traka i pogrešna geometrija se slučajno poklapa sa ispravnom. Sve više od jedne trake odmah ga otkriva
Šta garantuje negativni offset
Popravljena implementacija svaku traku vodi kroz RenderTile, jedino mesto u komponenti koje je već razumelo tu razliku. RenderTile prima početak tile-a u koordinatama piksela cele stranice i zasebne PageWidth i PageHeight, a PDFium-u predaje -Left i -Top uz nepromenjenu veličinu stranice. Negiranje offset-a pomera stranicu pune veličine nagore dok tražena traka ne sedne u red nula odredišne bitmapе; PDFium zatim native clip-uje prema granicama bitmapе, pa se ništa izvan trake nikada ne rasterizuje. Mapiranje stranice u uređaj opisano u ISO 32000-1 tačka 8.3.2 ostaje identično od prve do poslednje trake, što je cela poenta: traka N bitovski je identična redovima od BandTop do BandTop + h jednog rendera cele stranice, a regression suite to proverava upravo tako, piksel po piksel, prema izlazu RenderPage na istim dimenzijama
// Jedna traka ručno. Odredišna bitmapа visoka je samo BandHeight,
// ali ciljna veličina stranice ostaje puna širina x visina
Band := Pdf.RenderTile(0, BandTop, // početak tile-a u pikselima stranice
Width, BandHeight, // veličina odredišne bitmapе
Width, Height); // ciljna veličina cele stranice
try
// Band sada sadrži redove BandTop .. BandTop + BandHeight - 1 stranice
finally
Band.Free;
end;
Javni band API jeste callback petlja. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) vraća broj traka koje je zaista renderovao ili 0 kada su argumenti odbijeni, a render lock komponente drži tokom celog prolaza. Callback potpis je TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmapа je pf32bit, široka Width piksela i visoka najviše BandHeight, a oslobađa se čim handler vrati rezultat, pa kopirajte sve što nameravate da zadržite. Vraćanje False zaustavlja prolaz posle trenutne trake i daje isti kooperativni model otkazivanja koji koristi otkazivo progresivno renderovanje PDF-a u Delphi-ju, samo na granularnosti trake umesto granularnosti PDFium continuation-a
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 nestaje kada se ova metoda vrati - potroši je ovde
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
Streaming PNG-a i TIFF-a bez bitmapе cele stranice
Renderovanje u trakama pomaže samo ako je i encoder sekvencijalan, pa je v3.66.0 dodao RenderPageBandedToStream, koji PNG ili TIFF upisuje pravo u stream pozivaoca. TPdfBandedImageStreamOptions.Default postavlja početnu visinu trake na 256 redova, PNG compression level 6 i MaxOutputBytes na 0, što znači bez ograničenja. Vraćeni TPdfBandedImageReport nosi Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes i Completed. PeakBandBytes jeste broj koji vas stvarno zanima pri dimenzionisanju posla: Width * BandHeight * 4, pa gornji A0 list vrh dostiže na približno 19 MB bafera trake umesto na 2 GB bafera stranice
PNG encoder je namerno uzak. Emituje fiksni RGB8, upisuje IHDR sa bit depth 8 i color type 2, zatim svaku scanline gradi sa filter type 0 (ISO/IEC 15948 filter method 0, filter type None) i šalje je kroz platform compression stream zlib-a. Kompresovani bajtovi izlaze kao IDAT chunk-ovi sa CRC-jem, upisani redom. Zanimljivo ograničenje je stream ispod deflate sloja: odgovara na upite pozicije jer ih compression stream traži, ali svaki pravi seek podiže grešku. To je namerno. Kada su IDAT chunk i njegov CRC već na žici, nema povratka da se isprave, a tihi seek bi pokvario izlaz koji i dalje izgleda strukturno ispravno
TIFF encoder upisuje little-endian classic TIFF, oznaku redosleda bajtova II praćenu magijom 42, sa jednom trakom po band-u. Pikseli izlaze prvi, a desetounosni IFD generiše se na kraju, kada se saznaju offset-i i brojevi bajtova traka. Kompresija je tag 259 vrednosti 1, pa nema nikakvog entropy coding-a: payload je tačno Width * Height * 3 bajtova, PhotometricInterpretation je RGB, PlanarConfiguration je chunky, a RowsPerStrip beleži visinu trake dok se poslednja kratka traka opisuje sopstvenim StripByteCounts unosom. Visina trake zato menja vršnu memoriju i broj strip-ova, ali ne i veličinu izlaza, što je važno znati pre podešavanja. Ako želite male fajlove umesto lossless izlaza, putanja po stranici u tekstu o pretvaranju PDF stranica u JPEG slike pomoću PDFium VCL komponente ostaje bolji alat
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, a ne 19866 * 28086 * 4
end;
Gde se banded export zaustavlja?
Dva plafona ograničavaju izlaz i namerno otkazuju na različitim mestima. Prvi je budžet pozivaoca: MaxOutputBytes sprovodi bounded write stream koji podiže EPdfError pre svakog upisa koji bi prešao limit, pa je budžet hard cap, a ne naknadni izveštaj. Drugi je strukturalan. Classic TIFF čuva strip offset-e kao 32-bitne vrednosti, pa BeginImage proverava Width * Height * 3 zajedno sa zaglavljem i directory-jem prema tom plafonu i odbija posao pre nego što se upiše ijedan piksel; ista provera radi se unapred prema MaxOutputBytes, jer TIFF čiji budžet ne može da pokrije sopstveni pixel payload ne vredi ni započeti. PNG nema ekvivalentan limit, jer su IDAT chunk-ovi čisto sekvencijalni i nema 32-bitne offset tabele koja bi mogla da se prelije
Budite jasni šta zaustavljen export ostavlja iza sebe. Kada prolaz ne stigne do poslednjeg reda, Completed ostaje False, a encoder se gasi sa EndImage(False), koje namerno ne upisuje ni PNG IEND chunk ni TIFF IFD. Delimični fajl je zato nevažeći i svaki decoder će to reći, umesto da proizvede uverljivu sliku sa nedostajućim redovima. To čišćenje je umotano tako da sekundarni neuspeh unutar EndImage ne može da zameni originalni izuzetak, što je razlika između stack trace-a koji imenuje pravi uzrok i onog koji imenuje čistača. Ako vam treba napredak koji preživljava, checkpoint-ujte po traci unutar sopstvenog callback-a; taktike keširanja na nivou strip-a iz PDFium Delphi vodiča za render cache i zoom važe i ovde
Uključivanje sopstvenog codec-a
Kada PNG i TIFF nisu cilj, RenderPageBandedToEncoder prima naslednika TPdfBandedImageEncoder i pokreće istu petlju. Životni ciklus je eksplicitan i kratak: BeginImage(Width, Height), zatim WriteBand(BandIndex, BandTopY, Bitmap) jednom po stripu strogo rastućim redom, pa EndImage(Completed), dok GetBytesWritten puni Report.OutputBytes. Ugrađeni encoder-i odmah odbijaju traku van redosleda umesto da je baferuju, a svaki encoder koji napišete treba da uradi isto, jer codec koji tiho preuredi strip-ove proizvodi fajl koji se otvara i laže. Ovo je šav za JPEG 2000 tile-ove, JPEG writer koji dobija po jedan MCU row band ili direktan dovod u print spooler
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;
// Prosledi Bitmap.ScanLine[0 .. Bitmap.Height - 1] codec-u ovde
Inc(FNextBand);
Result := True;
end;
Jedna cross-compiler zamka koju vredi znati
Zlib jedinica piše se drugačije na svakom podržanom toolchain-u: Delphi XE5 i noviji koriste System.ZLib, FPC koristi zstream, a stariji Delphi koristi samo ZLib. Toliko je uobičajena conditional compilation. Zamka je u tome što sva tri izvoze konstante nivoa kompresije imena clNone i clDefault, koje se sudaraju sa TColor članovima istog imena u graphics jedinici. Kada se zlib jedinica pojavi u implementation uses klauzuli, nekvalifikovani clNone u render kodu može da se razreši kao compression level umesto kao boja, bez ikakve dijagnostike. PDFiumPas ovo fiksira eksplicitnim alias-ima za color sentinel-e, PdfGraphicsColorNone i PdfGraphicsColorDefault, vezanim jednom za fully qualified graphics konstante i korišćenim svuda gde se poredi render pozadina ili sentinel šeme boja. Tri linije koda i razrešavanje simbola prestaje da driftuje između kompajlera
Banded rendering izgleda kao pogodnost sve dok ne naiđete na stranicu koja ne staje u RAM, a tada je to jedina putanja koja radi. Ispravljena geometrija traka, sekvencijalni PNG i TIFF encoder-i i seam za prilagođeni encoder isporučuju se kao deo PDFium Delphi komponente, uz punu regresionu proveru piksela trake naspram cele stranice kroz Delphi, Lazarus i C++Builder