Glyph hasil shaping tampil sebagai kotak .notdef ketika sebuah font subsetter hanya menyimpan glyph yang bisa dijangkau dari code point yang diterbitkan. HotPDF, komponen PDF VCL native untuk Delphi dan C++Builder, membawa persis defek itu hingga versi 2.435.0: output OpenType GSUB dicatat dalam sebuah usage bitmap internal yang dinyatakan oleh subsetter akan dihormati, lalu sebenarnya tidak pernah dibaca
Ini adalah kegagalan yang berbeda dari yang dijelaskan di bug EndDoc yang diam-diam menonaktifkan font subsetting. Bug itu soal kapan subsetting berjalan relatif terhadap serialisasi, dan itu menonaktifkan subsetting secara total. Yang ini soal apa isi subset ketika subsetting berjalan tepat sesuai jadwal. Pipeline-nya menyala pada momen yang benar, prefix subset enam-huruf muncul pada /BaseFont persis seperti yang disyaratkan ISO 32000-1 §9.6.4, berkasnya jadi lebih kecil, setiap halaman Latin proof-nya bersih, dan sebuah halaman Arab keluar sebagai deretan rectangle kosong. Bug ordering itu ribut begitu Anda melihatnya. Bug closure tetap diam selamanya, karena subsetnya valid secara struktural dan hanya salah soal daftar keanggotaannya sendiri
Kenapa glyph hasil shaping tampil sebagai .notdef?
Karena himpunan code point yang diterbitkan sebuah dokumen bukanlah himpunan glyph yang digambar dokumen tersebut, dan sebuah subsetter yang mencampuradukkan keduanya membuang setiap glyph yang dihasilkan shaping. Text shaping mengubah urutan karakter logis menjadi urutan glyph yang sudah diposisikan, dan tujuan utamanya justru menghasilkan glyph yang tidak dipetakan oleh satu karakter input pun: sebuah heh medial Arab, sebuah ligature fi, sebuah conjunct Devanagari, sebuah alternate kontekstual yang dipilih oleh fitur rclt. Masing-masing adalah glyph ID yang dihasilkan oleh sebuah GSUB lookup, bukan yang diberikan tabel cmap untuk karakter mana pun dalam string Anda. Subsetter yang murni digerakkan oleh cmap karena itu sedang menelusuri indeks yang salah. Ia dengan setia mempertahankan setiap glyph yang bisa dipakai teks sebelum shaping dan membuang justru glyph yang benar-benar dipakai teks setelah shaping. Renderer kemudian meminta font yang di-embed untuk GID 1847, subsetnya sudah menolkan entri itu di loca, dan glyph index 0 yang kembali sebagai gantinya. Glyph index 0 adalah .notdef menurut definisi OpenType, dan itulah sebabnya ciri kegagalannya berupa kotak kosong, bukan huruf yang salah atau crash. Tidak ada yang malformed dalam PDF-nya; fontnya sekadar tidak mengandung glyph yang diminta content stream
Code point bukan glyph: tiga sumber sebuah subset
Subset closure yang benar harus menggabungkan tiga sumber independen, masing-masing dengan accumulator-nya sendiri. Yang pertama adalah himpunan turunan code-point: HotPDF mengakumulasi FUnicodeUsedCps saat karakter BMP diterbitkan dan FUnicodeSmpUsed untuk karakter supplementary-plane yang dijangkau lewat surrogate pair, lalu memetakan masing-masing lewat FUnicodeCpToGid menjadi sebuah glyph ID. Yang kedua adalah himpunan turunan shaping, glyph ID yang dihasilkan sebuah substitusi GSUB, dicatat lewat MarkUnicodeGlyphUsed dan EnableShapingFeatureForSubset ke dalam FUnicodeExtraUsedGlyphs. Yang ketiga adalah composite closure: sebuah glyph yang numberOfContours-nya -1 di glyf dirakit dari glyph ID komponen, dan mempertahankan composite-nya sambil membuang komponennya menghasilkan outline kosong, bukan sebuah .notdef, yang bisa dibilang lebih buruk karena terbaca sebagai bug spacing
HotPDF selalu menangani yang pertama dan ketiga. BuildAndApplyUnicodeFontSubset, entry point subsetting yang dipanggil EndDoc sebelum serialisasi, mengisi array used-glyph dengan GID 0, menelusuri code point BMP, menelusuri daftar usage SMP, dan menyerahkan array itu ke sebuah subset builder yang meresolusi komponen composite secara internal. Sumber kedua ditulis tapi tidak pernah dikonsumsi, dan karena ketiga sumber ini gagal pada konten yang berbeda, celah ini bisa bersembunyi bertahun-tahun dalam codebase yang corpus regresinya sebagian besar berbahasa Latin
Array yang ditulis dan tidak pernah dibaca
Kontraknya didokumentasikan di tiga tempat dan tidak dihormati di satu pun. Deklarasi FUnicodeExtraUsedGlyphs menyatakan bahwa subsetter EndDoc menggabungkannya dengan usage turunan code-point; komentar header pada ApplyArabicGSUBRefinement menjanjikan bahwa setiap substitute GID yang diterbitkan diteruskan lewat MarkUnicodeGlyphUsed sehingga subsetter menarik glyph itu ke dalam font yang di-embed; janji yang sama muncul verbatim pada ApplyArabicGSUBContextualRefinement untuk jalur rclt. Kedua caller menepati bagian mereka. Sebuah grep atas setiap referensi ke field ini menuntaskan separuh lainnya dalam sekitar sembilan puluh detik: satu deklarasi, satu alokasi SetLength di dalam RegisterUnicodeTTF, dan penulisan di dua rutin penandaan. Tidak satu pun pembacaan. Itulah diagnosis yang layak diinternalisasi, karena berlaku jauh melampaui font. Ketika sebuah field ditulis oleh beberapa call site dan tidak dibaca oleh satu pun, fitur yang diwakilinya tidak ada, sedetail apa pun ia dikomentari. Step 1 dari subsetter cukup kecil untuk dibaca dalam satu layar, dan celahnya jadi jelas begitu Anda tahu harus mencarinya
// Step 1: derive the used-glyph set (as it stood before 2.435.0)
SetLength(UsedGlyphs, FUnicodeNumGlyphs);
for I := 0 to FUnicodeNumGlyphs - 1 do
UsedGlyphs[I] := False;
UsedGlyphs[0] := True; // .notdef is always present
for Cp := 0 to $FFFF do // source 1a: BMP code points
if (Cp < Length(FUnicodeUsedCps)) and FUnicodeUsedCps[Cp]
and (Cp < Length(FUnicodeCpToGid)) then
begin
GID := FUnicodeCpToGid[Cp];
if (GID > 0) and (GID < FUnicodeNumGlyphs) then
UsedGlyphs[GID] := True;
end;
for I := 0 to High(FUnicodeSmpUsed) do // source 1b: SMP code points
begin
GID := FUnicodeSmpUsed[I].GID;
if (GID > 0) and (GID < FUnicodeNumGlyphs) then
UsedGlyphs[GID] := True;
end;
// source 2 was missing here: nothing ever consulted FUnicodeExtraUsedGlyphs
Perbaikan satu loop, dan menandai glyph sendiri
Perbaikannya adalah sebuah union, dan argumen keamanannya berasal dari arah operasinya: ia hanya menyalakan bit, tidak pernah mematikannya, sehingga tidak ada glyph yang tadinya bertahan dalam subset yang bisa mulai dibuang
// v2.435.0: pull GSUB-derived extra glyphs into the subset.
// MarkUnicodeGlyphUsed / EnableShapingFeatureForSubset record GIDs that
// shaping produced but that no emitted code point maps to directly.
for I := 0 to FUnicodeNumGlyphs - 1 do
if (I < Length(FUnicodeExtraUsedGlyphs)) and FUnicodeExtraUsedGlyphs[I] then
UsedGlyphs[I] := True;
Ada tiga sifat yang membuat ini menjadi perubahan berisiko rendah, bukan penulisan ulang font-engine. Sifatnya monoton, seperti di atas. Ia menjadi no-op pada font yang tidak pernah melakukan shaping apa pun, karena FUnicodeExtraUsedGlyphs tetap semua-False dan output byte untuk dokumen Latin-saja tidak berubah. Dan ia diterapkan sebelum Step 2, sehingga kedua subset builder mewarisinya: sparse builder yang mempertahankan penomoran GID asli, dan compact builder _BuildCompactSubsetTTF yang dipilih HotPDF di bawah PDF/A untuk menomori ulang glyph yang dipertahankan ke dalam rentang yang padat, mengecilkan maxp.numGlyphs, dan menerbitkan pemetaan lama-ke-baru sebagai stream /CIDToGIDMap yang disyaratkan ISO 32000-1 §9.7.4.2. Keduanya memanggil _TTFWalkCompositeClosure secara internal, sehingga sebuah glyph hasil shaping yang kebetulan composite kini juga menarik masuk komponennya. Composite closure tidak pernah rusak; ia sekadar tidak pernah terjangkau untuk glyph ID ini, karena glyph ID-nya tidak berada dalam himpunan yang ditelusurinya. Jika Anda menggerakkan mesin GSUB secara langsung alih-alih mengandalkan pass refinement bawaan, closure menjadi tanggung jawab Anda, dan setiap substitute glyph ID yang Anda terbitkan harus ditandai sebelum EndDoc membekukan himpunan used-glyph
var
Pdf: THotPDF;
GIDs: array[0..1] of Word;
LigGID: Word;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'shaped.pdf';
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoNaskhArabic-Regular.ttf');
Pdf.ShapingFeatures := [sfArabicGSUB, sfStandardLigatures,
sfContextualAlternates];
GIDs[0] := Pdf.GetUnicodeGlyphForCodepoint($0644); // lam
GIDs[1] := Pdf.GetUnicodeGlyphForCodepoint($0627); // alef
if Pdf.ApplyLigatureSubstitution(GIDs, 0, 'liga', LigGID) then
Pdf.MarkUnicodeGlyphUsed(LigGID); // omit this and you get .notdef
Pdf.EnableShapingFeatureForSubset('rclt');
Pdf.CurrentPage.RtLTextOut(50, 700, 0, WideString(ArabicText));
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
EnableShapingFeatureForSubset adalah rekan batch dari pemanggilan single-GID, dan ia sengaja dibuat konservatif. Ia menelusuri daftar lookup GSUB untuk lookup yang terhubung ke satu feature tag empat-byte di bawah jalur script dan language yang sedang dipilih, dan menandai glyph ID substitute yang bisa dihasilkan lookup itu. Ia menjadi no-op defensif ketika font tidak membawa tabel GSUB atau ketika fitur itu tidak ada pada jalur tersebut, sehingga memanggilnya tanpa syarat aman dilakukan. Ia juga sengaja dibuat sebagai over-approximation: ia mungkin mempertahankan glyph yang tidak pernah digambar dokumen tertentu. Untuk subsetting, over-inclusion memakan biaya byte dan under-inclusion memakan biaya kebenaran, yang membuat trade-off itu mudah diambil. Struktur lookup ini, dan coverage table yang menentukan glyph mana yang berpartisipasi, dibahas di penjelasan lengkap GSUB stylistic alternates dalam Delphi murni
Bagaimana Anda membuktikan glyph itu benar-benar ada dalam subset?
Dengan membaca font yang diterbitkan, bukan dengan melihat sekilas halaman di sebuah viewer yang mungkin diam-diam mengganti dengan font sistem. Pemeriksaan yang menangkap seluruh kelas bug ini bersifat mekanis: ekstrak stream /FontFile2 dari PDF output, urai loca, dan pastikan glyph ID yang Anda harapkan membawa entri yang tidak kosong, artinya offset awal dan akhirnya berbeda. Entri kosong berarti subsetter memutuskan glyph itu tidak dipakai. Dua kebiasaan kemudian membuat jauh lebih sulit untuk merilis kegagalan ini lagi. Pertahankan sebuah halaman shaped-script dalam corpus smoke otomatis, bukan hanya dalam set proofing manual, karena Arab, Devanagari, dan Khmer menguji jalur closure yang tidak akan tersentuh sebanyak apa pun cakupan Latin. Dan setiap kali ada sebuah accumulator, pastikan ada yang mengonsumsinya, karena field yang hanya-ditulis adalah fitur yang berhasil dikompilasi, lolos tes hijau pada corpus yang salah, dan tidak melakukan apa-apa
Di mana perbaikan ini berhenti
Subset closure diperlukan agar sebuah glyph hasil shaping bisa dirender, tetapi itu belum cukup. Glyph itu juga harus bisa dialamati dari content stream, yang merupakan masalah terpisah dengan batasannya sendiri. Pass refinement Arab bawaan HotPDF hanya melakukan commit sebuah substitusi ketika setiap substitute glyph ID bisa dijangkau lewat sebuah code point presentation-form Unicode via scan cmap terbalik atas kira-kira 690 code point di U+FB50 hingga U+FDFF dan U+FE70 hingga U+FEFF. Ketika sebuah substitute jatuh pada glyph ID di luar rentang itu, window input tersebut diteruskan tanpa perubahan alih-alih menerbitkan sesuatu yang tidak bisa dialamati pembaca; alternate khusus-font pada glyph ID sembarang membutuhkan sebuah code point private-use sintetis yang dialokasikan di U+E000 hingga U+F8FF untuk membawanya melalui jalur penerbitan. Jadi ringkasan yang jujur adalah bahwa perbaikan 2.435.0 menghilangkan sebuah hard blocker, bukan menuntaskan seluruh cerita. Sebelumnya, sebuah glyph bisa di-shape dengan benar, diterbitkan dengan benar, dan tetap lenyap pada saat subsetting, yang berarti mesin shaping tidak bisa dipercaya sepenuhnya end-to-end sebagus apa pun lookup-nya. Yang tersisa adalah addressability, dan batasan itu setidaknya gagal secara terlihat pada titik penerbitan, bukan diam-diam dalam sebuah build step yang berjalan setelah semua yang sedang Anda amati. Untuk sisi penerbitan dari pipeline yang sama, lihat panduan shaping teks Arab dan RTL dalam PDF Delphi
Font subsetting, mesin GSUB, dan shaping complex-script yang dijelaskan di sini hadir dalam HotPDF Component standar untuk Delphi dan C++Builder; halaman produk memuat referensi API lengkap untuk pemanggilan font Unicode dan shaping yang disebutkan di atas