Teknik Makale

Delphi'de HotPDF ile PDF'e CFF2 Değişken Font Gömmek

HotPDF, CFF2 dış hatlı OpenType değişken fontlarını, seçilen eksen değerlerinde, PDF okuyucularının çoktan anladığı bir CID-keyed CFF1 font programına çevirerek gömer. RegisterUnicodeTTF üzerinden kaydedilen sıradan yerel metin, fontun kendi cmap ve hmtx tablolarını kullanır ve yazıcı, dönüştürülen programı her CID orijinal glyph ID'ye eşit kalacak biçimde /Subtype /CIDFontType0C ile /FontFile3 altına gömer

Delphi'de HotPDF'in bir CFF2 değişken fontu, kayıtlı baytlar yolunun GDI ikamesini atladığı FontFile3 altında CID-keyed CFF1 olarak gömme hattı
HotPDF varyasyonu seçilen eksen değerlerinde dondurur ve her CID'in orijinal glyph ID'ye eşit olduğu bir CID-keyed CFF1 programı yazar; böylece numaralandırmalar arasında çeviri yapan bir katman olmaz

Bariz yolu anlatmaya değer, çünkü kimseye haber vermeden başarısız oluyor. CFF2 tablolarını çözümleyip önbelleğe alın, sonra düz metin yolu fontu yalnızca TrueType eşlemesi yapan Windows GDI üzerinden çözsün: GDI, CFF2 dış hatlarla ne yapacağını bilmez ve sessizce ArialUnicodeMS ikamesine geçer. İkameyi okuyan her şey, yanlış dış hatları, yanlış genişlikleri ve tamamlayıcı düzlem karakterler için yanlış çıkarımı alır. PDF sorunsuz açılır; yalnızca istediğiniz fontla dizilmemiştir. Metin yolu bu yüzden GDI sorgusu yerine kayıtlı baytlardan çalışmak zorunda

PDF neden bir CFF2 fontu doğrudan gömemiyor?

PDF'in CFF2 programı için bir yuvası yoktur. ISO 32000-2 §9.9, bir okuyucunun işlemesi gereken gömülü font program türlerini listeler: /FontFile altında Type 1, /FontFile2 altında TrueType ve /FontFile3 altında çıplak bir CFF programı (Type1C ya da CIDFontType0C) ya da TrueType ya da CFF dış hatlarını saran bir OpenType sarmalayıcısı. O listedeki her CFF türü, okuyucuların yirmi yıldır çözümlediği biçim olan CFF sürüm 1 demektir

Üç PDF gömülü font yuvası, tamamı CFF sürüm 1 ile sınırlı; HotPDF'i Delphi'de gömmeye başlamadan dönüştürmeye zorlayan eksik CFF2 yuvası
PDF okuyucularının işlemesi gereken her CFF türü CFF sürüm 1'dir; dolayısıyla bir CFF2 değişken fontun, dönüştürülmeden bir yuvası yoktur

CFF2, bir isim paylaşması dışında ortak noktası olmayan başka bir biçimdir. Name ve String INDEX'i düşürür, charset'i düşürür, glyph ilerlemelerini CharStrings'ten çıkarır ve hmtx'e bırakır; tek bir dış hattın bütün tasarım uzayını anlatmasını sağlayan blend operatörlerini ekler. Bir CFF1 çözücünün CFF2 tablosuyla karşılaştığında tahmin yürütmesine dayanak yoktur ve PDF belirtiminin hiçbir yerinde bunu denemesi zorunlu tutulmaz. CFF2 fontunu bir OpenType akışına sarmayıp şansını denemek, bir makinede render olup öbüründe başarısız olan dosyaların ta kendisidir; HotPDF bunu yapmıyor

Dönüşümün statik olması da gerekiyor. Değişken font statik örnekleme yazısının açıkladığı gibi, PDF fontu sabit metrikli sabit bir dış hat kümesidir ve hiçbir okuyucunun eksen koordinatlarını uygulaması gerekmez. HotPDF bu yüzden önce varyasyonu çözümler, ancak ondan sonra CFF1 yazar

CFF2 bir fontta yerel metin yazmak

Uygulama kodu için CFF2 bir font, kayıtlı herhangi bir Unicode fontu gibi davranır: eksen değerlerini seçin, dosyayı kaydedin, aileyi adıyla seçin ve çizin. RegisterUnicodeTTF orijinal name tablosunun ayrı bir kopyasını tuttuğu için SetFont aile adını, tam adı, PostScript adını ya da tipografik aile adını eşler ve donmuş statik font baytlarını seçer; hiçbir Windows kurulumu ya da GDI sorgusu söz konusu değildir

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'brochure.pdf';
    Pdf.BeginDoc;
    // Eksen değerleri, bu çağrıdan sonra kaydedilen fontlara uygulanır
    Pdf.SetVariableFontAxis('wght', 650);
    Pdf.RegisterUnicodeTTF('C:\Fonts\AcmeSerifVF.otf');
    // Name tablosundan aile, tam, PostScript ya da tipografik aile adı
    Pdf.CurrentPage.SetFont('Acme Serif VF', [], 12, DEFAULT_CHARSET);
    Pdf.CurrentPage.UnicodeTextOut(72, 100, 0, 'Variable weight 650, CFF2 outlines');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Glyph ID'ler kayıtlı cmap'ten, ilerlemeler kayıtlı hmtx'ten gelir. İlerlemesi gerçekten sıfır olan bir glyph, GDI ölçümüne düşmek yerine sıfır kalır ve bir UTF-16 surrogate çifti tam olarak bir ilerleme üretir, ikisini değil. BMP karakterleri, tamamlayıcı karakterler ve sentetik shaping taşıyıcıları font başına Unicode eşlemelerini korur; başka bir gömülü fonte geçmek ya da yeni bir sayfa açmak, öteki fontun bu fontun alt küme durumunu güncellemesine ya da glyph ID'lerini ödünç almasına izin vermez

Ölçüm de aynı baytları izler. THPDFPage.GetWideTextWidth ve THPDFPage.GetWideCharAdvances, seçilen bayt tabanlı fontun metriklerini okur; yani satır kırma ve sağa hizalama, çizilenle uyuşur. İnsanları yakalayan bir sıralama ayrıntısı var: kayıttan sonra yapılan SetVariableFontAxis değişiklikleri, kaydedeceğiniz sonraki fontu etkiler, çoktan donmuş örneği değil. İkinci bir ağırlık istiyorsanız yeni değeri ayarlayın ve yeniden kaydedin

HPDFConvertCFF2ToCFF, CID-keyed programı nasıl kuruyor?

HPDFConvertCFF2ToCFF, tek bir Font DICT taşıyan, boş ve kullanılmayan yuvalar dahil her yuvada CID'in orijinal glyph ID'ye eşit olduğu ham bir CID-keyed CFF1 programı üretir. O kimlik eşitliği, tasarımın tamamıdır. Font adı, PDF /W genişlikleri, CFF ilerlemeleri ve ToUnicode CMap hep aynı numaralandırmaya atıf yapar; glyph ID'ler ile CID'ler arasında çeviri yapan bir katman olmaz ve hiçbir şey uyumsuzluğa kayamaz

PDF tarafında torun, Identity-H ya da Identity-V kodlamalı bir CIDFontType0 fonttur ve program akışı /Subtype /CIDFontType0C ile /FontFile3 altında durur. CID zaten GID'ye eşit olduğu için torun bir /CIDToGIDMap taşımaz ve akış, kayıtlı TrueType fontlar için kullanılan sonraki akış değiştirme kancasının dışında tutulur. /Subtype /OpenType sarmalayıcısının aksine, dönüştürülen akış PDF 1.6 sürüm gereksinimini yükseltmez

Genişlikler küçük bir numara istedi. Bir CFF1 Type 2 CharString, ilerlemeyi nominalWidthX'e göreli isteğe bağlı bir ilk operand olarak saklar ve tam sayı operandlar 16 bit işaretli aralıkla sınırlıdır. Bir OpenType ilerlemesi ise 65535'e kadar çıkabilen işaretsiz 16 bitlik bir değerdir. HotPDF nominalWidthX'i 32768'e, defaultWidthX'i 0'a ayarlar ve her ilerlemeyi açıkça advance - 32768 olarak yazar. Sıfır ilerleme -32768 olur, 65535 ilerleme 32767 olur; böylece tam uint16 aralığı, tek bir glyph bile kırpılmadan temsil edilebilir

HotPDF'in işaretsiz 16 bitlik ilerlemeleri nominalWidthX 32768 ile işaretli CFF1 operandlarına eşlemesi; Delphi PDF çıktısında her font ilerlemesi kırpılmadan kalıyor
nominalWidthX 32768 operand penceresini kaydırır; sıfır ilerleme -32768 olarak, 65535 ise hiçbir glyph kırpılmadan 32767 olarak saklanır

Geometri de aynı özen alıyor. FontMatrix, [1/UnitsPerEm 0 0 1/UnitsPerEm 0 0] olarak kurulur; 2048 ünitelik bir font kendi doğal koordinatlarını korur. Tek bir operanda sığamayacak kadar büyük göreli hareket ve çizgiler temsil edilebilir adımlara bölünür ve kübik eğriler biçimlerini koruyacak biçimde alt bölümlere ayrılır. Orijinal telif ve lisans kayıtları, orijinal name tablosu ve gömme bayrakları, CFF Notice dizgesiyle birlikte yol alır

Dönüştürücüyü belge dışında çağırmak

Dönüştürücü, HPDFOpenTypeAdvanced unit'indeki, Pascal ile yazılmış, HarfBuzz bağımlılığı olmayan ve sistem fontu kurma adımı gerektirmeyen herkese açık bir işlevdir. Bu, onu tek başına kullanılabilir kılar; örneğin bir fontu derleme sırasında önceden dönüştürmek ya da PDF yazıcının ne gömeceğini incelemek için

uses
  System.SysUtils, System.IOUtils, HPDFOpenTypeAdvanced;

procedure ConvertWeight650;
var
  Source, Cff: TBytes;
  Axes: THPDFVariableFontAxisValueArray;
begin
  Source := TFile.ReadAllBytes('C:\Fonts\AcmeSerifVF.otf');
  SetLength(Axes, 1);
  Axes[0].Tag := 'wght';
  Axes[0].Value := 650;
  Cff := HPDFConvertCFF2ToCFF(Source, Axes);
  if Length(Cff) = 0 then
    raise Exception.Create('Not convertible: not CFF2, embedding denied or bad axes');
  TFile.WriteAllBytes('AcmeSerif-650.cff', Cff);
end;

Değişken girdi, verilen koordinatlarda yalnızca geometriye dokunan statik örneklemeden geçer; dış hatları da ilerlemeleri de kapsar. Statik bir CFF2 font yalnızca boş bir seçim kabul eder; ona eksen değerleri vermek yok sayılmak yerine hata sayılır. Sonuç, sfnt kapsayıcısı değil çıplak bir CFF1 programıdır; OpenType dosya bekleyen koda vermeyin

Beklenen durumlarda başarısızlık istisna değil, boş bir TBytes olur: CFF2 olmayan girdi, örneklemeleyicinin kabul etmediği bir eksen seçimi, NaN gibi sonlu olmayan değerler ve OS/2 fsType alanı kısıtlı lisanslı gömmeyi (0x0002) ya da yalnızca bitmap gömmeyi (0x0200) belirleyen fontlar. Uzunluğu her zaman test edin. EndDoc içinde yazıcı bunu sizin yerinize yapar ve "StoreFont: CFF2 outline conversion failed" istisnasını fırlatır; bu doğru karardır, dağıtılan bir PDF'teki boş bir font programı, günlüğünüzde görünen bir hatadan çok daha kötüdür

Sunucu iş yükleri için bütçeler ve iptal

İsteğe bağlı üçüncü parametre THPDFVariableFontControl, belleği sınırlar ve uzun bir dönüşümü iptal etmenize izin verir. Verilmezse dönüştürücü varsayılanlarını kullanır: font girdi ve çıktısı için 128 MiB sınırı, kümülatif 1 GiB'lik çalışma bütçesi ve CharString başına 65535 baytlık CFF1 sınırı

type
  TFontJob = class
  private
    FCancelled: Boolean;
    procedure CheckCancel;
  public
    function Convert(const Source: TBytes): TBytes;
    property Cancelled: Boolean read FCancelled write FCancelled;
  end;

procedure TFontJob.CheckCancel;
begin
  if FCancelled then
    Abort;  // HPDFConvertCFF2ToCFF'den dışarı taşar
end;

function TFontJob.Convert(const Source: TBytes): TBytes;
var
  Control: THPDFVariableFontControl;
begin
  Control := THPDFVariableFontControl.Create(
    32 * 1024 * 1024,           // azami çıktı baytı
    Int64(256) * 1024 * 1024,   // kümülatif çalışma bütçesi
    CheckCancel);
  try
    // nil seçim: değişken fontun varsayılan örneği ya da statik bir CFF2 fontu
    Result := HPDFConvertCFF2ToCFF(Source, nil, Control);
  finally
    Control.Free;
  end;
end;

Bütçe aşımı ve iptal istisna olarak raporlanır; boş sonuç yolundan bilinçli olarak ayrı dururlar. Boş dizi, fontun dönüştürülemeyeceği anlamına gelir; istisna, bu denemenin durdurulduğu anlamına gelir. İkisini hata işlemenizde ayrı tutun, çünkü dönüştürülemeyen bir fontu daha büyük bütçeyle yeniden denemek işe yaramaz

Gömülü bir CFF2 font ne kadar ağırdır?

Planlanması gereken ana şey dosya boyutudur: HotPDF, EnableFontSubsetting açık ya da kapalı olsun, CFF2 fontlar için dönüştürülmüş glyph setinin tamamını gömer. Birkaç bin glyphli bir font, sayfa tek bir başlık bile gösterse hepsini onu kullanan her belgeye ekler. Bir broşür için bu kabul edilebilir. Binlerce küçük PDF yazan bir rapor sunucusu için ise taahhüt etmeden önce ölçmeniz gereken gerçek bir maliyettir

İlgili boyut özellikleri de aynı kurala uyar. Dönüştürülmüş CFF2 fontlar bütün olarak gömülür; CompressDocument'in açtığı kompakt font alt kümeleri de kalıcı font alt küme önbelleği de onlara uygulanmaz. Aynı belgedeki TrueType fontlar her zamanki gibi alt kümelenir ve önbelleklenir. Her glyphi tutmanın tek getirisi, lisansı alt kümelemeyi yasaklayan fontların onu saygı duyan bir biçimde gömülmesidir

Kaynak sınırları sonlu dönüşüm sınırlarıdır ve CFF1'in kendisi tek bir CharString'in ne alabileceğini sınırlar. Dönüştürülmüş CharString'i 65535 baytı aşan bir glyph ya da çalışma bütçesini delen bir font, boş dış hatla yazılmak yerine görünür biçimde başarısız olur. Kasıtlı takas budur: egzotik bir fontta gürültülü bir başarısızlık, sessizce eksik glyphli bir belgeden iyidir

Yerel CFF2 metin ve taşınabilir CFF1 dönüştürücü, bu blogda başka yerde anlatılan değişken font örnekleme, renk glyph ve font alt kümeleme özelliklerinin yanında, Delphi ve C++Builder için HotPDF Delphi PDF bileşeni ile gelir