PDF Library for Delphi כותבת טווחי תוויות עמודים עם AddPageLabels, ומאז v3.539.10 הקריאה הזאת עובדת גם על קבצים טעונים שעץ המספור של /PageLabels שלהם מפוצל לצמתי /Kids: השורש מושטח לעלה /Nums יחיד לפני שהטווח החדש נכנס, כך שהתווית באמת מופיעה במציג במקום להתעלם בשקט. הקורבן הטיפוסי הוא PDF בסגנון ספר מתוך כלי עימוד, עם ספרות רומיות בפתח הדבר, מספור ערבי בגוף ונספח עם תוויות A-1, A-2, שבו רציתם רק לתייג מחדש את הנספח — ושום דבר לא השתנה
מהן תוויות עמודים ב-PDF ואיך הן מאוחסנות?
תוויות עמודים הן המחרוזות שמציג מציג בתיבת העמוד שלו במקום אינדקס העמוד הפיזי, ו-ISO 32000-1 §12.4.2 מאחסן אותן כעץ מספור תחת מפתח הקטלוג /PageLabels. כל מפתח הוא אינדקס עמוד מבוסס-0 שפותח טווח תיוג, וכל ערך הוא מילון תווית עמוד עם עד שלוש רשומות: /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. כיוון שתוויות עמודים הגיעו ב-PDF 1.3, הקריאה גם מריצה EnsureMinVersion('1.3', '/PageLabels'), שמעלה את גרסת הפלט של קובץ ישן יותר אלא אם נעלתם במפורש את גרסת השמירה
למה תוויות עמודים חדשות נעלמות כשהעץ מכיל /Kids?
תוויות חדשות נעלמות כי ISO 32000-1 §7.9.7 (טבלה 37) מכתיב שהשורש של עץ מספור נושא או /Kids או /Nums, לעולם לא את שניהם, וה-helper 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 מאוזנים אחר כך. עבור תוויות עמודים זה לא עולה כלום, כי גם ספר עיון גדול בקושי חורג מכמה תריסרי טווחים, ועלה יחיד הוא בכלל מה שרוב המפיקים כותבים
// תיוג מחדש של הנספח בקובץ שהשורש של /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 בדקה כל אלמנט לסוג מספרי, ולכן ערך שקרה להיות מספר הושווה כאילו היה מפתח; פגיעת less-than יכולה הייתה לקבוע את נקודת ההכנסה לאינדקס אי-זוגי ולהפיל את הזוג החדש לתוך זוג קיים, ולהסיט כל זוג מאוחר יותר ממקומו. גם ה-EnumNumTree היה עם אותה הליכה חד-צעדית. שניהם כעת מאיטרטים זוגות בצעד של שניים, קוראים את המפתח ב-X * 2 ואת הערך ב-X * 2 + 1, והתאמת מפתח מדויקת מחליפה את הערך ויוצאת עם Break. בהגינות, ערכי תוויות עמודים הם מילונים, ולכן הבאג השני הזה כמעט ולא הופעל על /PageLabels עצמו, אבל helper של עץ מספור שקורא בצעד שגוי מתקלקל ברגע שכל ערך הוא מספרי, והוא תוקן באותו מעבר
קריאת תוויות בחזרה וביצוע להן round trip
TPDFlib.GetPageLabel(Page) מחזיר את התווית עבור עמוד מבוסס-1 ויש לו שני fallbacks ששווה להכיר. בלי רשומת /PageLabels בכלל הוא מחזיר את מספר העמוד העשרוני, כך שקורא ל-API יכול להשתמש בו בלי תנאי. עם עץ קיים אבל בלי טווח שמכסה את העמוד הוא מחזיר מחרוזת ריקה, וזה בדיוק מה שקורה כשקובץ מדלג על רשומת אינדקס 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 טריות. התוויות גם קשורות לאינדקסי עמודים ולא לאובייקטי עמודים, ולכן כל פעולה שמשנה את מספר העמודים או הסדר משאירה את הטווחים במקומם. החלפה במקום כמו החלפת עמודים תוך שימור מספרי אובייקטים שומרת על המספר ולכן על יישור התוויות, בזמן שמיזוג כמו מיון משולב של סריקות duplex משובצות מייצר רצף עמודים חדש שראוי לו סט טווחים שנכתב טריים
קריאות התוויות לעמודים, הטיפול בעץ המספור וייצוא וייבוא נתוני המסמך שמתוארים כאן מגיעים כולם בPDF Library for Delphi עבור Delphi, C++Builder ו-Lazarus, עם רשומת ההתייחסות של AddPageLabels שמתעדת את ערכי הסגנון ואת קודי ההחזרה