Band đầu tiên chứa toàn bộ drawing bị ép thành một strip, còn năm band sau trả về trống. Đó là banded export cũ, và PDFiumPas đã sửa trong v3.66.0: RenderPageBanded giờ truyền full-page target width và height vào FPDF_RenderPageBitmap ở từng band, kèm vertical offset âm, để native clip chỉ ghi các row của band hiện tại trong khi page vẫn giữ full-page coordinate geometry. Use case đứng sau chuyện này khá buồn tẻ nhưng không thể tránh. Ai đó đưa cho bạn một E-size plot hoặc panorama page ghép nối và muốn raster 600 DPI. Một sheet ISO A0 ở 600 DPI là 19866 x 28086 pixel, còn destination bitmap 32-bit cỡ đó cần hơn 2 GB contiguous memory. Trên Delphi 32-bit allocation đó đơn giản là fail. Trên 64-bit nó thành công đủ thường xuyên để failure biến thành vấn đề của customer thay vì của test. Banded rendering tồn tại để peak allocation là một strip chứ không phải cả page
Vì sao mỗi band lại chứa cả page?
Code cũ nhầm hai cặp argument khác nhau trong lời gọi render page của PDFium. FPDF_RenderPageBitmap nhận start_x, start_y, size_x và size_y, trong đó cặp size nói page hoàn chỉnh được scale lớn đến đâu, còn cặp start nói page đã scale nằm ở đâu trong destination bitmap. Band loop trước v3.66.0 gọi helper RenderPage của library với band top làm destination offset và band height làm page height. Hai số đó đi thẳng vào native call, nên PDFium scale toàn page vào rectangle chỉ cao BandHeight row rồi vẽ nó tại y = BandTop trong bitmap vốn cũng chỉ cao BandHeight row. Khi đã thấy geometry, kết quả đúng như dự đoán. Band zero nhận cả page bị ép dọc vào band height. Mọi band sau nhận cùng page bị ép đó nhưng bị đẩy xuống dưới đáy bitmap, nên trả về background fill. Bug ẩn trong case smoke test thường dùng nhất, page có render height nhỏ hơn band height vì khi đó chỉ có một band và geometry sai tình cờ trùng geometry đúng. Bất cứ page nào cao hơn một band sẽ lộ ngay
Offset âm bảo đảm điều gì?
Implementation đã sửa route mọi band qua RenderTile, nơi duy nhất trong component vốn hiểu distinction này. RenderTile nhận tile origin theo full-page pixel coordinate cùng PageWidth và PageHeight riêng, rồi đưa cho PDFium -Left và -Top trong khi giữ nguyên page size. Negate offset sẽ trượt page full-size lên trên cho tới khi band yêu cầu nằm ở row zero của destination bitmap; PDFium sau đó native clip theo bounds bitmap nên không rasterize gì ngoài band. Page-to-device mapping mô tả trong ISO 32000-1 clause 8.3.2 giữ nguyên từ band đầu tới band cuối, đó mới là mục tiêu: band N bit-identical với row BandTop tới BandTop + h của một full-page render duy nhất, và regression suite assert đúng điều đó từng pixel so với output RenderPage ở cùng dimension
// Một band viết thủ công. Destination bitmap chỉ cao BandHeight row,
// nhưng page target size vẫn là full Width x Height
Band := Pdf.RenderTile(0, BandTop, // tile origin theo page pixel
Width, BandHeight, // kích thước destination bitmap
Width, Height); // target size toàn page
try
// Band giờ chứa row BandTop .. BandTop + BandHeight - 1 của page
finally
Band.Free;
end;
Public band API là một callback loop. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) trả về số band thực sự render, hoặc 0 khi argument bị reject, và giữ component render lock trong toàn pass. Callback signature là TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmap là pf32bit, rộng Width pixel và không cao quá BandHeight, được free ngay khi handler return, nên hãy copy thứ bạn định giữ. Trả về False sẽ dừng pass sau band hiện tại, cho bạn cùng cancellation model cooperative mà progressive PDF rendering có thể hủy trong Delphi dùng, chỉ ở granularity strip thay vì granularity continuation của 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 chết khi method return - consume nó ngay tại đây
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
Stream PNG và TIFF mà không cần bitmap toàn page
Render theo band chỉ có ích nếu encoder cũng tuần tự, nên v3.66.0 thêm RenderPageBandedToStream, ghi PNG hoặc TIFF thẳng vào caller stream. TPdfBandedImageStreamOptions.Default khởi tạo band height 256 row, PNG compression level 6 và MaxOutputBytes bằng 0, nghĩa là không giới hạn. TPdfBandedImageReport trả về Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes và Completed. PeakBandBytes là con số bạn thực sự cần khi sizing job: nó là Width * BandHeight * 4, nên sheet A0 ở trên peak khoảng 19 MB band buffer thay vì 2 GB page buffer
PNG encoder cố ý hẹp. Nó emit fixed RGB8, ghi IHDR với bit depth 8 và color type 2, rồi dựng từng scanline bằng filter type 0 (ISO/IEC 15948 filter method 0, filter type None) và đẩy qua platform zlib compression stream. Compressed byte đi ra dưới dạng IDAT chunk có CRC, ghi theo thứ tự. Constraint thú vị nằm ở stream dưới deflate layer: nó trả lời position query vì compression stream hỏi, nhưng mọi attempt seek thật đều raise error. Điều này có chủ ý. Một khi IDAT chunk và CRC đã ra wire thì không thể quay lại sửa, còn seek im lặng sẽ corrupt output vẫn trông có cấu trúc hợp lệ
TIFF encoder ghi classic TIFF little-endian, byte-order mark II theo sau bởi magic 42, mỗi band là một strip. Pixel stream ra trước còn IFD mười entry được dựng ở cuối, sau khi biết strip offset và byte count. Compression là tag 259 value 1, nên hoàn toàn không entropy coding: payload đúng bằng Width * Height * 3 byte, PhotometricInterpretation là RGB, PlanarConfiguration là chunky, còn RowsPerStrip ghi band height trong khi short strip cuối được mô tả bởi StripByteCounts entry riêng. Vì vậy band height chỉ thay đổi peak memory và số strip chứ không thay đổi output size, đáng biết trước khi tune. Nếu bạn muốn file nhỏ thay vì lossless, page path cho từng page trong chuyển PDF page thành JPEG bằng PDFium VCL component vẫn là công cụ phù hợp hơn
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, không phải 19866 * 28086 * 4
end;
Banded export dừng ở đâu?
Hai ceiling giới hạn output và cố ý fail ở hai nơi khác nhau. Đầu tiên là budget của caller: MaxOutputBytes được enforce bởi bounded write stream, raise EPdfError trước mọi write có thể vượt limit, nên budget là hard cap chứ không phải report sau sự việc. Thứ hai là giới hạn cấu trúc. Classic TIFF lưu strip offset bằng value 32-bit, nên BeginImage validate Width * Height * 3 cộng header và directory so với ceiling đó rồi reject job trước khi ghi một pixel; check tương tự chạy trước với MaxOutputBytes vì TIFF có budget không đủ chứa pixel payload của chính nó thì không đáng start. PNG không có limit tương đương vì IDAT chunk thuần tuần tự và không có offset table 32-bit để overflow
Cần nhìn rõ thứ export bị dừng để lại. Khi pass chưa tới row cuối, Completed vẫn False và encoder bị tear down bằng EndImage(False), cố ý không ghi PNG IEND chunk hay TIFF IFD. Partial file vì thế invalid và decoder nào cũng nói vậy, thay vì image nhìn có vẻ hợp lệ nhưng thiếu row. Cleanup đó được wrap để secondary failure bên trong EndImage không thay thế exception gốc, khác biệt giữa stack trace nêu nguyên nhân thật và stack trace nêu janitor. Nếu cần progress có thể resume, hãy checkpoint theo từng band trong callback của bạn; strip-level caching tactic trong hướng dẫn PDFium Delphi render cache và zoom cũng áp dụng ở đây
Cắm codec của riêng bạn
Khi PNG và TIFF không phải target, RenderPageBandedToEncoder nhận descendant TPdfBandedImageEncoder và drive cùng loop. Lifecycle rõ ràng và ngắn: BeginImage(Width, Height), rồi WriteBand(BandIndex, BandTopY, Bitmap) một lần mỗi strip theo thứ tự tăng nghiêm ngặt, sau cùng EndImage(Completed), còn GetBytesWritten cấp dữ liệu cho Report.OutputBytes. Built-in encoder reject thẳng band out-of-order thay vì cố buffer, và encoder bạn viết cũng nên vậy, vì codec âm thầm reorder strip sẽ tạo file mở được nhưng nói dối. Đây là seam dùng cho JPEG 2000 tile, JPEG writer nhận từng MCU row band hoặc feed trực tiếp vào 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;
// Feed Bitmap.ScanLine[0 .. Bitmap.Height - 1] cho codec tại đây
Inc(FNextBand);
Result := True;
end;
Một bẫy cross-compiler đáng biết
Unit zlib được viết khác nhau trên từng toolchain hỗ trợ: Delphi XE5 trở đi dùng System.ZLib, FPC dùng zstream, còn Delphi cũ dùng plain ZLib. Conditional compilation đó là chuyện thường. Bẫy nằm ở chỗ cả ba đều export compression-level constant tên clNone và clDefault, đụng thẳng với member cùng tên của TColor trong graphics unit. Khi zlib unit xuất hiện trong implementation uses clause, clNone không qualify trong render code có thể resolve thành compression level thay vì color mà không diagnostic. PDFiumPas chốt điều này bằng alias sentinel color tường minh, PdfGraphicsColorNone và PdfGraphicsColorDefault, bind một lần vào graphics constant fully qualified rồi dùng ở mọi nơi so sánh render background hoặc color-scheme sentinel. Ba dòng code, và symbol resolution dừng drift giữa các compiler
Banded rendering trông như convenience feature cho tới khi gặp page không thể vừa trong RAM, lúc đó nó là path duy nhất hoạt động. Geometry band đã sửa, PNG và TIFF encoder tuần tự cùng custom encoder seam đều nằm trong PDFium Delphi component, với full band-versus-page pixel comparison chạy trong regression suite trên Delphi, Lazarus và C++Builder