Первая полоса содержала весь рисунок, сжатый в один strip, а пять следующих вернулись пустыми. Так выглядел старый banded export, и PDFiumPas исправил его в v3.66.0: RenderPageBanded теперь передаёт FPDF_RenderPageBitmap полные width и height целевой страницы для каждой полосы, вместе с отрицательным вертикальным offset, поэтому native clip записывает только строки текущей полосы, а страница сохраняет свою полную coordinate geometry. Сценарий, ради которого всё это нужно, скучен и неизбежен. Кто-то передаёт вам plot формата E или stitched panorama page и хочет raster с разрешением 600 DPI. Лист ISO A0 при 600 DPI имеет размер 19866 x 28086 пикселей, а 32-битный destination bitmap такого размера требует чуть больше 2 ГБ contiguous memory. В 32-битном Delphi такая allocation просто проваливается. В 64-битном она достаточно часто проходит, чтобы проблема стала проблемой заказчика, а не теста. Banded rendering существует для того, чтобы peak allocation занимала одна полоса, а не целая страница
Почему каждая полоса содержала всю страницу?
Старый код перепутал две разные пары аргументов в вызове рендеринга страницы PDFium. FPDF_RenderPageBitmap принимает start_x, start_y, size_x и size_y, где пара size задаёт, до какого размера масштабируется вся страница, а пара start — куда эта масштабированная страница попадает внутри destination bitmap. Цикл до v3.66.0 вызывал helper библиотеки RenderPage, передавая top полосы как destination offset и height полосы как page height. Эти два числа напрямую проходили в native call, поэтому PDFium масштабировал всю страницу в rectangle высотой всего BandHeight строк, а затем рисовал её с y = BandTop внутри bitmap, который сам был высотой только BandHeight. Результат ровно такой, какой предсказываешь, когда его видишь. Band zero получала всю страницу, вертикально сжатую до высоты полосы. Каждая следующая band получала ту же сжатую страницу, сдвинутую ниже нижнего края bitmap, поэтому возвращала фон. Ошибка скрывается в единственном случае, который используют большинство smoke tests: высота рендера страницы меньше высоты полосы, поэтому получается одна полоса и неправильная geometry случайно совпадает с правильной. Всё выше одной полосы раскрывает её немедленно
Что гарантирует отрицательное смещение
Исправленная реализация направляет каждую band через RenderTile, единственное место компонента, уже понимающее это различие. RenderTile принимает origin tile в полных page pixel coordinates плюс отдельные PageWidth и PageHeight, а PDFium получает -Left и -Top при неизменном размере страницы. Инверсия offset сдвигает страницу полного размера вверх, пока запрошенная band не окажется в строке zero destination bitmap; затем PDFium нативно обрезает всё по границам bitmap, поэтому за пределами полосы ничего не растеризуется. Page-to-device mapping из ISO 32000-1 clause 8.3.2 остаётся одинаковым от первой band до последней — в этом весь смысл: band N побайтно совпадает со строками от BandTop до BandTop + h одного full-page render, и regression suite проверяет это ровно так, pixel by pixel, против output RenderPage тех же размеров
// Одна полоса вручную. Destination bitmap имеет высоту только BandHeight,
// но target size страницы остаётся полным Width x Height
Band := Pdf.RenderTile(0, BandTop, // origin tile в page pixels
Width, BandHeight, // размер destination bitmap
Width, Height); // target size полной страницы
try
// Band теперь содержит строки BandTop .. BandTop + BandHeight - 1 страницы
finally
Band.Free;
end;
Public band API — это callback loop. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) возвращает число фактически отрендеренных bands или 0, когда аргументы отклонены, и удерживает render lock компонента весь pass. Сигнатура callback — TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmap имеет формат pf32bit, ширину Width и высоту не больше BandHeight, а освобождается сразу после возврата вашего handler, поэтому всё, что нужно сохранить, надо скопировать. Возврат False останавливает pass после текущей band, что даёт ту же модель cooperative cancellation, которую использует отменяемый progressive PDF rendering в Delphi, только на уровне strip, а не continuation 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
// Bitmap исчезает при возврате из этого метода — обработать его здесь
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
Потоковый PNG и TIFF без bitmap полной страницы
Рендеринг полосами помогает только при последовательном encoder, поэтому в v3.66.0 появился RenderPageBandedToStream, записывающий PNG или TIFF прямо в stream вызывающего. TPdfBandedImageStreamOptions.Default задаёт высоту band 256 строк, уровень PNG compression 6 и MaxOutputBytes 0, то есть отсутствие ограничения. Возвращаемый TPdfBandedImageReport содержит Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes и Completed. PeakBandBytes — число, которое нужно учитывать при планировании задания: это Width * BandHeight * 4, поэтому у листа A0 выше пик составляет примерно 19 МБ band buffer вместо 2 ГБ page buffer
PNG encoder намеренно узок. Он выдаёт fixed RGB8, записывая IHDR с bit depth 8 и color type 2, затем строит каждую scanline с filter type 0 (filter method 0 ISO/IEC 15948, filter type None) и передаёт её через platform zlib compression stream. Сжатые байты выходят как CRC-bearing IDAT chunks, записанные по порядку. Интересное ограничение связано с stream под deflate layer: он отвечает на position queries, потому что их задаёт compression stream, но любая настоящая попытка seek вызывает ошибку. Это сделано намеренно. После того как IDAT chunk и его CRC ушли в поток, вернуться и исправить их невозможно, а тихий seek испортил бы output, который всё ещё выглядел бы структурно корректным
TIFF encoder записывает classic TIFF в little-endian: byte order mark II, за которым следует magic 42, с одной strip на каждую band. Сначала выходят pixels, а десятиэлементный IFD генерируется в конце, когда известны offsets и byte counts strips. Compression — tag 259 со значением 1, поэтому entropy coding вообще нет: payload ровно равен Width * Height * 3 байтам, PhotometricInterpretation — RGB, PlanarConfiguration — chunky, а RowsPerStrip фиксирует высоту band, при этом последняя короткая strip описывается собственной записью StripByteCounts. Поэтому высота band меняет peak memory и число strips, но не размер output, что стоит знать до настройки. Если нужны маленькие файлы, а не lossless, per-page path из статьи о преобразовании страниц PDF в JPEG-изображения с 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, а не 19866 * 28086 * 4
end;
Где останавливается banded export?
Output ограничивают два потолка, и намеренно они срабатывают в разных местах. Первый — budget вызывающего: MaxOutputBytes контролируется bounded write stream, который вызывает EPdfError до любой записи, пересекающей лимит, поэтому budget является жёстким cap, а не отчётом постфактум. Второй потолок структурный. Classic TIFF хранит strip offsets как 32-битные значения, поэтому BeginImage проверяет Width * Height * 3 вместе с header и directory против этого ceiling и отклоняет job до записи единственного пикселя; та же проверка заранее выполняется против MaxOutputBytes, поскольку TIFF, чей budget не покрывает собственный pixel payload, начинать не стоит. Для PNG эквивалентного лимита нет, потому что IDAT chunks идут последовательно и 32-битной таблице offsets нечего переполнять
Трезво учитывайте, что оставляет остановленный export. Если pass не доходит до последней строки, Completed остаётся False, а encoder завершается через EndImage(False), намеренно не записывая ни PNG IEND chunk, ни TIFF IFD. Поэтому partial file недействителен, и каждый decoder сообщит об этом, вместо того чтобы показать правдоподобную картинку с пропущенными строками. Эта очистка обёрнута так, чтобы вторичный failure внутри EndImage не заменил исходное exception, и это разница между stack trace, называющим настоящую причину, и stack trace, называющим уборщика. Если нужен сохраняемый progress, создавайте checkpoint на band внутри собственного callback; приёмы strip-level caching из руководства по PDFium Delphi render cache и zoom здесь тоже применимы
Подключение собственного codec
Когда целью является не PNG и TIFF, RenderPageBandedToEncoder принимает descendant TPdfBandedImageEncoder и запускает тот же цикл. Lifecycle явно задан и короток: BeginImage(Width, Height), затем по одному WriteBand(BandIndex, BandTopY, Bitmap) для каждой strip в строго возрастающем порядке, затем EndImage(Completed), а GetBytesWritten заполняет Report.OutputBytes. Встроенные encoders сразу отклоняют band вне порядка, а не пытаются буферизовать её, и любой ваш encoder должен делать то же, потому что codec, молча переставляющий strips, создаёт файл, который открывается и врёт. Это seam для JPEG 2000 tiles, JPEG writer, получающего по одной MCU row band за раз, или прямой передачи в 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 ловушка, о которой стоит знать
Модуль zlib называется по-разному в каждой поддерживаемой toolchain: Delphi XE5 и новее используют System.ZLib, FPC — zstream, а старый Delphi — обычный ZLib. Это обычная conditional compilation. Ловушка в том, что все три экспортируют constants уровней compression с именами clNone и clDefault, которые лоб в лоб сталкиваются с членами TColor тех же имён в graphics unit. Как только zlib unit появляется в implementation uses clause, неквалифицированный clNone в render code может разрешиться в уровень compression вместо цвета, и compiler ничего не сообщит. PDFiumPas фиксирует это явными aliases цветовых sentinel, PdfGraphicsColorNone и PdfGraphicsColorDefault, однажды привязанными к fully qualified graphics constants и используемыми везде, где сравниваются render background или color-scheme sentinel. Три строки кода, и resolution символов перестаёт плавать между compilers
Banded rendering выглядит как удобная возможность ровно до страницы, которая не помещается в RAM, после чего это единственный работающий путь. Исправленная geometry полос, последовательные PNG и TIFF encoders и seam для custom encoder входят в PDFium-компонент для Delphi, а полное pixel comparison band против страницы работает в regression suite для Delphi, Lazarus и C++Builder