První pás obsahoval celý výkres namačkaný do jediného proužku a pět pásů po něm se vrátilo prázdných. Tak vypadal starý banded export a PDFiumPas ho ve v3.66.0 opravil: RenderPageBanded nyní při každém jednotlivém pásu předává do FPDF_RenderPageBitmap plnou cílovou šířku i výšku stránky společně se záporným vertikálním offsetem, takže nativní clip zapíše pouze řádky aktuálního pásu, zatímco stránka si ponechá plnou souřadnicovou geometrii. Use case za tím vším je nudný a nevyhnutelný. Někdo vám předá výkres formátu E nebo slepenou panoramatickou stránku a chce z ní raster v 600 DPI. List ISO A0 v 600 DPI má 19866 x 28086 pixelů a 32bitová cílová bitmapa takové velikosti je o něco větší než 2 GB souvislé paměti. V 32bitovém Delphi tato alokace prostě selže. V 64bitovém uspěje dost často na to, aby se problém stal zákaznickým, nikoli testovacím. Banded rendering existuje proto, aby špičková alokace byla jeden pás, ne jedna stránka
Proč každý pás obsahoval celou stránku
Starý kód zaměnil dvě různé dvojice argumentů v renderovacím volání PDFium stránky. FPDF_RenderPageBitmap přijímá start_x, start_y, size_x a size_y, přičemž dvojice size říká, na jak velkou plochu se má celá stránka škálovat, a dvojice start říká, kam tato škálovaná stránka dopadne uvnitř cílové bitmapy. Smyčka před v3.66.0 volala helper knihovny RenderPage s horním okrajem pásu jako destination offsetem a výškou pásu jako výškou stránky. Tato dvě čísla prošla přímo do nativního volání, takže PDFium škálovalo celou stránku do obdélníku vysokého jen BandHeight řádků a potom ji kreslilo na y = BandTop uvnitř bitmapy, která sama měla pouze BandHeight řádků. Jakmile geometrii uvidíte, výsledek je přesně předvídatelný. Pás nula dostal celou stránku vertikálně namačkanou do výšky pásu. Každý pozdější pás dostal stejnou namačkanou stránku posunutou pod dolní hranu své bitmapy, takže se vrátil jako výplň pozadí. Chyba se skrývá v jediném případě, který používá většina smoke testů, na stránce, jejíž výška renderu je menší než výška pásu, protože pak existuje jediný pás a špatná geometrie se náhodou shoduje se správnou. Cokoli vyšší než jeden pás ji odhalí okamžitě
Co zaručuje záporný offset
Opravená implementace posílá každý pás přes RenderTile, jediné místo v komponentě, které už tento rozdíl chápalo. RenderTile přijímá počátek tile v pixelech celé stránky plus samostatné PageWidth a PageHeight a PDFiumu předá -Left a -Top, přičemž velikost stránky ponechá beze změny. Negace offsetu posune stránku plné velikosti nahoru tak, aby požadovaný pás seděl na řádku nula cílové bitmapy; PDFium potom nativně clipuje proti hranicím bitmapy, takže nic mimo pás se nikdy nerasterizuje. Mapování page-to-device popsané v ISO 32000-1 clause 8.3.2 zůstane stejné od prvního pásu po poslední, a právě o to jde: pás N je bitově identický řádkům BandTop až BandTop + h jediného renderu celé stránky a regresní sada to přesně assertuje pixel po pixelu proti výstupu RenderPage ve stejných rozměrech
// Jeden pás ručně. Cílová bitmapa je vysoká jen BandHeight,
// ale cílová velikost stránky zůstává plná Width x Height
Band := Pdf.RenderTile(0, BandTop, // počátek tile v pixelech stránky
Width, BandHeight, // velikost cílové bitmapy
Width, Height); // cílová velikost celé stránky
try
// Band nyní obsahuje řádky BandTop .. BandTop + BandHeight - 1 stránky
finally
Band.Free;
end;
Veřejné band API je callback loop. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) vrátí počet pásů, které skutečně vyrenderovalo, nebo 0 při odmítnutí argumentů, a po celý průchod drží render lock komponenty. Signatura callbacku je TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmapa je pf32bit, široká Width pixelů a ne vyšší než BandHeight, a po návratu handleru se uvolní, takže cokoli, co chcete uchovat, musíte zkopírovat. Návrat False zastaví průchod po aktuálním pásu a dá vám stejný kooperativní model rušení jako zrušitelné progresivní renderování PDF v Delphi, jen na úrovni proužku místo granularit pokračování PDFium
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
// Bitmapa zanikne po návratu z této metody - spotřebuj ji zde
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
Streamování PNG a TIFF bez bitmapy celé stránky
Renderování po pásech pomůže jen tehdy, když je sekvenční i encoder, proto v3.66.0 přidalo RenderPageBandedToStream, který zapisuje PNG nebo TIFF přímo do streamu volajícího. TPdfBandedImageStreamOptions.Default nastaví výšku pásu 256 řádků, kompresní úroveň PNG 6 a MaxOutputBytes 0, což znamená bez omezení. Vrácený TPdfBandedImageReport nese Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes a Completed. PeakBandBytes je číslo, které vás při dimenzování úlohy skutečně zajímá: je to Width * BandHeight * 4, takže výše uvedený list A0 vrcholí přibližně na 19 MB bufferu pásu místo 2 GB bufferu stránky
PNG encoder je záměrně úzký. Emituje fixed RGB8, zapíše IHDR s bit depth 8 a color type 2, potom sestaví každý scanline s filter type 0 (filter method ISO/IEC 15948 0, filter type None) a pošle ho přes platformní kompresní stream zlib. Komprimované bajty vyjdou jako IDAT chunks s CRC zapsané v pořadí. Zajímavé omezení je stream pod deflate vrstvou: odpovídá na dotazy na pozici, protože se na ni kompresní stream ptá, ale každý skutečný seek vyvolá chybu. Jakmile je IDAT chunk a jeho CRC na drátě, není cesty zpět k jejich opravě a tichý seek by poškodil výstup, který stále vypadá strukturálně validně
TIFF encoder zapisuje klasický TIFF little-endian, marker pořadí bajtů II následovaný magickou hodnotou 42, s jedním stripem na pás. Pixely proudí ven jako první a desetipoložkový IFD se vytvoří na konci, až jsou známé offsety stripů a jejich počty bajtů. Komprese je tag 259 value 1, takže neprobíhá žádné entropy coding: payload má přesně Width * Height * 3 bajtů, PhotometricInterpretation je RGB, PlanarConfiguration je chunky a RowsPerStrip zaznamená výšku pásu, zatímco poslední krátký strip popíše vlastní položka StripByteCounts. Výška pásu proto mění špičkovou paměť a počet stripů, ale ne velikost výstupu, což je dobré vědět před laděním. Pokud chcete malé soubory místo bezztrátových, lepším nástrojem zůstává page path v převodu PDF stránek na obrázky JPEG pomocí PDFium VCL component
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, ne 19866 * 28086 * 4
end;
Kde se banded export zastaví
Výstup omezují dva stropy a záměrně selhávají na různých místech. Prvním je budget volajícího: MaxOutputBytes vynucuje bounded write stream, který vyvolá EPdfError před zápisem, jenž by limit překročil, takže budget je tvrdý strop místo reportu po faktu. Druhý je strukturální. Klasický TIFF ukládá offsety stripů jako 32bitové hodnoty, takže BeginImage ověří Width * Height * 3 plus hlavičku a directory proti tomuto limitu a úlohu odmítne před zápisem jediného pixelu; stejná kontrola se předem provede proti MaxOutputBytes, protože TIFF, jehož budget nepokryje vlastní pixelový payload, nemá smysl začínat. PNG ekvivalentní limit nemá, protože IDAT chunky jsou čistě sekvenční a neexistuje tabulka 32bitových offsetů, která by přetekla
Ukončený export si neidealizujte. Když průchod nedojde k poslednímu řádku, Completed zůstane False a encoder se zruší přes EndImage(False), který záměrně nezapíše ani PNG chunk IEND, ani TIFF IFD. Částečný soubor je proto neplatný a každý decoder to řekne místo plausibilně vypadajícího obrázku s chybějícími řádky. Tento cleanup je obalen tak, aby sekundární selhání uvnitř EndImage nenahradilo původní výjimku, což je rozdíl mezi stack tracem pojmenovávajícím skutečnou příčinu a stack tracem pojmenovávajícím uklízeče. Pokud potřebujete přežívající progress, checkpointujte po pásech ve vlastním callbacku; taktiky cacheování na úrovni stripů v průvodci render cache a zoomem PDFium Delphi platí i zde
Napojení vlastního kodeku
Když PNG a TIFF nejsou cílem, RenderPageBandedToEncoder přijímá potomka TPdfBandedImageEncoder a pohání stejnou smyčku. Lifecycle je explicitní a krátký: BeginImage(Width, Height), potom jednou na každý strip v přísně vzestupném pořadí WriteBand(BandIndex, BandTopY, Bitmap), nakonec EndImage(Completed), přičemž GetBytesWritten napájí Report.OutputBytes. Vestavěné encodery out-of-order pás rovnou odmítnou místo pokusu o bufferování a stejně byste to měli udělat ve vlastním encoderu, protože kodek, který potichu přerovná stripy, vyrobí soubor, který se otevře a lže. Toto je seam pro JPEG 2000 tiles, JPEG writer krmený po jednom MCU row bandu nebo přímý feed do print spooleru
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;
// Předej Bitmap.ScanLine[0 .. Bitmap.Height - 1] kodeku zde
Inc(FNextBand);
Result := True;
end;
Jedna past cross-compileru, kterou stojí za to znát
Jednotka zlib se v každém podporovaném toolchainu jmenuje jinak: Delphi XE5 a novější používá System.ZLib, FPC zstream a starší Delphi prosté ZLib. To je běžná conditional compilation. Past spočívá v tom, že všechny tři exportují kompresní konstanty s názvy clNone a clDefault, které se čelně střetnou se členy TColor stejného jména v graphics unitě. Jakmile se zlib unit objeví v implementation uses clause, nequalifikované clNone v renderovacím kódu může resolveovat na compression level místo barvy bez jediné diagnostiky. PDFiumPas to fixuje explicitními aliasy barevných sentinelů, PdfGraphicsColorNone a PdfGraphicsColorDefault, jednou navázanými na plně kvalifikované graphics konstanty a používanými všude, kde se porovnává sentinel pozadí renderu nebo barevného schématu. Tři řádky kódu a symbol resolution se mezi kompilátory přestane posouvat
Banded rendering vypadá jako convenience feature až do chvíle, kdy narazíte na stránku, která se nevejde do RAM, a pak je jedinou cestou, která funguje. Opravená geometrie pásů, sekvenční PNG a TIFF encodery i seam vlastního encoderu jsou součástí PDFium Delphi component s úplným pixelovým porovnáním band-versus-page v regresní sadě napříč Delphi, Lazarus a C++Builderem