Първият band държеше цялата drawing смачкана в една strip, а следващите пет bands се връщаха празни. Това беше старият banded export и PDFiumPas го поправи във v3.66.0: RenderPageBanded вече подава full-page target width и height към FPDF_RenderPageBitmap при всеки отделен band, заедно с negative vertical offset, така че native clip да записва само rows на текущия 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 просто fail-ва. В 64-bit тя успява достатъчно често, за да стане проблем на customer, а не на test. Banded rendering съществува, за да е peak allocation една strip, а не една page
Защо всеки band съдържаше цялата page?
Старият code смесваше две различни двойки arguments в PDFium page-rendering call-а. FPDF_RenderPageBitmap приема start_x, start_y, size_x и size_y, където size pair казва колко голяма трябва да бъде цялата page след scaling, а start pair казва къде се поставя scaled page в destination bitmap-а. Преди v3.66.0 band loop-ът извикваше library RenderPage helper-а с band top като destination offset и band height като page height. Тези две numbers отиваха директно към native call-а, така че PDFium scale-ваше цялата page в rectangle с едва BandHeight rows и после я рисуваше при y = BandTop в bitmap, който самият беше само BandHeight rows висок. Резултатът е точно този, който бихте предвидили, щом го видите. Band zero получаваше цялата page, vertical-но смачкана до band height. Всеки следващ band получаваше същата смачкана page, изместена под долния ръб на bitmap-а си, и затова се връщаше като background fill. Bug-ът се крие в единствения случай, който повечето smoke tests използват, page, чиято render height е по-малка от band height, защото тогава има един band и грешната 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 след това clip-ва native спрямо bitmap bounds, така че нищо извън band-а не се rasterize-ва. Page-to-device mapping-ът, описан в ISO 32000-1 clause 8.3.2, остава идентичен от първия band до последния, което е цялата идея: band N е bit-identical на rows BandTop до BandTop + h от един full-page render, а regression suite-ът го assert-ва pixel по pixel спрямо RenderPage output при същите dimensions
// Един band ръчно. Destination bitmap е само BandHeight висок,
// но page target size остава пълният Width x Height
Band := Pdf.RenderTile(0, BandTop, // tile origin в page pixels
Width, BandHeight, // размер на destination bitmap
Width, Height); // размер на target за цялата page
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, които действително е render-нал, или 0, когато arguments са rejected, и държи component render lock-а през целия pass. Callback signature-ът е TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmap-ът е pf32bit, широк Width pixels и не по-висок от BandHeight, а се освобождава веднага след като handler-ът ви върне, така че копирайте всичко, което искате да запазите. Връщането на 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-не - consume-нете го тук
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. Върнатият TPdfBandedImageReport носи Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes и Completed. PeakBandBytes е числото, което действително ви интересува при sizing на job: то е Width * BandHeight * 4, така че A0 sheet-ът по-горе достига приблизително 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-то му са на wire, няма връщане назад, за да ги поправите, а silent seek би повредил output, който все още изглежда structurally valid
TIFF encoder-ът записва little-endian classic TIFF, byte-order mark 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. Ако искате малки файлове, а не lossless, 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-а и нарочно се провалят на различни места. Първият е caller budget: MaxOutputBytes се налага от 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 се пуска предварително срещу MaxOutputBytes, защото TIFF, чийто budget не може да покрие собствения pixel payload, не си струва да започва. PNG няма equivalent limit, защото IDAT chunks са pure sequential и няма 32-bit offset table, която да overflow-не
Бъдете реалисти какво оставя спреният export след себе си. Когато pass-ът не стигне до последния row, Completed остава False и encoder-ът се teardown-ва с EndImage(False), което нарочно не записва нито PNG IEND chunk, нито TIFF IFD. Partial file-ът следователно е invalid и всеки decoder ще го каже, вместо да е правдоподобна image с липсващи rows. Този cleanup е wrapped така, че secondary failure вътре в EndImage да не замени original exception, което е разликата между stack trace, назоваващ истинската причина, и stack trace, назоваващ janitor-а. Ако ви трябва progress, който оцелява, checkpoint-вайте per band вътре в собствения си callback; strip-level caching tactics от PDFium Delphi render cache и zoom guide важат и тук
Включване на собствен codec
Когато PNG и TIFF не са target-ът, RenderPageBandedToEncoder приема descendant на TPdfBandedImageEncoder и задвижва същия loop. Lifecycle-ът е explicit и кратък: BeginImage(Width, Height), после WriteBand(BandIndex, BandTopY, Bitmap) веднъж за всяка strip в строго ascending order, след това EndImage(Completed), като GetBytesWritten подава към Report.OutputBytes. Built-in encoder-ите отхвърлят 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-ът се изписва различно във всеки supported toolchain: Delphi XE5 и по-нови използват System.ZLib, FPC използва zstream, а по-стар Delphi — plain ZLib. Това е обикновена conditional compilation. Trap-ът е, че и трите export-ват compression-level constants с имена clNone и clDefault, които се сблъскват директно с 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, а после е единственият path, който работи. Corrected band geometry, sequential PNG и TIFF encoder-ите и custom encoder seam-ът са част от PDFium Delphi component, с пълното band-versus-page pixel comparison в regression suite през Delphi, Lazarus и C++Builder