Artikel Teknis

Merender Font PDF yang Disematkan di Delphi dengan HotPDF

HotPDF merender font PDF yang disematkan di Delphi tanpa menginstal apa pun di mesin: saluran rendering glyph yang disematkan dalam HPDFGlyphRender.pas mengurai program font yang disimpan di dalam PDF itu sendiri — outline glyf TrueType dari FontFile2, charstring CFF Type 2 dari FontFile3, dan aliran konten glyph Type 3 — dan memutarnya kembali sebagai jalur vektor GDI yang terisi. Artikel ini adalah pembahasan mendalam tentang ketepatan font di balik merender halaman PDF ke TBitmap dengan HotPDF: artikel tersebut membahas perender secara keseluruhan, sedangkan artikel ini membahas bagaimana teks di halaman tersebut mendapatkan bentuk pastinya

Mengapa PDF dirender dengan kotak alih-alih teks?

Kotak, ruang kosong, atau karakter yang sedikit salah dalam output PDF yang dirender hampir selalu berarti perender meminta font kepada sistem operasi alih-alih menggunakan font yang disematkan dalam berkas. Keluhan datang dengan cara yang sama setiap saat: dokumen terlihat sempurna di mesin yang memproduksinya, kemudian pelanggan membukanya di server bersih atau desktop yang terkunci dan faktur Jepang menampilkan kotak-kotak tahu (tofu squares), atau font pengganti yang serupa menggeser setiap jeda baris. Font-font tersebut tidak pernah ada di mesin tersebut — hanya ada di dalam PDF — dan perender yang hanya melakukan substitusi font sistem tidak dapat melihatnya. Font subset membuatnya lebih buruk: sebuah subset dapat membawa empat puluh glyph di bawah kode karakter yang ditetapkan secara pribadi untuk satu berkas tersebut, penetapan yang tidak dimiliki oleh font yang terinstal

ISO 32000-1 §9.9 mendefinisikan tiga pembawa untuk program font yang disematkan dalam deskriptor font: FontFile menampung program Type 1 asli, FontFile2 program TrueType, dan FontFile3 program CFF kosong (Type1C atau CIDFontType0C) atau pembungkus OpenType. Rasa keempat, font Type 3 dari ISO 32000-1 §9.6.5, sama sekali tidak menyematkan biner apa pun — setiap glyph adalah aliran konten PDF kecil yang dieksekusi di tempat. Ketiga pembawa tersebut berbeda dalam matematika outline (B-spline kuadrat versus charstring kubik versus operator halaman sewenang-wenang), sehingga perender yang setia membutuhkan interpreter terpisah untuk masing-masing tipe, ditambah lapisan pengodean yang mengubah kode karakter menjadi indeks glyph yang tepat sebelum outline disentuh

Bagaimana cara mengonversi outline glyf TrueType ke jalur GDI?

THPDFEmbeddedTTF dalam HPDFGlyphRender.pas membaca tabel loca untuk menemukan setiap rekaman glyph, menyusuri kontur glyf titik demi titik, dan memancarkan jalur GDI. Dua konvensi TrueType memerlukan penanganan eksplisit. Pertama, titik off-curve berturut-turut menyiratkan titik on-curve di titik tengahnya, dan kontur yang titik-titiknya semuanya off-curve dimulai dari titik tengah dari titik terakhir dan pertamanya — lewatkan salah satu aturan tersebut dan glyph bulat akan memiliki sisi datar atau runtuh. Kedua, kurva TrueType adalah Bézier kuadrat sedangkan PolyBezierTo milik GDI menggunakan kubik, sehingga setiap segmen kuadrat dinaikkan derajatnya (degree-elevated) secara tepat alih-alih diratakan menjadi segmen garis

// Kenaikan derajat tepat: kuadrat (P0, Q, P2) -> kubik (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0),  C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// kemudian PolyBezierTo dengan C1, C2, P2 — kurva yang identik secara geometris

Kenaikan derajat bersifat lossless: kubik melacak kurva yang identik, sehingga outline yang dirender cocok dengan apa yang digambar viewer yang patuh dari tabel yang sama, pada zoom apa pun. Pekerjaan yang tersisa adalah penempatan. Setiap glyph dibuat dalam satuan font (biasanya kisi 1000 atau 2048 unit-per-em), dan perender menyusun matriks skala, matriks teks, dan matriks transformasi saat ini menjadi satu transformasi glyph-ke-perangkat sebelum jalur diisi. Urutan sangat penting di sini daripada yang terlihat: susun tiga matriks yang sama secara terbalik dan setiap glyph akan runtuh ke arah asal — halaman yang terlihat salah yang bug sebenarnya hanyalah satu baris aljabar matriks

Bagaimana interpreter charstring Type 2 menangani font CFF

THPDFEmbeddedCFF memberikan program FontFile3 interpreter charstring Type 2 yang asli: ia mengurai struktur CFF INDEX, Top DICT dan Private DICT, lalu mengeksekusi setiap charstring dan memancarkan segmen jalur langsung ke GDI. Pembungkus OpenType (kontainer OTTO) dilucuti terlebih dahulu untuk mencapai tabel CFF kosong; aliran CIDFontType0C dan Type1C telanjang dikonsumsi secara langsung. Charstring adalah bahasa tumpukan (stack language) yang ringkas, dan tiga konvensinya menentukan apakah interpreter tetap sinkron dengan aliran byte. Awalan lebar opsional berarti operator pembersihan tumpukan pertama dapat membawa satu operan utama ekstra. Operator hintmask menyiratkan vstemhm ketika operan masih berada di tumpukan, dan jumlah byte mask yang dilewati tergantung pada jumlah stem yang terakumulasi — salah menghitung sekali saja dan setiap opcode berikutnya akan salah dibaca. Dan panggilan subrutin menambahkan bias pada indeksnya (107, 1131, atau 32768 tergantung pada jumlah subrutin) sebelum pencarian, sehingga panggilan tanpa bias mendarat di subrutin yang salah sama sekali

CFF dengan kunci CID (CID-keyed CFF) menambahkan satu ketidaklangsungan yang sering membingungkan implementasi naif: kode karakter memilih CID, tetapi indeks charstring adalah GID, dan charset font memetakan GID ke CID — sehingga perender membangun pemetaan CID-ke-GID terbalik sebelum menggambar, dan memilih Private DICT per-glyph melalui FDSelect untuk font yang membawa beberapa DICT. Program Type1C dengan kunci nama (name-keyed), pembawa biasa untuk font Type 1 sederhana, sebagai gantinya menyelesaikan kode satu byte melalui pengodean bawaan program CFF atau melalui mekanisme pengodean tingkat PDF yang dijelaskan berikutnya. Satu catatan jujur: interpreter membaca operator petunjuk (hint) untuk menjaga aliran tetap sinkron tetapi tidak mengeksekusi hinting, batasan yang dibahas di akhir

Apa itu font Type 3 dan bagaimana cara menggambarnya?

Glyph Type 3 sama sekali bukan outline — ISO 32000-1 §9.6.5 mendefinisikannya sebagai aliran konten, sehingga HotPDF merendernya dengan mendorong status grafis, menyusun matriks font, ukuran font, dan matriks teks ke CTM, serta mengeksekusi prosedur glyph melalui interpreter operator yang sama yang menggambar halaman, dengan /Resources milik font itu sendiri dalam cakupan. Dua detail spesifikasi penting untuk kebenaran. /Widths Type 3 diekspresikan dalam ruang glyph alih-alih ruang teks 1/1000 yang digunakan setiap tipe font lainnya, sehingga kemajuan (advance) harus melewati /FontMatrix — font barcode dengan matriks 0.01 sebaliknya akan melangkah salah dengan urutan besarnya (order of magnitude). Dan prosedur glyph yang dibuka dengan operator d1 membuat dua janji yang ditegakkan perender: pengecatan dipotong ke kotak pembatas (bounding box) yang dideklarasikan, dan menurut ISO 32000-1 §9.6.5.2 glyph mengabaikan operator warnanya sendiri dan mengecat dengan warna isi pemanggil saat ini, sehingga rg, g, k dan kembaran stroking mereka di dalam prosedur ditekan selama durasi glyph tersebut. Lewatkan aturan warna dan font barcode d1 yang dicap biru oleh halaman akan keluar sebagai hitam; lewatkan klip dan glyph yang cacat akan mengecat di luar selnya

Bagaimana kode karakter menjadi ID glyph

Interpreter outline hanyalah setengah dari pekerjaan, karena byte dalam string teks PDF adalah kode karakter, bukan indeks glyph, dan ISO 32000-1 mendedikasikan dua subklausul untuk pemetaan tersebut. Untuk font sederhana, §9.6.6 menentukan prioritas yang ketat: array /Differences mengesampingkan pengodean dasar (WinAnsiEncoding, MacRomanEncoding, atau StandardEncoding), yang mengesampingkan peta milik program font itu sendiri. HotPDF menyelesaikan rantai tersebut menjadi tabel kode-ke-GID 256-entri, menerjemahkan nama glyph ke indeks glyph melalui tiga rute: kecocokan charset persis di dalam program CFF, nama numerik gNN/glyphNN yang diambil sebagai indeks harfiah, dan terjemahan nama-ke-Unicode Adobe Glyph List yang diikuti oleh pencarian cmap untuk program TrueType. Untuk font komposit, §9.7 menempatkan CIDToGIDMap yang bertanggung jawab: kasus umumnya adalah /Identity, tapi entri tersebut mungkin berupa aliran pasangan big-endian yang diindeks oleh CID — dan output Unicode milik HotPDF sendiri menggunakan bentuk aliran tersebut secara persis untuk subset kompak, sehingga jalur aliran bukan sudut yang eksotis

// /CIDToGIDMap sebagai aliran: pasangan Word big-endian diindeks oleh CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // di luar rentang dipetakan ke .notdef

Ketika pencarian cmap TrueType diperlukan, HotPDF menyusuri rantai fallback alih-alih memercayai satu subtabel saja: subtabel Unicode Windows (format 4, lalu format 12 for bidang tambahan) berada di urutan pertama, lalu subtabel simbol (3,0) dengan konvensi area penggunaan pribadi F000-nya yang dicerminkan ke byte rendah — alasan font simbol seperti Wingdings menjawab untuk kode ASCII biasa — lalu format warisan 6 dan 0. Subtabel format 2 sengaja tidak diinterpretasikan: mereka memetakan halaman kode multi-byte warisan seperti Shift-JIS dan Big5, bukan Unicode, dan font CJK modern selalu membawa subtabel format 4 atau format 12. Kode apa pun yang tidak lolos dari rute ini akan beralih ke penggambaran GDI untuk satu glyph tersebut, sehingga satu karakter yang tidak dapat dipetakan menurunkan performa satu glyph saja, bukan seluruh rangkaian teks

Apa yang tidak dilakukan oleh jalur yang disematkan

Hinting tidak dieksekusi — outline diisi seperti yang dibuat, yang tidak dapat dibedakan dari output yang di-hint pada DPI 150 ke atas tetapi dapat berbeda satu piksel dari rasterizer yang di-hint pada ukuran yang sangat kecil. Program Type 1 asli di FontFile (charstring terenkripsi eexec) tidak diinterpretasikan, dan sumbu font variabel OpenType tidak diterapkan; kedua kasus tersebut, seperti program font yang rusak atau tabel glyf tanpa cmap yang dapat digunakan, akan beralih ke penggambaran font sistem alih-alih menggagalkan halaman. Pendekatan ketepatan-pertama yang sama meluas ke bagian lain di perender — pola bayangan aksial dan radial mendapatkan perlakuan yang sama seperti gradien — dan sisi generasi memiliki keunikan font-nya sendiri dalam bagaimana EndDoc mengurutkan subset font

Menggunakan pipa ini tidak memerlukan kode khusus font sama sekali — setiap mekanisme di atas bekerja secara otomatis di dalam panggilan render halaman

Menggunakan pipa ini tidak memerlukan kode khusus font sama sekali — setiap mekanisme di atas bekerja secara otomatis di dalam panggilan render halaman

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Font TrueType, CFF, and Type 3 yang disematkan dirender dari
      // berkas itu sendiri — tidak perlu menginstal apa pun di mesin ini
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Konsekuensi praktisnya adalah hal yang diperhatikan kotak masuk dukungan Anda: PDF yang membawa font-nya akan dirender dengan font tersebut, di server kompilasi, dalam kontainer Windows, atau di desktop pelanggan yang belum pernah memiliki jenis font tersebut. Saluran rendering glyph yang disematkan dikirimkan sebagai bagian dari HotPDF Component untuk Delphi dan C++Builder — pustaka VCL asli yang mencakup pembuatan, pengeditan, ekstraksi teks, dan rendering halaman PDF tanpa dependensi DLL eksternal