Teknik Makale

Delphi'de PDFium Component ile PDF Yazı Tipi Özelliklerini Okuma

Bir PDF'teki görünür her karakter, kendisini çizen yazı tipine (font) bir referans taşır ve PDFium Component, bu referansı yazı tipi nesnesine kadar izlemenize ve bildiklerini okumanıza olanak tanır. Erişim birimi belge değil karakterdir: sayfa metnindeki indeksine göre bir karakter seçer ve aile adını, temel adını, ağırlığını, italik açısını ve temel yazı tipinin (face) dosya içinde gerçekten taşınıp taşınmadığını sorarsınız. Son özellik, çoğu analizin asıl peşinde olduğu özelliktir, çünkü gömülü bir yazı tipi belgeyle birlikte seyahat ederken, gömülü olmayan bir yazı tipi okuyucunun makinesinde aynı yazı tipinin kurulu olduğuna dair bir varsayımdan ibarettir

Bileşen bunları, işleme (rendering) ve metin çıkarma için kullandığınız aynı TPdf ve TPdfView nesneleri aracılığıyla sunar. Açılacak ayrı bir "yazı tipi tablosu" nesnesi yoktur. Bir sayfanın metni ayrıştırıldıktan sonra, yazı tipi özellikleri karakter indeksine asılır ve bunları her seferinde bir glif (glyph) olmak üzere okursunuz. Bu tasarım, PDF'in bilgiyi en başta nasıl sakladığına uyar: tek bir sayfa onlarca kez yazı tipi değiştirebilir ve "bu belge hangi yazı tipinde" sorusunun tek dürüst cevabı "hangi karakteri kastettiğinize bağlıdır" olur

Bir karakterin arkasındaki yazı tipini okuma

En küçük yararlı işlem, bir karakter indeksi almak ve PDFium'un yazı tipi hakkında size söyleyebileceği her şeyi dökmektir. TPdf ve TPdfView üzerindeki her yazı tipi özelliği karakter konumuna göre indekslenir, bu nedenle indeks hepsinin içinden geçer. Ayrıca indeksin doğru metne karşı çözümlenmesi için sayfanın mevcut sayfa olması gerekir; bu durum birinci sayfayı geçtiğinizde önem kazanır

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;

Diğer kütüphanelerden gelenler için imzalardan (signatures) birkaçı şaşırtıcı olabilir. FontAscent ve FontDescent iki argüman alır: karakter indeksi ve punto (point) boyutu; çünkü PDFium bu metrikleri yalnızca metnin ayarlandığı boyuta göre ölçeklendirdiğinizde piksel haline gelen glif uzayı birimlerinde raporlar. FontSize[CharIndex] üzerinden zaten okuduğunuz değeri ilettiğinizde, düzenin (layout) geri kalanıyla aynı punto cinsinden üst (ascent) ve alt (descent) çıkıntı değerlerini alırsınız. Alt çıkıntı, taban çizgisinin altını ölçtüğü için negatif olarak döner. Aile adı ve temel ad bilerek ayrı dizelerdir (strings): temel ad PDF'teki ham /BaseFont girdisidir ve genellikle ABCDEF+ gibi bir alt küme ön eki taşırken, aile adı işleyicinin çözümlediği temizlenmiş addır

Bir tıklamayı karakter indeksine dönüştürme

Bir görüntüleyicide (viewer) indeksi önceden nadiren bilirsiniz. Kullanıcı bir glife tıklar ve piksel koordinatını altındaki karaktere çevirmeniz gerekir. CharacterIndexAtPos tam olarak bunu yapar; fare konumunu ve bir tolerans değerini alarak en yakın karakterin indeksini veya tıklama boşluğa ya da boş bir sayfaya denk geldiğinde negatif bir değer döndürür

procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Index: Integer;
begin
  if not PdfView.Active then
    Exit;

  // 4 px of slack in each direction so a near-miss still hits the glyph.
  Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
  if Index < 0 then
    Exit;                      // clicked between glyphs; leave the panel alone

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

Toleransı ayarlamaya değer. Çok sıkı olursa kullanıcılar harfin tam sapına (stem) denk getirmek zorundaymış gibi hissederler; çok gevşek olursa kenar boşluğuna yapılan bir tıklama, kastedilenle hiç ilgisi olmayan uzak bir karaktere yapışır. Ekran görüntülemesi için üç ila beş cihaz pikseli makul bir başlangıç noktasıdır. Döndürülen indeks, mevcut sayfanın ayrıştırılmış metnine aittir; yani her yazı tipi özelliğinin beklediğiyle aynı indeks alanıdır, bu nedenle yukarıdaki rutine doğrudan iletebilirsiniz. Bunu CurrentCharIndex içinde saklamak isteğe bağlıdır ancak kullanışlıdır: görünüm (view) bunu odaklanılan glif kavramı olarak tutar ve bu da kullanıcı arayüzünün diğer kısımları seçimi yeniden türetmeden okumak isterse elverişlidir

Önemli olan özellik gömülü olmasıdır

Çoğu gerçek iş için cevaplanmaya değer tek soru, her bir yazı tipinin gömülü (embedded) olup olmadığıdır. Tüm yazı tiplerini içinde barındıran bir belge; bir baskı bürosunun RIP'inde, bir iş arkadaşının dizüstü bilgisayarında ve hiçbir GUI'si olmayan bir sunucuda aynı şekilde işlenir (renders). Gömülü olmayan bir Helvetica'ya dayanan bir belge, bu makinelerin her birinin eşleşen bir yazı tipine sahip olduğu üzerine kumar oynar ve kumar başarısız olduğunda okuyucu yakın bir şeyle değiştirir, metrikler kayar ve özenle düzenlenmiş bir form kırılacak kadar yeniden şekillenir (reflows). Sayfa metnini gezinmek ve yazı tiplerini gömülme durumlarına göre gruplamak (bucketing) size bu cevabı ucuz bir şekilde verir

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;              // generated spaces and the like have no font
      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;

İki ayrıntı bunu dürüst tutar. İlk olarak, CharacterCount sayfa başınadır, bu nedenle tüm belgenin denetimi, Pdf.PageNumber'ı sırayla her sayfaya ayarlamak ve döngüyü yeniden çalıştırıp sonuçları birleştirmek anlamına gelir. İkinci olarak, metin katmanı okuyucunun kelimeler arasında çıkarsadığı boşluklar gibi oluşturulmuş karakterler içerir ve bunların arkasında hiçbir yazı tipi nesnesi yoktur; boş temel ad (empty-base-name) kontrolü, hayalet bir girdi oluşturmak yerine bunları atlar. Taşıdığı alt küme (subset) ön eki, genellikle bilmek isteyeceğiniz aynı ailenin iki farklı alt kümesini ayırdığı için, burada tekilleştirme (de-duplication) için doğru anahtar temel addır

Gömülü yazı tipini dışarı çıkarma

Bir yazı tipi gömüldüğünde baytlarını doğrudan okuyabilirsiniz. FontData, ham yazı tipi programını, yani PDF'in taşıdığı aynı TrueType veya CFF verilerini döndürür; bu, bağımsız bir yazı tipi dosyası yazmak veya yazı tipinin bilinen bir kütüphaneye karşı parmak izini çıkarmak (fingerprint) için yeterlidir. Yazı tipi gömülü olmadığında boş bir dizi döndürür, bu nedenle gömülme kontrolü ve uzunluk kontrolü birlikte yazma işlemini korur

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;

Baytlar, orijinal perakende yazı tipi değil gömülü alt kümedir, bu nedenle geri aldığınız şey genellikle yalnızca belgenin gerçekte kullandığı glifleri kapsar. Bu, adli tıp ve doğrulama için tam olarak doğrudur, ancak yeniden kullanım için zayıf bir uyumdur; otuz glif içeren bir Times New Roman alt kümesi, yükleyip yazabileceğiniz bir yazı tipi değildir. Çıkarmayı (extraction), bir yazı tipi kurtarma aracı olarak değil, neyin gönderildiğini incelemenin bir yolu olarak ele alın. Çıktıyı etiketlemek için eşleşen temel ada ihtiyacınız varsa, veriyle birlikte FontBaseName[CharIndex] değerini okuyun ve salt aileyi (bare family) istiyorsanız baştaki alt küme etiketini (subset tag) çıkarın

Ağırlık sayısını anlamlandırma

FontWeight, CSS'in kullandığı aynı 100'den 900'e kadar olan ölçekte (400'ün normal, 700'ün kalın/bold olduğu) sayısal ağırlık sınıfını döndürür. PDFium yazı tipi ne beyan ediyorsa onu raporlar, bu her zaman yuvarlak bir yüzlük dilim değildir; bir yazı tipi 350 veya 650 bildirebilir ve 600 veya üzerindeki herhangi bir değeri "önemli olacak kadar kalın" olarak değerlendirmek, tam olarak 700'ü test etmekten daha iyi dayanır. İtalik açısı eşlik eden bir sinyaldir: genellikle negatif olan sıfır dışı bir değer, yazı tipinin eğik (oblique) veya gerçek italik bir tasarım olduğu anlamına gelir ve sıfır dik olduğu anlamına gelir. Birlikte, hiçbir şeyi işlemeden (rendering) kalın-italik (bold-italic) bir diziyi normal bir diziden ayırt etmenizi sağlarlar; bu, bir baskı öncesi (preflight) geçişinin veya bir erişilebilirlik denetiminin toplu olarak yapmak isteyeceği türden bir kontroldür

Bu okumaların hiçbiri işlenmiş bir bitmap gerektirmez. Ayrıştırılmış metin katmanından gelirler, bu nedenle doğru sayfada açık bir belge ihtiyacınız olan tüm kurulumdur; bu da yazı tipi incelemesini tüm bir arşiv boyunca çalıştırmayı ucuz hale getirir. Bunu metin çıkarma işlemiyle eşleştiriyorsanız, aynı karakter indeksleri çıkardığınız metinle hizalanır, böylece bir glifin yazı tipi ve Unicode değeri tek bir indekse karşı yapılan iki okumadır. PDFium Component ile PDF belgelerinden metin çıkarma hakkındaki eşlik eden makale, metin katmanının o yönünü daha derinlemesine ele almaktadır

Burada gösterilen yazı tipi özellikleri PDFium Delphi VCL Component'in bir parçasıdır