PDFium Component for Delphi meng-embed font sistem yang dipakai TPdf.AddText sebagai CID font yang dikunci per code point Unicode, sehingga setiap CID membawa tepat satu pemetaan ToUnicode. Itulah yang menghentikan spasi hasil ekstraksi kembali sebagai U+00A0 (no-break space) dan hyphen sebagai U+00AD (soft hyphen), baik dari dokumen live maupun dari berkas tersimpan
Gejalanya menjengkelkan karena tak terlihat. Index pencarian melewatkan "two-x" karena string tersimpannya mengandung soft hyphen, export CSV memecahnya secara berbeda, tool diff menandai baris-baris yang tampak identik di semua viewer. Tak ada yang salah di halaman yang dirender; yang salah cuma Unicode di balik glyph-nya
Mengapa spasi hasil ekstraksi kembali sebagai U+00A0?
Spasi hasil ekstraksi berubah menjadi U+00A0 karena CMap ToUnicode yang dihasilkan PDFium di FPDFText_LoadFont dikunci per glyph, dan satu glyph bisa dicapai dari dua code point. Di Arial, glyph 3 melayani U+0020 sekaligus U+00A0, dan glyph hyphen melayani U+002D sekaligus U+00AD. CMap yang dihasilkan karena itu memetakan CID yang sama dua kali, sekali lewat entri bfchar dan sekali lewat bfrange berbentuk array, dan entri mana yang diutamakan aturan presedensi pembaca itulah yang menjadi teks hasil ekstraksi
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange
Lama sekali kontradiksi ini tak berbahaya, karena reader PDFium membiarkan pemetaan terendah menang. Perubahan upstream mengalihkan reader ke terakhir-menang, dan sejak build itu setiap spasi yang ditulis dengan AddText terekstraksi sebagai NBSP dan setiap hyphen sebagai soft hyphen. Perhatikan polanya di pasangan itu: 0x20/0xA0 dan 0x2D/0xAD hanya berbeda di bit tertinggi, persis yang bisa Anda duga dari font yang cmap-nya mengirim kembaran Latin-1 ke outline yang sama. Kalau kode ekstraksi Anda kemarin masih baik dan kini gagal pada karakter tak terlihat, dump code point-nya daripada percaya tampilan debugger; dasar-dasar menarik teks dibahas di mengekstrak teks dari dokumen PDF dengan PDFium di Delphi
uses
SysUtils, PDFium;
const
// Space/U+00A0 dan hyphen/U+00AD berbagi satu glyph Arial, begitu juga
// Omega Yunani (U+03A9) dan tanda Ohm (U+2126)
Sample: WString = 'two-x'#$00A0'y'#$00AD'z '#$03A9#$2126;
function CodePoints(const S: WString): string;
var
I: Integer;
begin
Result := '';
for I := 1 to Length(S) do
Result := Result + 'U+' + IntToHex(Ord(S[I]), 4) + ' ';
end;
var
Pdf: TPdf;
Live, Reloaded: WString;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
Pdf.AddPage(1, 595, 842);
Pdf.AddText(Sample, 'Arial', 12, 72, 770);
Live := Pdf.Text; // dokumen live, belum tersimpan
Pdf.SaveAs('codepoints.pdf');
finally
Pdf.Free;
end;
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'codepoints.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
Reloaded := Pdf.Text; // setelah simpan penuh dan muat ulang
finally
Pdf.Free;
end;
if (Live <> Sample) or (Reloaded <> Sample) then
Writeln('Mismatch: ', CodePoints(Live), '/ ', CodePoints(Reloaded));
end;
Mengapa menambal CMap setelah menyimpan tidak cukup
Menambal berkas tersimpan hanya memperbaiki berkas tersimpan itu, dan hanya kalau tambalannya menjaga struktur CMap tetap utuh byte demi byte. Perbaikan pertama, RepairSubsetToUnicodeCMaps di unit FPdfCompress, berjalan setelah setiap TPdf.SaveAs non-inkremental dan me-resolve setiap CID yang bertabrakan: entri bfchar menang, pasangan yang hanya berbeda di bit tertinggi di-resolve ke code point base-Latin yang lebih kecil, dan sisanya mempertahankan pemetaan pertamanya
Bagian menariknya justru hasil negatifnya. Membangun ulang CMap yang bertabrakan secara bersih, dalam bentuk start-code maupun array, tampak seperti langkah yang jelas, dan PDFium menolak setiap CMap hasil bangun ulang itu bulat-bulat, jatuh kembali ke Identity. Satu-satunya output yang diterima reader native adalah penggantian in-place sepanjang sama atas nilai hex yang bertabrakan, dengan tata letak blok dan cakupan CID tak tersentuh. Pelajaran kedua lebih membumi: catatan kami saat itu menyalahkan kasus in-memory pada dokumen live yang konon tak punya stream ToUnicode sama sekali. Memanggil DLL langsung membantahnya, karena dokumen live membawa stream ambigu yang sama, yang berarti perbaikan sesungguhnya harus terjadi sebelum PDFium sempat menghasilkan CMap-nya. Rutinitas perbaikan tetap tinggal di library sebagai pertahanan bagi PDF yang dihasilkan tool lain berbasis PDFium
uses
Classes, FPdfCompress;
var
Source, Dest: TFileStream;
begin
Source := TFileStream.Create('from-other-tool.pdf',
fmOpenRead or fmShareDenyWrite);
try
Dest := TFileStream.Create('repaired.pdf', fmCreate);
try
// Hanya suntingan sepanjang sama; berkas tanpa konflik yang bisa diperbaiki,
// dan berkas cross-reference-stream atau object-stream, disalin apa adanya
RepairSubsetToUnicodeCMaps(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
Mengunci font per code point alih-alih per glyph
Perbaikan akarnya adalah berhenti meminta PDFium menghasilkan CMap sama sekali. TPdf.LoadCachedFont kini menyerahkan byte font sistem ke TPdf.LoadUnicodeKeyedCidFont, yang membaca tabel sfnt cmap milik font itu sendiri, mengutamakan subtable format 12 dan jatuh ke format 4. Code point kembali terurut dan ter-deduplikasi, dan CID k+1 ditugaskan ke code point ke-k, dengan CID 0 dibiarkan sebagai .notdef. CIDToGIDMap eksplisit mengirim tiap CID ke glyph-nya, sehingga U+0020 dan U+00A0 mendapat dua CID berbeda yang menggambar outline yang sama, dan CMap ToUnicode memetakan tiap CID ke satu code point saja. Font lalu dimuat lewat FPDFText_LoadCidType2Font, entry point yang sama di balik penulisan level glyph di embedding font CID Type 2 dengan CID-to-GID map eksplisit
// Diringkas dari TPdf.LoadUnicodeKeyedCidFont
SetLength(CidToGidMap, (Length(Entries) + 1) * 2); // CID 0 = .notdef
for I := 0 to High(Entries) do
begin
CidToGidMap[(I + 1) * 2] := Byte(Entries[I].GlyphID shr 8);
CidToGidMap[(I + 1) * 2 + 1] := Byte(Entries[I].GlyphID and $FF);
end;
ToUnicode := BuildUnicodeKeyedCidCMap(Entries); // satu CID, satu code point
Result := FPDFText_LoadCidType2Font(Document, @Data[0], Length(Data),
PAnsiChar(ToUnicode), @CidToGidMap[0], Length(CidToGidMap));
Ketika FPDFText_SetText kemudian menulis sebuah string, lookup terbaliknya mendarat di satu CID per karakter, sehingga NBSP, soft hyphen, dan tanda Ohm masing-masing selamat sebagai dirinya sendiri di bawah aturan presedensi mana pun, di memori maupun setelah simpanan apa pun. Karena berkas tersimpan membawa stream ToUnicode milik komponen sendiri alih-alih hasil generasi engine, RepairSubsetToUnicodeCMaps tak menemukan apa pun untuk diperbaiki di dalamnya
Mengapa satu entri bfrange bisa menghapus satu blok utuh?
Satu bfrange yang rentang CID-nya melewati batas xxFF membuat PDFium membuang seluruh blok tempat ia berada. ISO 32000-1 §9.10.3 hanya membolehkan byte terakhir tujuan bervariasi di dalam satu rentang, tapi sisi CID punya jebakannya sendiri: HandleBeginBFRange milik PDFium menurunkan CID tinggi sebagai (low and $FFFFFF00) or (high and $FF). Rentang dari CID 00FE ke 0101 karena itu terbaca sebagai 00FE ke 0001, low lebih besar dari high, dan seluruh blok ditandai tidak valid. Kegagalannya senyap: SetText sukses, halaman tergambar sempurna, dan ekstraksi mengembalikan U+0000 untuk setiap karakter di blok itu
BuildUnicodeKeyedCidCMap mengakhiri rentang sebelum code point maupun CID mencapai low byte FF, menjaga setiap blok dalam batas 100 entri milik tata bahasa CMap, dan menulis code point supplementary-plane sebagai entri bfchar individual dengan tujuan pasangan surrogate UTF-16, karena menaikkan pasangan surrogate di dalam rentang tak punya makna yang terdefinisi; sisi surrogate dari kisah itu ada di penanganan emoji, CJK, dan surrogate pair di Delphi. CMap yang hanya bfchar bisa menghindari masalah batas sepenuhnya, dengan ukuran beberapa kali lipat
Apa yang tidak tercakup oleh font berkunci code point?
Jalur berkunci code point mencakup setiap font yang memaparkan subtable cmap Unicode, dan jatuh kembali ke perilaku berkunci glyph yang lama untuk sisanya. Batas-batas yang layak diketahui sebelum Anda mengandalkannya:
- Font simbol yang hanya punya cmap (3,0), dan font mana pun yang gagal dimuat jalur CID-nya, tetap lewat
FPDFText_LoadFontseperti dulu, sehingga glyph yang dibagi dua code point masih bisa terekstraksi ambigu di sana - Tanpa subtable format 12, peta terbatas ke BMP, dan jumlah entri dicapped di 65535 agar setiap CID muat dalam dua byte di atas nol
- Simpanan inkremental (
saIncremental) melewatiRepairSubsetToUnicodeCMapsmemang oleh desain, karena revisi inkremental harus tetap append-only; font berkunci code point membuat hal itu tak relevan untuk teks yang ditulis komponen sendiri - TrueType Collections butuh perhatian ekstra:
GetFontDatamilik GDI mengembalikan seluruh .ttc, danFPDFText_LoadCidType2Fonttak punya parameter indeks face, sehingga meminta NSimSun dari simsun.ttc dulu meng-embed dan merender SimSun, face 0. Komponen kini mencocokkan nama keluarga terhadap name table (nameID 1 dan 16) dan mengekstrak face yang diminta sebagai sfnt berdiri sendiri sebelum cmap diurai; bila penguraian gagal, byte koleksi diteruskan dan perilakunya kembali ke face 0
Penulisan teks, embedding font, dan ekstraksi berbagi satu model halaman di Delphi, C++Builder, dan Lazarus, dan API lengkapnya dideskripsikan di halaman produk PDFium Component for Delphi