Artikel Teknis

Page Labels PDF di Delphi: Membenahi Number Tree /Kids

PDF Library for Delphi menulis rentang page label dengan AddPageLabels, dan sejak v3.539.10 panggilan itu juga bekerja pada file termuat yang number tree /PageLabels-nya terbelah menjadi node /Kids: root-nya diratakan menjadi satu daun /Nums sebelum rentang baru masuk, jadi labelnya benar-benar tampil di viewer alih-alih diam-diam diabaikan. Korbannya yang khas adalah PDF bergaya buku dari tool tata letak, dengan angka romawi di bagian awal, penomoran arab di badan, dan lampiran berlabel A-1, A-2, di mana Anda cuma ingin mengganti label lampirannya dan tidak ada yang berubah

Apa itu page label PDF dan bagaimana penyimpanannya?

Page label adalah string yang ditampilkan viewer di kotak halamannya alih-alih indeks halaman fisik, dan ISO 32000-1 §12.4.2 menyimpannya sebagai number tree di bawah key catalog /PageLabels. Setiap key adalah indeks halaman berbasis 0 yang memulai satu rentang pelabelan, dan setiap value-nya adalah dictionary page label dengan paling banyak tiga entri: /S untuk gaya penomoran (D, R, r, A, atau a), /P untuk string prefiks, dan /St untuk nilai numerik halaman pertama dalam rentang itu, yang bawaannya 1. Satu rentang berjalan sampai key berikutnya, dan spesifikasi mensyaratkan tree memuat value untuk indeks halaman 0, jadi setiap halaman tercakup oleh suatu rentang

Penyimpanan page label dalam istilah PDFlibPas: number tree /PageLabels memberi key pada setiap rentang menurut halaman awalnya yang berbasis nol, setiap value adalah dictionary label dengan gaya /S, prefiks /P dan nomor pertama /St, dan contoh bukunya memetakan bagian awal romawi, halaman badan arab, dan lampiran A- ke tiga rentang
Satu rentang berjalan sampai key berikutnya, spesifikasi mensyaratkan value untuk indeks halaman 0, dan GetPageLabel menerapkan rentang terakhir yang key-nya sama dengan atau di bawah halaman, jadi setiap halaman teresolusi ke sesuatu
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
      Exit;
    // Halaman 1-4: i, ii, iii, iv (romawi huruf kecil)
    Lib.AddPageLabels(1, 3, 1, '');
    // Halaman 5-120: 1, 2, 3 ... (desimal)
    Lib.AddPageLabels(5, 1, 1, '');
    // Halaman 121 ke atas: A-1, A-2 ... (desimal dengan prefiks)
    Lib.AddPageLabels(121, 1, 1, 'A-');
    WriteLn(Lib.GetPageLabel(5));    // 1
    WriteLn(Lib.GetPageLabel(122));  // A-2
    Lib.SaveToFile('handbook-labeled.pdf');
  finally
    Lib.Free;
  end;
end;

TPDFlib.AddPageLabels(Start, Style, Offset, Prefix) memetakan argumennya ke dictionary itu tanpa kejutan begitu Anda tahu tiga aturannya. Start berbasis 1 seperti setiap argumen halaman lain di library ini dan ditulis ke tree sebagai Start - 1. Style berjalan dari 0 sampai 5, di mana 0 berarti prefiks saja dan 1 sampai 5 menjadi nilai /S berupa D, R, r, A, dan a; apa pun di luar rentang itu mengembalikan 0 dan tak menyentuh apa pun. Offset menjadi /St hanya ketika lebih besar dari nol, jadi mengirim 0 sekadar menghilangkan key itu dan viewer kembali ke bawaan 1. Karena page label hadir sejak PDF 1.3, panggilan ini juga menjalankan EnsureMinVersion('1.3', '/PageLabels'), yang menaikkan versi output file yang lebih tua kecuali Anda sudah mengunci versi simpannya secara eksplisit

Kenapa page label baru lenyap ketika tree-nya punya /Kids?

Label baru lenyap karena ISO 32000-1 §7.9.7 (Tabel 37) membuat root number tree membawa entah /Kids entah /Nums, tidak pernah keduanya, dan helper NumTreeSet yang lama hanya tahu cara mencari /Nums. Produser yang mengeluarkan dokumen panjang sering membelah tree menjadi node-node perantara, masing-masing dengan sepasang /Limits, dan menggantungkannya pada root yang hanya punya /Kids. Kode lama tidak menemukan /Nums di root itu, membuat yang baru di sebelah /Kids yang sudah ada, dan menyelipkan rentang barunya di sana. Hasilnya adalah root dengan dua titik masuk yang saling eksklusif. Viewer turun lewat /Kids dan tak pernah melirik array nyasar itu, EnumNumTree milik library sendiri juga memeriksa /Kids lebih dulu, dan NumTreeLookup menolak node yang HasKids xor HasNums-nya false. AddPageLabels tetap mengembalikan 1 dan file tersimpannya tetap terbuka mulus, dan itulah jenis kegagalan terburuk: tidak ada yang mengeluh, labelnya sekadar tetap sama

Perbaikan di NumTreeSet mengubah root menjadi daun sebelum menyisipkan apa pun. Ketika root membawa /Kids, EnumNumTree menyusuri setiap daun berurutan dan mengumpulkan setiap pasangan key dan value, array /Nums datar yang baru dibangun dari daftar itu, dan /Kids, /Limits, serta /Nums basi mana pun dibersihkan dari root sebelum array datarnya dipasang. Membuang /Limits bukan sekadar kosmetik, karena Tabel 37 mengizinkan entri itu hanya pada node perantara dan daun, tidak pernah pada root. Dari titik itu penyisipannya adalah insert terurut biasa ke satu array, dan rentang yang sudah ada selamat dengan dictionary label aslinya. Trade-off-nya memang disengaja: tree tidak dibangun ulang menjadi node /Kids berimbang setelahnya. Untuk page label itu tak berbiaya apa-apa, karena bahkan manual referensi yang besar jarang punya lebih dari beberapa lusin rentang, dan satu daun memang yang ditulis kebanyakan produser

Perbaikan number tree di PDFlibPas: root yang membawa /Kids dan array /Nums nyasar tak terlihat oleh viewer karena ISO 32000-1 hanya mengizinkan salah satunya, jadi NumTreeSet meratakan setiap daun menjadi satu array /Nums dan membersihkan /Kids serta /Limits, yang tak pernah diizinkan Tabel 37 pada root
Tak ada yang mengeluh karena setiap pemeriksaan lolos: AddPageLabels mengembalikan 1, file tersimpannya terbuka mulus, dan hanya reader yang menuruni /Kids lebih dulu — cara kerja viewer maupun library sendiri — yang tak pernah menemukan rentang barunya
// Ganti label lampiran pada file yang root /PageLabels-nya memakai /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
  WriteLn('Before: ', Lib.GetPageLabel(121));  // e.g. A-1
  // Gantikan rentang yang mulai di halaman 121: App-a, App-b ...
  if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
    Lib.SaveToFile('vendor-manual-relabeled.pdf');
  // Rentang romawi dan desimal yang ada tetap berada di daun hasil perataan
  WriteLn('After: ', Lib.GetPageLabel(121));   // App-a
  WriteLn('Front: ', Lib.GetPageLabel(2));     // ii, tak berubah
end;

Bagaimana array /Nums bisa terbaca salah sebagai key?

Array /Nums terbaca salah ketika kode menyusurinya satu elemen sekaligus, karena array itu adalah deretan datar pasangan berselang-seling, [key0 value0 key1 value1 ...], dan hanya posisi genap yang berupa key. Loop NumTreeSet yang lama menguji setiap elemen atas tipe numeriknya, jadi value yang kebetulan berupa angka dibandingkan seolah-olah key; kena kurang-dari bisa menyetel titik sisip ke indeks ganjil dan menjatuhkan pasangan baru ke tengah pasangan yang sudah ada, menggeser setiap pasangan setelahnya keluar fase. EnumNumTree punya langkah tunggal yang sama. Keduanya kini mengiterasi pasangan dengan langkah dua, membaca key di X * 2 dan value di X * 2 + 1, dan kecocokan key persis menggantikan value-nya lalu keluar dengan Break. Sejujurnya, value page label berupa dictionary, jadi bug kedua ini jarang menyala pada /PageLabels itu sendiri, tapi helper number tree yang membaca langkah yang salah sudah korup begitu ada satu value yang numerik, dan itu diperbaiki di putaran yang sama

Perbaikan langkah pasangan di number tree PDFlibPas: array /Nums adalah deretan datar entri key dan value berselang-seling, jadi penyusuran yang menguji setiap elemen bisa menyisipkan pasangan baru di indeks ganjil dan menggeser pasangan setelahnya keluar fase, sementara penyusuran yang diperbaiki membaca key di X*2 dan value di X*2+1
Bug ini jarang menyala pada /PageLabels karena value labelnya berupa dictionary, tapi helper number tree yang membaca langkah yang salah jadi korup begitu ada value yang numerik, jadi kedua penyusuran kini melangkah berpasangan

Membaca label kembali dan round-trip-nya

TPDFlib.GetPageLabel(Page) mengembalikan label untuk halaman berbasis 1 dan punya dua fallback yang layak diketahui. Tanpa entri /PageLabels sama sekali, ia mengembalikan nomor halaman desimal, jadi pemanggil bisa memakainya tanpa syarat. Dengan tree yang ada tapi tak ada rentang yang mencakup halamannya, ia mengembalikan string kosong, dan persis itulah yang terjadi saat sebuah file melompati entri indeks 0 yang wajib; dokumentasi referensinya menyatakan rentang yang mulai di halaman 1 harus ada agar label tampil dengan benar, dan kodenya membuat persyaratan itu terlihat. Gaya huruf mengikuti spesifikasi, bukan kolom spreadsheet: setelah Z datang AA, lalu BB — hurufnya diulang, tidak di-carry

var
  P: Integer;
  Data: WideString;
begin
  // Audit cepat atas apa yang akan ditampilkan viewer di kotak halamannya
  for P := 1 to Lib.PageCount do
    WriteLn(P, ' -> ', Lib.GetPageLabel(P));

  // Nilai opsi 4 mengekspor hanya rentang label sebagai record PageLabelBegin
  Data := Lib.ExportDocumentData(4);
  // Impor memutarnya ulang lewat ClearPageLabels + AddPageLabels
  Lib.ImportDocumentData(Data, 0);
end;

Untuk penyuntingan massal, ExportDocumentData dengan nilai opsi 4 menulis setiap rentang sebagai blok PageLabelBegin dengan baris-baris PageLabelNewIndex, PageLabelStart, PageLabelPrefix, dan PageLabelNumStyle, dan ImportDocumentData memperlakukan record label pertama yang dilihatnya sebagai penggantian penuh: ia memanggil ClearPageLabels sekali lalu menyuapi setiap record ke AddPageLabels. Itu membuat round trip teks menjadi deterministik bahkan ketika file aslinya memakai tree /Kids, karena pembersihannya menghapus seluruh entri catalog dan tree yang dibangun ulang sudah satu daun sejak awal

Apa yang masih tak dijamin oleh perbaikan ini?

Perataannya satu arah dan memercayai urutan yang ditemuinya. EnumNumTree mengumpulkan pasangan dalam urutan file, dan GetPageLabel menerapkan rentang terakhir yang key-nya kurang dari atau sama dengan indeks halaman, jadi file asing yang daun-daunnya tak berurutan — yang dilarang §7.9.7 tapi memang beredar — masih bisa menghasilkan label yang salah sampai Anda membangun ulang rentangnya dengan ClearPageLabels dan panggilan AddPageLabels yang segar. Label juga terikat pada indeks halaman, bukan objek halaman, jadi operasi apa pun yang mengubah jumlah atau urutan halaman membiarkan rentang-rentangnya di tempat semula. Penukaran di tempat seperti mengganti halaman sambil mempertahankan nomor objek menjaga jumlahnya sehingga label-labelnya tetap selaras, sedangkan merge seperti collating hasil pindai duplex yang diselingi menghasilkan urutan halaman baru yang layak dapat set rentang yang ditulis segar

Panggilan page label, penanganan number tree, serta ekspor dan impor data dokumen yang dijelaskan di sini semuanya ikut terkirim di PDF Library for Delphi untuk Delphi, C++Builder, dan Lazarus, dengan entri referensi AddPageLabels yang mendokumentasikan nilai gaya dan kode kembaliannya