Artikel Teknis

Edit Outline PDF dan Pemetaan Ulang Halaman di Delphi

Buang tujuh halaman dari handbook 200 halaman dan setiap bookmark mendarat di tempat yang salah. Solusinya bukan membangun ulang outline dari daftar judul yang datar. PDFiumPas mengekspos TPdfOutlineEditor, yang memuat pohon outline sesungguhnya, membiarkan Anda memindahkan dan mengalihkan item, lalu menjalankan ApplyPageMap untuk menggeser setiap destinasi eksplisit melewati rencana halaman Anda

Mengapa menghapus halaman merusak setiap bookmark?

Karena item outline tidak menyimpan nomor halaman. Ia menyimpan referensi ke objek halaman, dan saat objek halaman berubah, referensi itu entah menunjuk ke halaman yang berpindah atau tidak menunjuk ke apa pun. ISO 32000-1 §12.3.2.2 mendefinisikan destinasi eksplisit sebagai array yang elemen pertamanya referensi indirect ke kamus halaman, diikuti nama fit seperti /Fit atau /XYZ. Hapus halamannya dan Anda mendapat referensi menggantung; susun ulang halaman dan referensinya tetap valid tetapi kini mendeskripsikan bab yang berbeda. PDFiumPas me-resolve array itu kembali ke nomor halaman saat memuat, sehingga TPdfOutlineItem.PageNumber memberi Anda indeks halaman berbasis satu yang cocok dengan API publik TPdf, bukan nomor objek. Itulah inti abstraksinya: logika pemetaan ulang Anda bekerja di sistem koordinat yang sama dengan rencana halaman yang sudah Anda bangun saat memecah, menyusun ulang, atau meng-impose dokumen. Jika Anda sedang membangun rencana itu, konvensi berbasis satu yang sama berlaku di memecah dokumen PDF menjadi beberapa berkas dan di imposisi n-up dan penataan ulang halaman

Outline adalah pohon bertaut ganda, bukan daftar

Alasan Anda tidak bisa sekadar menserialisasi array judul yang datar adalah ISO 32000-1 §12.3.3 menghubungkan setiap item outline ke lima tautan terpisah: /Parent, /Prev, /Next, /First, dan /Last. Memindahkan satu subtree karenanya menulis ulang parent lama, parent baru, kedua tetangga sibling di kedua sisi potongan dan titik penyisipan, serta pointer parent milik node yang dipindahkan. Salah satu pun salah, reader yang patuh menampilkan pohon terpotong, atau berputar. PDFiumPas menyimpan state edit sebagai array depth-first record TPdfOutlineItem dengan Id integer yang stabil, sehingga sebuah subtree adalah irisan kontinu dan rantai sibling diturunkan, tidak pernah dirawat manual. TPdfOutlineEditor.Move mengangkat irisan itu, menyisipkannya kembali di bawah parent baru pada indeks sibling yang diminta, dan hanya menetapkan ulang akar bloknya. Ia juga menolak dua pemindahan yang akan merusak graf: memindahkan item ke dalam subtree-nya sendiri, dan menyebut parent yang tidak ada

Pengeditan outline PDFiumPas di Delphi: memindahkan Bab 3 keluar dari Bagian I ke bawah akar dokumen menulis ulang pointer /Parent node yang dipindah ditambah tautan /First serta /Prev dan /Next sibling di sekitar potongan dan titik penyisipan
Satu pemanggilan Move menulis ulang pointer parent subtree yang diangkat dan tautan sibling di kedua sisi potongan serta titik penyisipan

Mengapa /Count bertanda?

Karena tanda membawa state terbuka, bukan ukuran. /Count positif berarti item terbuka dan angkanya adalah jumlah keturunan yang sedang terlihat; /Count negatif berarti item terlipat. PDFiumPas menulis jumlah keturunan untuk setiap item yang punya anak dan menegatifkannya saat IsOpen bernilai False, dan saat memuat ia membaca state balik sebagai IsOpen := HasCount and (CountValue > 0). Inilah bug paling lazim yang ditulis tangan di penulis outline: mengeluarkan count tak bertanda dan diam-diam memaksa seluruh pohon terbuka

Cara PDFiumPas mengenkode state ekspansi outline di Delphi: /Count positif berarti item terbuka dan menghitung keturunan yang terlihat, /Count negatif berarti terlipat, dan count tak bertanda memaksa setiap reader membentangkan seluruh pohon
Tanda /Count adalah state ekspansi dan magnitudonya adalah jumlah keturunan yang terlihat, sehingga count tak bertanda diam-diam membentangkan seluruh pohon
var
  Source, Dest: TMemoryStream;
  Editor: TPdfOutlineEditor;
  Options: TPdfOutlineEditOptions;
  Report: TPdfOutlineValidationReport;
  RootId, ChapterId: Integer;
begin
  Source := TMemoryStream.Create;
  Dest := TMemoryStream.Create;
  Editor := nil;
  try
    Source.LoadFromFile('handbook.pdf');
    Options := TPdfOutlineEditOptions.Default;   // MaxItems 100000, MaxDepth 64
    if not TPdfOutlineEditor.TryLoad(Source, Options, Editor, Report) then
      raise Exception.Create(Report.ErrorMessage);

    RootId := Editor[0].Id;
    ChapterId := Editor[2].Id;

    Editor.Move(ChapterId, RootId, 1);           // menjadi anak kedua dari root
    Editor.SetTitle(ChapterId, 'Appendix B');
    Editor.SetStyle(ChapterId, [posBold, posItalic]);
    Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
    Editor.SetExpanded(RootId, False);           // menulis /Count negatif
    Editor.Retarget(ChapterId, 12, '/XYZ 10 20 1');

    if not Editor.SaveIncremental(Source, Dest, Report) then
      raise Exception.Create(Report.ErrorMessage);
    Dest.SaveToFile('handbook-edited.pdf');
  finally
    Editor.Free;
    Dest.Free;
    Source.Free;
  end;
end;

Retarget menangani kedua bentuk yang diizinkan spesifikasi. Berikan DestinationInAction sebagai False dan PDFiumPas menulis array /Dest langsung; berikan True dan ia menulis aksi Go-To, /A << /S /GoTo /D [ page ref suffix ] >>, sesuai ISO 32000-1 §12.6.4.2. Bagaimana pun, ia lebih dulu membuang /Dest dan /A yang sudah ada dari item sehingga keduanya tak bisa hidup berdampingan dan saling bertentangan. Sufiks defaultnya /Fit dan harus diawali nama PDF, itulah alasan sufiks kosong atau cacat langsung melempar alih-alih menghasilkan array destinasi yang tak bisa diparse reader mana pun

Bagaimana ApplyPageMap mengonsumsi rencana halaman?

ApplyPageMap menerima persis array yang sudah divalidasi rencana halaman Anda: NewPageNumbers, terindeks halaman lama dikurangi satu, berisi nomor halaman baru berbasis satu atau nol ketika halaman itu tidak bertahan. Ia menelusuri array item dari belakang sehingga menghapus subtree tidak pernah membatalkan indeks yang belum dikunjungi, dan ia melaporkan apa yang dilakukannya lewat RemappedDestinationCount dan RemovedDanglingItemCount

var
  NewPageNumbers: array of Integer;
  Report: TPdfOutlineValidationReport;
  I: Integer;
begin
  // Satu entri per halaman dokumen ORIGINAL
  SetLength(NewPageNumbers, OriginalPageCount);
  for I := 0 to OriginalPageCount - 1 do
    NewPageNumbers[I] := 0;              // 0 == halaman ini dibuang

  NewPageNumbers[0] := 1;                // halaman lama 1 -> halaman baru 1
  NewPageNumbers[1] := 2;
  NewPageNumbers[9] := 3;                // halaman lama 10 -> halaman baru 3

  // True: hapus seluruh subtree menggantung. False: pertahankan item, lepas targetnya
  if not Editor.ApplyPageMap(NewPageNumbers, True, Report) then
    raise Exception.Create(Report.ErrorMessage);

  WriteLn(Format('%d remapped, %d dangling items removed',
    [Report.RemappedDestinationCount, Report.RemovedDanglingItemCount]));
end;

Flag DeleteDangling menentukan kebijakan untuk destinasi yang terpetakan ke nol, dan kedua cabangnya disengaja. Dengan True, PDFiumPas menghapus item beserta seluruh subtree-nya, karena node outline yang targetnya lenyap lazimnya memimpin bab yang lenyap bersamanya. Dengan False, item bertahan dengan judul dan hierarkinya utuh tetapi /Dest dan /A-nya dilepas, yang Anda inginkan ketika seorang manusia akan mengalihkannya saat review. Input yang benar-benar cacat tetap gagal dengan keras alih-alih ditambal: entri negatif atau destinasi yang menunjuk melampaui ujung map yang diberikan mengembalikan False dengan IssueKind disetel poviInvalidPageMap

Cara ApplyPageMap PDFiumPas mengalihkan bookmark PDF di Delphi: page map yang terindeks halaman lama dikurangi satu mengirim destinasi yang bertahan ke nomor halaman barunya, sementara entri yang terpetakan ke nol dihapus bersama subtree-nya atau dilepas targetnya
Page map terindeks halaman lama dikurangi satu, dan entri nol entah menghapus subtree menggantung atau membiarkan item dengan targetnya dilepas

Entri opak, dan trade-off yang diakui terus terang

Tidak setiap item outline punya nomor halaman yang bisa dipahami PDFiumPas. Tiga jenis dibawa lewat tanpa disentuh: destinasi bernama, aksi yang bukan /S /GoTo, dan kunci kamus tak dikenal yang ditambahkan pembuat berkasnya. Ketiganya dimuat dengan PageNumber bernilai nol, menyimpan byte aslinya di item, dan ditulis balik apa adanya kecuali Anda secara eksplisit memanggil Retarget padanya

  • Destinasi bernama adalah kunci ke name tree dokumen, jadi memetakan ulangnya dengan benar berarti me-resolve pohon itu dan menulis ulang entri targetnya, bukan menebak di level outline
  • Aksi /URI, /Launch, atau JavaScript tidak punya semantik halaman sama sekali dan tidak boleh dikonversi diam-diam menjadi Go-To
  • Kunci spesifik vendor dan destinasi struktur dipertahankan karena menjatuhkan yang tidak Anda pahami adalah cara round-trip kehilangan data

Biayanya nyata dan layak dinyatakan terus terang: ApplyPageMap melewatkan item-item itu sepenuhnya, sehingga dokumen yang bookmark-nya semuanya memakai destinasi bernama akan lolos dari penghapusan halaman dengan outline yang valid secara struktural tapi basi secara semantik. Itu pilihan yang disengaja — tautan basi yang bisa ditangkap reviewer mengalahkan tautan yang salah dengan yakin dan tak ada yang menyadarinya. Jika Anda sedang mentriase berkas masuk sebelum mengeditnya, satu pass inventaris di workbench review intake PDF akan memberi tahu dokumen mana yang masuk kelompok itu

Menyimpan: revisi inkremental, lalu muat ulang independen

TPdfOutlineEditor.SaveIncremental menambahkan revisi inkremental sparse alih-alih menulis ulang berkas. Item yang dimuat mempertahankan referensi objek indirect aslinya termasuk generasi persisnya, sehingga cross-reference yang ada tetap valid; hanya item yang Anda tambahkan yang menarik nomor segar, dialokasikan dari satu di atas nomor objek maksimum revisi. Katalog diperbarui di revisi yang sama, dan entri /Outlines yang hilang ditambahkan ke sana saat sumber sama sekali tidak punya outline

Apa yang terjadi setelah penulisan adalah bagian yang layak ditiru. PDFiumPas membuka ulang stream tujuan dengan editor yang sepenuhnya independen dan membandingkan pohon yang dimuat ulang dengan yang di memori — jumlah item, judul, nomor halaman, sufiks destinasi, bentuk aksi-versus-destinasi-langsung, style, state ekspansi, dan relasi parent. Ketidakcocokan apa pun, atau kegagalan muat apa pun, mengosongkan stream tujuan dan mengembalikan poviVerificationFailure alih-alih menyerahkan berkas yang tampak masuk akal. Sumber terenkripsi ditolak di muka dengan poviEncryptedInput, karena judul dan destinasi baru menciptakan konten string yang tidak bisa dihasilkan dengan menyalin trailer /Encrypt ke depan

if not Editor.SaveIncremental(Source, Dest, Report) then
  case Report.IssueKind of
    poviEncryptedInput:
      Log('Source is encrypted; outline editing needs an unprotected copy');
    poviInvalidDestination:
      Log(Format('Item %d %d targets a missing page',
        [Report.ObjectNumber, Report.Generation]));
    poviVerificationFailure:
      Log('Reload check rejected the written revision: ' + Report.ErrorMessage);
  else
    Log(Report.ErrorMessage);
  end;

Perlakukan outline sebagai apa adanya — graf objek bertaut dengan invariannya sendiri — dan penghapusan halaman berhenti menjadi bencana bookmark serta menjadi page map yang Anda serahkan ke satu pemanggilan metode. TPdfOutlineEditor, ApplyPageMap, dan penulis inkremental terverifikasi tersedia di PDFiumPas sejak v3.98.0 untuk Delphi, C++Builder, dan Lazarus; Anda dapat meninjau API lengkapnya dan mengunduh trial di halaman produk PDFium Delphi Component