Mengganti halaman 3 dari sebuah kontrak yang sudah disetujui seharusnya tidak menggeser daftar isi. Hapus halaman lama, sisipkan yang baru, dan setiap bookmark yang tadinya menunjuk ke sana kini mendarat di tempat lain. PDFlibPas Delphi PDF library menghindari ini dengan mempertahankan objek halaman target itu sendiri dan hanya memindahkan entri yang membawa konten visual
Kenapa bookmark rusak setelah mengganti halaman PDF?
Bookmark rusak karena sebuah destination PDF menyebut halaman lewat indirect object reference, bukan lewat nomor halaman. ISO 32000-1 §12.3.2.2 mendefinisikan explicit destination sebagai array yang elemen pertamanya adalah indirect reference ke objek halaman. Hapus objek itu dan tambahkan penggantinya, dan reference tersebut jadi dangling: kebanyakan viewer merespons dengan menjatuhkan pembaca ke halaman 1, yang persis merupakan gejala yang dilaporkan orang setelah penggantian dengan pola hapus-lalu-sisipkan. Page tree terlihat sempurna, jumlah halaman benar, rendering benar, dan seluruh lapisan navigasi diam-diam salah
Named destination pun tidak menyelamatkan Anda. §12.3.2.3 merutekan sebuah nama lewat name tree /Dests di document catalogue, tetapi leaf tempat nama itu diresolusi tetap berupa array explicit destination yang menyimpan page reference yang sama. Penamaan menambahkan lapisan indireksi di atas page reference, bukan di sekelilingnya. Alasan yang sama berlaku untuk sisa lapisan interaktif yang dijelaskan di §12.5: sebuah link annotation membawa /Dest atau aksi GoTo /A yang /D-nya adalah array itu, setiap annotation boleh membawa entri /P yang merupakan indirect reference ke halamannya, dan sebuah form field widget adalah annotation dengan kedudukan yang persis sama. Satu penggantian halaman yang naif melepaskan empat subsistem sekaligus, dan bila Anda ingin melihatnya didaftar pada berkas sungguhan, object graph yang sama itulah yang ditelusuri introspeksi outline dan annotation
Entri halaman mana yang membawa identitas dan mana yang membawa tampilan
Sebuah page dictionary mencampur dua jenis entri, dan penggantian in-place berhasil justru ketika Anda memisahkan keduanya. Sisi tampilan bersifat terbatas dan bisa didaftar: /Contents, /Resources, lima page box /MediaBox, /CropBox, /BleedBox, /TrimBox, dan /ArtBox, ditambah /Rotate, /Group, /UserUnit, dan /BoxColorInfo. Sebelas entri ini menentukan segala sesuatu yang dihasilkan sebuah rasteriser untuk halaman tersebut, dan tidak ada yang lain dalam berkas yang menunjuk ke mereka lewat nama
Sisi identitas adalah apa yang telah diikat oleh sisa dokumen: nomor object halaman dan generation-nya, back-link /Parent ke dalam page tree, dan /Annots. PDFlibPas mempertahankan semuanya tanpa disentuh. ReplacePageRanges membersihkan sebelas entri visual dari page dictionary target dan menambahkannya kembali dari halaman sumber yang diimpor, sehingga objek halaman target dimutasi di tempat, bukan diganti. Struktur page tree yang disyaratkan §7.7.3 juga tetap byte-identical bentuknya: urutan /Kids, /Count, dan setiap /Parent yang bertahan tetap sama sebelum dan sesudah, karena tidak ada satu node pun yang pernah dilepas tautannya
Bagaimana PDFlibPas mengganti halaman tanpa menomori ulang objek?
Pemanggilannya mengambil sebuah dokumen sumber, halaman awal target berbasis 1, sebuah ekspresi rentang sumber, dan sebuah flag opsi. Kedua dokumen harus terbuka dalam instance yang sama, dan dokumen target adalah yang sedang dipilih. Karena jumlah halaman target tidak pernah berubah, rentang yang Anda minta harus muat di dalam dokumen mulai dari TargetStartPage, dan itu diperiksa sebelum apa pun dibuat
var
Lib: TPDFlib;
TargetDoc, SourceDoc: Integer;
begin
Lib := TPDFlib.Create;
try
// The document whose bookmarks and links must survive
if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
Exit;
TargetDoc := Lib.SelectedDocument;
// The revised clause page, rendered by whatever produced it
if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
Exit;
SourceDoc := Lib.SelectedDocument;
Lib.SelectDocument(TargetDoc);
// Source page 1 overwrites the visuals of target page 3.
// Page count, page 3 object number, bookmarks and annotations are kept.
if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
Lib.SaveToFile('contract-final.pdf');
finally
Lib.Free;
end;
end;
Secara internal, halaman sumber tidak bisa begitu saja dibaca lintas batas dokumen, karena setiap indirect reference di dalamnya masih memakai penomoran objek dari sumber. Karena itu rentang sumber pertama-tama diimpor dengan cara biasa, sebagai halaman sementara yang ditambahkan setelah halaman terakhir yang sungguhan, yang menjalankan remapping penuh atas object graph: content stream, font, XObject, shading, dan colour space semuanya dinomori ulang ke dalam dokumen target. Baru setelah itu sebelas entri visual disalin dari setiap halaman sementara ke halaman targetnya, dan baru setelah itu halaman sementara dilepas tautannya dari page tree. Pekerjaan remapping terjadi di tempat yang murah dan aman, dan pengeditan yang destruktif direduksi menjadi pertukaran level-dictionary pada halaman yang sudah ada
Jalur hapus yang akan menghancurkan apa yang baru saja Anda pindahkan
Menghapus halaman sementara itu adalah langkah yang terlihat sepele padahal tidak. Jalur penghapusan halaman biasa di pustaka ini melakukan lebih dari sekadar melepas tautan sebuah node: ia menggabungkan layer dari setiap halaman yang dihapus, mengosongkan content stream pertama, dan mengambil kembali resource yang tidak dipakai bersama halaman lain. Itu adalah perilaku yang benar untuk penghapusan sungguhan, dan bencana di sini, karena pada saat halaman sementara dihapus, halaman target sudah merujuk persis ke content stream dan resource object tersebut. Mengosongkannya akan mengosongkan halaman yang baru saja Anda ganti, dan sapuan resource akan mengumpulkan font dan gambar yang kini sudah punya pemilik yang hidup
Perbaikannya adalah mode preserve-referenced-objects pada jalur hapus internal. Ketika mode ini diaktifkan, penghapusan melewati baik sapuan resource-tak-dibagi maupun pembersihan content stream, dan tidak melakukan apa pun selain melepas halaman dari page tree dan memperbaiki pembukuan tree. Objek yang dipindahkan tetap bertahan dengan pemilik baru, dan kepemilikan objek setelah operasi ini adalah seperti yang akan Anda gambar di whiteboard: satu content stream, satu halaman pemilik, satu nomor objek yang tidak pernah berpindah. Aturan siklus hidup terkait untuk membuat, menghapus, dan menyusun ulang halaman dibahas terpisah dalam catatan tentang operasi siklus hidup dokumen dan halaman
Urutan, duplikat, dan kegagalan semua-atau-tidak-sama-sekali
Flag opsi memilih bagaimana rentang sumber diinterpretasikan. 0 mengurutkan nomor halaman yang sudah diurai dan membuang duplikat, yang merupakan default yang masuk akal ketika caller mengirim sesuatu seperti '4-6,2' dan cukup berarti keempat halaman itu. 1 mempertahankan urutan yang Anda tulis dan mengizinkan sebuah halaman berulang, sehingga '2,1,2' benar-benar berarti tiga penggantian yang diambil dari dua halaman sumber. Validasi berjalan lebih dulu dan berjalan lengkap: sintaks rentang, setiap nomor halaman terhadap jumlah halaman sumber, nilai opsi itu sendiri, dan kapasitas target semuanya diperiksa sebelum satu objek pun dibuat. Pemanggilan yang ditolak mengatur LastErrorCode ke 412, mengembalikan halaman yang sebelumnya dipilih, dan meninggalkan dokumen persis seperti semula
var
Replaced: Integer;
begin
Lib.SelectDocument(TargetDoc);
// Options = 1: source order is preserved and repeats are allowed, so
// target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
if Replaced = 0 then
raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
[Lib.LastErrorCode]);
// On success the selection is the first replaced page
Assert(Lib.SelectedPage = 5);
end;
Atomisitas ini meluas melampaui validasi hingga ke proses transfer itu sendiri. Sebelum halaman sumber pertama diimpor, sebelas entri visual dari setiap halaman target dalam rentang di-snapshot sebagai nilai yang sudah diencode. Jika impor gagal, atau jumlah halaman yang diimpor tidak sesuai dengan yang diminta, snapshot itu di-decode kembali ke halaman target dan halaman sementara dihapus, sehingga kegagalan di tengah proses tetap meninggalkan tampilan asli di tempatnya pada objek aslinya. Itu lebih penting daripada kedengarannya: rentang halaman yang setengah terganti dalam sebuah kontrak lebih buruk daripada pemanggilan yang gagal, karena tidak ada apa pun dalam berkas yang menandainya sebagai setengah selesai
// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);
Apa yang masih tidak dilakukan penggantian in-place untuk Anda?
Anotasi sumber, form field sumber, dan outline sumber sengaja tidak diimpor. Membawa sebuah widget tanpa entri field /AcroForm-nya, atau sebuah annotation pembawa marked-content tanpa kepemilikan structure tree-nya, menghasilkan objek interaktif setengah-impor yang tidak bisa dipahami viewer mana pun, sehingga operasi ini hanya memindahkan tampilan saja. Konsekuensi praktisnya adalah jika halaman pengganti seharusnya membawa form field baru atau link baru, Anda menambahkannya ke halaman target belakangan, terhadap objek halaman target yang masih ada di sana menunggu mereka
Ada dua batasan lagi yang layak diperiksa pada berkas Anda sendiri. Pertama, /Annots dipertahankan tapi geometri halaman tidak, sehingga mengganti halaman 220 mm dengan halaman 320 mm membuat rectangle annotation tetap di koordinat lamanya di dalam /MediaBox yang ukurannya berbeda; jika geometri berubah, atur ulang posisi annotation yang Anda pertahankan. Kedua, entri di luar sebelas key visual tetap bersama halaman target sesuai desain, yang benar untuk /Trans atau /AA tetapi basi untuk /Thumb, sehingga regenerasi thumbnail setelah sebuah penggantian. Dokumen tagged membutuhkan satu pemikiran tambahan: elemen structure tetap menunjuk ke objek halaman yang benar lewat /Pg, tetapi identifier marked-content miliknya mendeskripsikan konten yang sudah tidak ada lagi di sana, sehingga penggantian halaman di dalam workflow PDF/UA adalah pengeditan structure-tree sekaligus pengeditan konten. Bila pekerjaan Anda sebenarnya adalah compositing, bukan penggantian, melapiskan artwork ke halaman yang Anda pertahankan, pendekatan page stitching dan template adalah alat yang lebih murah
Semua yang dijelaskan di sini, termasuk sintaks ekspresi rentang, nilai opsi, dan API manipulasi halaman di sekitarnya, hadir dalam PDFlibPas Delphi PDF Library standar untuk Delphi dan C++Builder, yang dokumentasi referensinya memuat entri lengkap untuk pemanggilan penggantian halaman dan kode errornya