Teknik Makale

PDFium ile Delphi'de PDF Yazı Tipi İkamesini Kontrol Etme

PDFium Component, bir PDF gömmediği bir yazı tipine başvurduğunda hangi yazı tipi baytlarının kullanılacağına bir Delphi uygulamasının karar vermesine izin verir. ConfigureSystemFontProvider, PDFium'un yaptığı her yazı tipi eşleme isteğini — yüz adı, ağırlık, italik bayrağı, karakter kümesi ve aralık ailesiyle (pitch family) birlikte — alan ve kullanılacak TrueType, TrueType Collection veya OpenType baytlarıyla yanıt veren bir IPdfSystemFontProvider uygulaması kurar

Bu, gömülü olmayan yazı tiplerinin bir render piyangosu olmasından dolayı vardır. Arial'i adlandıran ve hiçbir şey gömmeyen bir PDF, bir iş istasyonunda Arial ile, bir Linux sunucusunda metrik uyumlu bir ikameyle ve kilitli bir konteyner görüntüsünde ana bilgisayar eşleyicisinin bulduğu her neyse onunla render edilir. Aynı fatura her birinde farklı görünür, satır sonları kayar ve bir müşteri arşivlenen kopyayla eşleşmeyen bir belge alır

Yazı tiplerini neden doğrudan sunucuya kurmuyoruz?

Bazen cevap budur ve öyle olduğunda bunu tercih edin. Ama üç yaygın durumda başarısız olur. Lisanslama, otomatik render için bir yazı tipinin bir sunucuya kurulmasını yasaklayabilir. Konteyner görüntüleri sık sık yeniden derlenir ve elle kurulmuş bir yazı tipi bir sonraki dağıtımla (deployment) kaybolur. Ve düzenlenmiş (regulated) iş akışları, render yığınının sürüm kontrolü altındaki eserlerden yeniden üretilebilir olmasını gerektirir — makine genelinde bir yazı tipi kurulumu ise öyle değildir

Bir sağlayıcı, kararı uygulamanıza taşıyarak üçünü de ele alır. Yazı tipleri, kontrol ettiğiniz kaynaklar olarak gelir, eşleme ilkesi incelenebilir bir koddur ve aynı ikili dosya her yerde aynı şekilde render eder, çünkü hiçbir şey o an kurulu olana bağlı değildir

Bir sağlayıcı kurmak

Yapılandırma, kütüphane yüklenmeden önce gerçekleşmelidir. PDFium, başlatma sırasında bir sistem yazı tipi bilgisi yapısını kabul eder ve sonrasında dağıttığı tanıtıcıları (handle) tutar, bu yüzden belgeler açıkken bir sağlayıcıyı değiştirmek, PDFium'un hâlâ elinde tuttuğu yazı tipi tanıtıcılarını geçersiz kılardı; bileşen, bunun bir render'ı bozmasına izin vermek yerine doğrudan reddeder:

uses
  PDFium;

type
  TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
  public
    function ResolveFont(const Request: TPdfSystemFontRequest;
      out Font: TPdfSystemFontData): Boolean;
  end;

function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
  out Font: TPdfSystemFontData): Boolean;
var
  Path: string;
begin
  // Belirlenimci eşleme: yüz adı artı ağırlık ve italik, bu istek için
  // hangi dosyayı gönderdiğimize karar verir
  Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
    Request.Italic, Request.Charset);
  Result := Path <> '';
  if not Result then
    Exit;
  Font.FaceName := Request.FaceName;
  Font.FontData := LoadFileBytes(Path);   // eksiksiz sfnt veya TTC baytları
  Font.Charset := Request.Charset;
  Font.TTCIndex := 0;                     // bir koleksiyon içindeki indeks
end;

var
  Policy: TPdfSystemFontPolicy;
begin
  Policy := TPdfSystemFontPolicy.Default;
  Policy.AllowDefaultFallback := False;   // her şeye ana bilgisayar karar verir
  Policy.AllowFaceSubstitution := False;  // farklı bir yüz adını reddet
  Policy.MaxFontBytes := 32 * 1024 * 1024;
  Policy.MaxCacheEntries := 64;

  ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
  // Kütüphaneyi ve belgeleri ancak şimdi yükleyin
end;

Söküm (teardown) ters sırada çalışır: önce sağlayıcı PDFium'dan ayrılır, ardından kütüphane kaldırılır. Ayırmayı atlamak, yerel yazı tipi tanıtıcılarının serbest bırakılmak üzere olan Pascal nesnelerine işaret etmesine yol açar; bu, referans sayımlı arayüzleri bir C kütüphanesiyle karıştıran kodlardaki klasik kapatma erişim ihlalidir

İlke bayrakları gerçekte neye karar verir?

AllowDefaultFallback, iki çalışma modu arasındaki anahtardır. Kapalıyken, sağlayıcının reddettiği bir istek basitçe başarısız olur; bir kaynaktaki (corpus) her yazı tipinin hesaba katıldığını kanıtlarken istediğiniz şey de budur: herhangi bir boşluk, üzerinin örtülmesi yerine hemen görünür hale gelir. Açıkken, çözülmemiş istekler FPDF_GetDefaultSystemFontInfo'nun döndürdüğü eşleyiciye devredilir; dış dünya yine de tek bir tek tip tanıtıcı sarmalayıcısı görürken, yüz adı, karakter kümesi, tablo verisi ve yazı tipi silme kökenine göre doğru şekilde yönlendirilir

AllowFaceSubstitution, bir sağlayıcının istenenden farklı bir yüz adıyla yanıt verip veremeyeceğini yönetir. Bunu kapatmak, ikameyi bir kaza yerine açık bir karar haline getirir; bu, bir belge, metrikleri sayfalamayı değiştirecek kadar farklı olan bir yazı tipini adlandırdığında önemlidir

Bileşen, PDFium'a ulaşmadan önce her sağlayıcı yanıtını doğrular: boş veri reddedilir, aşırı büyük yazı tipleri MaxFontBytes'a karşı reddedilir, TTC indeksi denetlenir ve PDFium tüm dosya yerine bir tablo istediğinde tekil sfnt tabloları yazı tipi dizininden sunulur. Bu son yetenek, bir sağlayıcının ham Pascal nesnelerini C ABI'si üzerinden açığa çıkarmak yerine, eksiksiz bir yazı tipi dosyasını teslim edip bileşenin tablo düzeyi sorguları yanıtlamasına izin verebileceği anlamına gelir

Sarkan (dangling) yazı tipi verisi olmadan önbelleğe alma

Yazı tipi eşleme istekleri, render sırasında sürekli olarak tekrarlanır, bu yüzden yanıtlar, her yazı tipi seçim parametresini kapsayan bir anahtarla önbelleğe alınır ve sınırlı bir en-son-kullanılan (least-recently-used) sırayla tahliye edilir. İncelik, yaşam süresidir: PDFium, önbellek girdisi az önce tahliye edilmiş bir yazı tipinin baytlarını hâlâ okuyor olabilir

Önbellek, referans sayımlı dinamik diziler saklar ve her yerel tanıtıcı kendi anlık görüntüsünü tutar, bu yüzden tahliye, kullanımdaki belleği serbest bırakmak yerine bir referansı düşürür. Silme geri çağırması tanıtıcıyı serbest bırakır ve etkin bir sayaç tutar. Pratikte bu, MaxCacheEntries'in, devam eden bir render'ın altından veri çekme riski olmadan bellek için ayarlanabileceği anlamına gelir

Sağlayıcı benim iş parçacığımda mı çağrılır?

Hayır, mutlaka değil. PDFium, eşleyiciyi kendi işçi iş parçacıklarından çağırabilir, bu yüzden bir uygulamanın iş parçacığı açısından güvenli (thread safe) olması gerekir. Paylaşılan sayaçlar, önbellek ve yapılandırma gözlemi, bileşen içinde her biri kendi kritik bölümüyle korunur, ama ResolveFont içindeki kodu güvenli kılmak sizin işinizdir

En güvenli biçim, değiştirilebilir hiçbir paylaşılan duruma dokunmayan bir sağlayıcıdır: başlangıçta oluşturulmuş bir tablodan okuyun, bir dosyadan veya bir kaynaktan baytları yükleyin, döndürün. Bir arama kendi paylaşılan önbelleğinize ihtiyaç duyuyorsa onu koruyun (guard). Ve istisnaları uygulamanızın içinde tutun, çünkü bir Pascal istisnası hiçbir zaman PDFium yığınından geçerek sarmalanmamalıdır; bileşen C ABI sınırında yakalar ve bir başarısızlığa veya isteğe bağlı bir varsayılan geri düşüşe (fallback) dönüştürür, ama buna normal kontrol akışı olarak güvenmek performansa mal olur ve hataları gizler. Bileşenin geri kalanı için iş parçacığı kuralları, render kilidi disiplini yazısındakiyle aynı ilkeleri izler

Eşlemeyi üretimde kanıtlamak

İstatistikler, yazı tipi ikamesini tahminden, üzerinde iddia (assert) yapabileceğiniz bir şeye dönüştürür. GetSystemFontProviderStatistics, bir sağlayıcının yapılandırılıp kurulduğunu, kaç eşleme isteği yapıldığını ve bunların önbellek isabetleri, sağlayıcı isabetleri ve varsayılan geri düşüş isabetlerine bölünmüş olarak nasıl karşılandığını, reddedilen yanıtlar, başarısız istekler, canlı tanıtıcılar ve önbelleğe alınmış yazı tipleriyle birlikte bildirir:

var
  Stats: TPdfSystemFontStatistics;
begin
  Stats := GetSystemFontProviderStatistics;
  Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
    [Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
     Stats.DefaultFallbackHits]));
  Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
    [Stats.RejectedProviderResponses, Stats.FailedRequests,
     Stats.ActiveHandles, Stats.CachedFonts]));

  // Geri düşüş devre dışıyken yapılan bir uygunluk çalıştırmasında, herhangi
  // bir geri düşüş isabeti veya başarısız istek, bir belgenin göndermediğimiz
  // bir yazı tipine başvurduğu anlamına gelir
  if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
    raise Exception.Create('unmapped font encountered - update the font set');
end;

Yükselen bir RejectedProviderResponses sayısı, bir sağlayıcının ilkenin reddettiği verilerle — genellikle aşırı büyük bir dosya veya ikame edilmiş bir yüz — yanıt verdiğinin işaretidir ve bu istekler sessizce geri düşüşe veya başarısızlığa gerilediğinden bunun için uyarı kurmaya değer. Eşleme tablosunu oluşturmadan önce bir belgenin gerçekte hangi yazı tiplerine ihtiyaç duyduğunu teşhis etmek için, PDF yazı tipi özelliklerini analiz etme yazısındaki inceleme yolu, belge başına gömülü ve gömülü olmayan yazı tiplerini listeler

Yazı tipi sağlama, render etme ve metin çıkarma, Delphi, C++Builder ve Lazarus genelinde aynı kütüphane örneğini paylaşır; dağıtım ayrıntıları Delphi için PDFium Component sayfasında anlatılmıştır