Artikel Teknis

Banded PDF Rendering di Delphi: Negative Y Offset

Band pertama menampung seluruh drawing yang dipadatkan menjadi satu strip, sedangkan lima band setelahnya kembali kosong. Itulah banded export lama, dan PDFiumPas memperbaikinya pada v3.66.0: RenderPageBanded kini meneruskan full-page target width dan height kepada FPDF_RenderPageBitmap pada setiap band, bersama negative vertical offset, sehingga native clip hanya menulis row dari band saat ini sementara halaman mempertahankan full-page coordinate geometry. Use case di balik semua ini membosankan dan tidak dapat dihindari. Seseorang memberikan E-size plot atau stitched panorama page lalu meminta raster 600 DPI. Lembar ISO A0 pada 600 DPI berukuran 19866 x 28086 pixel, dan destination bitmap 32-bit berukuran demikian membutuhkan sedikit lebih dari 2 GB contiguous memory. Pada Delphi 32-bit, allocation itu langsung gagal. Pada 64-bit, allocation cukup sering berhasil untuk mengubah kegagalan menjadi masalah customer, bukan masalah test. Banded rendering ada agar peak allocation menjadi satu strip, bukan satu halaman

Mengapa setiap band berisi seluruh halaman?

Code lama mencampur dua pasangan argument berbeda dalam pemanggilan PDFium page-rendering. FPDF_RenderPageBitmap menerima start_x, start_y, size_x, dan size_y, di mana pasangan size menyatakan seberapa besar seluruh halaman harus diskalakan dan pasangan start menyatakan posisi halaman yang sudah diskalakan di dalam destination bitmap. Loop band sebelum v3.66.0 memanggil helper RenderPage library dengan band top sebagai destination offset dan band height sebagai page height. Kedua angka tersebut diteruskan langsung ke native call, sehingga PDFium menskalakan seluruh halaman ke rectangle setinggi BandHeight lalu menggambarnya pada y = BandTop di dalam bitmap yang juga hanya setinggi BandHeight. Hasilnya persis seperti yang dapat Anda prediksi setelah melihatnya. Band zero menerima seluruh halaman yang dipadatkan secara vertikal ke band height. Setiap band berikutnya menerima halaman padat yang sama tetapi didorong melewati bottom edge bitmap, sehingga kembali sebagai background fill. Bug ini tersembunyi pada satu kasus yang paling sering dipakai smoke test, yaitu halaman yang render height-nya lebih kecil daripada band height, karena hanya ada satu band dan geometri yang salah kebetulan bertepatan dengan yang benar. Apa pun yang lebih tinggi dari satu band langsung membukanya

Apa yang dijamin oleh negative offset?

Implementasi yang diperbaiki mengarahkan setiap band melalui RenderTile, satu-satunya tempat dalam component yang sudah memahami perbedaan tersebut. RenderTile menerima tile origin dalam full-page pixel coordinate ditambah PageWidth dan PageHeight terpisah, lalu memberikan -Left dan -Top kepada PDFium dengan page size tetap utuh. Menegasikan offset menggeser halaman berukuran penuh ke atas sampai band yang diminta berada pada row zero destination bitmap; PDFium kemudian melakukan clipping secara native terhadap bitmap bounds, sehingga tidak ada yang berada di luar band dirasterisasi. Page-to-device mapping yang dijelaskan dalam ISO 32000-1 clause 8.3.2 tetap identik dari band pertama sampai terakhir, dan itulah inti desainnya: band N byte-identical dengan row BandTop sampai BandTop + h dari single full-page render, dan regression suite menegaskan tepat hal itu, pixel demi pixel, terhadap output RenderPage pada dimensi yang sama

// Satu band secara manual. Destination bitmap hanya setinggi BandHeight,
// tetapi target size halaman tetap full Width x Height
Band := Pdf.RenderTile(0, BandTop,          // tile origin dalam page pixel
                       Width, BandHeight,   // ukuran destination bitmap
                       Width, Height);      // target full-page
try
  // Band kini memuat row BandTop .. BandTop + BandHeight - 1 dari halaman
finally
  Band.Free;
end;

Public band API adalah callback loop. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) mengembalikan jumlah band yang benar-benar dirender, atau 0 ketika argument ditolak, dan menahan component render lock sepanjang pass. Callback signature-nya adalah TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmap berupa pf32bit, lebarnya Width pixel, dan tingginya tidak lebih dari BandHeight, lalu dibebaskan segera setelah handler Anda return, sehingga copy apa pun yang ingin dipertahankan. Return False menghentikan pass setelah band saat ini, memberi Anda cooperative cancellation model yang sama dengan yang digunakan oleh cancellable progressive PDF rendering di Delphi, hanya pada strip granularity, bukan 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 mati ketika method ini return - konsumsi di sini
  Inc(FRows, Bitmap.Height);
  Result := not FCancelled;
end;

// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);

Streaming PNG dan TIFF tanpa full-page bitmap

Rendering dalam band hanya membantu jika encoder juga sequential, sehingga v3.66.0 menambahkan RenderPageBandedToStream, yang menulis PNG atau TIFF langsung ke caller stream. TPdfBandedImageStreamOptions.Default mengatur band height 256 row, PNG compression level 6, dan MaxOutputBytes 0 yang berarti unbounded. TPdfBandedImageReport yang dikembalikan membawa Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes, dan Completed. PeakBandBytes adalah angka yang benar-benar perlu Anda perhatikan saat mengukur job: nilainya Width * BandHeight * 4, sehingga lembar A0 di atas mencapai sekitar 19 MB band buffer, bukan 2 GB page buffer

PNG encoder sengaja dibuat sempit. Encoder mengeluarkan fixed RGB8, menulis IHDR dengan bit depth 8 dan color type 2, lalu membangun setiap scanline dengan filter type 0 (ISO/IEC 15948 filter method 0, filter type None) dan mendorongnya melalui platform zlib compression stream. Compressed byte keluar sebagai IDAT chunk yang membawa CRC dan ditulis berurutan. Constraint menariknya berada pada stream di bawah deflate layer: stream menjawab position query karena compression stream memintanya, tetapi setiap real seek mengangkat error. Ini disengaja. Setelah IDAT chunk dan CRC-nya berada di wire, tidak ada jalan kembali untuk memperbaikinya, dan silent seek akan merusak output yang tetap tampak valid secara struktural

TIFF encoder menulis classic TIFF little-endian, byte-order mark II diikuti magic 42, dengan satu strip per band. Pixel mengalir lebih dulu dan IFD sepuluh entry dibuat di akhir, setelah strip offset serta byte count diketahui. Compression adalah tag 259 value 1, jadi tidak ada entropy coding sama sekali: payload tepat Width * Height * 3 byte, PhotometricInterpretation adalah RGB, PlanarConfiguration adalah chunky, dan RowsPerStrip mencatat band height sementara short strip terakhir dijelaskan oleh entry StripByteCounts sendiri. Karena itu band height mengubah peak memory dan jumlah strip tetapi tidak mengubah output size, yang perlu diketahui sebelum melakukan tuning. Jika yang Anda inginkan adalah file kecil, bukan lossless, page path dalam mengonversi halaman PDF menjadi image JPEG dengan PDFium VCL component tetap merupakan tool yang lebih tepat

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, bukan 19866 * 28086 * 4
end;

Di mana banded export berhenti?

Dua ceiling membatasi output, dan keduanya gagal di tempat yang berbeda dengan sengaja. Yang pertama adalah caller budget: MaxOutputBytes ditegakkan oleh bounded write stream yang mengangkat EPdfError sebelum write apa pun yang akan melewati limit, sehingga budget menjadi hard cap, bukan report setelah fakta. Yang kedua bersifat struktural. Classic TIFF menyimpan strip offset sebagai value 32-bit, sehingga BeginImage memvalidasi Width * Height * 3 ditambah header serta directory terhadap ceiling itu dan menolak job sebelum satu pixel pun ditulis; pemeriksaan yang sama dilakukan terhadap MaxOutputBytes sejak awal, karena TIFF yang budget-nya tidak dapat menutup pixel payload-nya sendiri tidak layak dimulai. PNG tidak memiliki limit setara karena IDAT chunk sepenuhnya sequential dan tidak ada offset table 32-bit yang dapat overflow

Perhatikan dengan jernih apa yang ditinggalkan stopped export. Ketika pass tidak mencapai row terakhir, Completed tetap False dan encoder dibongkar dengan EndImage(False), yang sengaja tidak menulis PNG IEND chunk maupun TIFF IFD. Karena itu partial file invalid dan setiap decoder akan mengatakannya, bukan menghasilkan image yang tampak masuk akal tetapi kehilangan row. Cleanup ini dibungkus agar secondary failure di dalam EndImage tidak menggantikan exception asli, dan itulah perbedaan antara stack trace yang menyebut penyebab sebenarnya dan stack trace yang menyebut petugas kebersihan. Jika Anda membutuhkan progress yang dapat dilanjutkan, lakukan checkpoint per band di dalam callback Anda sendiri; strip-level caching tactic dalam panduan PDFium Delphi render cache dan zoom juga berlaku di sini

Memasang codec Anda sendiri

Ketika PNG dan TIFF bukan targetnya, RenderPageBandedToEncoder menerima descendant TPdfBandedImageEncoder dan menjalankan loop yang sama. Lifecycle-nya eksplisit dan singkat: BeginImage(Width, Height), lalu WriteBand(BandIndex, BandTopY, Bitmap) sekali per strip dalam urutan strictly ascending, kemudian EndImage(Completed), dengan GetBytesWritten mengisi Report.OutputBytes. Built-in encoder menolak band out-of-order secara langsung, bukan mencoba men-buffer-nya, dan encoder yang Anda tulis seharusnya melakukan hal yang sama karena codec yang diam-diam mengurutkan ulang strip menghasilkan file yang terbuka tetapi berbohong. Inilah seam untuk JPEG 2000 tile, JPEG writer yang diberi satu MCU row band setiap kali, atau feed langsung ke 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;
  // Berikan Bitmap.ScanLine[0 .. Bitmap.Height - 1] ke codec di sini
  Inc(FNextBand);
  Result := True;
end;

Satu jebakan lintas compiler yang perlu diketahui

Unit zlib ditulis berbeda pada setiap toolchain yang didukung: Delphi XE5 dan sesudahnya menggunakan System.ZLib, FPC menggunakan zstream, dan Delphi lama menggunakan ZLib biasa. Itu conditional compilation yang rutin. Jebakannya adalah ketiganya mengekspor constant compression-level bernama clNone dan clDefault, yang bertabrakan langsung dengan member TColor bernama sama di graphics unit. Setelah unit zlib muncul dalam implementation uses clause, clNone tanpa qualifier dalam render code dapat di-resolve menjadi compression level, bukan color, tanpa diagnostic. PDFiumPas mengunci ini dengan explicit color sentinel alias, PdfGraphicsColorNone dan PdfGraphicsColorDefault, yang sekali di-bind ke graphics constant fully qualified lalu digunakan di setiap tempat render background atau color-scheme sentinel dibandingkan. Tiga baris code, dan symbol resolution berhenti drift antar-compiler

Banded rendering tampak seperti feature kenyamanan sampai Anda bertemu halaman yang tidak muat di RAM, lalu berubah menjadi satu-satunya jalur yang bekerja. Corrected band geometry, sequential PNG dan TIFF encoder, serta custom encoder seam semuanya tersedia sebagai bagian dari PDFium Delphi component, dengan full band-versus-page pixel comparison yang berjalan di regression suite lintas Delphi, Lazarus, dan C++Builder