אתה כותב מאמת קטן. הוא פותח PDF, מחפש את הסוף, מוצא את startxref, קורא את ההיסט, ומצפה לנחות על מילת המפתח xref עם טבלת הפניות צולבות קבועה מתחתיה. מהטבלה הזו הוא אוסף היסטים של אובייקטים, ואז סורק לאחור עבור מילת המפתח trailer כדי ללמוד את ה-/Root וה-/Size. זה עובד בצורה מושלמת על כל קובץ שייצרת כדי לבדוק אותו. אז מגיע קובץ שהופק על ידי גרסה נוכחית של Word, או על ידי ספרייה המכוונת ל-PDF 1.5, והמאמת מצהיר שהוא שבור. אין מילת מפתח xref במקום שבו ההיסט מצביע, אין מילון trailer בשום מקום, וטבלת האובייקטים שהמאמת בנה ריקה כמעט. הקובץ תקין. המאמת קורא אותו דרך עדשה בת חמש עשרה שנים
זו הסיבה הנפוצה ביותר לכך שבדיקת PDF ברמת בית שנכתבה נגד הפריסה הקלאסית נכשלת במסמכים מודרניים. המבנה שהיא תלויה בו, טבלת ההפניות הצולבות בטקסט פשוט ומילת המפתח trailer, הפכו לאופציונליים ב-PDF 1.5 ולעיתים קרובות אינם נוכחים. שתי תכונות החליפו אותם: זרם ההפניות הצולבות (cross-reference stream) וזרם האובייקטים הדחוסים. שניהם מתוארים ב-ISO 32000-1, ומאמת שאינו יודע עליהם רואה קובץ בריא כערימה של אובייקטים חסרים
מה ש-PDF 1.5 שינה לגבי קצה הקובץ
תקן ISO 32000-1 §7.5.8 מגדיר את זרם ההפניות הצולבות, וסעיף 7.5.7 מגדיר את זרם האובייקטים מטיפוס /ObjStm. יחד הם מאפשרים לכותב להשמיט את שני המבנים שמאקנח קלאסי מתבסס עליהם. קובץ PDF 1.5 עשוי להסתיים ללא טבלת xref כלל. במקומה, האובייקט ש-startxref מצביע עליו הוא אובייקט זרם רגיל שהמילון שלו נושא את /Type /XRef, והזרם הזה מחזיק את נתוני ההפניה הצולבת בצורה בינארית קומפקטית. אין גם מילת מפתח trailer, מכיוון שהטריילר הוא כעת המילון של הזרם עצמו. המפתחות שמאקנח קלאסי חיפש, /Root, /Size ו-/ID, חיים בתוך המילון הזה
השינוי השני מזיז את האובייקטים עצמם. במקום לכתוב כל אובייקט עקיף בהיסט בתים משלו בקובץ, כותב יכול לארוז אובייקטים קטנים רבים, מילוני העמודים, מילוני האנוטציות, עץ המבנה, לתוך זרם אובייקטים יחיד ולדחוס את המכל כולו עם Flate. לאובייקטים הבודדים אין עוד היסט בתים בקובץ. יש להם מיקום בתוך בועה דחוסה. מאמת הסורק את הבתים הגולמיים עבור 1 0 obj לעולם אינו מוצא אותם, מכיוון שהטקסט הזה קיים רק לאחר ניפוח (inflation). עבור מאקנח קלאסי, חצי מהמסמך פשוט נעלם
מפתחות הטריילר הם טקסט פשוט, אפילו בקובץ דחוס
החלק המרגיע הוא שקריאת הטריילר של זרם הפניות צולבות אינה דורשת ניפוח של דבר. אובייקט זרם נכתב כמילון ולאחריו מילת המפתח stream ואז הבתים הדחוסים. המילון הוא טקסט פשוט. כך שכאשר startxref מצביע על זרם הפניות צולבות, הבתים מיד לאחר מספר האובייקט נראים כמילון רגיל, ו-/Root, /Size ו-/ID יושבים שם בברור, לפני שמילת המפתח stream ונתוני ה-Flate מתחילים
פירוש הדבר שמאמת יכול ללמוד את שלוש העובדות שהוא הכי זקוק להן, היכן הקטלוג, כמה אובייקטים הקובץ טוען, ומזהה הקובץ, על ידי פענוח מילון הזרם בלבד. הוא אינו חייב לבצע דה-קומפרסיה לנתוני ההפניות הצולבות, והוא אינו חייב לפרש את הערכים הבינאריים בתוכו. העבודה שמכשילה מאקנח תמים אינה קריאת הטריילר; היא מציאת האובייקטים. אלו שתי בעיות נפרדות, ופתרון הראשונה הוא זול
זרמי אובייקטים: כותרת, ואז בועת Flate
זרם אובייקטים הוא מכל. המילון שלו נושא את /Type /ObjStm, ערך /N המציין את מספר האובייקטים הארוזים בפנים, וערך /First המציין את היסט הבתים, בתוך הנתונים המנופחים, שבו גוף האובייקט הראשון מתחיל. המטען הדחוס, לאחר שניפח אותו, מתחיל בכותרת קטנה של /N זוגות של מספרים שלמים. כל זוג הוא מספר אובייקט וההיסט של גוף האובייקט ההוא ביחס ל-/First. לאחר הכותרת מגיעים גופי האובייקטים עצמם, משורשרים
הרחבה של אחד כזה היא מכנית ברגע שהבתים מנופחים. אתה קורא את המילון כדי לקבל את /N ו-/First, מנפח את הזרם עם מפענח Flate, עובר על /N הזוגות המובילים כדי ללמוד איזה מספר אובייקט חי באיזה היסט, ואז שולף כל גוף כאילו היה אובייקט עקיף רגיל. התלות האמיתית היחידה היא במפענח Flate, וכבר יש לך אחד: Delphi שולח את System.ZLib, ו-Free Pascal שולח את יחידת zstream, שניהם עוטפים את zlib ומנפחים זרם Flate גולמי ללא קוד של צד שלישי. שגרה המצרפת כל אובייקט שחולץ לטבלת האובייקטים של המאמת גורמת לשאר המאמת, החלק שעובר על /Root ובודק את עץ העמודים, להתנהג בדיוק כפי שהיה מתנהג בקובץ קלאסי
מה שאינך חייב לממש
אינך חייב גם לטפל בפונקציות החיזוי (predictor functions) של PNG ו-TIFF שזרם הפניות צולבות עשוי להחיל דרך /DecodeParms שלו רק כדי לקבל את מפתחות הטריילר. חוזים מסננים את שורות ההפניות הצולבות הבינאריות כדי לגרום להן להידחס טוב יותר; אין להם שום קשר למילון שקודם לזרם. השדרוג המינימלי שהופך מאקנח קלאסי למודע ל-PDF מודרני הוא לכן קטן: כאשר startxref נוחת על זרם ולא על מילת המפתח xref, פענח את מילון הזרם עבור מפתחות הטריילר, והרחב כל אובייקט /ObjStm שאתה נתקל בו כדי שתוכנו ייכנס לטבלת האובייקטים. פענוח ערכי טיפוס 2 וחוזים הוא משימה גדולה יותר שתוכל לדחות עד שתצטרך באמת פתרון אובייקטים אקראי
מדוע בדיקת תאימות חייבת להרחיב זרמים תחילה
זה מפסיק להיות אקדמי ברגע שאתה מריץ בדיקת פרופיל. מאמת PDF/A או PDF/X בודק אובייקטים ספציפיים: קטלוג המסמך עבור מערך /OutputIntents, זרם /Metadata עבור חבילת XMP עם המזהה הנכון, כל תיאור גופן עבור קובץ גופן מוטמע, הטריילר עבור /ID. בקובץ דחוס, רוב האובייקטים הללו נמצאים בתוך זרמי אובייקטים. מאמת שלא הרחיב את זרמי האובייקטים אינו יכול לראות את מפתחות הקטלוג, אינו יכול למצוא את המטא-נתונים, ואינו יכול לפרט את הגופנים. הוא ידווח על מסמך תואם לחלוטין כחסר את כוונת הפלט שלו, חסר את ה-XMP שלו, וחסר חצי מהמבנה שלו, מכיוון שהראיות שהוא זקוק להן עדיין יושבות בבועת Flate שהוא מעולם לא ניפח
הסדר חשוב. ההרחבה חייבת להתרחש לפני שהבדיקות רצות, ולא לצידן, מכיוון שכל בדיקה מניחה שהיא יכולה להגיע לאובייקט לפי מספר. אם תחבר בדיקת פרופיל ישירות על סריקת בתים גולמית, היא תירש את העיוורון של המאקנח הקלאסי ותייצר הפרות שווא בדיוק על הקבצים המודרניים שהם בעלי הסיכוי הגבוה ביותר להיות בנויים היטב, מכיוון שהם הגיעו משרשראות כלים חדשות מספיק כדי לכתוב זרמי הפניות צולבות מלכתחילה
לאפשר ל-PDFium לבצע את הפענוח עבורך
הרכיב PDFium Component מפענח זרמי הפניות צולבות וזרמי אובייקטים כחלק מטעינת מסמך, וזו הדרך המעשית להימנע מפיתוח עצמי של שלב הניפוח וההרחבה. כאשר אתה טוען קובץ עם רכיב TPdf, האובייקטים הארוזים בתוך מכלים של /ObjStm כבר נפתרים, ונקודות הכניסה של האימות רואות את המסמך המורחב במלואו. ValidatePdfA מחזיר רשומת TPdfAValidationResult שהשדה Conformance שלה הוא ערך TPdfAConformance כגון pac1b או pacNone, שהשדה Issues שלה הוא קבוצה של הבעיות הספציפיות שנמצאו, ושהמתודה IsCompliant שלה היא true רק כאשר רמת תאימות זוהתה וקבוצת הבעיות ריקה. מכיוון שהאובייקטים הורחבו במהלך הטעינה, מערך /OutputIntents או גופן מוטמע שחיו בתוך זרם אובייקטים יימצאו, ולא ידווחו כחסרים
uses
PDFium, FPdfPdfa;
function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // parses xref/object streams on load
Result := Pdf.ValidatePdfA; // sees the expanded object table
finally
Pdf.Free;
end;
end;
הדבר נכון גם לגבי ValidatePdfX, המחזירה TPdfXValidationResult בעלת אותה צורה. הנקודה של ניתוב דרך PDFium היא שדה-קומפרסיה המבנית המתוארת לעיל מתרחשת פעם אחת, בצורה נכונה, בתוך הטוען, כך שקוד האימות שלך לעולם אינו רואה את ההבדל בין קובץ קלאסי לקובץ דחוס לחלוטין. שניהם מגיעים למאמת כקבוצה פתורה של אובייקטים
var
Pdf: TPdf;
R : TPdfXValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Press_Ready.pdf';
Pdf.Active := True;
R := Pdf.ValidatePdfX;
if R.IsCompliant then
Writeln('PDF/X conformance: ', Ord(R.Conformance))
else
Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
finally
Pdf.Free;
end;
end;
אם הבתים כבר נמצאים בזיכרון ולא בדיסק, אותו רצף טעינה ולאחריו אימות עובד דרך עומס היתר LoadDocument(const Data: TBytes), המקבל את תוכן הקובץ הגולמי ומפענח את זרמי ההפניות הצולבות והאובייקטים שלו באותו אופן שנתיב הקובץ עושה. הלקח עבור מאמת שנכתב ביד הוא הלקח המבני, לא ה-API: קרא את מפתחות הטריילר ממילון הזרם בטקסט פשוט, הרחב כל אובייקט /ObjStm עם מפענח Flate לפני שאתה עובר על המסמך, והתייחס לפענוח ערכי ההפניות הצולבות הבינאריות כאל העבודה הגדולה והאופציונלית שהיא
ברגע שהמבנה מורחב, מאמת יכול להניע את שאר תזרים העבודה מעליו. עבור מערכת קו פקודה לבדיקת תאימות המדווחת על תאימות על פני תיקיית קלטים, ראה את המדריך שלנו על בניית דוח בדיקת תאימות באצוות בקו פקודה. כאשר אימות הוא שער לפני פירוק מסמך גדול לגורמים, הטכניקות במדריך שלנו לפיצול מסמכי PDF למספר קבצים מצטרפות באופן טבעי לתבנית הטעינה והבדיקה המוצגת כאן. שניהם נבנים על משטח הטעינה והאימות של PDFium Component עבור Delphi ו-C++Builder