כשלעמוד PDF אין TrimBox, ה-TrimBox האפקטיבי שלו הוא ה-CropBox של העמוד, וכשגם ה-CropBox חסר, זהו ה-MediaBox. BleedBox ו-ArtBox נאמנים לאותו כלל. PDFlibPas, ה-PDF Library for Delphi, מיישמת את שרשרת ברירות המחדל הזאת באופן עקבי ב-GetPageBox, HasPageBox ו-CapturePageEx מאז v3.539.44, והיא מתעלמת מתיבות ייצור שהוצבו על צומת /Pages, כי ISO 32000-1 לא מרשה להן לרשת
זה נשמע כמו הערת שוליים עד שמפרסמים עבודה. תארו לכם פנים ספר עם MediaBox של 6.25 × 9.25 אינץ', CropBox שמוגדר ל-trim של 6 × 9 אינץ', ובלי TrimBox כלל, כי מי שייצא אותו אף פעם לא חשב לכתוב אחד. מבקשים את תיבת ה-trim, מקבלים במקום את תיבת ה-media, וכל תא בגיליון הדפוס שלכם גורר שמינית אינץ' של bleed ו-slug אל השכן שלו. ל-PDFlibPas היו פגמים בדיוק באזור הזה, תוקנו ב-v3.539.42 וב-v3.539.44, והאופן שבו הם תוקנו אומר משהו על איך סמנטיקת תיבות עמוד צריכה להתממש בכל ספריית PDF
איזו תיבה נכנסת לפעולה כשלעמוד אין TrimBox?
התשובה היא שרשרת ברירות מחדל קבועה מתוך ISO 32000-1 §14.11.2: ה-CropBox בורר ברירת מחדל אל ה-MediaBox, וה-BleedBox, ה-TrimBox וה-ArtBox כל אחד בורר אל ה-CropBox. שום דבר מלבד ה-CropBox לא בורר ישירות אל ה-MediaBox. עמוד שמגדיר רק MediaBox יש לו אפוא חמש תיבות זהות, ולעמוד שמגדיר MediaBox ובנוסף CropBox יש ארבע תיבות שוות ל-CropBox
| תיבה | BoxType של PDFlibPas | ברירת מחדל כשחסר | ניתנת לירושה מ-/Pages |
|---|---|---|---|
| MediaBox | 1 | אין, הרשומה נדרשת | כן |
| CropBox | 2 | MediaBox | כן |
| BleedBox | 3 | CropBox | לא |
| TrimBox | 4 | CropBox | לא |
| ArtBox | 5 | CropBox | לא |
שרשרת שני הצעדים משנה כי ה-CropBox עצמו עשוי להיות בירושה. ה-TrimBox האפקטיבי של עמוד שאין לו גם TrimBox וגם CropBox משלו הוא ה-CropBox של האב הקרוב ביותר שיש לו כזה, ואם אין, ה-MediaBox המיורש. המפרט מוסיף עוד כלל שקל לשכוח: תיבות ה-crop, ה-bleed, ה-trim וה-art לא רשאיות להתפרש מעבר ל-media box, ואם הן כן, הן מצטמצמות בפועל לחיתוך שלהן עם ה-media box. PDFlibPas מדווחת כל תיבה כפי שהיא אגורה בקובץ, ולכן ולידטור שמטפל בקלט לא מהימן צריך ללחוץ מול ה-MediaBox עצמו
אילו תכונות עמוד צומת /Pages יכול להוריש?
בדיוק ארבע: Resources, MediaBox, CropBox ו-Rotate. ISO 32000-1 §7.7.3.4 מגדיר ירושת תכונות, ו-Table 30 מסמן רק את ארבע רשומות אובייקט-העמוד האלה בתור ניתנות לירושה. BleedBox, TrimBox ו-ArtBox שייכים לעמוד העלה. TrimBox שנכתב לתוך צומת /Pages אינו ערך מיורש; הוא מפתח לא סטנדרטי שקורא מציית מתעלם ממנו
קבצים לא סטנדרטיים כאלה קיימים, בדרך כלל עם TrimBox יחיד על צומת שורש עץ העמודים בתור קיצור ל"לכל עמוד יש את ה-trim הזה". הקיצור נראה נכון בכל כלי שהולך על /Parent עבור כל מפתח, וזו הבעיה: הקובץ משמעותו עכשיו שני דברים בהתאם למי קורא אותו. קורא שנאמן למפרט רואה שאין TrimBox ומשתמש ב-CropBox, בזמן שקורא שיורש הכול רואה את ערך ההורה. בצינור פריפרס הדו-משמעות הזאת נגמרת על גיליון הדפוס
זרימות עבודה של PDF/X (ISO 15930) נשענות על ה-TrimBox עבור הגודל המוגמר, ופרופילי ה-PDF/X דורשים מכל עמוד להכריז על TrimBox או ArtBox. תיבה שחונה על צומת /Pages לא עומדת בדרישה הזאת, כי המפתח לעולם לא מגיע אל אובייקט העמוד. Preflight צריך לסמן קבצים כאלה במקום לקרוא אותם בשקט בכל כיוון שיהיה
מה PDFlibPas עשתה לא נכון לפני v3.539.44?
ל-PDFlibPas היו שלושה פגמים נפרדים, כולם בפער שבין מה שהמפרט אומר למה ששני נתיבי קוד בלתי תלויים עשו. הראשון תוקן ב-v3.539.42, והשניים האחרים ב-v3.539.44
תיבות ייצור בררו אל ה-MediaBox בלכידה
לפני v3.539.42, הרוטינה הפנימית שמכינה עמוד ללכידה (היא מעתיקה רשומות מיורשות אל העמוד וממלאה תיבות חסרות) נתנה ל-BleedBox, ל-TrimBox ול-ArtBox את ערכי ה-MediaBox כשהם חסרים. CapturePageEx עם אפשרויות 2 עד 4 קורא את המלבן החוסם שלו מאותן רשומות ממולאות בדיוק, ולכן על עמוד שמגדיר רק CropBox, בקשת תיבת ה-trim לכדה את תיבת ה-media כולה. GetPageBox כבר יישם את ברירת המחדל של ה-CropBox, וההפניה של CapturePageEx אמרה מאז ומתמיד שתיבת ה-crop נמצאת בשימוש כשהתיבה המבוקשת חסרה; קוד הלכידה חלק על שניהם. מאז v3.539.42 שלוש תיבות הייצור בוררות אל ה-CropBox של העמוד, שבנקודה הזאת כבר נמצא על העמוד (משלו, מועתק מאב, או ממולא מתוך ה-MediaBox), ורק ה-CropBox עצמו נופל בחזרה אל ה-MediaBox
שני נתיבי ירושה, כלל סמנטי אחד
הפגם השני היה הירושה הלא סטנדרטית עצמה, והחלק העדין היה ש-PDFlibPas פיענחה תיבות לאורך שני נתיבים בלתי תלויים. שאילתות תיבות (GetPageBox ו-HasPageBox) הלכו על שרשרת ה-/Parent דרך helper אחד, והלכידה הלכה עליה דרך helper מקומי נפרד. שניהם ירשו כל מפתח, תיבות ייצור כלולות. תיקון של אחד מהם לבדו היה מייצר סתירה בתוך מסמך יחיד: עם TrimBox ברוחב 180 נקודות על צומת ה-/Pages ו-CropBox ברוחב 380 נקודות על העמוד, GetPageBox היה עדיין מדווח רוחב trim של 180 בזמן ש-CapturePageEx בונה form ברוחב 380. ב-v3.539.44 שני הנתיבים מגבילים את ההליכה על /Parent לארבעת המפתחות הניתנים לירושה, תיבות ייצור נקראות מהעלה בלבד, ורשומת ההורה התועה נשארת בקובץ ללא מגע, לא נמחקה ולא נכתבה מחדש
HasPageBox החטיא מערכי הורה ישירים
HasPageBox מחזירה 0 כשלעמוד אין תיבה מהסוג המבוקש, 1 כשלעמוד יש תיבה משלו (אגורה ישירות או דרך הפניה עקיפה), ו-2 כש-MediaBox או CropBox מיורשים מאב. הקוד הישן החזיר 2 רק כשהערך המיורש היה הפניה עקיפה, ולכן מערך ישיר מיורש החזיר 0. התיקון מפריד בין פענוח ההפניה לבדיקת המערך, ושתי הצורות מחזירות עכשיו 2. מאז v3.539.44, HasPageBox עבור BleedBox, TrimBox או ArtBox יכולה להחזיר רק 0 או 1
הלקח מתכלל הרבה מעבר לתיבות עמוד. כשלפיסת סמנטיקה אחת של המפרט יש שתי נקודות כניסה של מימוש בספרייה, מתקנים אותן יחד ובודקים אותן בתור מטריצה ולא עם קובץ אחד שהכול בו כשורה. קבוצת ה-regression של PDFlibPas מצליבה שתי צורות ייצוג של תיבת הורה (מערך ישיר ועקיף) עם שלושה מצבי עלה (חסר, מערך ישיר, מערך עקיף) ושלוש אפשרויות לכידה (bleed, trim, art), ונותן 18 תרחישים, וכל אחד בודק את תוצאת השאילתה, את הגבולות שנלכדו, את ירושת ה-MediaBox וה-CropBox הלגיטימית, ואת רשומת ההורה שלא נגעו בה
איך קוראים את ה-TrimBox האפקטיבי ב-Delphi?
קוראים ל-GetPageBox(4, Dimension) על העמוד הנבחר. PDFlibPas מיישמת בשבילכם את שרשרת ברירות המחדל, ולכן התוצאה היא ה-TrimBox האפקטיבי בין אם לעמוד יש אחד ובין לאו. מצמידים אליה את HasPageBox כשצריך לדעת מאיפה הגיע הערך, מה שדוח preflight בדרך כלל רוצה
uses
System.SysUtils, PDFlibrary;
const
BOX_CROP = 2;
BOX_TRIM = 4;
DIM_LEFT = 0;
DIM_WIDTH = 2;
DIM_HEIGHT = 3;
DIM_BOTTOM = 5;
function DescribeTrim(Lib: TPDFlib; Page: Integer): string;
var
Source: string;
begin
Lib.SelectPage(Page);
if Lib.HasPageBox(BOX_TRIM) = 1 then
Source := 'own TrimBox'
else if Lib.HasPageBox(BOX_CROP) <> 0 then // 1 = משלו, 2 = בירושה
Source := 'defaulted to the CropBox'
else
Source := 'defaulted to the MediaBox';
Result := Format('page %d: trim %.2f x %.2f pt at (%.2f, %.2f), %s',
[Page,
Lib.GetPageBox(BOX_TRIM, DIM_WIDTH),
Lib.GetPageBox(BOX_TRIM, DIM_HEIGHT),
Lib.GetPageBox(BOX_TRIM, DIM_LEFT),
Lib.GetPageBox(BOX_TRIM, DIM_BOTTOM),
Source]);
end;
var
Lib: TPDFlib;
Page: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('interior.pdf', '') = 1 then
for Page := 1 to Lib.PageCount do
Writeln(DescribeTrim(Lib, Page));
finally
Lib.Free;
end;
end.
גם GetPageBox וגם SetPageBox עובדים בהגדרות הקואורדינטות הנוכחיות של המסמך. הדוגמאות כאן רצות עם ברירות המחדל: ראשית 0 (שמאל תחתון, תואם למרחב המשתמש של PDF) ונקודות בתור יחידת מדידה, כך שממד ה-Top הוא הקצה העליון כפי שנמדד כלפי מעלה מתחתית העמוד. אחרי SetOrigin(1) ממדי ה-Top וה-Bottom נמדדים במקום זאת כלפי מטה מראש העמוד, ואחרי SetMeasurementUnits(1) כל ערך חוזר במילימטרים. רוחב וגובה לא תלויים בראשית
איתור תיבות ייצור שנקלעו לצמתי /Pages
מאז v3.539.44 ה-API של התיבות כבר לא רואה TrimBox על צומת /Pages, וזה נכון, אבל כלי preflight בדרך כלל רוצה לדווח על קובץ כזה במקום לקרוא אותו בשקט בדרך המפרט. צמתי עץ עמודים הם אובייקטים רגילים, ולכן ה-API הנמוך של האובייקטים יכול למצוא אותם: הולכים על מספרי אובייקטים עד GetMaxObjectNumber, קוראים כל אחד עם GetObjectToString, ומחפשים מילון /Pages שנושא מפתח של תיבת ייצור. החצי השני של הבדיקה הוא המבחן לעמוד שאכפת ל-PDF/X ממנו, ו-HasPageBox עונה עליו עכשיו כפי שיענה ולידטור PDF/X, כי TrimBox של הורה כבר לא נספר
procedure PreflightTrim(Lib: TPDFlib; Log: TStrings);
const
ProductionKeys: array[0..2] of string = ('/BleedBox', '/TrimBox', '/ArtBox');
var
ObjNum, K, Page, Missing: Integer;
Src: string;
begin
// 1. תיבות ייצור על צמתי עץ עמודים: לא סטנדרטיות ומנוכות
for ObjNum := 1 to Lib.GetMaxObjectNumber do
begin
Src := ''; // מספרים פנויים מחזירים ללא טקסט
Src := string(Lib.GetObjectToString(ObjNum));
if Pos('/Type /Pages', Src) = 0 then
Continue;
for K := Low(ProductionKeys) to High(ProductionKeys) do
if Pos(ProductionKeys[K] + ' ', Src) > 0 then
Log.Add(Format('object %d: %s on a /Pages node is not inheritable',
[ObjNum, ProductionKeys[K]]));
end;
// 2. PDF/X: לכל עמוד נדרשים TrimBox או ArtBox משלו
Missing := 0;
for Page := 1 to Lib.PageCount do
begin
Lib.SelectPage(Page);
if (Lib.HasPageBox(4) = 0) and (Lib.HasPageBox(5) = 0) then
begin
Inc(Missing);
Log.Add(Format('page %d: no TrimBox or ArtBox', [Page]));
end;
end;
// 3. תיקון אופציונלי: trim של 6 x 9 אינץ' בתוך media box של 6.25 x 9.25 אינץ'
// (נקודות, ראשית בשמאל-תחתון: Left, Top, Width, Height)
if Missing > 0 then
Log.Add(Format('TrimBox written on %d pages',
[Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;
ההתאמה הטקסטואלית היא בדיקה פרגמטית, לא פרסר. היא נשענת על PDFlibPas שממספרת כל רשומת מילון בתור מפתח, רווח אחד וערך, מה שמתקיים עבור אובייקטים שנקראים חזרה דרך GetObjectToString. שלב התיקון ראוי להחלטה ולא לרפלקס: ייתכן שערך ההורה התועה הוא בדיוק מה שהמחבר התכוון, אבל מאשרים אותו מול כרטיס העבודה לפני שהופכים אותו לרשמי. SetPageBoxRange עם טווח ריק מיישם את התיבה על כל עמוד ומחזיר את מספר העמודים שעודכנו. כשהתיבה הקיימת של עמוד היא מערך עקיף, שעמוד אחר או צומת /Pages עשויים לחלוק, SetPageBox נותן לאותו עמוד מערך ישיר חדש במקום לשכתב את האובייקט המשותף. הגדרת BleedBox, TrimBox או ArtBox גם מעלה מסמך לא נעול אל PDF 1.3, הגרסה שהציגה את הרשומות האלה
פריסת עמודים על ה-TrimBox עם CapturePageEx
CapturePageEx(Page, 3) הופך עמוד ל-Form XObject שהמלבן החוסם שלו הוא ה-TrimBox האפקטיבי של העמוד, ו-DrawCapturedPage מציב את ה-form הזה על עמוד אחר בכל גודל. מאז v3.539.42, אפשרות 3 על עמוד בלי TrimBox נותנת לכם את ה-CropBox, כפי שההפניה מתארת, במקום ה-MediaBox עם כל ה-slug שלו
שתי תכונות של הלכידה מעצבות את הקוד. הלכידה הרסנית: העמוד הנלכד מוסר מהמסמך, והמסמך לעולם לא יכול לרדת לאפס עמודים, ולכן מצרפים את גיליון הפלט הראשון לפני שלוכדים משהו. הלכידה גם עובדת בתוך מסמך אחד בלבד, ולכן מושכים קודם כל קלט למסמך אחד; הטכניקות ל-איסוף ושילוב מקורות PDF במעבר אחד מיושמות ישירות
procedure ImposeTwoUp(const InFile, OutFile: string);
var
Lib: TPDFlib;
Captures: array of Integer;
SourceCount, I: Integer;
TrimW, TrimH: Double;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(InFile, '') <> 1 then
raise Exception.Create('Cannot open ' + InFile);
SourceCount := Lib.PageCount;
// גודל trim אפקטיבי של עמוד 1 (הפריסה הזאת מניחה trim אחיד)
Lib.SelectPage(1);
TrimW := Lib.GetPageBox(4, 2);
TrimH := Lib.GetPageBox(4, 3);
// מצרפים וממדים את הגיליון הראשון; NewPage בוחר את העמוד החדש
Lib.NewPage;
Lib.SetPageDimensions(2 * TrimW, TrimH);
// כל לכידה מסירה את עמוד 1, ולכן עמוד המקור הבא עולה
SetLength(Captures, SourceCount);
for I := 0 to SourceCount - 1 do
begin
Captures[I] := Lib.CapturePageEx(1, 3); // 3 = TrimBox
if Captures[I] = 0 then
raise Exception.CreateFmt('Capture of source page %d failed', [I + 1]);
end;
// נשאר רק הגיליון: שני עמודים חתוכים לכל גיליון, זה לצד זה
Lib.SelectPage(1);
for I := 0 to SourceCount - 1 do
begin
if (I > 0) and (I mod 2 = 0) then
Lib.NewPage; // באותו גודל כמו הגיליון הנוכחי
// ראשית ברירת מחדל: Top הוא הקצה העליון, נמדד מהתחתית
Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
end;
Lib.SaveToFile(OutFile);
finally
Lib.Free;
end;
end;
לכידה מבוססת trim חותכת הכול מחוץ ל-TrimBox, וזה מה שרוצים עבור proof דיגיטלי או פריסת cut-and-stack. עבור גיליון דפוס שנחתך אחרי ההדפסה, לוכדים עם אפשרות 2 כדי שה-bleed ישרוד, ומרווחים את התאים לפי רוחב ה-bleed. כי הלכידה מסירה את עמודי המקור, סימניות וקישורים שהצביעו אליהם מאבדים את היעדים שלהם, ולכן מפרסמים לקובץ פלט נפרד במקום לערוך מסמך שהניווט שלו עוד דרוש לכם; החלפת עמודים בלי לשבור סימניות מכסה את הצד הזה של ניתוח עמודים
כשהמקור חייב להישאר שלם, ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) מקבל את אותם ערכי אפשרות מ-0 עד 4 (מעבירים Lib.SelectedDocument עבור המסמך הנוכחי), משאיר את עץ העמודים של המקור ללא שינוי, מנרמל סיבוב עמוד מיורש אל מטריצת ה-form, ומחזיר handle ש-DrawCapturedPage מקבל. CapturePageEx לא מבטל את /Rotate, ולכן קלט מסובב זקוק לצעד הזה קודם, ו-שיטוח סיבוב עמוד בלי לשבור תיבות עמוד מציג מה קורה לכל תיבה כשעושים זאת. זהירות אחת עבור קלטים שעשויים לשאת תיבות ייצור על צמתי /Pages: נתיב הייבוא מפענח את התיבה שלו דרך איתור אב משלו, נפרד משני הנתיבים שיושרו ב-v3.539.44, ולכן בודקים HasPageBox(4) על עמוד המקור קודם ומעבירים אפשרות 1 (CropBox) כשהיא מחזירה 0. זה משאיר את התוצאה קשורה למפרט ולא לאופן שבו הקובץ במקרה נכתב
עזר זריז לתיבות עמוד
- CropBox אפקטיבי: ה-CropBox של העמוד עצמו, אחרת ה-CropBox המיורש הקרוב ביותר, אחרת ה-MediaBox האפקטיבי (ISO 32000-1 §14.11.2)
- BleedBox, TrimBox ו-ArtBox אפקטיביים: רשומת עמוד העלה עצמו, אחרת ה-CropBox האפקטיבי
- רק
Resources,MediaBox,CropBoxו-Rotateיורשים מצמתי/Pages(§7.7.3.4, Table 30); תיבות ייצור על צמתי/Pagesמתעלמים מהן GetPageBox(BoxType, Dimension): BoxType 1 MediaBox, 2 CropBox, 3 BleedBox, 4 TrimBox, 5 ArtBox; Dimension 0 Left, 1 Top, 2 Width, 3 Height, 4 Right, 5 BottomHasPageBox(BoxType): 0 אין תיבה, 1 תיבת העמוד עצמו (ישירה או עקיפה), 2 MediaBox או CropBox מיורשים (ישיר או עקיף)CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox עם נפילה אל MediaBox, 2 עד 4 BleedBox, TrimBox או ArtBox עם נפילה אל CropBox- שדרגו ל-v3.539.44 ומעלה לברירות מחדל וירושה עקביות על פני שאילתות תיבות ולכידה
תיבות עמוד הן המקום שבו ברירות המחדל השקטות של PDF פוגשות סובלנויות פריפרס הנמדדות בשברי מילימטר, וספרייה או מיישמת את ברירות המחדל האלה באותו אופן בכל מקום או מוסרת לכם שתי תשובות לשאלה אחת. ה-API המלא של תיבות, לכידה ו-Form XObject מתועד ב-עמוד המוצר של PDFlibPas PDF Library for Delphi