最初のbandはdrawing全体を1本のstripへ押しつぶし、その後の5つのbandはblankになりました。これが旧banded exportで、PDFiumPasはv3.66.0で修正しました。RenderPageBandedはすべてのbandでFPDF_RenderPageBitmapへfull-page target widthとheightを渡し、negative vertical offsetも渡します。native clipがcurrent bandのrowだけへwriteし、pageはfull-page coordinate geometryを保つためです。背景にあるuse caseは地味で避けられません。E-size plotやstitched panorama pageを渡され、600 DPI rasterを求められます。600 DPIのISO A0 sheetは19866 × 28086 pixelで、32-bit destination bitmapは連続memoryを2 GB強必要とします。32-bit Delphiではallocationが単純に失敗します。64-bitでは十分な頻度で成功するため、testではなくcustomerの問題になります。banded renderingがあるのは、peak allocationを1 pageではなく1 stripにするためです
すべてのbandがwhole pageを含んだ理由
旧codeはPDFium page-rendering callの異なる2組のargumentを混同していました。FPDF_RenderPageBitmapはstart_x、start_y、size_x、size_yを受けます。size pairはwhole pageをどれだけの大きさへscaleするか、start pairはそのscaled pageをdestination bitmap内のどこへ置くかを示します。v3.66.0より前のband loopは、band topをdestination offsetに、band heightをpage heightにしてlibraryのRenderPage helperを呼んでいました。その2つのnumberがnative callへそのまま通ったため、PDFiumはwhole pageをBandHeight rowしかないrectangleへscaleし、bitmap自体もBandHeight rowしかないのにy = BandTopでdrawしました。見れば結果は予測できます。band zeroはwhole pageをband heightへverticalに押しつぶしたものを受け取ります。後続のbandは同じ押しつぶされたpageをbitmapのbottomより下へpushされるため、background fillとして戻ります。bugが隠れるのはsmoke testで最も使うcase、render heightがband heightより小さいpageです。その場合はsingle bandなので、wrong geometryとright geometryが偶然一致します。1 bandを超えるものはすぐに露呈します
negative offsetが保証するもの
fixed implementationはすべてのbandをRenderTileへ通します。これはその区別をすでに理解していたcomponent内の唯一の場所です。RenderTileはfull-page pixel coordinateのtile originと、別のPageWidth、PageHeightを受け取り、page sizeを変更せずに-Leftと-TopをPDFiumへ渡します。offsetをnegateするとfull-size pageが上へslideし、要求されたbandがdestination bitmapのrow zeroに来ます。PDFiumがbitmap boundsに対してnativeにclipするため、band外はrasterizeされません。ISO 32000-1 clause 8.3.2に記されたpage-to-device mappingはfirst bandからlastまで同じです。ここが要点です。band Nは、同じdimensionのsingle full-page renderからBandTopからBandTop + hまでを抜き出したものとbit-identicalで、regression suiteは同じdimensionのRenderPage outputとpixel単位でまさにそれをassertします
// 1 bandを手動でrenderする。destination bitmapはBandHeightだけ高く、
// page target sizeはfull Width x Heightのままにする
Band := Pdf.RenderTile(0, BandTop, // page pixel内のtile origin
Width, BandHeight, // destination bitmapのsize
Width, Height); // full-page targetのsize
try
// BandはpageのBandTop .. BandTop + BandHeight - 1 rowを保持する
finally
Band.Free;
end;
public band APIはcallback loopです。RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color)は実際にrenderしたband数を返し、argumentがrejectされると0を返します。pass全体でcomponent render lockを保持します。callback signatureはTPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of objectです。bitmapはpf32bitで、widthはWidth pixel、heightはBandHeight以下です。handlerがreturnするとすぐfreeされるため、残したいものはcopyしてください。Falseを返すとcurrent band後にpassが停止します。これはDelphiでcancellableなprogressive PDF renderingと同じcooperative cancellation modelですが、PDFium continuation granularityではなくstrip 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);
full-page bitmapなしでPNGとTIFFをstreamする
bandでrenderするだけでは、encoderもsequentialでなければ助けになりません。そこでv3.66.0にRenderPageBandedToStreamを追加し、PNGまたはTIFFへcaller streamへ直接writeします。TPdfBandedImageStreamOptions.Defaultはband height 256 row、PNG compression level 6、MaxOutputBytes 0をseedします。0はunboundedです。返されるTPdfBandedImageReportはFormat、Width、Height、BandsRendered、BandsEncoded、RowsEncoded、PeakBandBytes、OutputBytes、Completedを持ちます。job sizeを決めるとき本当に見るべきなのはPeakBandBytesです。Width * BandHeight * 4で、上のA0 sheetならpage buffer 2 GBではなくband buffer約19 MBでpeakします
PNG encoderは意図的に狭く作られています。fixed RGB8を出力し、bit depth 8、color type 2のIHDRを書き、各scanlineをfilter type 0(ISO/IEC 15948 filter method 0、filter type None)で構築し、platform zlib compression streamへ通します。圧縮されたbyteはCRC付きIDAT chunkとして順に出ます。deflate layerの下にあるstreamには興味深い制約があります。compression streamが求めるためposition queryには答えますが、real seekを試みるとerrorをraiseします。意図的な動作です。IDAT chunkとCRCがwireへ出た後に戻って修正する方法はなく、silent seekは構造上validに見えるcorrupt outputを作るためです
TIFF encoderはlittle-endian classic TIFFを書きます。II byte order mark、magic 42、bandごとに1 stripです。pixelを先にstreamし、strip offsetとbyte countが分かってから最後に10-entry IFDを生成します。compressionはtag 259 value 1でentropy codingはありません。payloadは正確にWidth * Height * 3 byte、PhotometricInterpretationはRGB、PlanarConfigurationはchunkyです。RowsPerStripはband heightを記録し、final short stripは独自のStripByteCounts entryで記述します。そのためband heightはpeak memoryとstrip countを変えますが、output sizeは変えません。tuneする前に知っておく価値があります。losslessではなくsmall fileが欲しければ、PDFium VCL componentでPDF pageをJPEG imageへ変換するper-page pathのほうが適しています
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を制限するceilingは2つあり、意図的に異なる場所でfailureします。1つ目はcaller budgetです。MaxOutputBytesはbounded write streamが、limitを越えるwriteの前にEPdfErrorをraiseしてenforceします。そのためbudgetは事後報告ではなくhard capです。2つ目はstructuralです。classic TIFFはstrip offsetを32-bit valueで格納するため、BeginImageはheaderとdirectoryを含むWidth * Height * 3をそのceilingに対してvalidateし、pixelを1つもwriteする前にjobをrejectします。同じcheckをMaxOutputBytesにもup frontで行います。自身のpixel payloadをbudgetで賄えないTIFFを始める価値はないからです。PNGには同等のlimitがありません。IDAT chunkはpurely sequentialで、overflowする32-bit offset tableがないためです
stopped exportが残すものは、冷静に見ておく必要があります。passがlast rowへ到達しなければCompletedはFalseのままになり、encoderはEndImage(False)でtear downされます。PNG IEND chunkもTIFF IFDも意図的に書きません。そのためpartial fileはinvalidで、decoderはmissing rowがあるものをもっともらしいimageとして扱わず、invalidだと言います。このcleanupはwrapされ、EndImage内のsecondary failureがoriginal exceptionを置き換えないようにします。real causeを名指しするstack traceと、janitorを名指しするstack traceの違いです。残るprogressが必要なら、自分のcallback内でbandごとにcheckpointしてください。PDFium Delphi render cacheとzoom guideのstrip-level caching tacticもここで使えます
独自codecを接続する
PNGとTIFFがtargetでない場合、RenderPageBandedToEncoderはTPdfBandedImageEncoder descendantを受け取り、同じloopをdriveします。lifecycleは明示的で短いものです。BeginImage(Width, Height)、stripごとに厳密なascending orderで1回のWriteBand(BandIndex, BandTopY, Bitmap)、最後にEndImage(Completed)です。GetBytesWrittenがReport.OutputBytesを供給します。built-in encoderはout-of-order bandをbufferしようとせず即座にrejectします。自作encoderも同じにすべきです。stripを黙って並べ替えるcodecは、開くことはできても嘘をつくfileを生成するためです。JPEG 2000 tile、1 MCU row bandずつ受けるJPEG writer、print spoolerへのdirect feedにはこのseamを使います
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へfeedする
Inc(FNextBand);
Result := True;
end;
知っておく価値があるcross-compiler trap
zlib unitのspellingはsupport対象toolchainごとに違います。Delphi XE5以降はSystem.ZLib、FPCはzstream、older Delphiはplain ZLibです。そこまではroutineなconditional compilationです。trapは、3つすべてがclNoneとclDefaultという同名のcompression-level constantをexportし、graphics unitの同名TColor memberと正面衝突することです。zlib unitがimplementation uses clauseへ現れると、render codeのunqualified clNoneがcolorではなくcompression levelへresolveする可能性があり、diagnosticはありません。PDFiumPasはこれをexplicit color sentinel alias、PdfGraphicsColorNoneとPdfGraphicsColorDefaultで固定します。fully qualifiedなgraphics constantへ一度bindし、render backgroundやcolor-scheme sentinelをcompareする場所でどこでも使います。3行のcodeで、symbol resolutionがcompilerごとにdriftしなくなります
banded renderingは、RAMに収まらないpageに出会うまではconvenience featureに見えます。そのとき動く唯一のpathになります。修正されたband geometry、sequential PNGとTIFF encoder、custom encoder seamは、Delphi、Lazarus、C++Builderをまたぐfull band-versus-page pixel comparisonのregression suiteとともにPDFium Delphi componentの一部です