מאמר טכני

תפירת דפים ותבניות סימני מים ב-Delphi PDFs

תפירת דפים מרכיבה את התוכן של דפים קיימים על קנבס משותף, פעולה השונה ממיזוג קבצים. ספריית losLab PDF Library מיישמת תפירת דפים מבוססת Form XObject ותבניות סימני מים הניתנות להרכבה ב-Delphi: הפונקציות StitchPageOverlay, StitchPagesSideBySide ו-StitchPagesVertically מציבות דפים שנלכדו בכל קנה מידה ובנאמנות וקטורית מלאה, ו-TPDFlibCompositeWatermark עורמת סימני מים של טקסט ותמונות על פני טווח דפים בקריאה אחת. אם מה שאתם באמת צריכים זה לצרף דפים שלמים ממספר מסמכים לקובץ אחד, זוהי משפחת המיזוג (merge), המכוסה במאמר על מיזוג PDF מהיר ברמת הבייט; מאמר זה עוסק בשילוב תוכן דפים בתוך מרחב קואורדינטות אחד

הדרישות שמובילות לכאן הן שגרתיות ועקשניות. בית ספר רוצה להציב שני דפי מבחן על גיליון לרוחב (landscape) אחד כדי להפחית בחצי את השימוש בנייר. בודק לוקליזציה רוצה את הדף באנגלית ואת התרגום שלו זה לצד זה על דף בודד, נגללים יחד. צוות משפטי רוצה שכל טיוטה תוחתם בשלוש שכבות בבת אחת: לוגו בפינה, כיתוב CONFIDENTIAL באלכסון במרכז, ושורת כותרת תחתונה קטנה, בדפים 1 עד 3 בלבד. כל השלוש הן אותה בעיה בסיסית, כלומר מיקום תוכן דף שכבר עוצב במקום אחר ובגודל אחר מבלי להוריד את איכותו למפת סיביות (bitmap)

מדוע שרשור זרמי תוכן (content streams) של PDF נכשל?

תפירה נאיבית, כלומר העתקת זרם ה-/Contents של דף המקור אל דף היעד, נכשלת משלוש סיבות מבניות. ראשית, קואורדינטות מתנגשות: שני זרמי התוכן מניחים שנקודת המוצא שלהם היא בפינה השמאלית-תחתונה של ה-MediaBox שלהם, ולכן התוכן המודבק נוחת על גבי התוכן הקיים במיקום הלא נכון. שנית, שמות משאבים מתנגשים: כל דף מפרש אופרטורים כמו /F1 Tf ו-/Im0 Do מול מילון ה-/Resources שלו, ודפים מקשרים באופן שגרתי את אותו שם לגופנים או תמונות שונים, כך שאחד מהם מרונדר בשקט עם המשאב הלא נכון. שלישית, העתקת זרם גולמי אינה נותנת לכם שום שינוי קנה מידה (scaling); המקסימום שתוכלו לעשות הוא הזזה (translate)

אובייקטים מסוג Form XObject פותרים את שלושת הקשיים הללו בבת אחת, וזו הסיבה שספריית losLab PDF Library בונה עליהם את התפירה שלה. תקן ISO 32000-1 §8.10 מגדיר form XObject כזרם תוכן עצמאי עם מילון /Resources משלו ומרחב קואורדינטות משלו, המוצב בדף על ידי קריאה לשמו תחת מטריצת הטרנספורמציה הפעילה. CapturePage עוטף דף קיים שלם, תוכן פלוס משאבים, לתוך אובייקט /Subtype /Form XObject, ופונקציות התפירה מציבות אותו לאחר מכן באמצעות מטריצת cm: שינוי קנה מידה, הזזה, סיבוב, לפי הרצון. מכיוון שהמקור נשאר וקטורי, הטקסט נשאר טקסט והנתיבים נשארים נתיבים בכל רמת תקריב, אותה תכונה שהופכת גרפיקה וקטורית ותבניות הצללה לבלתי תלויות ברזולוציה. שום דבר אינו מומר לרסטר, ומילון המשאבים המבודד פירושו שהדף שנלכד אינו יכול להתנגש עם דף היעד על השם /F1

כיצד משלבים שני דפי PDF על דף אחד?

הפונקציה StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) היא התשובה הישירה: היא לוכדת את שני דפי המקור, יוצרת דף חדש ורחב יותר, ומציבה את שני הדפים שנלכדו משמאל ומימין עם מרווח ביניהם, כאשר כל אחד מהם מותאם לגובה TargetHeight. הפונקציה מחזירה את מספר הדף החדש (מבוסס 1) או 0 במקרה של כישלון, ושני דפי המקור נלכדים ומוסתרים כחלק מהפעולה. מקרה של מבחן עם שני דפים לגיליון הוא משימה של שלוש שורות

var
  Lib: TPDFlib;
  NewPage: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('exam.pdf', '');
    // Pages 1 and 2 on one new wider page with an 18pt gutter,
    // both scaled to a 595pt output height (A4 landscape)
    NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
    if NewPage > 0 then
      Lib.SaveToFile('exam-2up.pdf');
  finally
    Lib.Free;
  end;
end;

עבור הרכבה אנכית, StitchPagesVertically(PageRanges, Gap, TargetWidth) עורמת טווח דפים שלם לדף אחד גבוה בסגנון פוסטר. הפרמטר PageRanges מקבל תחביר טווח סטנדרטי כמו '1-3,5'. כל דף מקור מותאם כך שרוחבו יתאים ל-TargetWidth, כך שתרומת הגובה של כל דף היא הגובה שלו כפול מקדם קנה המידה שלו, וגובה דף הפלט הוא סכום אלה בתוספת Gap אחד בין כל זוג. זו הפריסה שתרצו עבור מסמכי בדיקה בגלילה רציפה או קבלות ארוכות המורכבות ממקורות בגודל letter

כיצד מכסים דף אחד על גבי דף אחר?

הפונקציה StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) מציירת דף קיים אחד על גבי דף קיים אחר, במקום ליצור דף חדש. דף המקור נלכד כ-form XObject ומצויר על היעד במלבן שאתם מגדירים, עם Opacity (אטימות) מ-0 עד 1 (1 פירושו אטום לחלוטין). רוחב וגובה הם פרמטרים חופשיים, כך שהמיקום יימתח אם היחס שלכם שונה מיחס הגובה-רוחב המקורי של דף המקור; העבירו זוג פרופורציונלי כאשר אתם רוצים תמונה ממוזערת ללא עיוות של המקור. שימוש נפוץ הוא מסמך שבו בלוק האישור מנוהל כדף משלו ומוחתם על גבי דף השער

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('report.pdf', '');
    // Page 5 holds the approval block; place it on page 1
    // in a 260 x 180 box at 45% opacity. Page 5 is consumed:
    // it is captured and hidden, so pages 6..n become 5..n-1
    if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
      Lib.SaveToFile('report-approved.pdf');
  finally
    Lib.Free;
  end;
end;

מה קורה למספרי הדפים לאחר לכידה?

הפעולה CapturePage צורכת את דף המקור: התוכן שלו מועבר ל-XObject והדף הריק כעת מוסתר, כך שכל דף שמספרו גבוה מדף המקור מוסט מיד למטה באחד. זוהי הסמנטיקה החשובה ביותר בכל ממשק תפירת הדפים. ספריית losLab PDF Library מטפלת בחישוב זה בתוך כל קריאת תפירה, StitchPageOverlay מתאימה את אינדקס היעד כאשר היעד נמצא מעל המקור, ו-StitchPagesSideBySide לוכדת תחילה את הדף בעל המספר הגבוה יותר כדי שאינדקס הלכידה השני עדיין יהיה תקף, אך הקוד העוטף שלכם חייב לקחת בחשבון את ההסטה בין קריאות. שני כללים שומרים על קוד תפירה מרובה תקין. רשמו את גדלי הדפים או מספרי הדפים שתצטרכו לפני הלכידה הראשונה, מכיוון שהם משתנים לאחר מכן. וכאשר אתם לוכדים מספר דפים בעצמכם בלולאה, בצעו איטרציה בסדר דפים יורד, כך שהסתרת דף מאוחר יותר לעולם לא תשבש את האינדקס של דף מוקדם יותר שעדיין לא לכדתם

כיצד מפעילים סימן מים רב-שכבתי על טווח דפים?

שכבת תבנית סימני המים בקובץ PDFlibWatermark.pas הופכת את הקונספט של "הגדר פעם אחת, החל על דפים רבים" לאובייקטים. המחלקה TPDFlibWatermarkBase נושאת את מה שכל סימני המים משתפים: מאפיין Position בעל תשע נקודות (מ-wpTopLeft עד wpBottomRight, בתוספת wpCustom המשתמש בקואורדינטות X/Y גולמיות), זווית Angle במעלות, אטימות Opacity מ-0 עד 1, ומחרוזת PageRange שבה ערך ריק פירושו כל הדפים. המחלקה TPDFlibTextWatermark מוסיפה טקסט, מזהה גופן (ברירת המחדל היא Helvetica), גודל וצבע RGB; המחלקה TPDFlibImageWatermark מוסיפה מזהה תמונה מתמונה שנוספה קודם לכן וכן רוחב וגובה מיקום. המחלקה TPDFlibCompositeWatermark מחזיקה רשימה של תת-סימני מים ומחילה אותם רקורסיבית, ו-TPDFlib.ApplyWatermark היא נקודת הכניסה בקריאה אחת המכבדת את טווח הדפים של התבנית ומחזירה את מספר הדפים שהוחתמו. חותמת הטיוטה המשפטית התלת-שכבתית נראית כך

var
  Lib: TPDFlib;
  Logo: TPDFlibImageWatermark;
  Diagonal, Footer: TPDFlibTextWatermark;
  Stack: TPDFlibCompositeWatermark;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('draft.pdf', '');
    Stack := TPDFlibCompositeWatermark.Create;
    try
      Logo := TPDFlibImageWatermark.Create;
      Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
      Logo.Position := wpTopRight;
      Logo.Width := 90;
      Logo.Height := 32;
      Stack.Add(Logo);

      Diagonal := TPDFlibTextWatermark.Create;
      Diagonal.Text := 'CONFIDENTIAL';
      Diagonal.Position := wpCenter;
      Diagonal.Angle := 45;
      Diagonal.FontSize := 60;
      Diagonal.Opacity := 0.15;
      Stack.Add(Diagonal);

      Footer := TPDFlibTextWatermark.Create;
      Footer.Text := 'Internal review copy';
      Footer.Position := wpBottomCenter;
      Footer.FontSize := 9;
      Footer.Opacity := 0.6;
      Stack.Add(Footer);

      Stack.PageRange := '1-3';   // children inherit this range
      Lib.ApplyWatermark(Stack);
      Lib.SaveToFile('draft-marked.pdf');
    finally
      Stack.Free;                 // composite frees its children
    end;
  finally
    Lib.Free;
  end;
end;

שני פרטי הרכבה ראויים להבהרה. ירושת הטווח היא חד-כיוונית ואינה הרסנית: ילד עם PageRange ריק יורש זמנית את הטווח של הקומפוזיט במהלך ה-Apply, בעוד שילד שמגדיר טווח משלו שומר עליו, כך שתוכלו להצמיד את הכותרת התחתונה לכל דף בעוד שחותמת האלכסון תכסה רק את חלק הטיוטה. והבעלות עוקבת אחר הקומפוזיט: TPDFlibCompositeWatermark.Destroy משחרר את הילדים שלו, כך שאתם משחררים את הקומפוזיט פעם אחת ולא שום דבר אחר, כמו בדוגמה למעלה

מגבלות שקדאי להכיר

שתי מגבלות כנות חלות כאן, ושתיהן נובעות מאופן הפעולה של המנגנון. ראשית, לכידה עוטפת רק את זרם התוכן והמשאבים של הדף. הערות (annotations), קישורים, שדות טופס והערות (comments) חיים כאובייקטים נפרדים במילון הדף, ולא בזרם התוכן, ולכן הם אינם עוברים ל-XObject; פריסה תפורה נושאת את תוכן הדף הנראה אך לא את השכבה האינטראקטיבית של המקורות שלה. שנית, מיקום טקסט בתשע נקודות הוא קירוב: ללא מנוע פריסה מלא הספרייה אינה יכולה לדעת את הרוחב המרונדר המדויק של מחרוזת, ולכן היא מעריכה את תיבת הטקסט ב-40% מרוחב הדף בעת פענוח מיקומים כמו wpCenter. זה מדויק מספיק עבור מרכוז תווית קצרה, אך עבור מיקום מדויק ברמת הפיקסל של מחרוזות ארוכות, עברו ל-wpCustom וחשבו את הקואורדינטות בעצמכם

שינוי קנה מידה אינו מהווה מגבלה מעשית כאן, מכיוון שלכידה ותפירה מניפולטיביות על הפניות לאובייקטים ולא מקודדות מחדש את תוכן הדף; אם המקורות שלכם הם מאות מגה-בייט, אותה משמעת המתוארת במדריך למיזוג ופיצול קבצים גדולים בגישה ישירה חלה לפני שאתם תופרים. פונקציות התפירה ומחלקות תבניות סימני המים המוצגות כאן מגיעות בגרסה הנוכחית של losLab PDF Library עבור Delphi, C# ו-VB.NET, לצד הפרימיטיביים ברמה נמוכה יותר CapturePage, DrawCapturedPage ו-DrawCapturedPageMatrix כאשר אתם זקוקים לפריסה ששלוש קריאות התפירה המובנות אינן מכסות