รายงาน veraPDF ที่บอกว่าความกว้างกลิฟไม่ตรงกับโปรแกรมฟอนต์ฝังตัวไม่ได้บอกอะไรคุณเลยว่ากลิฟไหน หรือเพราะอะไร PDFlibPas ตอบคำถามนั้นด้วยการ resolve รหัสอักขระแต่ละตัวผ่าน cmap ฝังตัวไปเป็นกลิฟ index ปรับเมตริกของโปรแกรมให้เป็น 1000 หน่วยต่อ em แล้วจึงเทียบกัน
ทำไมความกว้างกลิฟจึงไม่ตรงกัน
เพราะตัวเลขสองชุดที่ถูกเทียบอยู่คนละระบบพิกัด และไม่มีอะไรใน dictionary ของ PDF บอกวิธีแปลงให้ font dictionary เขียน /Widths ใน glyph space ซึ่ง PDF ตรึงไว้ที่หนึ่งในพันของ em (ISO 32000-1 §9.2.4) ตาราง hmtx ภายในโปรแกรม TrueType ฝังตัวเขียน advance เป็นหน่วยออกแบบฟอนต์ และตาราง head ตัดสินว่ากี่หน่วยต่อหนึ่ง em: 2048 สำหรับฟอนต์ TrueType ส่วนใหญ่ 1000 สำหรับฟอนต์ที่ derive จาก CFF บางครั้งก็ค่าอื่นทั้งหมด เทียบค่าดิบกันเมื่อไร ฟอนต์ 2048-upem ทุกตัวในคลังของคุณจะดูพังหมด นั่นคือกับดักที่ ISO 14289-1 §7.21.5 วางไว้ให้ใครก็ตามที่พยายามตรวจความกว้างโดยอ่านฟิลด์ dictionary
PDFlibPas ปรับให้เป็นมาตรฐานตอนโหลด TPDFTrueTypeParser เก็บ Advance * 1000 div unitsPerEm ในอาร์เรย์ความกว้าง Parser.GetWidth(GID) จึงตอบเป็นหน่วยพันส่วน em แบบเดียวกับที่ PDF ใช้แล้ว ส่วน GetRawWidth ยังพร้อมให้ใช้เมื่อคุณต้องการหน่วยออกแบบคืน สิ่งที่เหลือคือครึ่งที่ยากกว่า: การจากรหัสอักขระไปเป็นกลิฟ index สำหรับฟอนต์ TrueType แบบง่าย เส้นทางขึ้นกับ flag Symbolic ใน FontDescriptor บิตที่ 3 ของ /Flags
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// ฟอนต์ symbolic ถูกเรียกผ่าน cmap ของโปรแกรมโดยตรง
// โดยใช้ convention high-byte (3,0) เป็นทางถอย
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// ไม่ symbolic: รหัส -> ชื่อกลิฟผ่าน encoding, ชื่อ -> 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" ที่ diagnostic 10036 รายงาน และเงื่อนไข GID < Parser.GlyphCount ไม่ใช่ของประดับ GetWidth ถูกเขียนให้ใจกว้างเพื่อผู้เรียกด้านการแสดงผล หนีบ index ที่เลยช่วงไปที่ entry สุดท้ายของ hmtx และถอยไป 750 เมื่อตารางไม่อยู่ การใจกว้างถูกต้องสำหรับการเรนเดอร์และผิดสำหรับการตรวจสอบ ตัวตรวจจึงปฏิเสธ index ก่อนขอความกว้าง แทนที่จะไว้ใจการหนีบ
CIDFontType2 เพิ่มขั้นเลี้ยวหนึ่งขั้น
PDFlibPas เดินฟอนต์ composite ด้วยวิธีเดียวกัน โดยแทรก /CIDToGIDMap ระหว่าง CID กับกลิฟ ความกว้างมาเป็นอาร์เรย์ /W ซึ่ง ISO 32000-1 §9.7.4.3 ให้สองรูปทรงที่สลับกันได้อิสระในอาร์เรย์เดียว: CID เริ่มต้นตามด้วยอาร์เรย์ความกว้างต่อเนื่อง หรือ CID แรก, CID สุดท้าย และความกว้างเดียวใช้ทั่วช่วง ตัวตรวจ parse ทั้งสองรูป แล้วส่งทุกคู่ที่ได้ไปยังการเทียบเดียวกัน และรายงานผลรวมภายใต้ diagnostic 10037 ขั้น mapping คือจุดที่ฟอนต์ composite ต่างกัน และเป็นเหตุที่ diagnostic 10021 เรื่องแผนที่ขาดสำคัญก่อนคุณจะอ่านความกว้างใดทั้งสิ้น — /CIDToGIDMap ที่หายไปหรือเสียไม่ได้แค่ละเมิด §7.21.3.2 แต่ทำให้คำถามเรื่องความกว้างตอบไม่ได้เลย
// /CIDToGIDMap เป็นชื่อ /Identity หรือ stream ของกลิฟ index แบบ
// big-endian 16 บิต หนึ่งค่าต่อหนึ่ง CID (ISO 32000-1 หมวด 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; // สตริง Pascal นับจาก 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 บังคับ — diagnostics 10038 กับ 10039 — เป็นจุดที่ validator ที่เร่งรีบเกินกลายเป็นภาระ เพราะรายงานว่า "CharSet ของคุณไม่สมบูรณ์" กับ "decoder Type 1 ของเรายอมแพ้" แยกกันไม่ออกสำหรับคนอ่าน PDFlibPas จึงรายงาน entry ที่ขาดเมื่อสามสิ่งสำเร็จพร้อมกันเท่านั้น: โปรแกรมฟอนต์ถอดได้, mapping รหัสไปกลิฟ resolve ได้ และตัวเซ็ตเองถอดได้ TPDFType1Decoder.LoadPFBFromString ต้องคืน True พร้อมจำนวน charstring ก่อนชื่อกลิฟใดจะถูกเช็คกับสตริง /CharSet เส้นทาง /CIDSet ต้องมี stream ขยายได้และจำนวนกลิฟออกมาเป็นบวกก่อนบิตเดียวจะถูกทดสอบ ทุก exception ระหว่างทางยุบเป็น "ไม่มีข้อค้นพบ" ไม่ใช่เป็นข้อบกพร่อง
นั่นคือการเอนเอียงตั้งใจไปทาง false negatives และควรพูดตรง ๆ แทนการฝังไว้ ตาราง CFF เสีย, ตัวแปร Type 1 ที่ไม่รองรับ หรือ /CIDSet ที่สั้นกว่าช่วงกลิฟ ล้วนให้ความเงียบแทน diagnostic เหตุผลคือรายงาน PDF/UA ถูกส่งต่อไปยังผู้เขียนที่ไม่ได้สร้างเครื่องมือ และการกล่าวหาผิดแพงกว่าการพลาดหนึ่งข้อ ผู้เขียนเสียเวลาหนึ่งวันพิสูจน์ว่าไฟล์ที่ตรงตามกฎมันตรงตามกฎจริง แล้วก็เลิกเชื่อรายงานทั้งฉบับ Matterhorn Protocol แยกความต่างเดียวกันในรูปอื่น เมื่อแยกเช็คที่เครื่องตัดสินได้ออกจากเช็คที่คนต้องตัดสิน และ checkpoint Fonts (31) คือที่ที่ของพวกมันอยู่ ถ้าคุณต้องการการอ่านที่เข้มกว่า ให้รัน PDFlibPas เป็นประตูเร็ว แล้ว validator เฉพาะทางเป็นคำติชมที่สอง — คู่นี้คือคู่เดียวกับที่อธิบายใน บทเดินผ่าน preflight PDF/A และ PDF/UA
Page /Contents เป็นลิสต์ ไม่ใช่ stream
ความผิดพลาดที่แพงที่สุดในการตรวจ content stream คือการเอา /Contents เป็น stream เดียว ISO 32000-1 §7.7.3.3 ให้หน้าหนึ่งหน้าถืออาร์เรย์ของ stream ที่การต่อกัน พร้อมช่องว่างระหว่างส่วน คือโปรแกรมของหน้า ผู้ผลิตแบ่งได้ที่จุดใดก็ได้ และ BT อาจนั่งอยู่ในชิ้นหนึ่งพร้อม ET คู่มันอยู่ชิ้นถัดไป processor เนื้อหามี state — ความลึกการซ้อนของ marked-content, ฟอนต์ที่ Tf ล่าสุดเลือก, flag text-object — และ Process รีเซ็ต state นั้นตอนเข้า เรียกมันหนึ่งครั้งต่อหนึ่งสมาชิกอาร์เรย์เมื่อไร ทุก stream หลังชุดแรกจะเริ่มโดยไม่มีฟอนต์ปัจจุบัน ข้อความที่ถูกแท็กสวยงามจึงอ่านออกมาเป็นเสียงด่าง ๆ ไร้ฟอนต์ 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 XObject ตัวไหนนับเป็นไร้โครงสร้าง
เฉพาะตัวที่หน้าเรียกใช้จริง จาก call site ที่อยู่นอก marked content และเนื้อหาของมันเองมีข้อความ Diagnostic 10040 บังคับ ISO 14289-1 §7.20 โดยบันทึกสามข้อเท็จจริงอิสระต่อ object number — มีข้อความ, ถูกเรียกใช้, ถูกเรียกใช้ภายใน marked content — แล้วรายงานเฉพาะภาคตัดของสองข้อแรกตัดข้อที่สามออก ทางลัดทั้งสองผิดในแบบที่คุณจะส่งมอบไปกับรุ่น: ตั้งธงทุก Form ที่มีข้อความใน /Resources ลงโทษคลัง template ที่ไม่มีใครวาดเอาไว้ และการตั้งธงทุก Form ที่ถูกเรียกใช้ลงโทษโลโก้เวกเตอร์ที่ไม่มีข้อความและไม่ต้องการแท็กจุดเรียกใช้ถูก resolve ด้วย object number ไม่ใช่ชื่อรีซอร์ส เพราะ Form เดียวกันเดินทางผ่านชื่อต่างกันบนหน้าต่างกันเป็นปกติ diagnostic คู่หู 10041 เดินโปรแกรมต่อกันเดียวกันสำหรับ §7.21.8 resolve operand ที่แสดงข้อความแต่ละตัวผ่านฟอนต์ในขอบเขตแล้วนับรหัสที่ไปตก .notdef ซึ่งต้องห้ามเสมอไม่ว่าโหมด rendering ข้อความจะเป็นอย่างไร — รวมถึงโหมดล่องหนที่ใช้หลังภาพสแกน ส่วน Form ที่รอดพวกไหนควรถูกห่ออย่างไรเป็นคำถาม structure tree มีอยู่ในบทความเรื่อง การสร้างโครงสร้าง tagged PDF
ฟอนต์ที่ไม่มี FontDescriptor เลย
ฟอนต์ที่ไม่ฝังตัวเป็นอินพุตที่ถูกต้องของการตรวจนี้ ไม่ใช่สถานะ error และทุก helper ที่อยู่ใต้เช็คการฝังตัวต้องรอดจากมัน เมื่อ PDFlibPas หา /FontDescriptor ไม่เจอ หรือเจอ descriptor ที่ไม่มี FontFile, FontFile2 หรือ FontFile3 มันบันทึก diagnostic 10020 — หรือ 10022 เมื่อชื่ออยู่ในกลุ่ม Standard 14 ซึ่ง §7.21.4 NOTE 5 ปักหลักปฏิเสธจะยกเว้น — แล้วไปต่อทั่วไฟล์ที่เหลือ นั่นคือประเด็นทั้งหมดของรายงาน: ผู้เขียนอยากได้ทุกข้อค้นพบในหนึ่งรอบ ไม่ใช่หนึ่งข้อค้นพบต่อหนึ่งรอบรัน reference ของ descriptor ที่ถูกส่งให้ helper ด้านความกว้าง, cmap, CharSet และ CIDSet จึงเป็น Nil ได้ และแต่ละตัวเช็คเรื่องนี้ตอนเข้า แทนที่จะสมมติว่าเช็คก่อนหน้าได้ยุติการตรวจไปแล้ว ถ้าทางแก้คือการฝังสิ่งที่ขาด กลไกอยู่ในบันทึกเรื่อง การฝังฟอนต์ที่ขาดลง PDF ที่มีอยู่
รันการตรวจ
คำเรียกเดียว กับไฟล์ที่คุณไม่จำเป็นต้องเป็นผู้ผลิต TPDFlib.CheckFileCompliance รับตัวเลือกเทส compliance — 2 สำหรับ PDF/UA-1 ภายใต้ ISO 14289-1:2014 — และคืนศูนย์หรือ handle string list ที่ entry เป็นรหัสตัวเลข, โคลอน และข้อความที่อ่านได้ ข้อค้นพบด้านฟอนต์กับ content stream ที่กล่าวถึงที่นี่กิน 10020 ถึง 10041 ในช่วงนั้น ถูกเก็บแยกตัวเลขออกจากรหัส 00xxx ของ PDF/A เพื่อให้ log ผสมยังอ่านออก การส่ง 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;
ทั้งหมดนี้ไม่ต้องมีไบนารี validator ภายนอกบนเครื่อง ซึ่งเป็นความต่างระหว่างเช็คที่รันทุกบิลด์กับเช็คที่รันเมื่อมีใครนึกขึ้นได้ API ด้าน compliance กับ diagnostic ที่กล่าวถึงที่นี่ส่งมอบมาใน PDFlibPas Delphi PDF Library มาตรฐาน ซึ่งหน้าผลิตภัณฑ์แถมตารางรหัส diagnostic เต็มสำหรับ PDF/UA-1 คู่กับชุดเทส PDF/A, PDF/X และ PDF/E