บทความเทคนิค

ตรวจฟอนต์ PDF/UA ใน Delphi: ความกว้าง CharSet CIDSet

รายงาน 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 ใน Delphi: entry ใน /Widths ซึ่งอยู่ใน glyph space กับ advance ใน hmtx ซึ่งอยู่ในหน่วยออกแบบฟอนต์ ถูกนำมาอยู่ระบบพิกัดเดียวกันโดยปรับเมตริกโปรแกรมเป็น 1000 หน่วยต่อ em ก่อนเทียบใด ๆ
PDFlibPas ปรับ advance จาก hmtx ทุกตัวให้เป็นหนึ่งในพันของ em ก่อนเทียบกับความกว้างใน 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 แต่ทำให้คำถามเรื่องความกว้างตอบไม่ได้เลย

สามเส้นทางที่ PDFlibPas ใช้จากรหัสอักขระไปกลิฟ index ใน Delphi: cmap ของโปรแกรมสำหรับฟอนต์ TrueType แบบ symbolic ทางอ้อมผ่าน encoding กับ Adobe Glyph List สำหรับแบบไม่ symbolic และ CMap ตามด้วยขั้น /CIDToGIDMap สำหรับ CIDFontType2
การเทียบความกว้างเริ่มไม่ได้จนกว่ารหัสอักขระจะ resolve เป็นกลิฟ index และฟอนต์แต่ละชนิดไปถึง index นั้นด้วยเส้นทางต่างกัน
// /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 ระหว่างทางยุบเป็น "ไม่มีข้อค้นพบ" ไม่ใช่เป็นข้อบกพร่อง

กฎการรายงานแบบอนุรักษ์นิยมในการตรวจ PDF/UA ของ PDFlibPas: entry /CharSet หรือ /CIDSet ที่ขาดจะถูกรายงานเมื่อโปรแกรมฟอนต์ถอดได้ mapping รหัสไปกลิฟ resolve ได้และตัวเซ็ตเองถอดได้เท่านั้น และความล้มเหลวใด ๆ ให้ความเงียบ
ต้องสำเร็จอิสระสามข้อก่อนข้อค้นพบเรื่อง entry ที่ขาดจะถูกปล่อยออกมา ดังนั้น decoder ที่ยอมแพ้จึงมีราคาเป็น false negative ไม่ใช่การกล่าวหาผิด

นั่นคือการเอนเอียงตั้งใจไปทาง 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