Artikel Teknis

Kolase Scan Duplex di Delphi: Interleave Merge PDF

CollateDocumentsEx pada pustaka PDF Delphi PDFlibPas menggabungkan beberapa dokumen terbuka menjadi satu dokumen ter-interleave. Fungsi ini menambahkan GroupSize halaman dari setiap sumber per putaran, menerima daftar rentang halaman per sumber, dan memperlakukan rentang menurun seperti 3-1 sebagai pembalikan sumber tersebut. Satu pemanggilan mengubah tumpukan halaman depan dan tumpukan halaman belakang yang terbalik menjadi urutan baca yang benar

Skenario di balik API ini sangat umum dan sehari-hari. Scanner sheet-fed dengan jalur single-sided menjalankan seluruh tumpukan menghadap bawah, lalu operator membalik tumpukan dan menjalankannya lagi. Hasilnya dua PDF: halaman depan berurutan, halaman belakang terbalik. Yang diinginkan pengguna adalah satu file, halaman 1 depan, halaman 1 belakang, halaman 2 depan, dan seterusnya. Artikel ini membahas masalah pengurutan dan jebakan duplikasi resource yang mengintai di baliknya. Jika yang jadi perhatian Anda adalah throughput penggabungan mentah, lihat penggabungan PDF cepat dengan pergeseran ref level byte; jika input terlalu besar untuk dimuat sepenuhnya ke memori, lihat menggabung dan memecah PDF gigabyte dengan akses langsung

Scanner menghasilkan dua tumpukan, salah satunya terbalik

Kolase bukan penggabungan. Merge menyambungkan rentang halaman; kolase meng-interleave-nya, dan pola interleave itu adalah properti dari perangkat fisik yang menghasilkan input tersebut. Salah pola, filenya bukan sekadar sedikit keliru, tapi tidak bisa dibaca sama sekali: setiap halaman kedua milik lembar yang berbeda. Tiga variabel menjelaskan hampir semua kasus nyata: berapa banyak sumber dalam rotasi, berapa halaman diambil dari tiap sumber per putaran, dan apakah ada sumber yang perlu dibaca terbalik. CollateDocuments mencakup dua variabel pertama dengan array handle dokumen biasa dan integer GroupSize. CollateDocumentsEx menambahkan variabel ketiga dengan menerima daftar rentang halaman dipisah titik koma, satu segmen per sumber, di mana segmen kosong berarti seluruh halaman sumber tersebut dan rentang menurun membalikkannya. Kedua fungsi menambahkan ke akhir dokumen yang sedang terpilih dan mengembalikan 1 jika berhasil, 0 jika ditolak

Kenapa kolase naif melipatgandakan ukuran file?

Karena peta impor yang memetakan nomor objek sumber ke nomor objek target dibangun ulang setiap kali fungsi salin dipanggil, dan apa pun yang bisa dijangkau dari lebih dari satu chunk akan diimpor sekali per chunk. Di dalam PDFlibPas, TPDFDocument.CopyPagesFromDoc mereset NewIndObjList di awal setiap pemanggilan. Daftar itu adalah satu-satunya memori yang dimiliki penyalin tentang apa yang sudah dibawa masuk. Panggil sekali dengan rentang sepuluh halaman dan font yang dipakai bersama oleh sepuluh halaman itu akan tertanam satu kali. Panggil sepuluh kali dengan satu halaman masing-masing dan font yang sama itu akan tertanam sepuluh kali. Ini jauh lebih berdampak pada hasil scan dibanding dokumen teks, karena halaman hasil scan adalah satu image XObject besar dan objek bersama yang punya bobot nyata adalah: profil ICC yang tertanam, rantai /DecodeParms bersama, form XObject stempel atau watermark yang diterapkan pada tiap lembar, font layer teks OCR. Cara jelas untuk menulis kolase round-robin adalah loop di atas putaran, dan loop itulah kasus patologis sesungguhnya

// Do not do this. Each CopyPageRanges call rebuilds the import map,
// so anything the two sources share internally is imported once per
// round instead of once per source.
var
  RoundIndex: Integer;
begin
  for RoundIndex := 1 to 12 do
  begin
    PDF.CopyPageRanges(Fronts, IntToStr(RoundIndex));
    PDF.CopyPageRanges(Backs, IntToStr(13 - RoundIndex));
  end;
end;

Dua belas putaran, dua sumber, dua puluh empat peta impor. Tidak ada peringatan apa pun. Urutan halaman benar, setiap halaman ter-render, dan satu-satunya gejala adalah file yang beberapa kali lebih besar dari jumlah ukuran inputnya. Pada job batch 300 halaman, pengali ini bukan sekadar galat pembulatan, melainkan beda antara arsip yang muat dalam anggaran retensi dan yang tidak

Impor sekali, lalu urutkan ulang page tree

Solusinya adalah memisahkan dua hal yang tercampur dalam loop naif tadi. Penyalinan menentukan objek apa yang ada di target; pengurutan menentukan di mana halaman-halaman itu duduk di page tree. CollateDocumentsEx menyalin tiap sumber tepat satu kali, dalam satu pemanggilan CopyPagesFromDoc dengan rentang lengkap sumber tersebut, sehingga tiap sumber mendapat satu peta impor dan resource bersama ditulis sekali. Baru setelah semua sumber masuk, proses interleave berjalan, dan itu sepenuhnya lewat TPDFPageTree.MovePage

Pemindahan halaman "gratis" dalam arti yang relevan di sini. ISO 32000-1 §7.7.3 mendefinisikan page tree sebagai struktur node dictionary seimbang yang array /Kids-nya menampung referensi tak langsung, dengan /Count membawa total leaf di tiap node. Merelokasi halaman berarti menghapus satu referensi tak langsung dari satu array /Kids, menyisipkannya ke array lain, menyesuaikan kedua nilai /Count, dan mengarahkan ulang /Parent halaman. Tidak ada content stream yang disentuh, tidak ada resource yang diduplikasi, tidak ada objek yang dibuat. Objek halaman mempertahankan nomor objeknya, yang juga alasan mengapa nomor objek tetap stabil sebagaimana dibahas di penggantian halaman yang mempertahankan nomor objek. Ada satu detail lagi yang salah ditangani oleh pemindahan halaman naif tapi tidak oleh MovePage. ISO 32000-1 §7.7.3.4 memungkinkan /Resources, /MediaBox, /CropBox dan /Rotate diwariskan dari node leluhur alih-alih dinyatakan langsung pada halaman. Halaman yang mewarisi resource-nya dari node A lalu dipindahkan ke bawah node B secara diam-diam mewarisi sesuatu yang berbeda, atau tidak mewarisi apa-apa sama sekali. Karena itu MovePage menyelesaikan nilai warisan itu dan menuliskannya ke dictionary halaman sebelum relokasi, sehingga halaman membawa atributnya sendiri melintasi perpindahan

Apa sebenarnya yang dilakukan proses pengurutan ulang?

Prosesnya menjalankan selection sort terhadap semantik insert-at. Urutan blok-relatif yang diinginkan dihitung lebih dulu: telusuri sumber secara bergiliran, ambil hingga GroupSize indeks dari tiap sumber, lewati sumber yang sudah habis, ulangi sampai setiap halaman ditempatkan. Itu menghasilkan sebuah permutasi atas blok yang ditambahkan. Menerapkannya adalah bagian yang canggung, karena MovePage adalah insert, bukan swap, jadi setiap pemindahan menggeser semua yang ada di antara posisi lama dan baru sebesar satu

Implementasinya menyimpan array Current yang memodelkan di mana tiap halaman yang ditambahkan saat ini berada, memindai maju dari posisi K untuk mencari halaman yang seharusnya ada di K, menerbitkan pemindahan, lalu menggeser entri array untuk mencerminkan apa yang dilakukan pemindahan itu terhadap tree. Ini O(n kuadrat) dalam operasi array dan nol dalam penyalinan objek, yang merupakan trade-off tepat untuk beban kerja ini: kolase 500 halaman adalah seperempat juta pengocokan integer dan tidak satu byte pun data gambar terduplikasi. Rentang menurun dan halaman berulang tidak butuh penanganan khusus dalam proses ini karena PLParsePageRangeList dipanggil dengan pengurutan dinonaktifkan dan duplikat diizinkan, sehingga urutan yang diminta tetap utuh setelah parsing

Rentang terbalik dan penggabungan duplex satu pemanggilan

Dengan pembalikan diekspresikan sebagai rentang, kasus dua kali jalan pada flatbed menyusut jadi satu pemanggilan. Halaman depan menginginkan urutan alaminya dan halaman belakang menginginkan 12-1, dan segmen pertama yang kosong sebelum titik koma berarti sumber pertama menyumbangkan seluruh halamannya

var
  PDF: TPDFlib;
  Target, Fronts, Backs: Integer;
begin
  PDF := TPDFlib.Create;
  try
    Target := PDF.NewDocument;
    if PDF.LoadFromFile('fronts.pdf', '') <> 1 then
      Exit;
    Fronts := PDF.SelectedDocument;
    if PDF.LoadFromFile('backs.pdf', '') <> 1 then
      Exit;
    Backs := PDF.SelectedDocument;
    PDF.SelectDocument(Target);
    // fronts 1..12 in order, backs scanned in reverse: F1 B12 F2 B11 ...
    if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 1 then
      PDF.SaveToFile('duplex.pdf');
  finally
    PDF.Free;
  end;
end;

Ada dua perilaku dalam cuplikan itu yang perlu dinyatakan secara eksplisit. Halaman hasil kolase ditambahkan ke dokumen yang sedang terpilih, jadi dokumen yang dibuat dengan NewDocument menyumbangkan halaman kosong awalnya di depan halaman-halaman itu, dan Anda harus menghapusnya jika tidak menginginkannya. Dan sumber-sumbernya boleh tidak seimbang: dengan GroupSize 2 di atas sumber tiga halaman dan sumber lima halaman, putarannya menjadi A1 A2 B1 B2, lalu A3 B3 B4 saat A hampir habis, lalu B5 sendirian, karena sumber yang sudah habis cukup dilewati, bukan diisi padding

Rollback, form field, dan apa yang tidak ikut terbawa

Setiap argumen divalidasi sebelum target disentuh. Handle dokumen yang hilang, dokumen terpilih yang terdaftar sebagai sumbernya sendiri, GroupSize di bawah satu, jumlah segmen yang tidak cocok dengan jumlah sumber, rentang yang menyebutkan halaman yang tidak dimiliki sumber: semuanya mengembalikan 0 dengan target tidak berubah. Kegagalan saat penyalinan adalah kasus yang lebih rumit, dan ditangani lewat DeletePages publik, bukan PageTree.DeletePages mentah. Alasannya spesifik. Penyalinan berjalan dengan MergeFormData aktif, sehingga field form dari sumber sudah ditambahkan ke array /AcroForm /Fields target pada saat sumber berikutnya gagal. Menghapus halaman di level page tree akan melucuti halaman widget dan meninggalkan referensi field itu menggantung; jalur publik melepaskan referensi field, outline dan article-thread beserta halamannya

if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 0 then
  // Nothing was appended and the target is byte-identical to before.
  // 412 is the copy failure; 0 means the arguments were rejected
  // during validation, before any page was touched.
  Log(Format('collate rejected, LastErrorCode=%d', [PDF.LastErrorCode]));

Jujurlah pada pengguna Anda soal batasannya. Kolase membawa halaman, anotasinya dan field form-nya, dan menggabungkan daftar field AcroForm, array urutan kalkulasi dan dictionary resource default. Namun tidak membawa bookmark sumber: outline tree dari tumpukan halaman depan hasil scan hampir selalu kosong, jadi tidak ada yang hilang pada kasus duplex, tapi jika Anda mengolase dua dokumen yang sudah tersusun, outline-nya tertinggal dan Anda harus membangun ulang navigasinya sendiri. Named destination yang hanya ada di katalog sumber berada di posisi yang sama. Rencanakan hal ini sebelum Anda menjanjikan kolase tanpa kehilangan apa pun kepada pelanggan

PDFlibPas menyertakan fungsi kolase ini bersama seluruh permukaan API perakitan halaman lainnya, sehingga alur kerja scanner, ekstraksi berbasis rentang dan jalur file besar semuanya berada di bawah satu komponen di Delphi dan C++Builder. Referensi API lengkap dan build trial ada di halaman produk losLab Delphi PDF library