Artikel Teknis

Audit Font PDF/UA di Delphi: Widths, CharSet, CIDSet

Laporan veraPDF yang menyatakan lebar glyph tidak cocok dengan program font yang ter-embed hampir tidak memberi tahu Anda glyph mana yang bermasalah, atau mengapa. PDFlibPas menjawab pertanyaan itu dengan menyelesaikan setiap kode karakter melalui cmap yang ter-embed ke indeks glyph, menormalkan metrik program ke 1000 unit per em, dan membandingkannya di sana

Mengapa lebar glyph bisa tidak cocok?

Karena dua angka yang dibandingkan berada di sistem koordinat yang berbeda, dan tidak ada apa pun di kamus PDF yang memberi tahu konversinya. Kamus font menulis /Widths dalam glyph space, yang PDF tetapkan sebesar seperseribu em (ISO 32000-1 §9.2.4). Tabel hmtx di dalam program TrueType yang ter-embed menulis advance dalam unit desain font, dan tabel head menentukan berapa banyak unit itu yang membentuk satu em: 2048 untuk sebagian besar face TrueType, 1000 untuk yang diturunkan dari CFF, sesekali sesuatu yang sama sekali berbeda. Bandingkan nilai mentahnya dan setiap font 2048-upem di korpus Anda tampak rusak. Itulah jebakan yang dipasang ISO 14289-1 §7.21.5 bagi siapa pun yang mencoba mengaudit lebar dengan membaca field kamus

Audit lebar glyph PDFlibPas di Delphi: entri /Widths dalam glyph space dan advance hmtx dalam unit desain font disatukan ke sistem koordinat yang sama dengan menskalakan metrik program ke 1000 unit per em sebelum perbandingan dilakukan
PDFlibPas menskalakan setiap advance hmtx ke seperseribu em sebelum membandingkannya dengan lebar kamus, dan hanya melaporkan yang terpaut lebih dari satu unit

PDFlibPas menormalkan saat load. TPDFTrueTypeParser menyimpan Advance * 1000 div unitsPerEm di array lebarnya, sehingga Parser.GetWidth(GID) sudah menjawab dalam seperseribu em yang sama dengan yang dipakai PDF, dan GetRawWidth tetap tersedia saat Anda butuh unit desain kembali. Itu masih menyisakan separuh yang lebih sulit: berpindah dari kode karakter ke indeks glyph. Untuk font TrueType sederhana, rutenya bergantung pada flag Symbolic di FontDescriptor, bit 3 dari /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Face simbolik dialamatkan langsung lewat cmap program,
    // dengan konvensi high-byte (3,0) sebagai fallback
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Non-simbolik: kode -> nama glyph lewat encoding, nama -> Unicode
    // lewat Adobe Glyph List, Unicode -> GID lewat cmap program
    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;

Dua detail dalam fragmen itu membawa bobot. Toleransinya satu unit, bukan nol, karena normalisasi adalah pembagian bilangan bulat dan file yang diproduksi secara sah bisa meleset satu unit; itulah tepatnya redaksi "dalam seperseribu em" yang dilaporkan diagnostik 10036. Dan guard GID < Parser.GlyphCount bukan hiasan. GetWidth ditulis untuk bersikap memaafkan bagi pemanggil rendering, menjepit indeks di luar jangkauan ke entri terakhir di hmtx dan mundur ke 750 saat tabel tidak ada. Bersikap memaafkan tepat untuk rendering dan keliru untuk auditing, maka audit menolak indeksnya sebelum meminta lebar alih-alih memercayai jepitan itu

CIDFontType2 menambah satu lapis indireksi lagi

PDFlibPas menelusuri font komposit dengan cara yang sama, dengan /CIDToGIDMap disisipkan antara CID dan glyph. Lebar tiba di array /W, yang oleh ISO 32000-1 §9.7.4.3 diberi dua bentuk yang bergantian bebas dalam satu array: CID awal diikuti array lebar berurutan, atau CID pertama, CID terakhir, dan satu lebar yang berlaku untuk seluruh rentang. Audit meng-parse keduanya, lalu menyerahkan setiap pasangan hasil ke perbandingan yang sama, dan melaporkan totalnya di bawah diagnostik 10037. Langkah pemetaan inilah tempat font komposit berbeda, dan itulah alasan diagnostik peta-hilang 10021 penting sebelum Anda membaca lebar mana pun — /CIDToGIDMap yang tidak ada atau cacat tidak sekadar melanggar §7.21.3.2, ia membuat pertanyaan lebar tak terjawab

Tiga rute yang ditempuh PDFlibPas dari kode karakter ke indeks glyph di Delphi: cmap program untuk font TrueType simbolik, jalan memutar encoding dan Adobe Glyph List untuk yang non-simbolik, serta langkah CMap plus /CIDToGIDMap untuk CIDFontType2
Perbandingan lebar tidak bisa dimulai sebelum kode karakter terjawab menjadi indeks glyph, dan setiap jenis font mencapai indeks itu lewat rute yang berbeda
// /CIDToGIDMap adalah nama /Identity atau stream big-endian 16-bit
// indeks glyph, satu per CID (ISO 32000-1 bagian 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;                       // string Pascal berbasis 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;

Apa yang sebaiknya dilakukan auditor saat program font tidak mau ter-decode?

Berkata tidak apa-apa. Pemeriksaan kelengkapan /CharSet dan /CIDSet yang diwajibkan ISO 14289-1 §7.21.4.2 — diagnostik 10038 dan 10039 — adalah tempat validator yang terlalu bersemangat berubah menjadi beban, karena laporan "CharSet Anda tidak lengkap" tidak bisa dibedakan, bagi orang yang membacanya, dari "decoder Type 1 kami menyerah". Karena itu PDFlibPas hanya melaporkan entri yang hilang ketika tiga hal berhasil sekaligus: program font ter-decode, pemetaan kode-ke-glyph terselesaikan, dan set itu sendiri ter-decode. TPDFType1Decoder.LoadPFBFromString harus mengembalikan True dan menghasilkan jumlah charstring sebelum ada nama glyph yang diperiksa terhadap string /CharSet; jalur /CIDSet butuh stream yang ter-inflate dan jumlah glyph yang kembali positif sebelum satu bit pun diuji. Pengecualian apa pun di sepanjang jalan runtuh menjadi "tidak ada temuan", bukan menjadi cacat

Aturan pelaporan konservatif dalam audit PDF/UA PDFlibPas: entri /CharSet atau /CIDSet yang hilang hanya dilaporkan saat program font ter-decode, pemetaan kode-ke-glyph terselesaikan dan set itu sendiri ter-decode, dan setiap kegagalan menghasilkan keheningan
Tiga keberhasilan independen dibutuhkan sebelum temuan entri-hilang dikeluarkan, sehingga decoder yang menyerah membuat Anda menanggung false negative alih-alih tuduhan palsu

Itu adalah bias yang disengaja ke arah false negative, dan layak dinyatakan terus terang alih-alih dikubur. Tabel CFF yang rusak, varian Type 1 yang tidak didukung, atau /CIDSet yang lebih pendek dari rentang glyph semuanya menghasilkan keheningan alih-alih diagnostik. Alasannya, audit PDF/UA diteruskan ke penulis yang tidak membangun tooling-nya, dan tuduhan palsu lebih mahal daripada temuan yang terlewat: penulis membakar satu hari untuk membuktikan file yang patuh memang patuh, lalu berhenti memercayai seluruh laporan. Matterhorn Protocol membuat pembedaan yang sama dalam bentuk lain saat memisahkan pemeriksaan yang bisa diputuskan mesin dari pemeriksaan yang harus dilakukan manusia, dan checkpoint Fonts-nya (31) adalah tempat semua ini berada. Jika Anda butuh pembacaan yang lebih ketat, jalankan PDFlibPas sebagai gerbang cepat dan validator khusus sebagai opini kedua — pasangan itu sama dengan yang dijelaskan dalam panduan preflight PDF/A dan PDF/UA

/Contents halaman adalah daftar, bukan stream

Kesalahan termahal dalam auditing content stream adalah memperlakukan /Contents sebagai satu stream. ISO 32000-1 §7.7.3.3 membolehkan sebuah halaman memegang array stream yang konkatenasinya, dengan whitespace di antara bagian-bagiannya, adalah program halaman; produsen memotong di titik arbitrer, dan sebuah BT bisa berada di satu anggota sementara ET pasangannya di anggota berikutnya. Prosesor konten menyimpan state — kedalaman nesting marked-content, font yang dipilih Tf terakhir, flag text-object — dan Process mereset state itu saat masuk. Panggil sekali per anggota array dan setiap stream setelah yang pertama mulai tanpa font aktif, sehingga teks yang sebenarnya tertag dengan baik terbaca sebagai derau tanpa tag dan tanpa font. PDFlibPas menggabungkan dulu, lalu memproses sekali

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;

// Satu panggilan Process atas seluruh konkatenasi, jangan satu panggilan per anggota
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Form XObject mana yang benar-benar dihitung tak berstruktur?

Hanya yang benar-benar dipanggil oleh sebuah halaman, dari tempat panggilan di luar marked content, dan kontennya sendiri menampilkan teks. Diagnostik 10040 menegakkan ISO 14289-1 §7.20 dengan merekam tiga fakta independen per nomor objek — ada teks, pernah dipanggil, dipanggil di dalam marked content — dan hanya melaporkan irisan dua yang pertama dikurangi yang ketiga. Masing-masing jalan pintas salah dengan cara yang akan Anda kirim: menandai setiap Form yang membawa teks di /Resources menghukum pustaka templat yang tak pernah digambar dari situ, dan menandai setiap Form yang dipanggil menghukum logo vektor yang tidak membawa teks dan tak butuh penagihan. Lokasi pemanggilan diselesaikan berdasarkan nomor objek alih-alih nama resource, karena Form yang sama lazimnya dicapai lewat nama yang berbeda di halaman yang berbeda. Diagnostik pendamping 10041 menelusuri program terkonkatenasi yang sama untuk §7.21.8, menyelesaikan setiap operand penampil teks melalui font yang berlaku dan menghitung kode yang mendarat di .notdef, yang dilarang apa pun mode rendering teksnya — termasuk mode tak terlihat yang dipakai di balik gambar hasil pindai. Bagaimana Form yang tersisa sebaiknya dibungkus adalah pertanyaan structure tree, dibahas dalam artikel tentang membangun struktur PDF tertag

Font tanpa FontDescriptor sama sekali

Font yang tidak ter-embed adalah masukan yang sah untuk audit ini, bukan keadaan error, dan setiap helper di bawah pemeriksaan embedding harus selamat melewatinya. Saat PDFlibPas tidak menemukan /FontDescriptor, atau menemukan deskriptor tanpa FontFile, FontFile2, maupun FontFile3, ia mencatat diagnostik 10020 — atau 10022 saat namanya termasuk Standard 14, yang oleh §7.21.4 NOTE 5 dengan tegas menolak dikecualikan — lalu terus menelusuri sisa file. Itulah inti sebuah laporan: penulis ingin semua temuan dalam satu lintasan, bukan satu temuan per eksekusi. Maka referensi deskriptor yang diserahkan kepada helper lebar, cmap, CharSet dan CIDSet boleh bernilai Nil, dan masing-masing mengujinya saat masuk alih-alih mengasumsikan pemeriksaan sebelumnya membatalkan audit. Jika solusinya adalah meng-embed yang hilang, mekaniknya ada di catatan tentang menanam font yang hilang ke PDF yang sudah ada

Menjalankan audit

Satu panggilan, pada file yang belum tentu Anda produksi. TPDFlib.CheckFileCompliance menerima pemilih uji kepatuhan — 2 untuk PDF/UA-1 di bawah ISO 14289-1:2014 — dan mengembalikan nol atau handle string-list yang entrinya berupa kode numerik, titik dua, dan pesan yang terbaca. Temuan font dan content stream yang dibahas di sini menempati 10020 sampai 10041 dalam rentang itu, dipisahkan secara numerik dari kode 00xxx PDF/A agar log campuran tetap terbaca. Memberi 1 pada Options membuat pemrosesan berhenti di temuan pertama, yang Anda inginkan di build gate alih-alih di tool penulisan. Untuk dokumen yang masih terbuka di memori, GetPDFUADiagnostics menjalankan inspeksi setara tanpa bolak-balik lewat disk

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 memilih PDF/UA-1; Options = 0 melaporkan semua temuan
  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));   // mis. 10037 CIDFontType2 ...
  end;
end;

Semua ini tidak membutuhkan binary validator eksternal di mesin, dan itulah pembeda antara pemeriksaan yang berjalan di setiap build dan pemeriksaan yang berjalan saat ada yang ingat. API kepatuhan dan diagnostik yang dibahas di sini dikirim dalam PDFlibPas Delphi PDF Library standar, yang halaman produknya memuat tabel kode diagnostik lengkap untuk PDF/UA-1 bersama suite uji PDF/A, PDF/X dan PDF/E