בניתם חשבונית Factur-X וכל בדיקת מעטפת (container check) עוברת בהצלחה. הקטלוג (catalog) נושא מערך /AF, עץ השמות EmbeddedFiles מוביל למפרט הקובץ (file specification) הנכון, ל-factur-x.xml המוטבע יש את ה-/AFRelationship הנכון, שהוא Alternative, והפונקציה המובנית ValidateFacturXInvoice מחזירה 1. לאחר מכן אתם מריצים את אותו קובץ דרך veraPDF, בודק הייחוס (reference checker) שבו משתמשים פורטלי מס, והוא קובע שהמסמך כולו אינו PDF/A-3 תקין. המבנה נכון. המטא-נתונים הם הבעיה, והכישלון הוא אחד הקלים ביותר לפספוס בכל זרימת העבודה (workflow) של החשבונית האלקטרונית (e-invoice)
שווה להבין את הסיבה במלואה, משום שהיא מסבירה סוג (class) של פגם ב-PDF/A שאין לו שום קשר לעמוד הגלוי או לצרופה (attachment) ויש לו קשר הדוק לאופן שבו XMP מתאר את עצמו. זוהי המלכודת שמסתתרת מאחורי בדיקת מעטפת ירוקה (תקינה)
ארבעת המאפיינים שמכשילים את הקובץ
חשבונית Factur-X כותבת ארבעה מאפיינים מותאמים אישית (custom properties) לתוך מנת ה-XMP שלה כדי שתוכנות במורד הזרם (downstream software) יוכלו לקרוא את פרופיל החשבונית מבלי לנתח (parsing) את ה-XML המוטבע. הם חיים במרחב השמות (namespace) של Factur-X תחת הקידומת fx: המאפיינים הם fx:DocumentFileName, fx:DocumentType, fx:Version ו-fx:ConformanceLevel. אלו הם בדיוק המטא-נתונים שקורא צריך כדי לדעת שה-PDF הזה נושא חשבונית EN 16931 ששמה factur-x.xml בגרסה 1.0
אף אחד מארבעת המאפיינים הללו אינו חלק מסכמת XMP כלשהי ש-PDF/A מגדיר מראש. הסכמות של Dublin Core, XMP Basic, PDF ו-PDF/A identification ידועות לקורא תואם (conforming reader), אך fx: אינה כזו. כאשר veraPDF עובר על ה-XMP ומגיע למאפיין שהוא אינו מזהה את מרחב השמות שלו, הוא מחפש הצהרה שתגיד לו מה משמעות המאפיין. אם ההצהרה הזו חסרה, הוא מדווח על כישלון כנגד סעיף 6.6.2.3.1 של ISO 19005-3, הדורש שכל מאפיין שאינו לקוח מסכמה מוגדרת-מראש יתואר בתוך סכמת הרחבה (extension schema) של PDF/A. ארבעה מאפיינים לא מוצהרים הם ארבע דרכים לקובץ להידחות, ואף אחת מהן אינה גלויה לבדיקת מעטפת
מדוע PDF/A מסרב למאפיין מותאם אישית חשוף (bare)
הכלל נראה פדנטי עד שאתם נזכרים לשם מה קיים PDF/A. הפורמט קיים כדי שקובץ יוכל להיפתח ולהיות מובן בעוד עשרות שנים מהיום, על ידי תוכנה שמעולם לא סופר לה על המוסכמות של שנת 2026. קורא תואם מצופה להבין את המסמך מתוך המסמך בלבד, ללא רישום (registry) חיצוני שניתן להתייעץ איתו
מטא-נתונים מותאמים אישית מפרים את ההבטחה הזו אלא אם הקובץ נושא את התיאור של עצמו. בהינתן מאפיין fx:ConformanceLevel חשוף, קורא עתידי אינו יכול לדעת לאיזה URI של מרחב-שמות הקידומת fx נקשרת, האם הערך הוא טקסט, תאריך או מספר שלם, והאם המאפיין מתאר את המסמך עצמו או משאב חיצוני כלשהו. המנגנון של סכמת הרחבה ב-PDF/A סוגר את הפער הזה. הוא מאפשר לקובץ להצהיר, במבנה XMP קבוע, על מרחב השמות, הקידומת (prefix), ועבור כל מאפיין - סוג ערך (value type) וקטגוריה של internal (פנימי) או external (חיצוני). מרגע שההצהרה הזו קיימת, המאפיין מתאר-את-עצמו (self-describing), וסעיף 6.6.2.3.1 מסופק. בלעדיה, למאמת אין ברירה אלא להתייחס למאפיין כאל בלתי-מובן ולהכשיל את הקובץ. ההבחנה בקטגוריה חשובה כאן: מאפייני חשבונית כמו אלה מתארים נתונים שמגיעים מחוץ למעבד ה-PDF, לכן הם מוצהרים כ-external ולא כ-internal
מה מכילה הצהרת סכמת ההרחבה
ההצהרה היא אלמנט rdf:Description במנת ה-XMP העושה שימוש בשלושה מרחבי-שמות המוגדרים על ידי AIIM: המרחבים pdfaExtension, pdfaSchema, ו-pdfaProperty. בתוך אלמנט bag של pdfaExtension:schemas יושבת רשומת סכמה אחת שנותנת שם לסכמה של Factur-X, מספקת את ה-pdfaSchema:namespaceURI ואת ה-pdfaSchema:prefix שלה, ולאחר מכן מפרטת את ארבעת המאפיינים ברצף pdfaSchema:property. כל מאפיין נושא שם, pdfaProperty:valueType מסוג Text, ו-pdfaProperty:category מוגדר ל-external. תגיות ההמחשה להלן מראות את הצורה של הבלוק הזה
<rdf:Description rdf:about=""
xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#">
<pdfaExtension:schemas>
<rdf:Bag>
<rdf:li rdf:parseType="Resource">
<pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
<pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
<pdfaSchema:prefix>fx</pdfaSchema:prefix>
<pdfaSchema:property>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>DocumentFileName</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
</rdf:li>
<!-- DocumentType, Version, ConformanceLevel declared the same way -->
</rdf:Seq>
</pdfaSchema:property>
</rdf:li>
</rdf:Bag>
</pdfaExtension:schemas>
</rdf:Description>
ה-URI של מרחב-השמות והקידומת אינם מחרוזות קבועות. הם עוקבים אחר הפרופיל. מסמך Factur-X משתמש ב-urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# עם הקידומת fx, בעוד שקובץ ZUGFeRD 2.0 שנבחר דרך zugferd-invoice.xml מוביל ל-URI שונה תחת שם סכמה משלו. סכמת ההרחבה חייבת להצהיר על אותו URI של מרחב-שמות שבלוק המאפיינים משתמש בו בפועל, אחרת המאמת עדיין לא יוכל לקשר בין השניים. PDFlibPas גוזר (derives) את שני הערכים מתוך שם הקובץ והגרסה שאתה מעביר לו, כך שההצהרה ובלוק המאפיינים תמיד מסכימים זה עם זה
כיצד העוזר (helper) כותב את שני החצאים יחד
ב-PDFlibPas אינכם מרכיבים את ה-XML הזה באופן ידני. אתם שמים את המסמך במצב PDF/A-3 וקוראים למתודה (method) אחת. הדבר הראשון שיש להסדיר הוא דגל התאימות, מכיוון ש-Factur-X דורש PDF/A-3. קריאה ל-SetPDFAMode(7) בוחרת את הרמה PDF/A-3u, מה שמגדיר את pdfaid:part ל-3 ואת pdfaid:conformance ל-U בסכמת הזיהוי. מנת ה-XMP כעת נושאת את החלק והתאימות הנכונים לפני שמטא-נתונים כלשהם של החשבונית מתווספים
var
FileID: Integer;
begin
PDF.SetPDFAMode(7); // PDF/A-3u: pdfaid:part=3, conformance=U
PDF.NewDocument;
// draw the human-readable invoice page here
FileID := PDF.AddFacturXAssociatedFileFromString(
InvoiceXML, // raw UTF-8 XML bytes
'EN16931', // ConformanceLevel
'factur-x.xml', // embedded file name
'Factur-X invoice XML', // /Desc text
'Alternative', // /AFRelationship
'1.0', // profile version
''); // optional country code
if FileID = 0 then
Exit; // not PDF/A-3, or XML/profile mismatch
PDF.SaveToFile('factur-x.pdf');
end;
קריאה יחידה ל-AddFacturXAssociatedFileFromString מבצעת את העבודה שהייתה חסרה בקובץ שנכשל. היא מטביעה את ה-XML כקובץ מקושר (associated file) של PDF/A-3 עם מערכת היחסים (relationship) שנתתם לה, והיא רושמת את ארבעת מאפייני fx יחד עם שם הסכמה, ה-URI של מרחב-השמות והקידומת עבור הפרופיל הנבחר. כאשר המסמך נשמר, שלב פנימי בשם ApplyFacturXMetadata מזריק הן את בלוק המאפיינים והן את הצהרת pdfaExtension:schemas התואמת לתוך מנת ה-XMP, כך שהמאפיינים המותאמים אישית מגיעים כשהם כבר מתוארים. המתודה מחזירה 0 אם המסמך אינו במצב PDF/A-3 או אם ה-XML אינו תואם לפרופיל המוצהר, שזהו אותו השומר שעוצר חשבונית פגומה מלהגיע אל הקובץ מלכתחילה
השטח המת שבדיקת המעטפת אינה יכולה לראות
זהו החלק שיש לציין באופן מפורש, מכיוון שזוהי הסיבה שהבאג מסתתר. ValidateFacturXInvoice בודקת את המעטפת (container). היא מאשרת שלקטלוג יש רשומת /AF, שעץ השמות EmbeddedFiles נוכח, ש-XML של החשבונית קיים, ששם הקובץ המוטבע תואם לפרופיל, שמזהה ההנחיה (guideline ID) ב-XML מסכים עם רמת התאימות, ושה-/AFRelationship הוא אחד מאלו ש-PDF/A-3 מאפשר. אלו הן בדיקות אמיתיות והן תופסות פגמים אמיתיים. GetFacturXValidationIssues מדווח עליהן בשם, בעזרת מזהים כגון MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship, ו-InvalidFileNameProfile
מה שהיא לא בודקת הוא האם סכמת ההרחבה של XMP נוכחת ותקינה. קובץ שהמעטפת שלו ללא דופי אך מאפייני ה-fx שלו אינם מוצהרים, עובר כל בדיקת בעיות (issue check) ומחזיר 1, מכיוון ששום דבר ברשימה הזו אינו בודק את הבלוק של pdfaExtension:schemas. זו בדיוק הסיבה שחשבונית שנבנתה ידנית, או כזו שהופקה על ידי צינור עיבוד (pipeline) שכתב את בלוק המאפיינים ללא ההצהרה, יכולה להפליג בקלות דרך המאמת המובנה ועדיין להיכשל ב-veraPDF על סעיף 6.6.2.3.1. מאמת המעטפת ומאמת מטא-נתוני ה-PDF/A עונים על שאלות שונות, ורק בודק PDF/A מלא עונה על השאלה השנייה
קריאת בעיות כך שתדע איזו שכבה נשברה
מכיוון ששתי השכבות נכשלות באופן בלתי תלוי, הרגל האבחון הנכון הוא לקרוא קודם את בעיות המעטפת (container issues) ולהתייחס לתוצאה נקייה כהצהרה לגבי המעטפת בלבד, לעולם לא לגבי מטא-נתוני ה-PDF/A. הריצו את האימות המובנה, אספו את רשימת הבעיות, ופעלו על פיה לפני שאתם שולחים יד לכלי חיצוני
var
Issues: WideString;
begin
if PDF.ValidateFacturXInvoice = 0 then
begin
Issues := PDF.GetFacturXValidationIssues('|');
// container-level identifiers, for example:
// MissingCatalogAF, NotPDFA3, MissingEmbeddedFilesNameTree,
// ConformanceGuidelineMismatch, InvalidAFRelationship
WriteLn('Container issues: ', Issues);
end
else
WriteLn('Container OK; verify XMP extension schema with a PDF/A checker.');
end;
כאשר הקריאה ההיא מחזירה שם של בעיה, התקלה היא במעטפת וההודעה אומרת לכם באיזה חלק. כאשר היא מחזירה תוצאה נקייה ו-veraPDF עדיין דוחה את הקובץ, התקלה היא כמעט תמיד בסכמת ההרחבה של XMP, והתיקון הוא לתת ל-AddFacturXAssociatedFileFromString לכתוב את המטא-נתונים במקום להרכיב את בלוק המאפיינים בעצמכם. שמירה על שתי השאלות הללו נפרדות בראשכם היא מה שהופך דחייה מבלבלת (baffling) לאבחון של שורה-אחת: בעיות מעטפת צצות אל פני השטח דרך רשימת הבעיות, בעיות בהצהרת-סכמה צצות רק דרך מאמת PDF/A, והבלבול בין השתיים הוא מה שמאפשר לבאג להסתתר
התמונה הרחבה יותר של תאימות PDF/A ו-PDF/UA, כולל כיצד להריץ מעבר (pass) של preflight לפני שקובץ עוזב את ה-build שלכם, מכוסה במדריך בדיקות preflight של PDF/A ו-PDF/UA. אם החשבונית שלכם חייבת גם להיות נגישה, עץ המבנה שעליו נשענים PDF/A-3a ו-PDF מתויג הוא נושא מאמר הנגישות ל-PDF מתויג. הטיפול בסכמת ההרחבה שמתואר כאן מסופק כחלק מPDFlibPas Delphi PDF Library לצד התמיכה בפרופילים של Factur-X, ZUGFeRD, ו-XRechnung המתועדים ברחבי הבלוג הזה