מאמר טכני

מסמכי הנדסה PDF/E-1 ב-Delphi עם PDFlibPas

PDF/E-1 הוא פרופיל הארכוב למסמכי הנדסה, ו-PDFlibPas מממש אותו כמצב כתיבה שמפעילים עם SetPDFEMode בתוספת preflight מוגבל שקורא content stream אופרטור אחר אופרטור. הפרופיל הוא לא PDF/A עם תווית אחרת: יש לו מרחב זיהוי משלו, דרישת מטא-נתונים משלו למחזור החיים, וכלל אחד שהופך את אימות התוכן למחמיר יותר מכל פרופיל ארכוב שפגשת

תוצרי הנדסה הם הסיבה לקיומו של הפרופיל. סט שרטוטים שצריך להישאר קריא והוכח כבלתי שונה גם בעוד עשרים שנה, עם היסטוריית מהדורות ששורדת, ועם צבע שמשמעותו זהה גם על ה-plotter בבניין האחר. הדרישות האלה מייצרות מפרט שרוב דרישותיו יושבות מחוץ לתוכן העמוד, במטא-נתונים ובניהול צבע, וזה בדיוק המקום שבו כותב PDF גנרי טועה

זיהוי משלו, לא וריאציה על PDF/A

הדבר הראשון ליישר הוא שזיהוי PDF/E-1 לא ניתן להפקה על ידי התאמת התבנית של PDF/A או PDF/X. הוא משתמש במרחב שמות XMP נפרד, http://www.aim.org/pdfe/ns/id/, וערך הגרסה חייב להופיע בשני מקומות: כרשומה במידע המסמך וכמאפיין XMP ממורחב שם מרחב. הפקה של מאפיין ה-XMP בלבד, או של רשומת המידע בלבד, מניבה קובץ שנושא את הכוונה ונכשל באימות

ל-output intent יש צורה מפורטת לא פחות. PDF/E-1 דורש פרופיל ICC מוטמע עם מזהה המשנה ISO_PDFE1, ומספר הרכיבים של הפרופיל חייב להתאים למשפחת הצבע של ההתקן שהמסמך באמת משתמש בה. זה הסעיף האחרון שבו מימושים טועים בשקט, כי המשמעות היא שאת ה-intent אי אפשר לבחור מראש ואז לשכוח ממנו

מדוע צבע התקן צריך סריקה של המסמך כולו?

כי מרחבי צבע מסתתרים במילוני משאבים שסריקה ברמת עמוד לעולם לא מגיעה אליהם. PDF/E-1 מתייחס אל DeviceRGB ו-DeviceCMYK כמשפחות הדדית בלעדיות למסמך, ולכן אימות הפרופיל מחייב לדעת את כל מרחבי הצבע של ההתקן שכל דבר בקובץ משתמש בהם. ל-form XObject יש משאבים משלו. כך גם ל-pattern, וכך גם לתמונה. tiling pattern בתוך form XObject בתוך עמוד הוא בעומק של שלוש רמות, ומאמת שבודק רק את משאבי העמוד העליונים יעביר מסמך שמשתמש בשתי המשפחות

הסריקה לכן רושמת מרחבי צבע תוך הליכה על עמודים, טפסים, תמונות ו-patterns כמעבר אחד, ורק אז מחליטה אם המסמך קוהרנטי ואם ה-output intent תואם. אותו היגיון מניע את ארכיטקטורת ה-preflight בכלל: חצי מעבר מייצר מעברי שווא, ומעבר שווא על בדיקת תאימות גרוע מאשר אין בדיקה, כי הוא נרשם כראיה

var
  Lib: TPDFlib;
  Diag: WideString;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.LoadFromFile('assembly-drawings.pdf');

    if Lib.SetPDFEMode(1) = 0 then
      raise Exception.Create('PDF/E author mode was refused');

    // מצב הכתיבה שומר את מטא-נתוני מחזור החיים מסונכרנים בכל שמירה.
    // שואלים לפני השמירה אם המסמך יעבור את השער שלו עצמו
    if not Lib.PDFEReadyForSave then
    begin
      Diag := Lib.GetPDFEDiagnostics;
      Writeln('PDF/E blockers: ', Diag);
      Exit;
    end;

    Lib.SaveToFile('assembly-drawings-pdfe.pdf');
  finally
    Lib.Free;
  end;
end;

מטא-נתוני מחזור החיים הם חובה של כל שמירה

PDF/E-1 מבקש יותר ממזהה מסמך. הסט המינימלי כולל את מזהה המסמך של ניהול המדיה, מזהה גרסה, rendition class, זמן יצירה, זמן שינוי, זמן מטא-נתונים וכותרת. זה אוצר מילים למעקב מהדורות, והוא קיים כי מצפים שתוצר הנדסה יונפק מחדש ולא ייכתב פעם אחת

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

זה עיקרון כללי למטא-נתוני תאימות ושווה לנסח אותו בנפרד מ-PDF/E: מטא-נתונים נגזרים שייכים לנתיב השמירה, לא לנתיב העריכה. כל שדה שמחושב ממצב המסמך חייב להימחש מחדש ברגע שהמצב נקפא, או שהוא cache בלי אינוולידציה

תרשים PDF/E-1 של PDFlibPas לסריקת צבע ההתקן על פני המסמך כולו, ההולכת על מילוני משאבים של עמוד, form XObject, tiling pattern ותמונה ואוספת את משפחות DeviceRGB ו-DeviceCMYK לפני שיפוט קוהרנטיות, לצד שדות מטא-נתוני מחזור החיים שמצב הכתיבה מסנכרן מחדש מיד לפני כל שמירה כך שצילום ה-XMP תואם את הבייטים שעומדים להיכתב
קוהרנטיות צבע נשפטת רק אחרי מעבר אחד שמגיע לכל מילון משאבים, ומטא-נתונים נגזרים של מחזור החיים מחושבים מחדש ברגע שמצב המסמך נקפא, ולא כשהמצב הופעל

הכלל שהופך את אימות התוכן למחמיר

PDF/E-1 לא מתיר לאופרטורים של מקטעי התאימות לבלוע תוכן לא מוכר. ב-PDF רגיל, BX ו-EX תוחמים אזור שבו הצרכן חייב להתעלם מאופרטורים שאינו מזהה, וזו דלת המילוט שמאפשרת ליצרן להפיק מבנים חדשים בלי לשבור קוראים ישנים. תחת PDF/E-1 המילוט הזה סגור, ולכן כל אופרטור שה-preflight לא מזהה מדווח ללא תנאי, בין אם הוא יושב בתוך מקטע תאימות ובין אם לאו

ההשפעה על מאמת משמעותית. אי אפשר לדלג על אזורים שלא מובנים, וזה אומר שמנתח ה-operand חייב לנתח באמת כל אופרטור בכל content stream. כאן נכנסים התחומים. המעבר מוגבל ל-128 רמות קינון, מיליון אובייקטים ו-64 MiB של תוכן, והמגבלות האלה הן לא כיוונון ביצועים. קובץ עוין או סתם שבור יכול להציג גרף אובייקטים עם מעגלים או עומק קינון שהופך מאמת רקורסיבי לגלישת מחסנית, והמגבלות הן מה שמונע ממעבר אימות להפוך לווקטור של מניעת שירות. אותה גישה הגנתית מתוארת בניתוח בטוח של PDF לא מהימן

// אימות עצמאי של קובץ שלא אתה יצרת, בלי לטעון אותו
// לתוך מופע מסמך
var
  Issues: TStringList;
  Stream: TFileStream;
  I: Integer;
begin
  Issues := TStringList.Create;
  Stream := TFileStream.Create('incoming.pdf', fmOpenRead or fmShareDenyWrite);
  try
    if CheckCompliancePDFE(Stream, '', 0, Issues) = 0 then
      for I := 0 to Issues.Count - 1 do
        Writeln('PDF/E: ', Issues[I]);
  finally
    Stream.Free;
    Issues.Free;
  end;
end;

מה שער השמירה מתקן ומה שהוא מסרב

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

אחר כך הוא בודק את האילוצים שלא ניתנים לתיקון בלי לשנות את מה שהמסמך אומר: גרסה, זיהוי, הצפנה, output intent, קוהרנטיות צבע התקן ונוכחות תוכן טפסים דינמי. מסמך שנכשל באחד מהם נדחה, כי להמציא output intent או לבחור משפחת צבע בשם הכותב יניב קובץ שעובר אימות ומציג את התוכן באור שגוי

תרשים שער השמירה של PDF/E-1 ב-PDFlibPas ל-Delphi המציג את ה-preflight המוגבל שסורק כל אופרטור בכל content stream תחת מגבלות של 128 רמות קינון, מיליון אובייקטים ו-64 MiB, מתקן בשקט דגלי הדפסה, זום וסיבוב של הערות, מסרב לגרסה, זיהוי, הצפנה, output intent, צבע התקן או תוכן טפסים דינמי שגויים, ומדווח חוסמים דרך GetPDFEDiagnostics
השער מתקן בשקט רק מה שאינו נושא מידע, מסרב לכל אילוץ שתיקון יעוות, והופך סירוב לרשימת חוסמים דרך GetPDFEDiagnostics לפני שבייט אחד מגיע לדיסק

קריאת האבחון חזרה דרך GetPDFEDiagnostics לפני השמירה הופכת את הסירוב לרשימה שאפשר לפעול לפיה במקום פעולה שנכשלה. ב-pipeline אצווה, קוראים אותו לכל מסמך, רושמים את החוסמים לכל קובץ, ומפנים את הכישלונות לתור שבן אדם מסתכל עליו. זה שימושי הרבה יותר משמירה שמרימה חריגה, כי החוסמים נוטים להצטבר: ארבעים מסמכים שנכשלים על אותו output intent חסר הם תיקון אחד, לא ארבעים

בחירה בין פרופילי הארכוב

PDF/E-1 הוא היעד הנכון כשהתוצר הוא תיעוד הנדסה עם מחזור מהדורות, ובמיוחד כשקוהרנטיות צבע ההתקן חשובה כי הפלט הולך ל-plotters ולמדפסות פורמט גדול. PDF/A הוא היעד הנכון כשהמטרה היא קריאות ארוכת טווח למסמכים בכלל, והוא הפרופיל עם התמיכה הרחבה ביותר בכלי אימות. השניים לא ניתנים להחלפה, ומסמך יכול לעמוד באחד ולהיכשל בשני

תרשים החלטות של PDFlibPas המשווה פרופילי הארכוב PDF/E-1 ו-PDF/A עבור Delphi: PDF/E-1 לתוצרי הנדסה עם מחזורי מהדורות, צבע plotter ואימות חוזי תחת מרחב שמות XMP משלו עם output intent של ISO_PDFE1, PDF/A לקריאות ארוכת טווח כללית עם התמיכה הרחבה ביותר בכלי אימות
מתחילים ממי מאמת את הקובץ בקצה הרחוק: הפרופילים דורשים זיהוי, מטא-נתונים וערבויות צבע שונים, ומסמך יכול לעמוד באחד ולהיכשל בשני

אם בחרת, מתחילים ממי מאמת את הקובץ בקצה הרחוק. כלי אימות PDF/A הם בכל מקום, וה-preflight המקביל ב-PDFlibPas מתואר בpreflight של PDF/A ו-PDF/UA. אימות PDF/E מתמחה יותר ובדרך כלל הוא דרישה חוזית ולא ברירת מחדל. כשארכיון קיים צריך להימשך אל פרופיל שמעולם לא נכתב עבורו, נתיב תיקון המטא-נתונים בהמרה ל-PDF/A עם תיקון מטא-נתונים הוא התבנית ללכת לפיה, ואותה צורה חלה כאן: לזהות, לתקן מה שבטוח, לסרב לשאר עם רשימה

מצב הכתיבה, ה-preflight המוגבל לתוכן ובדיקת התאימות העצמאית מגיעים כולם עם ספריית ה-PDF של PDFlibPas ל-Delphi, כך שמסמך יכול להיות מופק תחת הפרופיל ולהיבדק באופן בלתי תלוי אחר כך דרך נתיב קוד נפרד, וזה המערך היחיד ששווה לסמוך עליו לתביעת תאימות