מאמר טכני

תיבות עמוד ב-PDFlibPas: TrimBox, BleedBox ו-CropBox

כשלעמוד 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
MediaBox1אין, הרשומה נדרשתכן
CropBox2MediaBoxכן
BleedBox3CropBoxלא
TrimBox4CropBoxלא
ArtBox5CropBoxלא

שרשרת שני הצעדים משנה כי ה-CropBox עצמו עשוי להיות בירושה. ה-TrimBox האפקטיבי של עמוד שאין לו גם TrimBox וגם CropBox משלו הוא ה-CropBox של האב הקרוב ביותר שיש לו כזה, ואם אין, ה-MediaBox המיורש. המפרט מוסיף עוד כלל שקל לשכוח: תיבות ה-crop, ה-bleed, ה-trim וה-art לא רשאיות להתפרש מעבר ל-media box, ואם הן כן, הן מצטמצמות בפועל לחיתוך שלהן עם ה-media box. PDFlibPas מדווחת כל תיבה כפי שהיא אגורה בקובץ, ולכן ולידטור שמטפל בקלט לא מהימן צריך ללחוץ מול ה-MediaBox עצמו

שרשרת ברירות המחדל של תיבות עמוד ב-PDFlibPas שבה ה-CropBox בורר אל ה-MediaBox וה-BleedBox, ה-TrimBox וה-ArtBox כל אחד בורר אל ה-CropBox, מצוירת לצד פנים ספר עם MediaBox של 450 על 666 נקודות ו-CropBox של 432 על 648 נקודות שהופך ל-trim האפקטיבי כשאין TrimBox
שום דבר מלבד ה-CropBox לא בורר ישירות אל ה-MediaBox, ולכן לעמוד עם 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, בזמן שקורא שיורש הכול רואה את ערך ההורה. בצינור פריפרס הדו-משמעות הזאת נגמרת על גיליון הדפוס

ירושת עץ עמודים ב-PDFlibPas שבה רק Resources, MediaBox, CropBox ו-Rotate עוברים בירושה מצומת Pages, כך ש-TrimBox שחונה על השורש הוא מפתח לא סטנדרטי שקוראים מצייתים מתעלמים ממנו; לפני v3.539.44 שני נתיבי קוד בלתי תלויים ירשו אותו ודיווחו גדלי trim שונים עבור מסמך אחד
הקובץ משמעותו שני דברים בהתאם למי קורא אותו, ובצינור פריפרס הדו-משמעות הזאת נוחתת על גיליון הדפוס

זרימות עבודה של 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 ב-PDFlibPas, כשמערכים ישירים ועקיפים כאחד נספרים בתור מיורשים מאז v3.539.44, לצד אפשרויות ה-CapturePageEx אפס עד ארבע שבהן BleedBox, TrimBox ו-ArtBox נופלים אל ה-CropBox במקום ה-MediaBox מאז v3.539.42
שתי נקודות כניסה של מימוש עבור כלל מפרט אחד מתוקנות יחד ונבדקות בתור מטריצה של 18 תרחישים, כששאילתה ולכידה מסכימים על כל קובץ

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 Bottom
  • HasPageBox(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