Artikel Teknis

Stempel Halaman yang Dapat Digunakan Kembali melalui Form XObjects dengan PDFium

Membubuhkan watermark atau logo ke setiap halaman sebuah dokumen terlihat seperti pekerjaan lima menit sampai Anda membuka hasilnya di sebuah pemeriksa ukuran file. Pendekatan yang jelas adalah menyusuri halaman-halaman dan, pada setiap halaman, membangun kembali objek teks atau gambar yang sama. Itu berhasil secara visual, dan itu boros dengan cara yang bertumpuk. Sebuah watermark diagonal "DRAFT" yang digambar langsung ke sebuah laporan seratus halaman adalah seratus salinan dari path dan data teks yang sama yang duduk di dalam content stream, dan file yang disimpan membawa semuanya

Form XObject adalah konstruksi yang disediakan PDF untuk menghindari persis hal ini. Ia membungkus sepotong konten yang dapat digunakan ulang, sebuah halaman utuh atau sebuah templat kecil, menjadi satu objek bernama tunggal yang dapat digambar berkali-kali pada berbagai posisi. Konten tersebut hidup di dalam file hanya satu kali. Setiap halaman yang menginginkan stempel tersebut menyimpan sebuah instruksi singkat yang mengatakan "gambar XObject N di sini, dengan transformasi ini." Sebuah watermark seratus halaman kemudian menambahkan satu objek konten ke file alih-alih seratus, dan itulah perbedaan antara sebuah dokumen yang tumbuh secara linear dengan jumlah halamannya dan satu yang tidak. Watermark, stempel logo, templat nomor halaman, dan segel semuanya merupakan bentuk masalah yang sama, dan Form XObject adalah alat yang tepat untuk masing-masingnya

Diagram yang mengontraskan menggambar operator watermark ke setiap halaman PDF dengan menyimpannya sekali dalam Form XObject dengan PDFium
Menggambar ulang stempel di setiap halaman menduplikasi byte-nya di setiap content stream, sementara Form XObject menyimpan karya itu sekali dan membiarkan setiap halaman mereferensikannya

Mengapa satu objek tersimpan mengalahkan seratus kali gambar ulang

Penghematannya bersifat struktural, bukan kosmetik. Sebuah halaman PDF di-render dengan mengeksekusi content stream-nya, sebuah urutan operator penggambaran. Ketika Anda menggambar ulang sebuah stempel per halaman, Anda menambahkan urutan operator lengkap untuk stempel tersebut ke stream setiap halaman, dan byte-byte tersebut diduplikasi sebanyak jumlah halaman yang Anda miliki. Sebuah Form XObject memindahkan operator-operator tersebut ke dalam satu stream yang disimpan sekali dalam dokumen. Referensi yang disimpan oleh setiap halaman individual berukuran kecil: ia mendorong sebuah matriks transformasi, memanggil XObject tersebut, dan memulihkan state. Jumlah halaman tidak lagi melipatgandakan biaya artwork tersebut

Ini paling penting ketika stempel tersebut berat. Sebuah segel vektor dengan ratusan segmen path, atau sebuah bitmap logo, mahal untuk disimpan. Disimpan sekali dan direferensikan, bagian yang berat tersebut dibayar hanya satu kali dan overhead per-halaman hanyalah beberapa byte pemanggilan. Hasil visual pada halaman identik dengan gambar ulang langsung, dan itulah intinya. Pembaca tidak dapat membedakannya; ukuran file sangat bisa

Menangkap sebuah halaman ke dalam XObject

PDFium membangun objek yang dapat digunakan ulang tersebut dari sebuah halaman yang sudah ada. Sumbernya adalah sebuah halaman di suatu dokumen yang Anda buka, sebuah PDF satu halaman kecil yang tidak berisi apa pun selain artwork watermark Anda, atau sebuah halaman tertentu dari sebuah file yang lebih besar. CreateXObjectFromPage menangkap konten halaman sumber tersebut ke dalam sebuah handle yang dapat digunakan ulang yang menjadi milik dokumen tujuan, yaitu dokumen yang sedang Anda bubuhi stempel

var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := 'Report.pdf';
    Dest.Active := True;
    Stamp.FileName := 'Watermark.pdf';   // satu halaman artwork
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // Tangkap halaman 0 dari dokumen stempel ke dalam sebuah handle yang dapat
    // digunakan ulang dan dimiliki oleh Dest. Source harus Active; indeksnya berbasis nol.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not build the stamp XObject');
    // ... tempatkan objek tersebut, lalu bebaskan sebelum menutup Stamp (lihat di bawah) ...

Signature-nya adalah CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metode ini memunculkan exception jika dokumen sumber tidak Active, dan ia mengembalikan nil alih-alih memunculkan exception ketika PDFium tidak dapat membangun objek tersebut, sehingga pemeriksaan eksplisit di atas bukanlah opsional. Handle yang dikembalikan adalah sebuah TPdfXObject yang Anda miliki, dan dua batasan masa pakai (lifetime) yang melekat padanya adalah bagian dari keseluruhan latihan ini yang sering menjebak orang, sehingga keduanya mendapat bagian tersendiri di bawah ini

Menempatkan stempel pada sebuah halaman

Sebuah XObject yang telah ditangkap tidak melakukan apa pun dengan sendirinya. Untuk membuatnya muncul, Anda menyisipkan sebuah salinan darinya ke halaman dokumen yang sedang aktif saat ini, yaitu halaman yang dipilih oleh properti PageNumber yang berbasis 1, dengan InsertFormObjectFromXObject. Panggilan tersebut mengembalikan objek halaman yang mendasarinya, sebuah FPDF_PAGEOBJECT, dan handle yang dikembalikan itulah cara Anda memposisikan penempatan tersebut. Tanpa sebuah transform, stempel tersebut mendarat di titik asal (origin) dalam koordinat halaman sumbernya sendiri, yang jarang menjadi tempat yang Anda inginkan

Karena InsertFormObjectFromXObject menyisipkan satu salinan per panggilan dan mengembalikan objek halaman baru setiap kali, Anda dapat menggambar XObject yang sama beberapa kali pada satu halaman dengan transform yang berbeda-beda, dan konten yang tersimpan tetap dihitung satu kali dalam file. Sebuah logo di sudut dan sebuah watermark satu halaman penuh yang samar dapat berasal dari objek yang sama yang telah ditangkap

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
begin
  // Halaman aktif dari Dest menerima satu salinan XObject.
  PageObj := Dest.InsertFormObjectFromXObject(XObject);
  if PageObj = nil then
    raise Exception.Create('Insert failed on this page');

  // Posisikan objek: geser 200 satuan ke kanan, 500 ke atas, pada skala 70%.
  M := TPdfMatrix.Create;
  try
    M.Scale(0.7, 0.7);
    M.Translate(200, 500);
    RawM := M.Handle;
    if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
      raise Exception.Create('Cannot assign the stamp matrix');
  finally
    M.Free;
  end;
  Dest.UpdatePage;   // komit perubahan halaman ini ke content stream-nya
  // if not Dest.SaveAs(...) then ... setelah setiap halaman selesai.
end;

Dua detail pemeliharaan membuat ini aman. Pertama, setelah disisipkan, objek halaman tersebut menjadi milik halaman, bukan milik XObject. Membebaskan XObject nanti tidak membatalkan penempatan yang sudah Anda buat. Itulah yang membuat urutan buat-tempatkan-bebaskan yang dijelaskan di bawah ini berfungsi. Kedua, penyisipan dan pemosisian hanya mengubah daftar objek halaman di dalam memori; UpdatePage adalah yang menserialisasi daftar tersebut kembali ke content stream halaman, sehingga sebuah halaman yang Anda edit tanpa memanggilnya tersimpan seolah-olah stempel tersebut tidak pernah ditempatkan

Aturan masa pakai handle yang sering menjebak orang

Dua batasan mengatur handle XObject, dan mengabaikan salah satunya menghasilkan sebuah kegagalan yang terlihat tidak berhubungan dengan penyebabnya. Pertama, dokumen sumber harus aktif pada saat Anda memanggil CreateXObjectFromPage. Penangkapan tersebut membaca konten halaman sumber dari dokumen sumber yang hidup, sehingga dokumen tersebut dan halamannya harus terbuka dan valid ketika handle tersebut dibangun. Kedua, dan inilah yang mengejutkan orang, handle tersebut harus dibebaskan sebelum halaman sumber ditutup, dan dalam praktiknya sebelum Anda menutup atau membebaskan dokumen sumber tempatnya berasal

Alasannya adalah XObject tersebut merupakan sebuah referensi ke dalam struktur yang masih dimiliki oleh dokumen sumber. Ini bukan sebuah salinan yang terlepas dan mandiri yang dapat Anda bawa ke mana-mana setelah sumbernya hilang. Tutup sumbernya lebih dulu dan handle tersebut tertinggal menunjuk ke konten yang telah dibongkar, sehingga membebaskannya nanti, atau penggunaan lain apa pun terhadapnya, beroperasi pada memori yang sudah tidak valid lagi. Gejalanya adalah gejala klasik untuk sebuah handle yang menggantung (dangling): sebuah access violation saat shutdown, atau korupsi yang muncul sesekali dan berpindah-pindah tergantung urutan alokasi, dengan sebuah stack yang menunjuk ke kode pembersihan alih-alih ke baris yang sebenarnya menyebabkan masalah tersebut. Perbaikannya adalah pengurutan, bukan pemrograman defensif. Bangun XObject tersebut, sisipkan ke setiap halaman yang membutuhkannya, bebaskan XObject tersebut, dan baru setelah itu tutup dokumen sumber. Destructor TPdfXObject melepaskan handle PDFium yang mendasarinya untuk Anda, sehingga membebaskan wrapper pada waktu yang tepat adalah keseluruhan tanggung jawab Anda

Diagram masa hidup terurut untuk stempel halaman PDFium yang menunjukkan penangkapan, penempatan, pembebasan handle TPdfXObject, dan penutupan dokumen stempel terakhir
Tangkap stempel sekali, tempatkan di setiap halaman, bebaskan XObject selama dokumen stempel masih terbuka, lalu simpan dan tutup sumbernya paling akhir

Matriks, dan arti keenam angkanya

Penempatan adalah sebuah transform affine 2D, sama seperti yang digunakan PDF di mana-mana untuk memposisikan konten (ISO 32000-1, bagian 8.3.4). Ini adalah enam angka, ditulis a, b, c, d, e, f, dan PDFium mengeksposnya sebagai record FS_MATRIX. Angka-angka tersebut memetakan sebuah titik dari ruang objek itu sendiri ke ruang halaman:

// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : skala horizontal dan vertikal
// b, c : suku shear / rotasi
// e, f : translasi (tempat titik asal mendarat pada halaman)

Anda dapat mengisi keenam nilai tersebut secara manual, tetapi menyusunnya secara manual adalah tempat di mana rotasi menjadi salah, karena rotasi mencampur keempat a, b, c, d bersama-sama. Wrapper TPdfMatrix, dari unit FPdfMatrix, menyusun operasi-operasi umum untuk Anda dan melakukan post-multiply seiring berjalannya, sehingga Translate, Scale, dan Rotate berantai dalam urutan Anda memanggilnya. Sebuah watermark diagonal adalah sebuah rotate yang diikuti oleh sebuah translate untuk mengembalikan posisinya ke tengah; sebuah logo di sudut adalah sebuah scale yang diikuti oleh sebuah translate. Ketika matriks tersebut siap, salin nilai mentahnya, properti Handle bertipe FS_MATRIX, ke dalam sebuah variabel lokal dan oper itu ke FPDFPageObj_SetMatrix; import tersebut mendeklarasikan matriks sebagai sebuah parameter var, sehingga sebuah properti tidak dapat dioper langsung kepadanya, dan hasilnya adalah 0 ketika gagal. FPDFPageObj_Transform tingkat rendah, yang menerima keenam nilai tersebut langsung sebagai double, tersedia ketika Anda lebih memilih mengoper angka-angka daripada membangun sebuah wrapper

Membubuhkan stempel pada setiap halaman, dalam urutan yang benar

Pola lengkapnya menyatukan bagian-bagian tersebut dengan urutan yang dituntut oleh aturan masa pakai. Buka kedua dokumen, tangkap stempel tersebut satu kali, susuri halaman-halaman tujuan dengan mengatur PageNumber yang berbasis 1 secara berurutan dan menyisipkan sekaligus memposisikan sebuah salinan, mengkomit setiap halaman dengan UpdatePage, lalu bebaskan XObject tersebut, lalu simpan dengan SaveAs, dan biarkan dokumen sumber ditutup terakhir

procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
  I: Integer;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := ASource;
    Dest.Active := True;
    Stamp.FileName := AStamp;
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 1. Tangkap artwork tersebut satu kali. Stamp bersifat Active di sini.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not capture the stamp page');
    try
      // 2. Tempatkan sebuah salinan pada setiap halaman Dest. PageNumber berbasis 1.
      for I := 1 to Dest.PageCount do
      begin
        Dest.PageNumber := I;                // jadikan halaman I sebagai halaman aktif
        PageObj := Dest.InsertFormObjectFromXObject(XObject);
        if PageObj = nil then
          Continue;

        M := TPdfMatrix.Create;
        try
          M.Rotate(45);                      // watermark diagonal
          M.Translate(150, 100);             // geser sedikit ke posisinya
          RawM := M.Handle;
          FPDFPageObj_SetMatrix(PageObj, RawM);
        finally
          M.Free;
        end;
        Dest.UpdatePage;                     // komit perubahan halaman ini
      end;
    finally
      XObject.Free;                          // 3. bebaskan SEBELUM Stamp ditutup
    end;

    // 4. Tulis hasilnya selagi Dest masih terbuka.
    if not Dest.SaveAs(AOutput) then
      raise Exception.Create('Could not save ' + AOutput);
  finally
    Stamp.Free;                              // sumber ditutup terakhir
    Dest.Free;
  end;
end;

Bentuk blok try tersebutlah yang melakukan pekerjaan sesungguhnya. finally bagian dalam membebaskan XObject sebelum kontrol dapat mencapai finally bagian luar yang membebaskan Stamp, sehingga handle tersebut selalu dilepaskan selagi sumbernya masih hidup, bahkan jika sebuah exception muncul di tengah loop. Buat nesting tersebut dengan benar dan aturan masa pakai akan mengurus dirinya sendiri

Anatomi FS_MATRIX yang menunjukkan enam koefisien affine yang dipakai PDFium untuk menskalakan, memutar, dan mentranslasikan Form XObject terstempel pada halaman
Enam angka memetakan koordinat stempel ke ruang halaman, dan TPdfMatrix menyusun Scale, Rotate, dan Translate dalam urutan panggilan untuk mendudukkan watermark diagonal

Pembubuhan stempel adalah satu sudut dari toolkit yang lebih besar untuk membangun dan mengedit konten halaman. Jika stempel Anda sendiri adalah sebuah gambar alih-alih sebuah halaman yang ditangkap, mengonversi gambar menjadi dokumen PDF dengan PDFium membahas cara memasukkan bitmap tersebut ke dalam sebuah dokumen terlebih dahulu. Dan ketika hal yang ingin Anda bawa di samping stempel yang terlihat adalah sebuah file alih-alih tinta di atas halaman, bekerja dengan lampiran PDF di Delphi menunjukkan sisi file yang tertanam (embedded). Semuanya dikirimkan bersama PDFium Component untuk Delphi dan C++Builder, bersama API rendering, pengeditan, dan dokumen yang dijelaskan di tempat lain di blog ini