Teknik Makale

PDF'te Gömülü Yazı Tiplerini Delphi'den Alt Kümeye Ayırmak

PDFium Component, mevcut bir PDF'e hâlihazırda gömülü yazı tiplerini FPdfDocumentFontSubset ünitesindeki SubsetPdfDocumentFonts ile alt kümeye ayırır. Fonksiyon, gömülü bir TrueType /FontFile2 programını yalnızca font kaynakları belgenin referans verebileceği her glifi kendileri kanıtlıyorsa daha küçüğüyle değiştirir; glif alanı kanıtlanamayan bir font olduğu gibi bırakılır

Bunu istemenin olağan nedeni, sizin üretmediğiniz bir arşivdir. Eski üreticilerden gelen faturalar, ekstreler ve raporlar çoğu zaman her dosyada eksiksiz bir Arial ya da tam bir CJK yazı yüzü taşır; iki sayfalık belge başına birkaç yüz kilobaytlık font, birkaç milyon belgede hızla birikir. Kaynaktan yeniden render etmek nadiren bir seçenektir. Elinizde olan PDF'tir ve tek güvenli soru şudur: gömülü her fontun ne kadarı, tek bir sayfa tek bir karakter kaybetmeden gidebilir

Mevcut bir PDF'i alt kümeye ayırmak glif kaybetme riskini neden taşır?

Mevcut bir PDF'i alt kümeye ayırmak glif kaybetme riski taşır, çünkü son işlemci özgün yazıcının hangi glifleri kullandığını bilmez. Fontu gömen üretici kendi metnini biliyordu; sonra çalışan bir araç o bilgiyi yeniden kurmak zorundadır ve kaçırdığı her glif, müşteri bakana dek kimsenin bakmadığı bir sayfada boş bir kutuya ya da bir .notdef şekline dönüşür

Bariz yaklaşım, her içerik akışını, her Form XObject'i, her açıklama görünümünü ve her alan varsayılan görünümünü ayrıştırmak, her fontla gösterilen kodları toplamak ve o glifleri tutmaktır. Sayfalar ile formlar arasında paylaşılan bir font kaynağına, görünümünü bir görüntüleyicinin /DA'dan yeniden ürettiği bir form alanına ya da tarayıcının modellemediği bir operatör dizisine sahip bir içerik akışına denk gelene kadar çalışır. SubsetPdfDocumentFonts bilinçli olarak içerik akışlarını taramaz. Glif alanını, PDF'in her font hakkında zaten taşıması gereken üst veriden türetir: /CIDToGIDMap, /W ile /W2 genişlik dizileri, /CIDSet bit eşlemi, /ToUnicode CMap'i, basit font /Widths dizisi ve /Encoding. O üst veri alanı sınırlamıyorsa font dokunulmadan kalır

Delphi'den gömülü TrueType fontlarını alt kümeye ayırırken PDFium Component glif alanını nasıl kanıtlar: /CIDToGIDMap, /W ve /W2 genişlikleri, /CIDSet, /ToUnicode, /Widths ile /Encoding alanı içerik akışlarını taramadan sınırlar ve kanıtlanamayan font dokunulmadan korunur
SubsetPdfDocumentFonts, bir şeyi kesmeden önce font üst verisini kanıtlanmış bir glif alanına çevirir; kaynakları alanı sınırlayamayan font raporlanır ve dokunulmadan bırakılır

Özgün glif kimliklerini korumak, böylece içerik akışları dokunulmaz kalır

SubsetPdfDocumentFonts'in arkasındaki alt küme arka ucu olan TPdfTrueTypeSubsetBackend, her özgün glif kimliği yuvasını korur; hiçbir içerik akışının, CID'in ya da /CIDToGIDMap'in yeniden yazılması gerekmez. glyf ile loca tablolarını yeniden kurar, kullanılmayan her glife sıfır uzunluklu bir loca aralığı verir ve maxp.numGlyphs'i değişmeden bırakır. 4711 glifi, alt kümeye ayrıldıktan sonra hâlâ 4711 glifidir; yalnızca belge ona ulaşabiliyorsa konturları vardır

O seçim, bitmiş bir belgeyi alt kümeye ayırmanın en riskli bölümünü ortadan kaldırır. Glifleri yeniden numaralandırmak, fontu kullanan her içerik akışındaki her dizgeyi, artı eşleme akışını, artı glif kimliklerini önbellekleyen her şeyi yeniden yazmak demek olurdu. Korunan kimlikler biraz boyut tutar, çünkü loca özgün glif başına bir girdi tutar; ama bir TrueType programının çoğu kontur verisidir ve giden de odur. Bileşik glifler bileşenlerine kadar izlenir; aksanlı bir harf, temel harfi ve onun kurulduğu aksanı korur. Döngüsel ya da fazla derin bileşik referanslar bozuk bir kapanış üretmek yerine fontu başarısız kılar

Delphi'de PDFium Component ile gömülü TrueType fontlarını alt kümeye ayırdıktan sonra korunan glif kimlikleri: 4711 glifi numarasını ve konturlarını korur; kullanılmayan glifler sıfır uzunluklu loca aralıkları alırken maxp.numGlyphs değişmeden kalır; böylece içerik akışları, CID'ler ve /CIDToGIDMap hiçbir zaman yeniden yazılmaz
Glif kimlikleri alt kümeyi sağ çıktığı için içerik akışları, CID'ler ve /CIDToGIDMap'in yeniden yazılması gerekmez — boyut kazancı atılan konturlardan gelir, yeniden numaralandırmadan değil

Yeniden adlandırılan font ayrıca ISO 32000-1 §9.6.4'ü izler; o, alt küme font adının altı büyük harf ile bir artı işaretiyle başlamasını ister. Bileşen altı harfi yeni font programının SHA-256 özetinden türetir, önce var olan bir ABCDEF+ önekini söker, aynı belgedeki fontlar arasındaki çakışmaları çözer ve yeni adı hem tanımlayıcı /FontName'e hem font sözlüğü /BaseFont'a yazar; bir CIDFont'un Type0 ebeveyni dahil

uses
  System.SysUtils, System.Classes, FPdfDocumentFontSubset;

procedure SubsetEmbeddedFonts(const SourceName, TargetName: string);
var
  Source, Target: TFileStream;
  Options: TPdfDocumentFontSubsetOptions;
  Report: TPdfDocumentFontSubsetReport;
  Output: TBytes;
begin
  Options := TPdfDocumentFontSubsetOptions.Default;
  Source := TFileStream.Create(SourceName, fmOpenRead or fmShareDenyWrite);
  try
    if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
      raise Exception.Create('Font subsetting failed: ' + Report.ErrorMessage);
  finally
    Source.Free;
  end;
  // Çıktı daima eksiksiz bir PDF'tir: Report.Applied True iken yeniden kurulmuş dosya,
  // değilse kaynağın bayt bayt kopyası
  Target := TFileStream.Create(TargetName, fmCreate);
  try
    if Length(Output) > 0 then
      Target.WriteBuffer(Output[0], Length(Output));
  finally
    Target.Free;
  end;
end;

SubsetPdfDocumentFonts bir CIDFont'un glif alanını nasıl kanıtlar?

SubsetPdfDocumentFonts, bir CIDFontType2 fontunun glif alanını CID'den glife eşlemesinden kanıtlar ve eşleme, ne kadar ek kanıt gerektiğine karar verir. Açık bir /CIDToGIDMap akışı (ISO 32000-1 §9.7.4.2), fontun adresleyebileceği her glifin listesidir; akıştaki her GID alana katılır. 65536 girdiden büyük bir eşleme akışı ya da tek sayıda bayt taşıyan biri, sınırlı profili başarısız kılar ve font korunur

/CIDToGIDMap /Identity zor durumdur ve modern dosyalarda en yaygın olanı da odur. Identity, CID'in GID'ye eşit olduğunu söyler; bu size belgenin hangi CID'leri kullandığı hakkında hiçbir şey söylemez. Bileşen bu yüzden birincil kanıt ister: üreticinin ölçü bildirdiği glifleri işaretleyen açık bir /W ya da /W2 genişlik dizisi (§9.7.4.3) ya da font tanımlayıcısındaki (§9.8.3) bir /CIDSet akışı — CID başına bir bitlik, en anlamlı bit önde bir bit eşlemi. İkisinden de yoksun çıplak bir Identity fontu, Identity CIDToGIDMap requires an explicit W array or CIDSet stream gerekçesiyle korunur

/W'yi kanıt olarak kullanmanın tuzağı /DW'dir. Bir üreticinin, genişliği varsayılan genişliğe eşit olan her CID'i dışarıda bırakmasına izin verilir ve iyi optimize edilmiş bir yazıcı en yaygın glif genişlikleri için tam olarak bunu yapar. Alan yalnızca /W'den gelseydi o glifler alt kümeden sessizce kaybolurdu. Bu boşluğu kapatmak için bileşen, /DescendantFonts'u CIDFont'u işaret eden her Type0 fontunu dolaşır; /Encoding'inin 2 baytlık kodun CID olması için /Identity-H ya da /Identity-V olmasını ister ve /ToUnicode CMap'inin (§9.10.3) her beginbfchar ile beginbfrange girdisinin kaynak kodlarını alana katar. Başka bir kodlama ya da bütçe içinde çözülemeyen bir /ToUnicode akışı fontu korur

Delphi için PDFium Component font alt kümelemede bir Identity CIDToGIDMap için kanıt kararı: açık bir eşleme akışı her glifi hâlihazırda listeler; Identity bir /W ya da /W2 genişlik dizisi ya da /CIDSet bit eşlemi ister; /ToUnicode kaynak kodları /DW varsayılan genişlik kör noktasını kapatır
Bir Identity CIDToGIDMap kendi başına kullanım hakkında hiçbir şey söylemez; alt kümeci bu yüzden /W ya da /CIDSet kanıtı ister ve /W'nin /DW'de attığı glifleri kapsamak için /ToUnicode kodlarını katar

Basit TrueType fontları: Widths'ten glif kimliklerine

Tek baytlık /Subtype /TrueType fontları kod aralıklarıyla ve kodlamalarıyla kanıtlanır, sonra font programının kendi cmap'i üzerinden glif kimliklerine çözülür. Bileşen 0 <= FirstChar <= LastChar <= 255 ile uzunluğu tam olarak LastChar - FirstChar + 1 olan bir /Widths dizisi ister; kısa, uzun ya da sayısal olmayan bir dizi, kod alanının kanıtlanmadığı anlamına gelir ve font olduğu gibi kalır

O aralıktaki her kod, ayrıca /Differences'te adı geçen ya da /ToUnicodece kapsanan her kod, sonra ISO 32000-1 §9.6.6'nın anlattığı biçimde eşlenir. /WinAnsiEncoding, /MacRomanEncoding ya da /StandardEncoding altındaki kodlar, eşleşen tablodan Unicode'a gider. /Differences glif adları Adobe Glyph List, uniXXXX ile uXXXX adları, 258 standart Macintosh glif adı ve fontun kendi post biçim 2.0 ad tablosu üzerinden çözülür. Sonuç değerler; Unicode (3,1), (3,10) ile (0,*), Mac Roman (1,0) ve F000 artı kod olan Windows Symbol (3,0) dahil, biçim 0, 4, 6 ve 12 cmap alt tablolarında aranır

Bir fontun hiç taban kodlaması olmadığında bileşen, bir tanesini tahmin etmek yerine her makul yorumun birleşimini işaretler. Bu, muhtemelen kullanılmayan birkaç glifi tutar; hata payının doğru yöndür. Ters durum katıdır: kanıtlanmış hiçbir glif kimliğine eşlenmeyen bir /Differences adı bütün fontu reddettirir; tek çözülemeyen ad, alanın eksiksiz olmadığı anlamına gelir

procedure ReportFontSubset(Source: TStream; Log: TStrings);
var
  Options: TPdfDocumentFontSubsetOptions;
  Report: TPdfDocumentFontSubsetReport;
  Output: TBytes;
  I: Integer;
begin
  Options := TPdfDocumentFontSubsetOptions.Default;
  Options.SubsetOptions.MinimumSavingPercent := 10;  // font başına
  Options.MinimumDocumentSavingPercent := 5;         // bütün dosya
  if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
  begin
    Log.Add('Rejected: ' + Report.ErrorMessage);
    Exit;
  end;
  Log.Add(Format('Fonts %d, subset %d, preserved %d, glyphs %d',
    [Report.FontCount, Report.SubsetFontCount, Report.PreservedFontCount,
     Report.GlyphCount]));
  Log.Add(Format('Font bytes %d -> %d, file bytes %d -> %d',
    [Report.OriginalFontBytes, Report.SubsetFontBytes,
     Report.SourceByteCount, Report.OutputByteCount]));
  for I := 0 to High(Report.Issues) do
    Log.Add(Format('  object %d kept: %s',
      [Report.Issues[I].ObjectNumber, Report.Issues[I].Reason]));
end;

SubsetPdfDocumentFonts neye dokunmaz?

SubsetPdfDocumentFonts, kanıtlayamadığı her fonta dokunmaz ve belgeyi başarısız kılmak yerine her birini Report.Issues içinde nesne numarası ve bir gerekçeyle raporlar. Report.Complete yalnızca hiçbir font korunmadığında True'dur. Boyut azaltımı sözü vermeden önce korunan durumları bilmeye değer:

  • Type 1 /FontFile ile CFF ya da OpenType /FontFile3 programları; Only TrueType FontFile2 programs are supported olarak raporlanır
  • /W, /W2 ya da /CIDSet taşımayan Identity CIDFont'ları; eşleme akışı da /Identity de olmayan CIDFont'lar
  • Font tanımlayıcıları dışında bir yerden referans verilen font programları; CIDFontType2 ya da basit TrueType olmayan bir tüketiciyle paylaşılan tanımlayıcılar
  • Alt kümesi SubsetOptions.MinimumSavingPercent'i kaçıran fontlar ve yeniden kurulan dosya MinimumDocumentSavingPercent'i kaçırdığında bütün fontlar; o durumda Report.Applied False olur ve çıktı özgün baytlardır

İki girdi korunmak yerine tümüyle başarısız olur. İmzalı belgeler ile imza alanı taşıyan belgeler reddedilir; çünkü font nesnelerini değiştirmek her imzanın byte aralığını geçersiz kılar ve şifreli dosyaların önce çözülmüş bir kaynağa ihtiyacı vardır. Başarılı bir çalıştırma, değiştirme nesnelerini seyrek bir güncelleme olarak yazar ve sonra dosyayı yetim nesne budama ve kaynak tekilleştirme rehberinde anlatılan aynı erişilebilirlik geçişiyle yeniden kurar; eski font programları ulaşılamaz nesneler olarak hayatta kalmaz ve sonuç tek bir temiz revizyondur

saSubsetNewFonts ile kaydetme sırasında yeni metni alt kümeye ayırmak

PDFium Component'in geçerli oturumda gömdüğü fontlar ayrı bir durumdur; SubsetPdfDocumentFonts tarafından değil saSubsetNewFonts kaydetme seçeneği tarafından işlenir. saSubsetNewFonts, PDFium FPDF_SUBSET_NEW_FONTS bayrağına eşlenir; bayrak, TPdf.AddText gibi yerel çağrıların sistemden yüklediği fontlar üzerinde — OpenType CFF yüzleri dahil — yerel HarfBuzz alt kümecisini çalıştırır. Upstream PDFium bayrağı hâlâ deneysel etiketliyor. Dosyada hâlihazırda olan fontlara dokunmaz

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    Pdf.AddPage(0, 595, 842);
    Pdf.AddText('Quarterly statement', 'Arial', 14, 72, 770, clBlack, $FF, 0.0);
    if not Pdf.SaveAs('statement.pdf', saSubsetNewFonts) then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

Bileşenin sondasında Arial metinli bir belge saSubsetNewFonts altında 590 KB'den 22 KB'ye indi ve kaydedilen dosyadan metin çıkarma yine tam olarak eşleşti. 7.0 MB'lik bir COLR v0 renk fontu olan Segoe UI Emoji aynı yoldan 10.5 KB'ye geçti; PDF'in COLR katmanı kavramı olmadığı için temel konturları olarak render edildi. Değişken fontlar istisnadır ve blok, alt kümecinin önünde durur: GDI GetFontData, 1.85 MB'lik Segoe UI Variable dosyası için 18.3 MB'lik bir yedek akışı döndürdü ve metin nesnesi oluşturma, hiçbir alt kümeleme başlamadan başarısız oldu. Delphi'den CID Type 2 font gömme gezintisi o fontların önce geçtiği gömme yolunu anlatır

Kanıtın nerede durduğu

SubsetPdfDocumentFonts'in içindeki kanıt, üreticinin yazdığı font üst verisi kadar güçlüdür. Kanıtı yalnızca /W olan bir Identity CIDFont'un hâlâ bir kör noktası vardır: genişliği /DW'ye eşit olan, /ToUnicode girdisi olmayan ve tanımlayıcının bir /CIDSet'te listelemediği bir glif. Böyle bir glif kaynaklardan kanıtlanamaz ve bileşen, bilinçli olarak kaçındığı içerik akışı taraması olmadan onun var olduğunu bilemez. /W'yi sıkıştırırken hem /ToUnicode'yi hem /CIDSet'i atan üreticiler nadirdir ama vardır; o yüzden bilinmeyen bir üreticiden gelen arşiv için öncesinde ve sonrasında bir örnek render edip pikselleri ve çıkarılan metni karşılaştırın

Toplu bir işi çalıştırmadan önce neyle karşı karşıya olduğunuzu bilmek de işe yarar. PDFium Component ile PDF font özelliklerini okuma yaklaşımı hangi fontların hiç gömülü olduğunu söyler; bir deneme çalıştırmasından gelen Report.Issues ise hangilerinin korunacağını ve nedenini söyler. Varsayılan seçenekler işi de sınırlar: 1024 font, toplamda yaklaşık bir milyon glif, 128 MB kaynak ve sabit bir iş bütçesi; uzun toplu işler için isteğe bağlı bir CancellationToken ile

SSS

SubsetPdfDocumentFonts, PDF'in metnini ya da düzenini değiştirir mi?

Hayır. Özgün glif kimlikleri, CID'ler, genişlikler ve içerik akışları korunur; metin çıkarma ile render öncekiyle aynı glifleri kullanır. Yalnızca ulaşılamayan konturlar kaldırılır ve font adları yeni bir alt küme öneki alır

Gömülü bir font neden alt kümeye ayrılmadı?

Report.Issues'i okuyun. Korunan her font bir gerekçe taşır: Identity bir CIDFont'ta eksik bir /W ya da /CIDSet, TrueType olmayan bir program, bozuk bir /Widths dizisi ya da eşiğin altında bir kazanç gibi

saSubsetNewFonts, SubsetPdfDocumentFonts ile aynı şey mi?

Hayır. saSubsetNewFonts, kaydederken geçerli oturumda gömülen fontları alt kümeye ayırır; SubsetPdfDocumentFonts ise mevcut bir PDF akışı üzerinde çalışır ve başka bir üretici tarafından hâlihazırda gömülmüş fontları değiştirir

Belge font alt kümelemesi, kaydetme sırasında font alt kümelemesi ve arkalarındaki yetim budama geçişi Delphi ve C++Builder için PDFium Component ile gelir; bileşen Delphi, C++Builder ve Lazarus projeleri için PDFium'u sarmalar