מאמר טכני

שמירת PDF בגרסה מדויקת ב-Delphi: תאימות PDFiumPas

PDFiumPas, העטיפה של Delphi ו-C++Builder סביב מנוע ה-PDFium של Google, שומרת מסמך בגרסת PDF מדויקת מ-1.3 עד 1.7 דרך הפרמטר PdfVersion של פונקציית TPdf.SaveAs. הקריאה FPDF_SaveWithVersion של PDFium עצמה רק כותבת מחדש את כותרת ה-%PDF-M.m, בלי לבדוק אם התוכן בפועל של המסמך חוקי בגרסה הזו. PDFiumPas סוגרת את הפער הזה עם מעבר תאימות אחרי-שמירה שעובר על שרשרת הרוויזיה הפעילה של הפניית-הצולבת (cross-reference) ובודק הצהרות רמת-הרחבה של Adobe לפני שהקובץ עוזב את הפונקציה

ההבחנה הזו הכי חשובה בייצור-דפוס, שם פרופיל PDF/X נוקב בגרסת PDF מדויקת וכלי preflight או RIP דוחה כל דבר שבשקט לא מסכים עם הכותרת של עצמו, תרחיש המכוסה מצד-הפלט באימות מסמכי PDF/X מוכני-דפוס עם PDFiumPas. ‏SaveAs חושפת את היעד כטיפוס-מנייה TPdfVersion, ‏pv13 עד pv17 לצד ערכי pv10 עד pv12 הישנים יותר, בתוספת TSaveOption עצמאי עבור כתיבות-מחדש הדרגתיות (incremental) או מלאות. מסור PdfVersion ו-PDFiumPas מבצעת שתי עבודות בקריאה אחת: היא מבקשת מ-PDFium להטביע את הכותרת המבוקשת, ואז קוראת מחדש את הבייטים שנכתבו טרי ומסרבת למסור בחזרה קובץ שהתוכן הפעיל שלו לא יכול חוקית להתקיים בגרסה הזו

var
  Pdf: TPdf;
begin
  Pdf:= TPdf.Create(nil);
  try
    Pdf.FileName:= 'source.pdf';
    Pdf.Active:= True;
    try
      Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
    except
      on E: Exception do
        // E.Message names the offending feature and the version or
        // extension level it actually needs, for example:
        // "RichMedia annotations and RichMediaExecute actions require
        // /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
        // or newer."
        raise;
    end;
  finally
    Pdf.Free;
  end;
end;

למה ההגדרה הפיזית האחרונה של אובייקט בקובץ היא הדבר הלא-נכון לבטוח בו?

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

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

איך PDFiumPas קובעת אילו הגדרות אובייקט בפועל פעילות?

PDFiumPas פותרת את קבוצת האובייקטים הפעילה באותו אופן שקורא תואם עושה, על ידי מעבר על שרשרת ההפניה-הצולבת במקום סריקת בייטים לחפש כותרות אובייקט. הפותר מתחיל בהיסט ה-startxref האחרון בקובץ ועוקב אחר כל קישור /Prev אחורה דרך רוויזיות ישנות יותר, מפענח טבלאות הפניה-צולבת קלאסיות, זרמים היברידיים מקושרי-/XRefStm, וזרמי הפניה-צולבת טהורים לאורך הדרך. המעבר רץ חדש-לישן ומיישב כל מספר אובייקט בפעם הראשונה שהוא נראה, כך שרשומה חופשית ברוויזיה מאוחרת יותר מצילה נכון גוף אובייקט שנכתב באחת מוקדמת יותר, והגדרה-מחדש תחת היסט או דור חדש תמיד מנצחת את מה שהיא החליפה

לחברי זרם-אובייקטים יש בדיקה נוספת שחיפוש-היסט פשוט לא יכול לספק בעצמו, מנגנון המכוסה לעומק רב יותר באימות זרמי אובייקט והפניה-צולבת עם PDFiumPas. אובייקט דחוס שמשוחזר מ-/ObjStm חייב שהזרם ההורה שלו יאושר פעיל באותו מעבר, והאינדקס שלו חייב להסכים עם המיקום של החבר עצמו בתוך כותרת הזרם הזה לפני ש-PDFiumPas מתייחסת אליו כתוכן חי. ‏ISO 32000-1 סעיף 7.5.8.4 אפילו מתאר מקרה הפניה-היברידית שבו טבלת תאימות קלאסית מסמנת אובייקט חופשי בעוד רשומת ה-/XRefStm של הטריילר בו-זמנית מגדירה את אותו אובייקט כחבר דחוס במקום אחר; PDFiumPas ממזגת את זרם ה-xref המשלים לתוך אותה רוויזיה לפני שהרשומות הקלאסיות מיושמות, כך שההגדרה הדחוסה מנצחת באופן שהמפרט מתכוון

רמות הרחבה של Adobe: השער מעל מספר הגרסה

כותרת %PDF-1.7 מבטיחה רק את קבוצת התכונות ש-ISO 32000-1 תקנן ב-2008, בעוד כמה יכולות שמייצרי PDF נשענים עליהן היום יצאו לדרך אחר-כך כתוספים בלעדיים-ל-Adobe שכובדו מעל אותו מספר גרסה עצמו. ‏Adobe רשמה כל תוסף כזוג BaseVersion ו-ExtensionLevel שנרשם במילון ה-/Extensions של קטלוג המסמך תחת קידומת מפתח, ‏ADBE עבור התוספים של Adobe עצמה, כך שקורא יכול להבחין בין קובץ PDF 1.7 פשוט לבין אחד שגם מיישם רמת הרחבה ממוספרת. שמירה ב-pv17 ללא ההצהרה ההיא אינה שגיאה בעצמה; היא הופכת לכזו רק ברגע שהתוכן הפעיל בפועל תלוי בתכונה שההצהרה אמורה לכסות

אילו תכונות גרסה-גבוהה מפעילות את שער-הגרסה-המפורש?

PDFiumPas בודקת רשימה ספציפית, מונעת-מפרט, ולא מנחשת ממספר הגרסה בלבד. מילוני תמונה שנושאים רשומת /SMaskInData מפורשת או ערך /BitsPerComponent של 16 שניהם דורשים PDF 1.5, כאשר מקרה שש-עשרה-הסיביות עוקב ישירות אחר כללי רכיב-תמונה של סעיף 4.8 של PDF Reference 1.5. הערות RichMedia ופעולות RichMediaExecute דורשות /BaseVersion /1.7 עם /ExtensionLevel 3 ומעלה. זרמי PRC ‏3D, מזוהים על ידי מילון שנושא גם /Type /3D וגם /Subtype /PRC, דורשים את אותה גרסת בסיס אבל רק /ExtensionLevel 1. מילוני Measure גיאו-מרחביים והערות Projection דורשים /BaseVersion /1.7 עם /ExtensionLevel 3, אותו תוסף Adobe ש-RichMedia תלוי בו

הבדיקה הגיאו-מרחבית נושאת פרט קריאת-מפרט ששווה לדעת אם אי-פעם תבנה לוגיקה מגודרת-גרסה משלך מעל PDFiumPas. ‏ISO 32000-1 טבלה 254 מסמנת את רשומת ה-/Type של מילון ה-Measure כאופציונלית, מציינת רק ש"אם נוכחת, חייבת להיות Measure," בעוד טבלה 311 הופכת את /Type לחובה עבור מילון זרם ה-3D ש-PRC חי בו. פלט GeoPDF אמיתי מכלי מיפוי בשגרה משמיט /Type על מילון ה-Measure וכותב רק /Subtype /GEO, כך שהגלאי הגיאו-מרחבי של PDFiumPas מתאים על /Subtype בלבד במקום לדרוש את שני המפתחות באופן שגלאי ה-PRC 3D שלו יכול לעשות בבטחה. דרישת /Type על שני המילונים הייתה נותנת לתוכן GeoPDF תואם לחמוק על פני השער בלתי-מזוהה, נוחת בקובץ PDF 1.7 פשוט ללא הצהרת רמת-הרחבה שתגבה אותו

האם PDFiumPas מורידה אוטומטית תכונות בלתי-נתמכות?

לא כיכולת כללית, וההנחה אחרת היא הטעות שכדאי להימנע ממנה כאן. ‏SaveAs מזרימה את גרסת היעד דרך פונקציה פנימית, ‏ValidatePdfVersionCompliance, וכאשר הפונקציה ההיא מוצאת תכונה שגרסת היעד או הצהרת רמת-ההרחבה שלה לא יכולה לתמוך בה, ‏SaveAs מעלה חריגה שנושאת את טקסט השגיאה של הפונקציה במקום לכתוב את הקובץ; הקוד הקורא מקבל בחזרה סיבה מדויקת, שמת-תכונה, אף פעם לא מסמך שנכתב מחדש בשקט. המקום היחיד ש-PDFiumPas כן כותבת תוכן מחדש אוטומטית הוא יעד PDF 1.3, שם היא מסירה את ברירות המחדל השקופות-סמנטית /BM /Normal, ‏/CA 1, ו-/ca 1 שהשקיפות ש-PDFium תמיד כותבת לתוך מילוני ExtGState ללא קשר לגרסת יעד, משום שהערכים הספציפיים האלה לא נושאים משמעות חזותית ו-PDF 1.3 קודם למפתחות לגמרי

// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode

שקיפות בלתי-ברירת-מחדל אמיתית ומסכות-רכות של תמונה עדיין נכשלות לגמרי ביעד PDF 1.3, משום שהסרתן הייתה משנה איך העמוד בפועל נראה, ו-PDFiumPas לא תקבל את ההחלטה הזו בשמך. שתי מגבלות קשורות שוות תכנון סביבן לפני שגרסה מדויקת נכנסת לצינור אצווה. פלט בגרסה-מפורשת אף פעם לא נושא מילון /Encrypt; השמירה נכשלת מיד אם המקור מוגן, מה שבמקרה מתיישר עם פרופילי PDF/X ו-PDF/A שאוסרים הצפנה בכל מקרה, אבל זה כן אומר שפענוח הוא שלב נפרד בזרימת העבודה שלך ולא משהו ש-SaveAs עושה עבורך. ל-PDFiumPas גם אין פונקציה ציבורית לכתיבת הצהרת /Extensions /ADBE לתוך קטלוג, כך שקובץ מקור שמכיל תוכן RichMedia, ‏PRC 3D, או גיאו-מרחבי אך חסר את ההצהרה ההיא לא יעבור את השער לא משנה איזה PdfVersion תבקש; ההצהרה חייבת כבר להתקיים במקור, בדרך כלל משום שכלי הכתיבה כתב אותה, או שהתכונה חייבת לצאת לפני השמירה. מאפיין TPdf.PdfVersion לקריאה-בלבד שווה בדיקה עוד לפני ששמירה בגרסה-מדויקת אפילו מנוסה, שכן הוא פותר את אותה גרסה אפקטיבית מודעת-קטלוג, כותרת או דריסת /Version, איזו שהיא נוכחית, שהמאמת בזמן-השמירה עצמו נשען עליה

Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);

התייחס לחריגת SaveAs על יעד גרסה-מדויקת כדוח preflight ולא כבאג: ההודעה נוקבת בשם הסעיף המדויק שמסמך המקור מפר, שזה בדיוק המידע שבית-דפוס או צינור ארכיון זקוקים לו לפני שקובץ ממשיך הלאה. נתיב השמירה בגרסה-מפורשת, פותר רוויזיית ה-xref הפעילה, ובדיקות רמת ההרחבה של Adobe המתוארות כאן נשלחים כחלק מרכיב PDFiumPas הסטנדרטי עבור Delphi ו-C++Builder; דף המוצר נושא את מסמך העזר המלא ל-TPdf.SaveAs לצד שאר ה-API של תאימות וטפסים