PDF Library for Delphi пише діапазони page labels через AddPageLabels, і з v3.539.10 цей виклик працює також на завантажених файлах, чиє число-дерево /PageLabels розрізане на вузли /Kids: корінь сплощується в єдиний лист /Nums ще до того, як туди піде новий діапазон, тож мітка насправді з'являється у переглядачі, а не тихо ігнорується. Типова жертва — PDF у книжковому стилі з видавничої програми, з римськими цифрами у вступі, арабською нумерацією в тілі і додатком із мітками A-1, A-2, де ви хотіли перемаркувати лише додаток — а не змінилося нічого
Що таке PDF page labels і як вони зберігаються?
Page labels — це рядки, які переглядач показує у своєму полі сторінки замість фізичного індексу сторінки, а ISO 32000-1 §12.4.2 зберігає їх як число-дерево під ключем каталогу /PageLabels. Кожен ключ — це індекс сторінки з нуля, з якого починається діапазон міток, а кожне значення — словник page label щонайбільше з трьома записами: /S для стилю нумерації (D, R, r, A чи a), /P для рядка-префікса і /St для числового значення першої сторінки діапазону, яке за замовчуванням дорівнює 1. Діапазон тягнеться до наступного ключа, і специфікація вимагає, щоб дерево містило значення для індексу сторінки 0, тож кожна сторінка вкрита якимось діапазоном
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Сторінки 1-4: i, ii, iii, iv (малі римські)
Lib.AddPageLabels(1, 3, 1, '');
// Сторінки 5-120: 1, 2, 3 ... (десяткові)
Lib.AddPageLabels(5, 1, 1, '');
// Сторінки 121 і далі: A-1, A-2 ... (десяткові з префіксом)
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) відображає аргументи на той словник без сюрпризів, щойно знаєш три правила. Start рахується від 1, як і будь-який інший сторінковий аргумент у бібліотеці, і записується в дерево як Start - 1. Style йде від 0 до 5, де 0 означає лише префікс, а від 1 до 5 перетворюється на значення /S D, R, r, A і a; усе поза цим діапазоном повертає 0 і нічого не торкає. Offset стає /St лише коли він більший за нуль, тож передача 0 просто пропускає ключ, і переглядач падає назад до усталеного значення 1. Оскільки page labels з'явилися в PDF 1.3, виклик також ганяє EnsureMinVersion('1.3', '/PageLabels'), який піднімає вихідну версію старішого файлу, якщо ви явно не закрили версію збереження
Чому нові page labels зникають, коли в дерева є /Kids?
Нові мітки зникають, бо ISO 32000-1 §7.9.7 (таблиця 37) велить кореневі числа-дерева нести або /Kids, або /Nums, але ніколи не обидва, а старий хелпер NumTreeSet умів шукати лише /Nums. Продюсери довгих документів часто ріжуть дерево на проміжні вузли, кожен із парою /Limits, і вішають їх на корінь, у якого є тільки /Kids. Старий код не знаходив /Nums на тому корені, створював свіжий поруч із наявним /Kids і вставляв новий діапазон туди. Результатом був корінь із двома взаємновиключними входами. Переглядачі спускаються крізь /Kids і ніколи не дивляться на випадковий масив, власний EnumNumTree бібліотеки теж спершу перевіряє /Kids, а NumTreeLookup відкидає вузол, де HasKids xor HasNums хибне. AddPageLabels усе ще повертав 1, і збережений файл усе ще відкривався чисто — а це найгірший тип збою: ніхто не скаржиться, мітки просто лишаються тими самими
Виправлення в NumTreeSet перетворює корінь на лист, перш ніж вставляти що-небудь. Коли корінь несе /Kids, EnumNumTree обходить кожен лист по порядку і збирає кожну пару ключ-значення, з того списку будується новий плаский масив /Nums, а /Kids, /Limits і будь-який застояний /Nums виганяються з кореня, перш ніж плаский масив буде причеплено. Викид /Limits — не косметика: таблиця 37 дозволяє цей запис лише на проміжних і листових вузлах, але ніколи на корені. З цієї миті вставлення — звичайне впорядковане вставляння в один масив, а наявні діапазони виживають із своїми оригінальними словниками міток. Компроміс навмисний: дерево не перебудовується назад у збалансовані вузли /Kids. Для page labels це нічого не коштує, бо навіть великий довідковий мануал рідко має більше кількох десятків діапазонів, а один лист — це те, що більшість продюсерів і так пишуть
// Перемаркувати додаток у файлі, чий корінь /PageLabels використовує /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // напр. A-1
// Замінити діапазон, що починається зі сторінки 121: App-a, App-b ...
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
Lib.SaveToFile('vendor-manual-relabeled.pdf');
// Наявні римські та десяткові діапазони досі в сплощеному листі
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, без змін
end;
Як масив /Nums можна неправильно прочитати як ключі?
Масив /Nums читається неправильно, коли код обходить його по одному елементу, бо масив — це пласка низка чергованих пар, [key0 value0 key1 value1 ...], і лише парні позиції — це ключі. Старий цикл NumTreeSet перевіряв кожен елемент на числовий тип, тож значення, яке випадково виявилося числом, порівнювалося так, ніби це ключ; спрацювання "менше" могло поставити точку вставляння на непарний індекс і вкинути нову пару в середину наявної, зсунувши всі пізніші пари з фази. EnumNumTree мав той самий однокроковий обхід. Тепер обидва ітерують парами з кроком два, читаючи ключ на X * 2 і значення на X * 2 + 1, а точний збіг ключа замінює значення і виходить через Break. Справедливості заради: значення page labels — словники, тож цей другий баг рідко спрацьовував на самому /PageLabels, але хелпер числа-дерева, який читає неправильний крок, — зіпсований щойно якесь значення числове, і його полагодили в тому самому проході
Зворотне читання міток і текстовий round-trip
TPDFlib.GetPageLabel(Page) повертає мітку для сторінки з нумерацією від 1 і має два запасні варіанти, про які варто знати. Коли запису /PageLabels немає взагалі, він повертає десятковий номер сторінки, тож викликач може користуватися ним безумовно. Коли дерево є, але жоден діапазон не вкриває сторінку, він повертає порожній рядок — рівно те, що станеться, коли файл пропускає обов'язковий запис для індексу 0; довідкова документація каже, що для коректного показу міток має існувати діапазон, що починається зі сторінки 1, і код робить цю вимогу видимою. Літерні стилі йдуть за специфікацією, а не за стовпцями електронної таблиці: після Z іде AA, потім BB — літера повторюється, а не переноситься
var
P: Integer;
Data: WideString;
begin
// Швидкий аудит того, що переглядач покаже у своєму полі сторінки
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// Значення опції 4 експортує лише діапазони міток як записи PageLabelBegin
Data := Lib.ExportDocumentData(4);
// Імпортування промотує їх через ClearPageLabels + AddPageLabels
Lib.ImportDocumentData(Data, 0);
end;
Для масових редагувань ExportDocumentData зі значенням опції 4 пише кожен діапазон як блок PageLabelBegin з рядками PageLabelNewIndex, PageLabelStart, PageLabelPrefix і PageLabelNumStyle, а ImportDocumentData трактує перший побачений запис мітки як повну заміну: він один раз викликає ClearPageLabels, а потім згодовує кожен запис у AddPageLabels. Це робить текстовий round trip детермінованим навіть тоді, коли оригінальний файл використовував дерево /Kids, бо очищення забирає весь запис каталогу, а перебудоване дерево — один лист від самого початку
Чого виправлення досі не гарантує?
Сплощення — одностороннє і довіряється порядку, який знаходить. EnumNumTree збирає пари у файловому порядку, а GetPageLabel застосовує останній діапазон, чий ключ менший або дорівнює індексу сторінки, тож чужий файл, чиї листи не впорядковані — §7.9.7 це забороняє, але таке в обігу, — досі може давати неправильні мітки, доки ви не перебудуєте діапазони через ClearPageLabels і свіжі виклики AddPageLabels. Мітки прив'язані до індексів сторінок, а не до об'єктів сторінок, тож будь-яка операція, що змінює кількість чи порядок сторінок, лишає діапазони там, де вони були. Заміна на місці, як-от заміна сторінок зі збереженням номерів об'єктів, тримає кількість, а отже, і мітки вирівняними, тоді як злиття на кшталт впорядкування переплетених дуплексних сканкопій дає нову послідовність сторінок, яка заслуговує на свіжий, написаний заново набір діапазонів
Виклики page labels, робота з числом-деревом і експорт-імпорт даних документа, описані тут, виходять у PDF Library for Delphi для Delphi, C++Builder і Lazarus, а довідковий запис AddPageLabels документує значення стилів і коди повернення