Перша band містила весь drawing, стиснутий в одну strip, а п’ять наступних bands поверталися blank. Це був старий banded export, і PDFiumPas виправив його у v3.66.0: RenderPageBanded тепер передає повну target width і height page у FPDF_RenderPageBitmap на кожній band разом із negative vertical offset, тому native clip записує лише rows current band, а page зберігає full-page coordinate geometry. Use case тут нудний і неминучий. Хтось передає вам E-size plot або stitched panorama page і хоче raster у 600 DPI. ISO A0 sheet у 600 DPI — це 19866 x 28086 pixels, а 32-bit destination bitmap такого розміру потребує трохи більше 2 GB contiguous memory. У 32-bit Delphi allocation просто провалюється. У 64-bit вона достатньо часто вдається, щоб failure став customer problem, а не test problem. Banded rendering існує, щоб peak allocation була одна strip, а не одна page
Чому кожна band містила всю page?
Старий code плутав дві різні pairs arguments у PDFium page-rendering call. FPDF_RenderPageBitmap приймає start_x, start_y, size_x і size_y, де size pair каже, до якого розміру масштабувати всю page, а start pair — де ця scaled page опиняється всередині destination bitmap. Pre-v3.66.0 band loop викликав library RenderPage helper із band top як destination offset і band height як page height. Ці два numbers безпосередньо проходили в native call, тому PDFium масштабував усю page в rectangle лише BandHeight rows заввишки, а потім малював її на y = BandTop у bitmap, який сам мав лише BandHeight rows. Результат передбачуваний, щойно його побачити. Band zero отримувала всю page, вертикально стиснуту до band height. Кожна наступна band отримувала ту саму стиснуту page, pushed below bottom edge bitmap, і тому поверталася як background fill. Bug ховається у випадку, який використовують більшість smoke tests: page, чия render height менша за band height, бо тоді band одна і wrong geometry випадково збігається з правильною. Все, що вище однієї band, одразу це показує
Що гарантує negative offset
Fixed implementation спрямовує кожну band через RenderTile, єдине місце в component, яке вже розуміло цю різницю. RenderTile приймає tile origin у full-page pixel coordinates плюс окремі PageWidth і PageHeight, а PDFium передає -Left і -Top, не змінюючи page size. Negating offset зсуває full-size page вгору, доки потрібна band не опиниться в row zero destination bitmap; PDFium потім native clip-ить за межами bitmap, тому нічого поза band ніколи не rasterize-иться. Page-to-device mapping, описаний в ISO 32000-1 clause 8.3.2, лишається однаковим від першої band до останньої, і саме це головне: band N byte-identical до rows BandTop through BandTop + h single full-page render, а regression suite перевіряє саме це pixel by pixel проти RenderPage output із тими самими dimensions
// Одна band вручну. Destination bitmap має лише BandHeight rows,
// але page target size зберігається full Width x Height
Band := Pdf.RenderTile(0, BandTop, // tile origin у page pixels
Width, BandHeight, // розмір destination bitmap
Width, Height); // розмір full-page target
try
// Band тепер містить rows BandTop .. BandTop + BandHeight - 1 page
finally
Band.Free;
end;
Public band API — це callback loop. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) повертає кількість bands, які справді rendered, або 0, коли arguments rejected, і тримає component render lock увесь pass. Callback signature — TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmap має pf32bit, width Width pixels і height не більше BandHeight, а звільняється одразу після return вашого handler, тож усе, що хочете залишити, потрібно copy-нути. Return False зупиняє pass після current band і дає ту саму cooperative cancellation model, що використовується в cancellable progressive PDF rendering у Delphi, лише на strip granularity, а не PDFium continuation granularity
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 помирає, коли цей method return-иться — спожити його тут
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
Streaming PNG і TIFF без full-page bitmap
Rendering bands допомагає лише тоді, коли encoder також sequential, тому v3.66.0 додав RenderPageBandedToStream, який пише PNG або TIFF прямо в caller stream. TPdfBandedImageStreamOptions.Default встановлює band height 256 rows, PNG compression level 6 і MaxOutputBytes 0, що означає unbounded. Returned TPdfBandedImageReport містить Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes і Completed. PeakBandBytes — число, яке справді потрібно під час sizing job: це Width * BandHeight * 4, тому наведена A0 sheet має peak приблизно 19 MB band buffer замість 2 GB page buffer
PNG encoder навмисно вузький. Він emit-ить fixed RGB8, записує IHDR із bit depth 8 і color type 2, потім будує кожну scanline з filter type 0 (ISO/IEC 15948 filter method 0, filter type None) і проштовхує її через platform zlib compression stream. Compressed bytes виходять як CRC-bearing IDAT chunks, записані по порядку. Цікаве обмеження — stream під deflate layer: він відповідає на position queries, бо compression stream їх запитує, але будь-яка справжня seek піднімає error. Це навмисно. Після того як IDAT chunk та його CRC опинилися on the wire, назад уже не повернутися для fix, а silent seek зіпсував би output, який усе ще виглядає structurally valid
TIFF encoder записує little-endian classic TIFF, byte-order marker II за magic 42, з однією strip на band. Pixels спочатку stream-яться, а ten-entry IFD генерується в кінці, коли відомі strip offsets і byte counts. Compression — tag 259 value 1, тож entropy coding немає: payload рівно Width * Height * 3 bytes, PhotometricInterpretation — RGB, PlanarConfiguration — chunky, а RowsPerStrip записує band height, тоді як final short strip описується власним StripByteCounts entry. Тому band height змінює peak memory і strip count, але не output size, що варто знати перед tuning. Якщо потрібні small files, а не lossless ones, per-page path у конвертації PDF pages у JPEG images із PDFium VCL component залишається кращим tool
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, а не 19866 * 28086 * 4
end;
Де зупиняється banded export?
Два ceilings обмежують output і навмисно дають різні failures. Перший — caller budget: MaxOutputBytes enforcement відбувається через bounded write stream, який піднімає EPdfError до будь-якого write, що перетнув би limit, тому budget є hard cap, а не after-the-fact report. Другий — structural. Classic TIFF зберігає strip offsets як 32-bit values, тому BeginImage перевіряє Width * Height * 3 плюс header і directory проти цього ceiling і відхиляє job до запису першого pixel; та сама check upfront виконується проти MaxOutputBytes, бо TIFF, чий budget не покриває власний pixel payload, не варто починати. PNG еквівалентного limit не має, оскільки IDAT chunks суто sequential і 32-bit offset table, яка могла б переповнитися, відсутня
Тверезо оцініть, що залишає stopped export. Коли pass не доходить до last row, Completed залишається False, а encoder розбирається через EndImage(False), який навмисно не пише ні PNG IEND chunk, ні TIFF IFD. Тому partial file invalid, і кожен decoder так і скаже, замість створити plausibly-looking image із missing rows. Cleanup обгорнутий так, щоб secondary failure всередині EndImage не замінив original exception, і це різниця між stack trace, який називає справжню причину, та trace, який називає janitor. Якщо потрібен progress, що переживає збій, checkpoint-іть per band у власному callback; strip-level caching tactics з PDFium Delphi render cache та zoom guide також застосовні тут
Підключення власного codec
Коли target — не PNG і не TIFF, RenderPageBandedToEncoder приймає descendant TPdfBandedImageEncoder і керує тим самим loop. Lifecycle explicit і короткий: BeginImage(Width, Height), потім WriteBand(BandIndex, BandTopY, Bitmap) один раз на кожну strip у строго ascending order, потім EndImage(Completed), а GetBytesWritten живить Report.OutputBytes. Built-in encoders відразу відхиляють out-of-order band, а не намагаються buffer-ити її, і будь-який ваш encoder має робити так само, бо codec, який тихо reorder-ить strips, створює file, що відкривається й бреше. Це seam для JPEG 2000 tiles, JPEG writer, який отримує по одній MCU row band, або direct feed у 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;
// Передати Bitmap.ScanLine[0 .. Bitmap.Height - 1] у codec тут
Inc(FNextBand);
Result := True;
end;
Одна cross-compiler trap, про яку варто знати
Zlib unit має різне spelling у кожному supported toolchain: Delphi XE5 і newer використовують System.ZLib, FPC — zstream, а older Delphi — plain ZLib. Це звичайна conditional compilation. Trap у тому, що всі три export-ять compression-level constants із names clNone і clDefault, які head-on collid-ять із TColor members із такими самими names у graphics unit. Щойно zlib unit з’являється в implementation uses clause, unqualified clNone у render code може розв’язатися в compression level замість color, без жодного diagnostic. PDFiumPas фіксує це explicit color sentinel aliases — PdfGraphicsColorNone і PdfGraphicsColorDefault, — які один раз bound до fully qualified graphics constants і використовуються всюди, де порівнюється render background або color-scheme sentinel. Три lines code — і symbol resolution перестає drift-ити між compilers
Banded rendering виглядає як convenience feature, доки не зустрінеш page, яка не влазить у RAM, а потім виявляється єдиним working path. Corrected band geometry, sequential PNG і TIFF encoders та custom encoder seam постачаються як частина PDFium Delphi component, а повне band-versus-page pixel comparison запускається в regression suite на Delphi, Lazarus та C++Builder