דוח של veraPDF שאומר שרוחב גליף חורג מהמדידה בתוכנית הגופן המוטמעת כמעט ולא אומר לכם איזה גליף, ובוודאי לא למה. PDFlibPas עונה על השאלה הזאת כשהוא מפענח כל קוד תו דרך ה־cmap המוטמע לאינדקס גליף, מנרמל את המדידה מהתוכנית ל־1000 יחידות ל־em ורק אז משווה
למה רוחבי גליפים לא תואמים?
כי שני המספרים שמשווים ביניהם חיים במערכות קואורדינטות שונות, ושום דבר במילון ה־PDF לא מספר לכם איך ממירים ביניהן. מילון גופן כותב את /Widths במרחב הגליפים, שה־PDF קובע לו אלףית אחת של ה־em (ISO 32000-1 §9.2.4). טבלת ה־hmtx בתוך תוכנית ה־TrueType המוטמעת כותבת התקדמויות ביחידות תכנון של הגופן, וטבלת ה־head מחליטה כמה מהן מרכיבות em אחד: 2048 אצל רוב גופני ה־TrueType, 1000 אצל כאלה הנגזרים מ־CFF, ולפעמים משהו שונה לגמרי. משווים את הערכים הגולמיים וכל גופן עם 2048 יחידות ל־em בקורפוס שלכם נראה שבור. זו המלכודת ש־ISO 14289-1 §7.21.5 טומנת לכל מי שמנסה לבקר רוחבים על ידי קריאת שדות המילון
PDFlibPas מנרמל בזמן הטעינה. TPDFTrueTypeParser שומר Advance * 1000 div unitsPerEm במערך הרוחבים שלו, כך ש־Parser.GetWidth(GID) כבר עונה באותן אלפיות של em שה־PDF משתמש בהן, ו־GetRawWidth נשאר זמין כשצריך לחזור ליחידות תכנון. נשאר החלק הקשה יותר: המעבר מקוד תו לאינדקס גליף. בגופן TrueType פשוט המסלול תלוי בדגל Symbolic ב־FontDescriptor, ביט 3 של /Flags
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// גופנים סימבוליים נפנים ישירות דרך ה־cmap של התוכנית,
// עם קונבנציית הבייט הגבוה (3,0) כגיבוי
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// לא סימבולי: קוד -> שם גליף דרך הקידוד, שם -> Unicode
// דרך Adobe Glyph List, Unicode -> GID דרך ה־cmap של התוכנית
UnicodeValue := GetGlyphUnicode(EncodingNames[Code and $FF]);
if UnicodeValue = 0 then
Continue;
GID := Parser.GetGlyphIndex(UnicodeValue);
end;
if (GID > 0) and (GID < Parser.GlyphCount) then
if Abs(PDFWidth - Parser.GetWidth(GID)) > 1 then
Inc(MismatchCount);
finally
Parser.Free;
end;
שני פרטים בקטע הזה נושאים משקל. סף הסובלנות הוא יחידה אחת ולא אפס, כי הנרמול הוא חלוקת מספרים שלמים וקובץ שנוצר כחוק יכול לגלוש ביחידה אחת; זו בדיוק הניסוח "בתוך אלףית אחת של em" שהאבחנה 10036 מדווחת. והשומר GID < Parser.GlyphCount אינו קישוט. את GetWidth כתבו סלחני כלפי קוראי הרינדור, עם לחיצת אינדקס מחוץ לטווח אל הערך האחרון ב־hmtx ונפילה ל־750 כשהטבלה חסרה. סלחנות נכונה לרינדור ושגויה לביקורת, ולכן הביקורת דוחה את האינדקס לפני שהיא מבקשת רוחב במקום לסמוך על הלחיצה
CIDFontType2 מוסיף עוד שכבת עקיפה אחת
PDFlibPas הולך בגופנים מורכבים באותה דרך, כש־/CIDToGIDMap משולב בין ה־CID לגליף. הרוחבים מגיעים במערך /W, שלו ISO 32000-1 §9.7.4.3 מגדיר שתי צורות המתחלפות בחופשיות בתוך מערך אחד: CID פותח ואחריו מערך של רוחבים עוקבים, או CID ראשון, CID אחרון ורוחב יחיד החל על כל הטווח. הביקורת מפענחת את שתיהן, מעבירה כל זוג שנוצר לאותה השוואה ומדווחת על הסכום תחת האבחנה 10037. שלב המיפוי הוא המקום שבו גופנים מורכבים נבדלים, וזו הסיבה שהאבחנה 10021 על מיפוי חסר חשובה לפני שקוראים רוחב כלשהו בכלל — /CIDToGIDMap נעדר או פגום לא רק מפר את §7.21.3.2, הוא הופך את שאלת הרוחב לבלתי ניתנת למענה
// /CIDToGIDMap הוא השם /Identity או זרם של אינדקסי גליף
// בני 16 סיביות בסדר big-endian, אחד לכל CID (ISO 32000-1 section 9.7.4.2)
Obj := DerefIndRef(FDoc, CIDFont.FindValueByKeyName('CIDToGIDMap'));
if (Obj is TPDFName) and (TPDFName(Obj).Name = 'Identity') then
begin
GID := CID;
Result := True;
end
else if Obj is TPDFStream then
begin
Data := TPDFStream(Obj).GetDecodedStream;
P := CID * 2 + 1; // מחרוזות פסקל ממוספרות מ־1
if (P >= 1) and (P + 1 <= Length(Data)) then
begin
GID := (Integer(Byte(Data[P])) shl 8) or Integer(Byte(Data[P + 1]));
Result := True;
end;
end;
מה עושה מבקר כשתוכנית הגופן לא מצליחה להתפענח?
לא לומר דבר. בדיקות השלמות של /CharSet ושל /CIDSet ש־ISO 14289-1 §7.21.4.2 דורש — האבחנות 10038 ו־10039 — הן המקום שבו מאמת קנאי מדי הופך לנטל, כי דיווח על "ה־CharSet שלך חסר" בלתי נבדל, בעיני קורא הדוח, מ"מפענח ה־Type 1 שלנו נתן יד". לכן PDFlibPas מדווח על ערך חסר רק כששלושה דברים הצליחו כולם: תוכנית הגופן התפענחה, מיפוי קוד־לגליף הסתיים, והקבוצה עצמה התפענחה. TPDFType1Decoder.LoadPFBFromString חייב להחזיר True ולהניב מספר charstring לפני ששם גליף כלשהו נבדק מול מחרוזת ה־/CharSet; המסלול של /CIDSet דורש שהזרם יתנפח ושמספר הגליפים יחזור חיובי לפני שסיבית אחת נבדקת. כל חריגה בדרך קורסת ל"אין ממצא", ולא לפגם
זו הטיה מכוונת לעבר שלילות שקר, ושווה לומר זאת בגלוי במקום לקבור אותה. טבלת CFF פגומה, וריאנט Type 1 שאינו נתמך, או /CIDSet קצר מטווח הגליפים — כולם מניבים שתיקה במקום אבחנה. ההיגיון הוא שביקורות PDF/UA מועברות למחברים שלא בנו את כלי העבודה, והאשמת שקר עולה יותר מהחמצה: המחבר שורף יום בהוכחה שקובץ תואם הוא תואם, ומפסיק לסמוך על הדוח כולו. ה־Matterhorn Protocol עושה את אותה הבחנה בצורה אחרת כשהוא מפריד בין בדיקות שמכונה יכולה להכריע לבדיקות שחייב בהן אדם, ונקודת הביקורת Fonts (31) שלו היא המקום שבו אלה יושבות. אם דרושה קריאה מחמירה יותר, מריצים את PDFlibPas כשער מהיר ומאמת ייעודי כדעה שנייה — אותו צמד מתואר במדריך ה־preflight ל־PDF/A ול־PDF/UA
ה־/Contents של עמוד הוא רשימה, לא זרם
הטעות היקרה ביותר בביקורת זרמי תוכן היא יחס ל־/Contents כאל זרם אחד. ISO 32000-1 §7.7.3.3 מאפשר לעמוד להחזיק מערך של זרמים שהשרשור שלהם, עם רווח לבן בין החלקים, הוא תוכנית העמוד; מייצרים חותכים בנקודות שרירותיות, ו־BT יכול לשבת באיבר אחד וה־ET התואם לו בזה שאחריו. מעבד תוכן שומר מצב — עומק הקינון של marked-content, הגופן שנבחר ב־Tf האחרון, דגל אובייקט הטקסט — וה־Process מאפס את המצב בכניסה. קוראים לו פעם לכל איבר במערך וכל זרם אחרי הראשון מתחיל בלי גופן נוכחי, כך שטקסט שתויג למופת נקרא כרעש לא מתויג וחסר גופן. PDFlibPas משרשר קודם ומעבד פעם אחת
function ContentObjectData(FDoc: TSmartPDFDocument; Obj: TPDFObject): AnsiString;
var
I: Integer;
begin
Result := '';
Obj := DerefIndRef(FDoc, Obj);
if Obj is TPDFStream then
Result := TPDFStream(Obj).GetDecodedStream
else if Obj is TPDFArray then
for I := 0 to TPDFArray(Obj).Count - 1 do
Result := Result + ContentObjectData(FDoc, TPDFArray(Obj).Item[I]) + #10;
end;
// קריאת Process אחת על כל השרשור, ולא קריאה לכל איבר
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));
אילו Form XObjects באמת נחשבים חסרי מבנה?
רק אלה שהעמוד באמת מפעיל, מאתר קריאה מחוץ ל־marked content, שהתוכן שלהם עצמו מציג טקסט. האבחנה 10040 אוכפת את ISO 14289-1 §7.20 כשהיא רושמת שלוש עובדות בלתי תלויות לכל מספר אובייקט — יש טקסט, הופעל, הופעל בתוך marked content — ומדווחת רק על החיתוך של שתי הראשונות בניכוי השלישית. כל אחד משני הקיצורים טועה באופן שהייתם משחררים: סימון כל Form נושא טקסט ב־/Resources מעניש ספריית תבניות שאף אחד לא שואב ממנה, וסימון כל Form מופעל מעניש לוגואים וקטוריים שאינם נושאים טקסט ואינם זקוקים לתיוג. אתר ההפעלה מפוענח לפי מספר אובייקט ולא לפי שם משאב, כי את אותו Form מגיעים בדרך כלל דרך שמות שונים בעמודים שונים. האבחנה הנלווית 10041 הולכת לאורך אותה תוכנית משורשרת עבור §7.21.8, מפענחת כל אופרנד מציג טקסט דרך הגופן שבתוקף וסופרת את הקודים שנוחתים על .notdef, האסור בלי קשר למצב עיבוד הטקסט — כולל המצב הבלתי נראה שבשימוש מאחורי תמונות סרוקות. איך עוטפים את ה־Forms ששרדו זו שאלה של עץ המבנה, והיא נסקרת במאמר על בניית מבנה PDF מתויג
גופנים בלי FontDescriptor בכלל
גופן שאינו מוטמע הוא קלט לגיטימי לביקורת הזאת, ולא מצב שגיאה, וכל פונקציית עזר מתחת לבדיקת ההטמעה חייבת לשרוד אותו. כש־PDFlibPas לא מוצא /FontDescriptor, או מוצא מתאר בלי FontFile, FontFile2 או FontFile3, הוא רושם את האבחנה 10020 — או 10022 כשהשם הוא אחד מה־Standard 14, ש־§7.21.4 NOTE 5 מסרב בפירוש לתת להם פטור — וממשיך בשאר הקובץ. זו כל תמצית הדוח: מחבר רוצה כל ממצא במעבר אחד, ולא ממצא אחד לכל הרצה. לכן ההפניה למתאר שנמסרת לפונקציות העזר של הרוחב, ה־cmap, ה־CharSet וה־CIDSet יכולה להיות Nil, וכל אחת מהן בודקת זאת בכניסה במקום להניח שבדיקה קודמת עצרה את הביקורת. אם התיקון הוא הטמעת מה שחסר, המכניקה נמצאת בהערה על הטמעת גופנים חסרים ב־PDF קיים
הרצת הביקורת
קריאה אחת, על קובץ שלא בהכרח יצרתם. TPDFlib.CheckFileCompliance מקבל בורר בדיקת תאימות — 2 עבור PDF/UA-1 תחת ISO 14289-1:2014 — ומחזיר אפס או מזהה של רשימת מחרוזות שערכיה הם קוד מספרי, נקודתיים והודעה קריאה. הממצאים של גופנים וזרמי תוכן שנדונו כאן תופסים את 10020 עד 10041 בטווח הזה, מופרדים מספרית מקודי ה־00xxx של PDF/A כדי שיומן מעורב יישאר קריא. העברת 1 ב־Options קוטעת בממצא הראשון, מה שרוצים בשער בנייה ולא בכלי עריכה. עבור מסמך שעדיין פתוח בזיכרון, GetPDFUADiagnostics מריץ את הבדיקה המקבילה בלי הסתובבות דרך הדיסק
var
Issues, Count, I: Integer;
begin
// ComplianceTest = 2 בוחר PDF/UA-1; Options = 0 מדווח על כל ממצא
Issues := PDF.CheckFileCompliance('delivery.pdf', '', 2, 0);
if Issues = 0 then
WriteLn('delivery.pdf: PDF/UA-1 conformant')
else
begin
Count := PDF.GetStringListCount(Issues);
for I := 1 to Count do
WriteLn(' ', PDF.GetStringListItem(Issues, I)); // למשל 10037 CIDFontType2 ...
end;
end;
אף אחד מאלה לא צריך בינארי של מאמת חיצוני במכונה, וזה ההבדל בין בדיקה שרצה בכל בנייה לבדיקה שרצה כשמישהו נזכר. ממשקי התאימות והאבחון שתוארו כאן מגיעים ב־PDFlibPas Delphi PDF Library הרגילה, שבעמוד המוצר שלה נמצאת טבלת קודי האבחון המלאה ל־PDF/UA-1 לצד חבילות הבדיקה של PDF/A, PDF/X ו־PDF/E