בית דפוס טרנזקציוני מחזיר ריצת דוחות של 80,000 עמודים עם דחייה בשורה אחת: "not PDF/VT, RIP cannot cache." הקובץ נפתח כראוי בכל מציג שעל השולחן שלך, הצבעים נכונים, והנתונים אוחדו כמו שצריך. אבל שום דבר מזה אינו מה שהמכבש הדיגיטלי ביקש. הדפסה מהירה של נתונים משתנים חיה או מתה לפי היכולת של המכבש לזהות שבלוק לוגו הלקוח בעמוד 1 הוא בדיוק אותו אובייקט ברמת הבייטים כמו זה שבעמוד 40,000, לעבד אותו פעם אחת ולמחזר אותו. PDF/VT הוא התקן שהופך את ההבטחה הזו לבדיקה אוטומטית, ו"נראה תקין" הוא בדיוק המלכודת, כי המבנה שה-RIP קורא אינו נראה על המסך
PDFiumPas חושף את המבנה הזה דרך ממשק קטן על TPdf: SaveAsPdfVT כותב אותו, ValidatePdfVT בודק אותו. המאמר הזה עוסק במה ששתי השיטות האלה באמת כותבות לדיסק ובודקות ממנו, היכן ISO 16612-2 מחמיר יותר ממה שנראה במבט ראשון, ואילו חלקים הם עוגנים מבניים אמיתיים ולא בדיקת preflight מלאה שאפשר לחייב עליה לקוח
מה PDF/VT מגדיר, ולמה PDF/X מגיע קודם
PDF/VT (ISO 16612-2:2010) אינו פורמט קובץ חדש. זו שכבת מטא-נתונים של אופטימיזציה שמולבשת על קובץ PDF/X, והסדר הזה הוא קריטי. התקן מגדיר שלוש רמות תאימות, אבל רק שתיים מהן מציינות קובץ PDF: PDF/VT-1, מסמך יחיד ועצמאי, ו-PDF/VT-2, מודל של file-set שבו עמודים מפנים למשאבים חיצוניים משותפים. הטוקן השלישי שאולי תראו, PDF/VT-2s, כלל אינו ערך ברמת הקובץ; הוא חי בכותרת זרם MIME המתוארת בנספח A. אם אתם מוצאים קוד שכותב GTS_PDFVTVersion = "PDF/VT-2s" לתוך XMP של מסמך, הקוד הזה שגוי
הכלל שאי אפשר לוותר עליו בקובץ יחיד הוא בסיס PDF/X. סעיף §6.2.1 ב-ISO 16612-2 דורש שכל קובץ PDF/VT-1 יהיה גם קובץ PDF/X-4 תקין. לעומת זאת, לפי §6.2.2, ה-file-set של PDF/VT-2 חייב להישען על PDF/X-4p, PDF/X-5g או PDF/X-5pg. לכן כותב PDF/VT לא יכול פשוט להוסיף כמה מפתחות מזהים: הוא צריך לשאת איתו את כל סט הסמנים של PDF/X-4, כלומר OutputIntent, פרופיל ICC מוטמע ליעד, רשומות XMP ו-Info תואמות, /ID ב-trailer, וללא הצפנה. דילוג על אחד מאלה ייצור קובץ שטוען ל-PDF/VT ונכשל ברגע שצרכן תואם בודק את הבסיס. PDFiumPas מתייחס לשכבת PDF/X-4 כחלק מהשמירה של PDF/VT, כך שאין צורך לקרוא תחילה ל-SaveAsPdfX נפרד; המנגנון כותב את שתי השכבות במעבר אחד
כתיבת קובץ עם SaveAsPdfVT
הקריאה המינימלית לא צריכה יותר ממסמך פעיל, כי TPdfVTSaveOptions.Default מספק פרופיל ICC מובנה של sRGB ותאימות pvc1. תהליך השמירה מבצע שלושה שלבים פנימיים: הוא מסיר כל אבטחה, כי הזרקת סמני טקסט לתוך זרם אובייקטים מוצפן תפגום בו; הוא מחבר בין מילון ה-Info הקיים של המסמך לבין /ID ב-trailer כדי שערכי XMP ו-Info יהיו עקביים; ואז הוא מוסיף את אובייקטי PDF/X-4 ו-PDF/VT באמצעות עדכון מצטבר
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
if Pdf.LoadFromFile('statements-merged.pdf') then
begin
// Default options: built-in sRGB OutputIntent, PDF/VT-1, synthesised DPart
if Pdf.SaveAsPdfVT('statements-pdfvt.pdf') then
Writeln('PDF/VT-1 written')
else
Writeln('Save failed (document not active?)');
end;
finally
Pdf.Free;
end;
end;
בהפקה אמיתית כמעט תמיד תרצו להחליף את ה-OutputIntent במאפייני ההדפסה של בית הדפוס שלכם, ולא להישען על ברירת המחדל הכללית של sRGB. את נתוני ה-ICC ואת מזהי התנאים מספקים דרך TPdfVTSaveOptions:
var
Pdf: TPdf;
Opt: TPdfVTSaveOptions;
Icc: TBytes;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('directmail-merged.pdf');
Icc := LoadIccProfile('GRACoL2013_CRPC6.icc'); // your own loader
Opt := TPdfVTSaveOptions.Default;
Opt.Conformance := pvc1; // pvc2 is normalised to pvc1 on write
Opt.IccProfileData := Icc;
Opt.OutputConditionIdentifier := 'CGATS21_CRPC6';
Opt.OutputCondition := 'Commercial print, coated, CRPC6';
Opt.RegistryName := 'http://www.color.org';
Opt.Title := 'Spring 2026 Direct Mail Run';
Opt.Trapped := ptvFalse; // PDF/X Info /Trapped state
Pdf.SaveAsPdfVT('directmail-pdfvt.pdf', Opt);
finally
Pdf.Free;
end;
end;
פרט אחד בקטע הזה הוא מעקה מכוון ולא מגבלה שאפשר להתווכח עליה. קביעה של Opt.Conformance := pvc2 לא מייצרת קובץ PDF/VT-2. הכותב מנרמל כל בקשה שאינה pvc1 בחזרה ל-pvc1, משום ש-PDF/VT-2 הוא פורמט file-set, וכותב של מסמך יחיד שמצרף מסמך פלט אחד פשוט אינו יכול להרכיב פיזית את סט המשאבים החיצוניים ש-§6.2.2 דורש. הערך pvc2 קיים בשביל נתיב הקריאה, כדי ש-ValidatePdfVT יוכל לזהות ולדווח על מסמך file-set קיים; הוא אינו יעד כתיבה
עץ ה-DPart: המבנה שה-RIP באמת קורא
הלב של PDF/VT הוא היררכיית Document Part (DPart). היא זו שמאפשרת למכבש לחלק ריצה ארוכה לרשומות, לקבץ רשומות לנמענים או לאגדות דואר, ולצרף Document Part Metadata כך שהציוד בהמשך השרשרת יוכל לנתב ולחייב כל פריט. סעיף §6.5 ב-ISO 16612-2 מתאר את החיווט: הקטלוג נושא /DPartRoot, צומת ה-DPart השורש נושא /DPartRootNode ו-/NodeNameList שממנה כל רמת היררכיה, DPart-ים עלים מכסים טווחים של עץ העמודים, וכל עמוד ששייך לחלק מצביע חזרה אל העלה שלו דרך רשומת /DPart ברמת העמוד
כאשר מסמך המקור כבר מכיל היררכיה שמישה, SaveAsPdfVT משמר אותה. כאשר אין כזו, הכותב יוצר אחת מינימלית: DPart יחיד ברמת המסמך שמכסה את עץ העמודים הנוכחי לפי הסדר, עם הפניית חזרה /DPart שמצורפת לכל אובייקט עמוד חי ו-/NodeNameList [/Document] ברמה אחת. חשוב להיות כנים לגבי מה שהעץ המינימלי הזה הוא. זהו עוגן מבני שמקיים את דרישות הצורה של §6.5; הוא אינו מטא-נתוני עסק. הוא אינו יכול להמציא נמענים, גבולות של פריטי דואר או אצוות מוצר, כי המידע הזה לא היה קיים במקור. אם יש לכם נתונים ברמת נמען, אתם אמורים לבנות עץ DPart עמוק יותר בעצמכם ולהרחיב את /NodeNameList כך שיתאים לרמות שאתם יוצרים
אימות שעובר מעבר לנוכחות מפתחות
ValidatePdfVT מחזיר רשומת TPdfVTValidationResult עם שלושה דברים: ה-Conformance שזוהה, קבוצה של Issues, ועוזר IsCompliant שמחזיר true רק כאשר רמת התאימות היא אמיתית וקבוצת הבעיות ריקה. מניית הבעיות מכוונת להיות מדויקת, כך שתוצאה כושלת תגיד לכם איזה סעיף פספסתם ולא רק "invalid":
var
Pdf: TPdf;
Res: TPdfVTValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('statements-pdfvt.pdf');
Res := Pdf.ValidatePdfVT;
if Res.IsCompliant then
Writeln('PDF/VT compliant: ', VTLevelName(Res.Conformance))
else
begin
if pvviMissingDPartRoot in Res.Issues then
Writeln('DPart hierarchy missing or unusable');
if pvviMissingPdfXIdentifier in Res.Issues then
Writeln('PDF/X-4 base identifier absent');
if pvviMissingOutputIntent in Res.Issues then
Writeln('OutputIntent / ICC profile missing');
if pvviEncryptionPresent in Res.Issues then
Writeln('Encrypted - PDF/X forbids this');
end;
finally
Pdf.Free;
end;
end;
שתי הבדיקות שכדאי להבין לעומק הן צימוד התאימות וההליכה בעץ DPart, משום ששניהם היו בעבר סלחניים מדי והודקו כך שיתאימו למפרט. בצד הצימוד, המאמת מבצע התאמה מדויקת, לא "כל PDF/X יעבוד": קובץ PDF/VT-1 מתקבל רק על בסיס PDF/X-4, וקובץ PDF/VT-2 רק על PDF/X-4p, PDF/X-5g או PDF/X-5pg. דגל PDF/VT-1 שיושב על בסיס PDF/X-1a מדווח כבעיה, לא עובר בשקט
ההליכה בעץ DPart היא המקום שבו רוב הנוקשות נמצאת. לא מספיק שלקטלוג יהיה מפתח /DPartRoot, כי אובייקט ריק ומזויף או כזה בלי קישורי עמודים עדיין לא ניתן לצריכה. HasValidDPartHierarchy והפונקציה הרקורסיבית ValidateDPartNode עוקבות אחרי כל המבנה: הן עוקבות אחרי קישורי הורה, דוחות ילדים כפולים ומחזורים, אוכפות ש-/Start ו-/DParts מוציאים זה את זה, ודורשות שטווחי עמודי עלה יכסו את עץ העמודים בסדר depth-first, כאשר /DPart של כל עמוד מצביע אל העלה שמכיל אותו. כל הכשלים הפנימיים האלה מתכנסים לביט שגיאה יחיד, pvviMissingDPartRoot, במקום להרחיב את המניין הציבורי, לכן יש להתייחס לדגל הזה כאל "היררכיית DPart אינה שמישה", לא מילולית כ"הליבה חסרה"
שלושה מוקשים תחביריים שהמאמת אוכף כעת
מעברים עוקבים מול טבלה 4 בסעיף §6.5 חשפו צורות שגרסאות קודמות קיבלו אבל התקן אינו מקבל. אלה בדיוק סוגי השגיאות שעץ DPart שנבנה ידנית נוטה לייצר, ולכן כדאי לציין אותן במפורש:
/DPartsהוא מערך של מערכים, לא מערך שטוח. כל איבר במערך החיצוני חייב להיות בעצמו מערך של הפניות עקיפות./DParts [9 0 R]שטוח נדחה; הצורה התואמת היא/DParts [[9 0 R] [10 0 R]]. כך נמנע ממצב שבו מבנה לא היררכי מתחזה לרמה תקינה/Endמסמן רק טווח אמיתי של כמה עמודים. DPart-עלה יכול לשאת/Endרק כשהוא גם כולל/Start, ו-/Endחייב לחול מאוחר יותר מ-/Startבסדר של עץ העמודים./Start 3 0 R /End 3 0 Rדגנרטיבי הופך כעת את ההיררכיה לבלתי שמישה במקום להיקרא כחלק של עמוד יחיד- שמות
/NodeNameListחייבים לשרוד את ה-unescaping של שמות PDF כ-XML NMTOKENs. שם כמו/Bad#20Nameמתרחב לשם שמכיל רווח, וזה אינו token תקין. המימוש מבצע בדיקת ASCII קלה, אותיות, ספרות,.,-,_,:, ובנוסף בתים שאינם ASCII, שתופסת טעויות של רווחים ומפרידים בלי לפסול שמות מקומיים או שמות ספציפיים לספק תקינים
סמני XMP: שתי דרכים לכתוב אותו מאפיין
זיהוי PDF/VT חי ב-XMP תחת המרחב pdfvtid, ובפרט GTS_PDFVTVersion ו-GTS_PDFVTModDate, לצד xmp:CreateDate ו-xmp:ModifyDate הסטנדרטיים. דקויות שגורמות בדיווחים שגויים של "חסר" אצל קוראים נאיביים הן שאפשר לסריאליזציה של כל אחד מהם בשתי דרכים: כטקסט של רכיב (<pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion>) או כמאפיין RDF על רכיב התיאור. PDFiumPas קורא את שתי הצורות, ולכן קובץ שכלי אחר כתב בסגנון של מאפיין לא נענש. הוא גם אוכף את כלל העקביות של §6.3 שלפיו GTS_PDFVTModDate חייב להיות שווה ל-xmp:ModifyDate; אי-התאמה מעלה את pvviModDateMismatch
כלל נוסף מאותו סעיף: ערך לא מוכר של GTS_PDFVTVersion נשמר כ-pvcUnknown במקום להידחף חזרה ל-pvcNone. ההבחנה הזו חשובה תפעולית. pvcNone פירושו "אין בכלל סימון PDF/VT, זה PDF רגיל", בעוד pvcUnknown פירושו "מישהו חתם עם גרסה שהמאמת הזה לא מזהה" (כולל המקרה PDF/VT-2s). איחוד של השניים היה מסתיר קובץ פגום בתוך אותה קטגוריה של מסמך רגיל
היכן ההבטחה מסתיימת
כדאי לדייק בגבול של מה שהשיטות האלה מבטיחות, כי לתאימות בהדפסת נתונים משתנים יש השלכה כספית ממשית. בדיקות DPart והצימוד הן אימות מבני ברמת הבייטים. הן מאשרות שסכמת האופטימיזציה, סמני הבסיס של PDF/X-4, ה-OutputIntent, וה-XMP קיימים ועקביים פנימית. הן אינן בדיקת preflight של תוכן ברמת PDF/X-4: הן אינן מאמתות שכל צבע נמצא בתחום תנאי הפלט המוצהר, שכל הגופנים מוטמעים, או שאף מקרה קצה אסור של blending עם שקיפות לא החליק פנימה. עבור עבודה שאתם שולחים לבית דפוס חיצוני, שלבו את האימות המבני של PDFiumPas עם מנוע preflight ייעודי ל-PDF/X והדפסת ניסיון, בדיוק כפי שהייתם בודקים כל טענת תאימות אחרת. השכבה המבנית תופסת את הכשלים ששוברים בשקט את ה-caching של ה-RIP; היא חצי מבדיקה מלאה, לא כולה
אם אתם בונים את הבדיקות האלה לתוך שער שחרור רחב יותר, אותה גישת סריקה ברמת הבייטים עומדת גם בבסיס עבודת התקנים האחרת של הספרייה, כולל אימות זרמי אובייקטים וזרמי הפניות צולבות לפני שקובץ בכלל מגיע ל-preflight, והמשמעת של אובייקטים משותפים מאחורי חותמות עמודים לשימוש חוזר עם Form XObjects שהופכת את המסמך לידידותי ל-RIP מלכתחילה. ממשקי ה-API של שמירה ואימות ל-PDF/VT ול-PDF/X המתוארים כאן הם חלק מ-רכיב PDFium VCL עבור Delphi ו-C++Builder, ודף המוצר שלו כולל את הפניה המלאה לתאימות