band แรกเคยเก็บภาพทั้ง drawing ไว้ใน strip เดียวจนถูกบีบ และ band อีกห้าอันกลับมาว่างเปล่า นั่นคือ banded export แบบเดิม และ PDFiumPas แก้ใน v3.66.0 ตอนนี้ RenderPageBanded ส่ง full-page target width กับ height ให้ FPDF_RenderPageBitmap ในทุก band พร้อม negative vertical offset ทำให้ native clip เขียนเฉพาะ row ของ band ปัจจุบัน ขณะที่ page ยังคง full-page coordinate geometry ไว้ use case ทั้งหมดนี้ธรรมดาแต่หลีกเลี่ยงไม่ได้ มีคนส่ง E-size plot หรือ panorama page ที่ stitch แล้วมาให้และต้องการ raster 600 DPI จากมัน ISO A0 sheet ที่ 600 DPI มีขนาด 19866 x 28086 pixel และ destination bitmap แบบ 32-bit จะใช้ contiguous memory มากกว่า 2 GB เล็กน้อย บน Delphi 32-bit allocation นี้ fail ทันที บน 64-bit มันสำเร็จบ่อยพอจะเปลี่ยน failure ให้กลายเป็นปัญหาของลูกค้าแทน test banded rendering มีไว้ให้ peak allocation เป็น strip เดียว ไม่ใช่ page เดียว
ทำไมทุก band จึงมีทั้ง page
code เดิมสับสน argument สองคู่ที่ต่างหน้าที่กันใน call สำหรับ render page ของ PDFium FPDF_RenderPageBitmap รับ start_x, start_y, size_x และ size_y โดย size pair บอกว่า page ทั้งหน้าควรถูก scale ใหญ่เท่าไร ส่วน start pair บอกว่า page ที่ scale แล้วจะวางตรงไหนใน destination bitmap ก่อน v3.66.0 band loop เรียก library RenderPage helper ด้วย band top เป็น destination offset และ band height เป็น page height ตัวเลขสองค่านี้ไหลตรงเข้า native call ดังนั้น PDFium จึง scale page ทั้งหน้าเข้า rectangle ที่สูงเพียง BandHeight row แล้ววาดที่ y = BandTop ภายใน bitmap ที่ตัวมันเองสูงแค่ BandHeight row ผลลัพธ์ตรงตามที่คาดได้เมื่อมองเห็น geometry แล้ว band zero ได้ page ทั้งหน้าที่ถูกบีบในแนวตั้งจนเท่าความสูง band ส่วน band หลังจากนั้นได้ page ที่ถูกบีบแบบเดียวกันแต่ถูกเลื่อนลงต่ำกว่าขอบ bitmap จึงคืนมาเป็น background fill bug นี้ซ่อนอยู่ในกรณีเดียวที่ smoke test ใช้บ่อยที่สุด คือ page ที่ render สูงน้อยกว่าความสูง band เพราะมี band เดียวและ geometry ที่ผิดบังเอิญตรงกับ geometry ที่ถูก ทุกอย่างที่สูงเกินหนึ่ง band จะเปิดเผยทันที
negative offset รับประกันอะไร
implementation ที่แก้แล้ว route ทุก band ผ่าน RenderTile ซึ่งเป็นจุดเดียวใน component ที่เข้าใจความแตกต่างนี้อยู่แล้ว RenderTile รับ tile origin ใน full-page pixel coordinate พร้อม PageWidth และ PageHeight ที่แยกกัน แล้วส่ง -Left กับ -Top ให้ PDFium โดยไม่แตะ page size การทำ offset ให้ติดลบจะเลื่อน full-size page ขึ้นจน band ที่ขออยู่ที่ row zero ของ destination bitmap จากนั้น PDFium จะ clip ตามขอบ bitmap ด้วย native code ดังนั้นสิ่งนอก band จะไม่ถูก rasterize เลย page-to-device mapping ตาม ISO 32000-1 clause 8.3.2 จึงเหมือนเดิมจาก band แรกถึง band สุดท้าย นี่คือหัวใจ band N จะ bit-identical กับ row BandTop ถึง BandTop + h ของ full-page render เดียว และ regression suite assert เรื่องนี้ทีละ pixel เทียบกับ output จาก RenderPage ที่มี dimension เดียวกัน
// ทำ band หนึ่งตัวด้วยมือ destination bitmap สูงเพียง BandHeight
// แต่ page target size ยังคงเป็น Width x Height เต็มหน้า
Band := Pdf.RenderTile(0, BandTop, // tile origin ใน page pixel
Width, BandHeight, // ขนาด destination bitmap
Width, Height); // target size ของ page เต็มหน้า
try
// Band มี row BandTop .. BandTop + BandHeight - 1 ของ page
finally
Band.Free;
end;
public band API เป็น callback loop RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) คืนจำนวน band ที่ render จริง หรือ 0 เมื่อ argument ถูกปฏิเสธ และจะถือ component render lock ตลอด pass signature ของ callback คือ TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object bitmap เป็น pf32bit กว้าง Width pixel และสูงไม่เกิน BandHeight มันจะถูก free ทันทีหลัง handler คืนค่า ดังนั้น copy สิ่งที่คุณต้องการเก็บไว้ หากคืน False pass จะหยุดหลัง band ปัจจุบัน ให้ cooperative cancellation model แบบเดียวกับ cancellable progressive PDF rendering ใน Delphi เพียงทำงานระดับ strip แทนระดับ PDFium continuation
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 นี้คืนค่า จึงต้อง consume ตรงนี้
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
stream PNG และ TIFF โดยไม่สร้าง full-page bitmap
การ render เป็น band จะช่วยก็ต่อเมื่อ encoder ทำงานแบบ sequential ด้วย ดังนั้น v3.66.0 จึงเพิ่ม RenderPageBandedToStream ซึ่งเขียน PNG หรือ TIFF ตรงลง caller stream TPdfBandedImageStreamOptions.Default ตั้ง band height 256 row, PNG compression level 6 และ MaxOutputBytes เป็น 0 ซึ่งหมายถึงไม่จำกัด TPdfBandedImageReport ที่คืนมามี Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes และ Completed ค่าที่สนใจเมื่อ sizing job คือ PeakBandBytes เพราะมันคือ Width * BandHeight * 4 ดังนั้น A0 sheet ด้านบนใช้ band buffer สูงสุดราว 19 MB แทน page buffer 2 GB
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 byte ที่ compress แล้วจะออกมาเป็น IDAT chunk ที่มี CRC และเขียนตามลำดับ constraint ที่น่าสนใจคือ stream ใต้ deflate layer ต้องตอบ position query เพราะ compression stream ถาม แต่การพยายาม seek จริงจะ raise error นี่เป็นพฤติกรรมที่ตั้งใจ เมื่อ IDAT chunk กับ CRC ถูกส่งออกไปแล้วจะย้อนกลับไปแก้ไม่ได้ และ silent seek จะทำให้ output เสียแม้โครงสร้างยังดู valid
TIFF encoder เขียน classic TIFF แบบ little-endian โดยมี byte order mark II ตามด้วย magic 42 และมีหนึ่ง strip ต่อ band pixel จะถูก stream ออกก่อน ส่วน IFD สิบ entry จะสร้างตอนท้ายเมื่อรู้ strip offset กับ byte count แล้ว compression เป็น tag 259 value 1 จึงไม่มี entropy coding payload จึงมีขนาดตรงกับ Width * Height * 3 byte, PhotometricInterpretation เป็น RGB, PlanarConfiguration เป็น chunky และ RowsPerStrip บันทึก band height ส่วน short strip สุดท้ายอธิบายด้วย StripByteCounts entry ของตัวเอง ดังนั้น band height เปลี่ยน peak memory กับจำนวน strip แต่ไม่เปลี่ยน output size ซึ่งควรรู้ก่อน tune หากต้องการไฟล์เล็กแทน lossless per-page path ใน การแปลง PDF page เป็น JPEG image ด้วย 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 และตั้งใจให้ fail คนละจุด อย่างแรกคือ caller budget MaxOutputBytes ถูกบังคับโดย bounded write stream ที่ raise EPdfError ก่อน write ใดก็ตามที่ทำให้ข้าม limit ดังนั้น budget จึงเป็น hard cap ไม่ใช่ report หลังเกิดเหตุ อย่างที่สองเป็นข้อจำกัดเชิงโครงสร้าง classic TIFF เก็บ strip offset เป็นค่า 32-bit ดังนั้น BeginImage จะ validate Width * Height * 3 รวม header และ directory เทียบกับเพดานนี้ และ reject job ก่อนเขียน pixel แรก check เดียวกันทำกับ MaxOutputBytes ตั้งแต่ต้น เพราะ TIFF ที่ budget จ่าย payload pixel ของตัวเองไม่ไหวไม่ควรเริ่ม PNG ไม่มี limit แบบเดียวกัน เพราะ IDAT chunk เป็น sequential ล้วนและไม่มี 32-bit offset table ให้ overflow
ต้องมองอย่างตรงไปตรงมาว่า stopped export ทิ้งอะไรไว้ เมื่อ pass ไปไม่ถึง row สุดท้าย Completed จะเป็น False และ encoder จะถูก teardown ด้วย EndImage(False) ซึ่งตั้งใจไม่เขียน PNG IEND chunk และไม่เขียน TIFF IFD ดังนั้น partial file จึง invalid และ decoder ทุกตัวจะบอกเช่นนั้น แทนการสร้าง image ที่ดูสมเหตุสมผลแต่ขาด row การ cleanup นี้ถูกห่อไว้เพื่อไม่ให้ failure รองใน EndImage มาแทนที่ exception เดิม ซึ่งคือความแตกต่างระหว่าง stack trace ที่บอกเหตุผลจริงกับ stack trace ที่บอกชื่อ janitor หากต้องการ progress ที่อยู่รอด ให้ checkpoint ต่อ band ใน callback ของคุณเอง เทคนิค strip-level caching ใน คู่มือ PDFium Delphi render cache และ zoom ก็ใช้ได้กับเรื่องนี้เช่นกัน
เสียบ codec ของคุณเอง
เมื่อเป้าหมายไม่ใช่ PNG หรือ TIFF RenderPageBandedToEncoder จะรับ descendant ของ TPdfBandedImageEncoder แล้วขับ loop เดียวกัน lifecycle ชัดและสั้น BeginImage(Width, Height) ตามด้วย WriteBand(BandIndex, BandTopY, Bitmap) หนึ่งครั้งต่อ strip ตามลำดับเพิ่มขึ้นอย่างเคร่งครัด แล้วปิดด้วย EndImage(Completed) โดย GetBytesWritten ป้อนค่าให้ Report.OutputBytes built-in encoder จะ reject band ที่มาผิดลำดับทันทีแทนการพยายาม buffer และ encoder ที่คุณเขียนควรทำเหมือนกัน เพราะ codec ที่ reorder strip อย่างเงียบ ๆ จะสร้างไฟล์ที่เปิดได้แต่โกหก นี่คือ seam สำหรับ JPEG 2000 tile, JPEG writer ที่รับทีละ MCU row band หรือการ 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 ที่ควรรู้หนึ่งเรื่อง
ชื่อ zlib unit ต่างกันในทุก toolchain ที่รองรับ Delphi XE5 และใหม่กว่าใช้ System.ZLib FPC ใช้ zstream และ Delphi รุ่นเก่าใช้ ZLib นั่นเป็น conditional compilation แบบปกติ กับดักคือทั้งสาม export compression-level constant ชื่อ clNone และ clDefault ซึ่งชนตรง ๆ กับ member ของ TColor ที่มีชื่อเดียวกันใน graphics unit เมื่อ zlib unit ปรากฏใน implementation uses clause clNone ที่ไม่ qualify ใน render code อาจ resolve ไปเป็น compression level แทน color โดยไม่มี diagnostic PDFiumPas จึง pin เรื่องนี้ด้วย color sentinel alias ที่ชัดเจน คือ PdfGraphicsColorNone และ PdfGraphicsColorDefault ซึ่ง bind ครั้งเดียวกับ graphics constant แบบ fully qualified แล้วใช้ทุกจุดที่ต้องเทียบ render background หรือ color-scheme sentinel สามบรรทัดนี้หยุด symbol resolution ไม่ให้ drift ระหว่าง compiler
banded rendering ดูเหมือน convenience feature จนกว่าจะเจอ page ที่ใส่ RAM ไม่ได้ และเมื่อนั้นมันคือ path เดียวที่ทำงาน corrected band geometry, sequential PNG และ TIFF encoder รวมถึง custom encoder seam อยู่ใน PDFium Delphi component พร้อม full band-versus-page pixel comparison ใน regression suite บน Delphi, Lazarus และ C++Builder