Technical Article

Imposisi N-up dan Pengaturan Ulang Halaman Dengan PDFium

Gabungkan (merge) dan pisahkan (split) adalah dua operasi halaman yang pertama kali dicari oleh semua orang, dan keduanya mencakup banyak kebutuhan. Namun keduanya tidak mencakup segalanya. Ada rumpun pekerjaan terpisah yang mengatur ulang halaman daripada memindahkan seluruh file: menyusun empat slide ke satu lembar untuk selebaran (handout), menyeret halaman dari bagian belakang dokumen ke depan, atau mengambil halaman 3, 7, dan 12 ke dalam kutipan pendek tanpa menyentuh bagian lainnya. PDFium mengekspos tiga metode untuk hal ini, dan masing-masing berperilaku berbeda dari penggabungan dan pemisahan yang sudah Anda ketahui. Artikel ini membahas apa yang mereka lakukan, di mana titik output berada, dan satu detail kepemilikan yang telah menyebabkan crash di lapangan.

Ketiganya adalah ImportNPagesToOne untuk imposisi N-up, MovePages untuk pengaturan ulang di tempat (in-place), dan ImportPagesByIndex untuk ekstraksi subset. Penggabungan menumpuk dokumen dari ujung ke ujung dan membiarkan jumlah halaman sama dengan jumlah input. Pemisahan menulis beberapa file output dari satu input. Tiga operasi di sini berada di antaranya: salah satunya mengubah berapa banyak halaman sumber berbagi satu lembar, salah satunya mengubah urutan di dalam satu dokumen tunggal, dan salah satunya menyalin beberapa halaman pilihan ke dokumen lain. Mengetahui perbedaan masing-masing menghindarkan Anda dari keharusan melakukan proses gabung-dan-hapus yang rumit saat satu pemanggilan fungsi saja sebenarnya sudah cukup.

Konteks praktis

Imposisi adalah istilah pracetak (prepress) untuk mengatur beberapa halaman sumber ke satu lembar yang lebih besar sehingga hasil cetakan dan lipatannya dapat dibaca dalam urutan yang benar. Versi sehari-harinya adalah selebaran 2-up, buklet 4-up, atau lembar kontak yang memuat selusin thumbnail pada satu halaman. PDFium menangani geometri tersebut melalui satu panggilan:

function ImportNPagesToOne(
  OutputWidth, OutputHeight: Single;
  NumX, NumY               : Cardinal): TPdf;

NumX dan NumY mendeskripsikan kisi (grid). Nilai 2, 1 menempatkan dua halaman sumber berdampingan; 2, 2 mengemas empat halaman ke dalam tata letak kuadran; 4, 3 membangun lembar kontak dua belas-up. PDFium membaca halaman sumber secara berurutan, memperkecil masing-masing skala agar sesuai dengan selnya, dan mengisi kisi dari kiri ke kanan, atas ke bawah, memulai lembar output baru setiap kali kisi saat ini penuh. Halaman sumber tidak diubah. Apa yang Anda dapatkan kembali adalah dokumen baru yang halamannya merupakan gabungan (composite).

Ukuran output dalam poin, bukan piksel

OutputWidth dan OutputHeight adalah unit pengguna PDF, and satu unit pengguna PDF adalah satu poin, yaitu sepertujuh puluh dua inci. Unit tersebut menyatakan ukuran fisik lembar output, dan tidak ada hubungannya dengan piksel layar atau render DPI. Ini adalah tempat paling umum di mana imposisi salah dilakukan, karena pengembang yang terbiasa dengan bitmap mencari hitungan piksel dan berakhir dengan lembar seukuran prangko atau papan reklame.

Angka yang layak dihafalkan adalah dua ukuran halaman yang paling sering Anda gunakan. US Letter is adalah 612 kali 792 poin, karena 8,5 inci dikali 72 adalah 612 dan 11 inci dikali 72 adalah 792. A4 kira-kira 595 kali 842 poin, dari dimensi 210 kali 297 milimeter. Header binding sendiri menyatakan aturannya dengan jelas, bahwa satu unit adalah sepertujuh puluh dua inci, dan unit tersebut menyediakan konstanta PointsPerInch bernilai 72 jika Anda lebih suka menghitung ukuran dari inci dalam kode daripada menulis literalnya.

const
  LetterW = 612.0;   // 8.5 in * 72
  LetterH = 792.0;   // 11  in * 72
var
  Source, Composite: TPdf;
begin
  Source := TPdf.Create(nil);
  Composite := nil;
  try
    Source.FileName := 'slides.pdf';
    Source.Active := True;

    // Four source pages per Letter sheet, 2 by 2 grid.
    Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
    if Composite = nil then
      raise Exception.Create('PDFium rejected the imposition arguments');

    Composite.SaveAs('slides-4up.pdf');
  finally
    Composite.Free;   // see the next section: this is mandatory
    Source.Free;
  end;
end;

Handle yang dikembalikan adalah milik Anda untuk dibebaskan

Baca tanda tangan (signature) fungsi tersebut sekali lagi. ImportNPagesToOne mengembalikan TPdf, bukan Boolean. Nilai kembalian tersebut adalah handle dokumen baru, dialokasikan terpisah dari sumbernya, dan pemanggil memilikinya. Sumber TPdf yang Anda panggil metodenya tidak tersentuh dan tetap memiliki handle-nya sendiri; dokumen komposit adalah objek kedua yang independen. Jika Anda membiarkan TPdf yang dikembalikan keluar dari cakupan (scope) tanpa membebaskannya, Anda membocorkan seluruh dokumen PDFium.

Kesalahan yang lebih berbahaya terjadi sebaliknya. Di bawahnya, metode meminta FPDF_DOCUMENT baru ke PDFium melalui FPDF_ImportNPagesToOne, lalu membungkus handle mentah tersebut di dalam TPdf yang dikembalikan sehingga masa pakai wrapper mengatur masa pakai handle tersebut. Sejak saat itu, hanya ada satu pemilik handle, dan tepat satu tempat di mana ia harus ditutup: saat Anda melakukan Free pada objek yang dikembalikan. Jalur kesalahan yang tidak hati-hati yang membebaskan wrapper dan juga memanggil FPDF_CloseDocument pada handle mentah yang ditangkapnya akan menutup dokumen PDFium yang sama dua kali. Itu adalah double-free, dan itu adalah bug spesifik yang pernah merugikan pemanggil di sini. Aturan yang mencegahnya singkat. Tutup dokumen pada satu jalur saja, dengan membebaskan TPdf yang diberikan metode kepada Anda, dan jangan pernah menjangkau melewati wrapper untuk menutup handle yang telah diadopsinya.

Dua konsekuensi logis (corollary) dihasilkan dari hal ini. Pertama, metode mengembalikan nil ketika PDFium menolak argumen, seperti nilai nol pada salah satu sumbu kisi atau kegagalan alokasi, sehingga pemeriksaan nil harus dilakukan sebelum Anda menyentuh hasilnya. Kedua, inisialisasi variabel output Anda ke nil sebelum blok try dan bebaskan di blok finally, seperti yang dilakukan oleh contoh di atas, sehingga kegagalan di tengah jalan tidak membuat Anda membebaskan referensi yang tidak terdefinisi atau melewatkan pembebasan seluruhnya.

Mengatur ulang halaman tanpa menulis ulang

Pengaturan ulang mengubah satu dokumen di tempat (in-place). MovePages mengangkat kumpulan halaman dari posisinya saat ini dan menjatuhkannya di tujuan, menggeser hal-hal lain di sekitar blok yang dipindahkan sehingga jumlah halaman tetap sama:

function MovePages(
  const PageIndices: array of Integer;
  DestPageIndex    : Integer): Boolean;

Indeks berbasis nol. PageIndices mencantumkan halaman yang akan dipindahkan, sesuai urutan akhir yang diinginkan, dan DestPageIndex adalah indeks halaman pertama yang dipindahkan mendarat setelah perpindahan selesai. Karena PDFium merelokasi halaman daripada menyalin dan mengompresi ulang kontennya, operasi ini murah dan tanpa kehilangan kualitas (lossless): objek halaman mempertahankan stream, sumber daya, dan ketepatannya (fidelity). Ini adalah pemanggilan di balik panel halaman drag-to-reorder, di mana pengguna menarik thumbnail ke slot baru dan Anda menerapkan urutan baru tersebut dengan satu perpindahan. Fungsi ini mengembalikan False ketika indeks berada di luar rentang, jadi validasikan hasilnya daripada mengasumsikan pengaturan ulang berhasil.

var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Move the last page (index 4 in a 5-page file) to the very front.
    if not Doc.MovePages([4], 0) then
      raise Exception.Create('MovePages rejected the index');

    Doc.SaveAs('report-reordered.pdf');
  finally
    Doc.Free;
  end;
end;

Mengambil subset berdasarkan indeks

Operasi ketiga menyalin kumpulan halaman eksplisit dari satu dokumen ke dokumen lain. ImportPagesByIndex mengambil dokumen sumber dan array indeks berbasis nol, lalu memasukkan halaman-halaman tersebut ke target pada posisi yang dipilih:

function ImportPagesByIndex(
  Source           : TPdf;
  const PageIndices: array of Integer;
  InsertAt         : Integer= 0): Boolean;

Anda memanggilnya pada dokumen target dan meneruskan sumber sebagai argumen pertama. PageIndices menentukan halaman sumber yang akan ditarik, dalam urutan yang Anda inginkan; InsertAt adalah slot berbasis nol di target tempat halaman impor pertama dimasukkan, sehingga 0 menempatkannya sebelum halaman pertama yang ada dan menambah jumlah halaman target saat ini. Array kosong mengimpor setiap halaman, yang membuat pemanggilan menjadi salinan lengkap saat Anda membutuhkannya. Fungsi ini mengembalikan False jika ada indeks di luar rentang di sumber.

Di sinilah perbedaan kontras dengan pemisahan (split) sangat penting. Pemisahan menulis file terpisah, satu operasi menghasilkan banyak output di disk. ImportPagesByIndex melakukan bentuk pekerjaan sebaliknya: ia mengumpulkan sekumpulan halaman pilihan ke dalam satu dokumen target di memori, yang kemudian Anda simpan sekali. Ketika tugasnya adalah "berikan saya halaman 3, 7, dan 12 sebagai satu PDF pendek", ini adalah rute langsung, dan fungsi ini membungkus FPDF_ImportPagesByIndex di bawahnya.

var
  Source, Excerpt: TPdf;
begin
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // start an empty target

    // Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
    if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
      raise Exception.Create('A requested page index is out of range');

    Excerpt.SaveAs('manual-excerpt.pdf');
  finally
    Excerpt.Free;
    Source.Free;
  end;
end;

Menyatukannya secara bersih

Bentuk ujung-ke-ujung (end-to-end) sama di ketiganya: buka sumber dengan menyetel FileName dan mengalihkan Active ke True, lakukan operasi, simpan dengan SaveAs, dan bebaskan apa yang Anda miliki. Satu cabang yang membutuhkan kehati-hatian adalah pemanggilan mana yang mengalokasikan dokumen baru. MovePages mengubah dokumen yang sudah Anda pegang, sehingga hanya ada satu objek yang dibebaskan. ImportPagesByIndex menulis ke target yang Anda buat sendiri, sehingga Anda membebaskan sumber dan target yang Anda buka. ImportNPagesToOne adalah pengecualian, karena dokumen baru adalah nilai kembalian metode, bukan sesuatu yang Anda bangun sendiri, dan melupakan fakta bahwa ini adalah handle terpisah milik pemanggil adalah cara kebocoran dan pembebasan ganda (double-free) terjadi. Inisialisasi hasil ke nil, periksa setelah panggilan, dan bebaskan pada satu jalur saja.

If the work you actually have is combining whole files rather than rearranging pages, see merging multiple PDF files into one document. If it is the reverse, breaking one document into several files, see splitting PDF documents into multiple files. The imposition and reordering methods described here ship as part of the PDFium Component for Delphi and C++Builder, alongside the loading, rendering, and editing APIs covered elsewhere on this blog.