HotPDF Delphi Component menghapus halaman dari PDF yang sudah dimuat lewat THotPDF.DeletePage, dan sejak versi 2.751.0 panggilan itu juga memangkas setiap referensi tingkat dokumen yang masih menunjuk halaman tersebut: named destination di pohon /Names /Dests, dictionary /Dests legacy di catalog, aksi /GoTo pada bookmark, elemen struktur di bawah /StructTreeRoot, ParentTree, entri OBJR untuk anotasi, dan anotasi link di halaman yang masih tersisa. Page tree-nya dibangun ulang paling akhir, setelah tidak ada apa pun lagi yang bisa mencapai objek yang dihapus
Kegagalan yang dicegahnya mudah direproduksi dan sulit didiagnosis. Hapus halaman sampul sebuah laporan bertag, simpan, lalu buka hasilnya: Acrobat menampilkan jumlah halaman yang benar, tapi bookmark "Contents" sekarang mendarat di tempat yang tidak ada, pemeriksa aksesibilitas melaporkan elemen struktur tanpa halaman, dan validator yang ketat mencantumkan referensi ke objek yang sudah dibebaskan. Tidak ada yang salah di page tree-nya. Masalahnya, halaman PDF bukan cuma daun dari /Pages; ia adalah target yang ditunjuk separuh catalog, dan membuang daunnya meninggalkan setiap pointer itu menggantung
Kenapa menghapus halaman dari /Kids tidak cukup?
Karena ISO 32000-1 mengizinkan setidaknya tujuh struktur independen memegang referensi ke objek halaman, dan hanya satu di antaranya adalah page tree. Membuang halaman dari /Kids dan mengurangi /Count memenuhi §7.7.3, dan setiap referensi lain berubah jadi pointer ke objek yang entah dibebaskan di xref atau sekadar tidak ada di file hasil penulisan ulang. Viewer yang mengikuti salah satu pointer itu mendapat null, dan apa yang dilakukannya dengan null itu terserah viewer-nya
- Name tree di bawah
/Names/Dests(§7.7.4, §12.3.2.3) memetakan nama ke array destination yang elemen pertamanya adalah halaman itu - Dictionary
/Destspra-1.2 yang berada langsung di catalog memegang array sejenis yang di-key berdasarkan nama - Item outline (§12.3.3) mencapai halaman entah lewat
/Destinline atau lewat aksi/Adengan/S /GoTodan array/D - Elemen struktur (§14.7.2) membawa key
/Pgyang menyebut halaman tempat marked content-nya berada, dan kid/K-nya bisa berupa referensi marked content serta referensi objek (§14.7.4.3) yang terikat pada halaman itu ParentTree(§14.7.4.4) memetakan nomor/StructParentshalaman dan anotasi kembali ke elemen struktur, dan sebuah elemen bisa berada di sana tanpa pernah muncul di rantai/Kdari root- Anotasi link di halaman lain (§12.5.6.5) membawa
/Destatau aksi/GoToyang menargetkan halaman itu, dan/OpenActiondi catalog bisa melakukan hal yang sama
Apa yang dibersihkan THotPDF.DeletePage sebelum menyentuh page tree?
THotPDF.DeletePage(PageIndex) pada dokumen yang sudah dimuat menjalankan seluruh penyapuan referensi lebih dulu, lalu menandai objek halamannya terhapus dengan DeleteObj, melepaskan anotasi widget apa pun dari pohon field AcroForm, menggeser array halaman internalnya, dan terakhir memanggil RebuildLoadedPageTree untuk menulis ulang /Kids, /Count, dan /Parent setiap halaman yang bertahan. Penyapuannya mengunjungi catalog dalam urutan tetap: name tree /Names /Dests, dictionary /Dests gaya lama, /OpenAction, pohon outline, /StructTreeRoot beserta ParentTree-nya, dan terakhir array /Annots setiap halaman yang tetap ada. Setiap langkah memutuskan apakah sebuah referensi dihapus, dialihkan targetnya, atau dibiarkan sesuai apa yang diizinkan spesifikasi bagi struktur itu tanpa halaman tersebut. Dua penjaga berlaku sebelum semua itu berjalan: DeletePage memunculkan Invalid page number untuk indeks di luar jangkauan dan menolak menghapus halaman terakhir, karena node /Pages tanpa kid bukan PDF yang valid, sedangkan DeletePages memakai notasi "1,3-5,7-" berbasis satu yang sama dengan operasi halaman lain pada dokumen termuat dan mengiterasi dari indeks terpilih tertinggi ke bawah supaya indeks yang Anda tulis tetap valid selama prosesnya
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
begin
// Berbasis nol: buang halaman sampul. Named destination,
// bookmark, structure tree, ParentTree, dan anotasi
// link yang menunjuknya dipangkas sebelum pohon
// /Pages dibangun ulang.
Pdf.DeletePage(0);
// Sintaks rentang berbasis satu untuk batch, di dalamnya
// indeks tertinggi lebih dulu supaya indeks sebelumnya tetap valid.
Pdf.DeletePages('3-4,9');
Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
end;
finally
Pdf.Free;
end;
end;
Bagaimana named destination dan bookmark ditangani secara berbeda?
Named destination dihapus dan bookmark dialihkan targetnya, karena nama yang sudah tidak ada adalah hasil yang bisa diterima sementara bookmark tanpa destination adalah cacat yang terlihat. Di pohon /Names /Dests HotPDF menelusuri setiap node, menguji setiap destination, baik dalam bentuk array polos maupun bentuk dictionary dengan key /D, terhadap halaman yang dihapus, lalu membuang pasangan nama/nilainya ketika elemen pertama array itu adalah halaman tersebut. Node yang /Names dan /Kids-nya sama-sama berakhir kosong ditandai terhapus dan dilepas dari parent-nya, jadi pohonnya tidak pernah menyimpan daun kosong. Uji yang sama berjalan atas dictionary /Dests gaya lama, dan /OpenAction di catalog langsung dibuang kalau ia membuka halaman yang dihapus. Satu batas di sini: ketika sebuah node name tree kehilangan entri, HotPDF menghapus pasangan /Limits node itu alih-alih menghitung ulang key terendah dan tertingginya yang baru, dan meskipun viewer meresolusi nama dengan baik tanpa itu, pemeriksa konformansi ketat yang membaca ISO 32000-1 §7.9.6 bisa menandai node bukan-root yang tidak punya /Limits
Item outline arahnya sebaliknya. RetargetOutlineDestinations menelusuri /First dan /Next dari root outline, dengan daftar yang sudah dikunjungi dan batas kedalaman 128 supaya pohon siklik yang rusak tidak bisa menggantungkan panggilannya, dan untuk setiap array /Dest atau array /D pada aksi /GoTo yang mengarah ke halaman itu ia mengganti elemen pertamanya dengan NearestRetainedPage: halaman yang menyusul halaman yang dihapus, atau halaman sebelumnya ketika yang dihapus adalah halaman terakhir. Parameter view setelah referensi halamannya dibiarkan seperti semula. Bookmark yang tadinya menunjuk pembuka bab yang dihapus karena itu mendarat di halaman pertama dari apa yang tersisa alih-alih hilang dari sidebar, dan itulah perilaku yang diharapkan peninjau dari dokumen yang dipangkas. Tapi uji destination-nya hanya mencocokkan array eksplisit: item outline yang /Dest-nya berupa string nama yang dulunya meresolusi ke halaman yang dihapus tidak dialihkan, karena entri name tree-nya sudah hilang dan referensinya sekarang tidak meresolusi ke apa pun alih-alih ke objek yang dibebaskan, sehingga viewer memperlakukannya sebagai bookmark mati. Mekanika pohon outline-nya sendiri, /First, /Next, dan semantik /Count yang tidak intuitif, dibahas di panduan menambahkan bookmark dan named destination pada PDF yang sudah dimuat
// Verifikasi penyapuannya alih-alih mempercayainya.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
ShowMessage('Named destination "cover" was pruned');
// Bookmark yang menargetkan sampul sekarang meresolusi ke
// halaman yang menyusulnya (indeks berbasis nol 0 setelah penghapusan).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
ShowMessage('Bookmark retargeted to the nearest retained page');
Apa yang terjadi pada structure tree dan ParentTree?
Elemen struktur yang hanya ada karena halaman yang dihapus akan dibuang, dan elemen yang membentang beberapa halaman kehilangan key /Pg-nya tapi tetap menyimpan child-nya. PruneStructureElement menuruni rantai /K dari /StructTreeRoot sampai kedalaman 128, menangani bentuk array maupun bentuk dictionary tunggal dari /K yang diizinkan §14.7.2. Untuk setiap elemen ia memangkas kid-nya lebih dulu, lalu menilai elemennya sendiri: kalau pemangkasan mengosongkan /K-nya, elemen itu ditandai terhapus dan parent-nya membuangnya. Kalau /Pg milik elemen itu sendiri menyebut halaman yang dihapus dan elemen itu masih punya kid plus parent /P, hanya /Pg yang dihapus, karena /Pg pada sebuah elemen adalah halaman default bagi kid marked content-nya dan kid itu bisa merujuk halaman lain secara eksplisit. Hanya elemen yang /Pg-nya adalah halaman yang dihapus dan tidak menyisakan apa pun di bawahnya yang dibuang sekalian
ParentTree mendapat perlakuan yang sama, dan alasannya adalah hal yang menggigit selama pengembangan: sebuah elemen struktur bisa terjangkau dari ParentTree dan tidak dari mana pun lagi. Number tree itu memetakan integer /StructParents ke satu elemen atau array elemen, dan PruneParentTreeNode menjalankan PruneStructureElement atas setiap nilai yang ditemuinya, membuang nilai yang sudah dipangkas, menghapus pasangan /Nums ketika array nilainya kosong, dan melepas node yang /Nums dan /Kids-nya sama-sama hilang. Memangkas hanya keturunan /K akan meninggalkan elemen yatim itu tetap menunjuk halaman yang sudah dibebaskan lewat /Pg dan menunjuk referensi marked content yang sudah dibebaskan lewat kid /MCR-nya. Kalau Anda mengekstrak teks dalam urutan struktur, itu berdampak langsung: ekstraksi teks berurutan struktur menelusuri persis pohon-pohon ini, dan elemen dengan /Pg null adalah paragraf yang diam-diam gugur dari urutan bacaannya
Anotasi link mana di halaman yang bertahan yang ikut dihapus?
Anotasi link apa pun di halaman yang bertahan yang array /Dest-nya atau aksi /GoTo-nya menunjuk halaman yang dihapus akan dibuang bersama kepemilikannya di structure tree. RemoveRetainedPageDestinationAnnotations menelusuri array /Annots setiap halaman selain targetnya, menerapkan uji destination yang sama seperti untuk outline, menandai anotasi yang cocok terhapus, membuangnya dari array-nya, lalu memanggil PruneAnnotationReferencesInStructureTree supaya dictionary OBJR yang /Obj-nya menyebut anotasi itu ikut dibuang dari elemen strukturnya, dan elemennya sendiri dibuang kalau OBJR itu satu-satunya kid-nya. Membiarkan OBJR-nya di tempatnya akan melanggar §14.7.4.3, yang mewajibkan /Obj merujuk objek yang ada, dan akan muncul di pemeriksaan PDF/UA sebagai link bertag tanpa anotasi di belakangnya. Perhatikan asimetrinya dengan bookmark: link dihapus, bukan dialihkan. Rujukan silang di badan teks yang berbunyi "lihat halaman 3" jadi salah begitu halaman 3 hilang, dan mengarahkannya ke halaman 4 akan jadi kebohongan dengan cara yang tidak dilakukan bookmark yang mendarat di bab terdekat, jadi kalau alur kerja Anda butuh link itu dipertahankan, alihkan sendiri sebelum memanggil DeletePage
Kenapa /MCR atau /OBJR yang dibuang tidak boleh pernah didaftarkan sebagai free?
Karena referensi marked content dan referensi objek biasanya berupa dictionary langsung di dalam array /K elemen parent-nya, dan registry perubahan inkremental meresolusi objek langsung ke objek tak langsung terdekat yang memuatnya. Ketika RemoveArrayItem membuang kid dari array /K, ia meng-free objek di memori hanya kalau objek itu THPDFLink atau nilai non-tak-langsung, dan MarkRemovedObject mendaftarkan objek ke free list hanya kalau nomor objeknya lebih besar dari nol. Versi pertama penyapuan ini tidak membuat pembedaan itu, dan efeknya pada incremental save persis seperti yang memang dirancang registry-nya: RegisterIncrementalChange menelusuri dari /MCR langsung ke atas sampai root transaksi graph-nya, yang ternyata elemen struktur yang bertahan dan memilikinya, lalu menuliskan elemen itu sebagai null. Dokumen yang kehilangan satu halaman kembali dengan konten bertag di halaman-halaman lain diam-diam tak lagi bertag. Satu-satunya langkah yang benar untuk kid langsung adalah menandai container-nya kotor lewat TouchContainer supaya container-nya ditulis ulang, dan membiarkan free list-nya
// Incremental update: hanya container yang tersentuh dan
// objek halaman yang dibebaskan yang masuk ke bagian tambahan.
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('tagged-report.pdf');
Pdf.DeletePage(0);
// Elemen struktur yang bertahan, yang /K-nya kehilangan /MCR
// langsung, ditulis ulang di tempat, tidak pernah jadi null.
Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
Pdf.Free;
end;
Kehati-hatian yang sama membentuk apa yang sengaja tidak dibebaskan DeletePage pada dokumen termuat. Content stream, XObject, dan anotasi non-widget milik halaman yang dihapus dibiarkan sebagai objek, karena file yang dimuat bisa saja memakai salah satunya bersama halaman yang bertahan dan tidak ada cara murah untuk membuktikan sebaliknya pada saat penghapusan. Membuang referensi page tree-nya sudah cukup untuk kebenaran; byte yang masih ditempati objek-objek itu adalah pertanyaan terpisah, dan analisis object dependency graph dan retained bytes adalah alat untuk mengukur apa yang masih dibawa dokumen yang dipangkas
DeletePage versus DeleteLoadedPage: mana yang harus Anda panggil?
Panggil DeletePage untuk penghapusan halaman apa pun yang dihadapi pengguna, dan simpan DeleteLoadedPage untuk kasus di mana seluruh dokumennya sedang dialirkan ulang dan tidak ada referensi tingkat dokumen yang layak dipertahankan. THotPDF.DeleteLoadedPage(PageIndex), yang ditambahkan di versi 2.508.0, adalah varian ringannya: ia menggeser array halaman internalnya, memanggil RebuildLoadedKidsArray untuk menulis ulang /Kids dan /Count, membatalkan validitas cache halaman terender, dan menembakkan OnLoadedDocumentModified. Ia tidak menelusuri name tree, outline, structure tree, atau anotasi halaman lain, dan tidak menandai objek halamannya terhapus. Itulah alat yang tepat di dalam N-up imposition, di mana HotPDF menambahkan lembar yang baru disusun lalu membuang setiap halaman aslinya dengan DeleteLoadedPage(0): halaman sumbernya diganti seluruhnya, dan konten lembarnya merujuk resource-nya alih-alih objek halamannya. Untuk pekerjaan biasa "hapus halaman 7 dari kontrak ini", DeletePage adalah satu-satunya panggilan yang meninggalkan dokumen bertag, ber-bookmark, dan ber-rujukan silang yang cukup konsisten untuk lolos validator, baik dalam penulisan ulang penuh lewat SaveLoadedDocument maupun dalam incremental update lewat SaveIncrementalUpdate. Kedua method itu dikirim dalam HotPDF Delphi Component untuk Delphi dan C++Builder, tanpa runtime viewer eksternal atau dependensi apa pun