Artikel Teknis

Membaca Properti Font PDF dengan PDFium Component di Delphi

Setiap karakter yang terlihat di dalam PDF membawa referensi ke font yang menggambarnya, dan PDFium Component membiarkan Anda mengikuti referensi itu kembali ke objek font-nya lalu membaca apa yang diketahuinya. Satuan aksesnya adalah karakter, bukan dokumen: Anda memilih sebuah karakter lewat indeksnya di dalam teks halaman lalu menanyakan nama family, base name, weight, sudut italic, dan apakah face di baliknya benar-benar dibawa di dalam file itu. Properti terakhir itulah yang sebenarnya diincar sebagian besar analisis, karena font yang tertanam ikut bepergian bersama dokumennya sementara font yang tidak tertanam hanyalah janji bahwa mesin pembacanya kebetulan memasang typeface yang sama

Component ini mengeksposnya lewat objek TPdf dan TPdfView yang sama yang Anda pakai untuk rendering dan ekstraksi teks. Tidak ada objek "tabel font" terpisah yang perlu dibuka. Begitu teks sebuah halaman selesai di-parsing, properti font-nya menggantung pada indeks karakter, dan Anda membacanya satu glyph pada satu waktu. Rancangan itu cocok dengan cara PDF menyimpan informasinya sejak awal: satu halaman bisa berganti font puluhan kali, dan satu-satunya jawaban jujur untuk "dokumen ini pakai font apa" adalah "tergantung karakter mana yang Anda maksud"

Membaca font di balik satu karakter

Operasi berguna yang terkecil adalah mengambil satu indeks karakter lalu menumpahkan semua yang bisa diberitahukan PDFium tentang font-nya. Setiap properti font pada TPdf dan TPdfView diindeks berdasarkan posisi karakter, sehingga indeksnya merajut semuanya. Halamannya juga harus menjadi halaman yang aktif supaya indeks itu terurai terhadap teks yang benar, dan hal itu mulai penting begitu Anda melewati halaman satu

Diagram PDFium Component di Delphi yang membaca nama family, base name, weight, sudut italic, ukuran, ascent dan descent, serta status penanaman untuk satu indeks karakter di dalam teks halaman yang sudah di-parsing
PDFium Component menggantungkan setiap properti font pada indeks karakter, sehingga sekali baca satu glyph sudah mencakup nama, weight, metrik, dan penanaman
procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
  Report: TStringList;
  PtSize: Single;
begin
  Report := TStringList.Create;
  try
    PtSize := Pdf.FontSize[CharIndex];

    Report.Add('Character : ' + Pdf.Character[CharIndex]);
    Report.Add('Family    : ' + Pdf.FontFamilyName[CharIndex]);
    Report.Add('Base name : ' + Pdf.FontBaseName[CharIndex]);
    Report.Add('Weight    : ' + IntToStr(Pdf.FontWeight[CharIndex]));
    Report.Add('Italic    : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' deg');
    Report.Add('Size      : ' + FormatFloat('0.0', PtSize) + ' pt');
    Report.Add('Ascent    : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
    Report.Add('Descent   : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
    Report.Add('Embedded  : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));

    ShowMessage(Report.Text);
  finally
    Report.Free;
  end;
end;

Beberapa signature-nya mengejutkan orang yang datang dari library lain. FontAscent dan FontDescent menerima dua argumen, indeks karakter dan sebuah ukuran poin, karena PDFium melaporkan metrik itu dalam satuan ruang glyph yang baru menjadi piksel setelah Anda menskalakannya dengan ukuran teks yang dipakai. Berikan nilai yang sudah Anda baca dari FontSize[CharIndex] dan Anda memperoleh ascent serta descent dalam poin yang sama dengan sisa tata letaknya. Descent kembali bernilai negatif, karena ia mengukur di bawah baseline. Nama family dan base name adalah dua string terpisah dengan sengaja: base name adalah entry /BaseFont mentah dari PDF-nya, sering membawa prefiks subset seperti ABCDEF+, sementara nama family adalah nama bersih yang menjadi hasil penguraian renderer-nya

Mengubah sebuah klik menjadi indeks karakter

Di dalam sebuah viewer, Anda jarang tahu indeksnya sejak awal. Pengguna mengklik sebuah glyph dan Anda harus menerjemahkan koordinat pikselnya menjadi karakter yang ada di bawahnya. CharacterIndexAtPos melakukan persis itu, menerima posisi mouse beserta sebuah toleransi lalu mengembalikan indeks karakter terdekat, atau nilai negatif ketika kliknya mendarat di ruang kosong atau di halaman yang kosong

Diagram alur sebuah viewer PDFium Component Delphi yang mengubah klik mouse menjadi indeks karakter dengan CharacterIndexAtPos dan toleransi piksel, mengenai sebuah glyph atau mendarat di ruang kosong
CharacterIndexAtPos mengubah sebuah klik menjadi indeks karakter yang sama seperti yang diharapkan setiap properti font, dengan toleransi yang sebaiknya dijaga antara tiga sampai lima piksel
procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Index: Integer;
begin
  if not PdfView.Active then
    Exit;

  // Kelonggaran 4 px tiap arah agar klik yang nyaris meleset tetap kena glyph.
  Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
  if Index < 0 then
    Exit;                      // klik di antara glyph; biarkan panelnya

  PdfView.CurrentCharIndex := Index;
  DescribeFontAt(PdfView.Pdf, Index);
end;

Toleransinya layak disetel. Terlalu ketat dan pengguna merasa harus mendarat tepat di batang sebuah huruf; terlalu longgar dan sebuah klik di margin melompat ke karakter jauh yang sama sekali tidak mereka maksud. Tiga sampai lima piksel perangkat adalah titik awal yang masuk akal untuk penglihatan di layar. Indeks yang dikembalikan menunjuk ke dalam teks halaman aktif yang sudah di-parsing, ruang indeks yang sama seperti yang diharapkan setiap properti font, sehingga Anda bisa menyerahkannya langsung ke rutin di atas. Menyimpannya di CurrentCharIndex itu opsional namun praktis: view tersebut memeliharanya sebagai gagasannya tentang glyph yang sedang difokuskan, dan itu berguna jika bagian lain UI ingin membaca pilihannya tanpa menurunkannya ulang

Penanaman adalah properti yang penting

Untuk sebagian besar pekerjaan nyata, satu-satunya pertanyaan yang layak dijawab adalah apakah setiap font tertanam. Sebuah dokumen yang seluruh font-nya menumpang di dalamnya akan ter-render sama di RIP milik biro cetak, di laptop seorang kolega, dan di server yang sama sekali tanpa GUI. Dokumen yang bersandar pada Helvetica yang tidak tertanam sedang berjudi bahwa setiap mesin tadi punya face yang cocok, dan ketika judinya kalah, pembacanya menggantinya dengan sesuatu yang mirip, metriknya bergeser, dan sebuah formulir yang tertata rapi mengalir ulang tepat cukup untuk rusak. Menelusuri teks halaman lalu mengelompokkan font berdasarkan status penanamannya memberi Anda jawaban itu dengan murah

Diagram sebuah audit PDFium Component Delphi yang menelusuri karakter halaman dan mengelompokkan font berdasarkan FontIsEmbedded untuk memisahkan face yang tertanam dari yang tidak lalu melaporkan yang terakhir
Mengelompokkan karakter berdasarkan FontIsEmbedded menyingkap font tidak tertanam yang harus diganti oleh biro cetak atau server
procedure ReportNonEmbeddedFonts(Pdf: TPdf);
var
  Embedded, External: TStringList;
  I: Integer;
  Name: string;
begin
  Embedded := TStringList.Create;
  External := TStringList.Create;
  try
    Embedded.Sorted := True;
    Embedded.Duplicates := dupIgnore;
    External.Sorted := True;
    External.Duplicates := dupIgnore;

    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      Name := Pdf.FontBaseName[I];
      if Name = '' then
        Continue;              // spasi bentukan dan sejenisnya tidak berfont
      if Pdf.FontIsEmbedded[I] then
        Embedded.Add(Name)
      else
        External.Add(Name);
    end;

    if External.Count > 0 then
      ShowMessage(IntToStr(External.Count) +
        ' non-embedded font(s):' + sLineBreak + External.Text)
    else
      ShowMessage('All ' + IntToStr(Embedded.Count) +
        ' font(s) on this page are embedded.');
  finally
    Embedded.Free;
    External.Free;
  end;
end;

Dua detail menjaga ini tetap jujur. Pertama, CharacterCount berlaku per halaman, sehingga audit seluruh dokumen berarti menyetel Pdf.PageNumber ke setiap halaman secara bergiliran lalu menjalankan loop-nya lagi dan menggabungkan hasilnya. Kedua, lapisan teksnya memuat karakter bentukan seperti spasi yang disimpulkan pembacanya di antara kata, dan karakter itu tidak punya objek font di belakangnya; pemeriksaan base name kosong melewatinya alih-alih mencatat hantu. Base name adalah kunci yang tepat untuk deduplikasi di sini karena prefiks subset yang dibawanya membedakan dua subset berbeda dari family yang sama, dan biasanya itulah yang ingin Anda ketahui

Menarik keluar face yang tertanam

Ketika sebuah font tertanam, Anda bisa membaca byte-nya secara langsung. FontData mengembalikan program font mentahnya, data TrueType atau CFF yang sama yang dibawa PDF-nya, dan itu sudah cukup untuk menulis sebuah file font mandiri atau untuk mengambil sidik jari face-nya terhadap sebuah pustaka yang sudah dikenal. Ia mengembalikan array kosong ketika font-nya tidak tertanam, sehingga pemeriksaan penanaman dan pemeriksaan panjangnya bersama-sama menjaga penulisannya

procedure SaveEmbeddedFont(Pdf: TPdf; CharIndex: Integer;
  const OutputFile: string);
var
  Data: TBytes;
  Stream: TFileStream;
begin
  if not Pdf.FontIsEmbedded[CharIndex] then
  begin
    ShowMessage('That glyph''s font is not embedded; nothing to extract.');
    Exit;
  end;

  Data := Pdf.FontData[CharIndex];
  if Length(Data) = 0 then
    Exit;

  Stream := TFileStream.Create(OutputFile, fmCreate);
  try
    Stream.WriteBuffer(Data[0], Length(Data));
  finally
    Stream.Free;
  end;
  ShowMessage('Wrote ' + IntToStr(Length(Data)) + ' bytes.');
end;

Byte-nya adalah subset yang tertanam, bukan font ritel aslinya, sehingga yang Anda peroleh biasanya hanya mencakup glyph yang benar-benar dipakai dokumen itu. Hal itu tepat sekali untuk forensik dan verifikasi namun buruk untuk dipakai ulang; subset Times New Roman yang berisi tiga puluh glyph bukanlah font yang bisa Anda pasang lalu pakai mengetik. Perlakukan ekstraksi sebagai cara memeriksa apa yang dikirimkan, bukan sebagai tool pemulihan font. Jika Anda butuh base name yang cocok untuk melabeli keluarannya, baca FontBaseName[CharIndex] berdampingan dengan datanya, lalu buang tag subset di depannya bila Anda menginginkan family-nya saja

Memahami angka weight

FontWeight mengembalikan kelas weight numeriknya, skala 100 sampai 900 yang sama seperti yang dipakai CSS, tempat 400 berarti regular dan 700 berarti bold. PDFium melaporkan apa pun yang dinyatakan font-nya, dan itu tidak selalu kelipatan seratus yang bulat; sebuah face bisa mengumumkan 350 atau 650, dan memperlakukan apa pun pada atau di atas 600 sebagai "cukup tebal untuk diperhitungkan" lebih tahan banting daripada menguji tepat 700. Sudut italic adalah sinyal pendampingnya: nilai bukan nol, biasanya negatif, berarti face-nya adalah desain oblique atau italic sejati, dan nol berarti tegak. Bersama-sama keduanya membiarkan Anda membedakan run bold-italic dari yang regular tanpa me-render apa pun, dan itulah jenis pemeriksaan yang ingin dilakukan secara massal oleh sebuah lintasan preflight atau audit aksesibilitas

Tak satu pun pembacaan ini membutuhkan bitmap yang sudah di-render. Semuanya berasal dari lapisan teks yang sudah di-parsing, jadi sebuah dokumen yang terbuka pada halaman yang tepat sudah menjadi seluruh penyiapan yang Anda perlukan, dan itu membuat pemeriksaan font murah untuk dijalankan pada seluruh arsip. Jika Anda memasangkannya dengan ekstraksi teks, indeks karakter yang sama berbaris dengan teks yang Anda tarik keluar, sehingga font sebuah glyph dan nilai Unicode-nya adalah dua kali pembacaan terhadap satu indeks. Artikel pendamping tentang ekstraksi teks dari dokumen PDF dengan PDFium Component membahas sisi lapisan teks itu lebih mendalam

Properti font yang ditampilkan di sini adalah bagian dari PDFium Delphi VCL Component