HotPDF osadza fonty OpenType ze zarysami CFF2, konwertując je przy wybranych wartościach osi na program fontowy CFF1 z kluczami CID, który czytniki PDF już rozumieją. Zwykły tekst natywny rejestrowany przez RegisterUnicodeTTF korzysta z własnych tabel cmap i hmtx fontu, a writer osadza skonwertowany program pod /FontFile3 z /Subtype /CIDFontType0C, trzymając każdy CID równy oryginalnemu ID glifu
Oczywista droga jest warta opisania, bo zawodzi bez powiadomienia kogokolwiek. Sparsuj i zcache'uj tabele CFF2, potem pozwól ścieżce zwykłego tekstu rozwiązać font przez Windows GDI z dopasowaniem wyłącznie TrueType: GDI nie ma pojęcia, co zrobić ze zarysami CFF2 i po cichu podstawia ArialUnicodeMS. Cokolwiek downstream czyta podstawienie dostaje złe zarysy, złe szerokości i złą ekstrakcję znaków z płaszczyzn uzupełniających. PDF otwiera się ładnie. Tylko nie jest złożony fontem, o który prosiłeś — dlatego ścieżka tekstu musi pracować z zarejestrowanych bajtów, a nie z lookupu GDI
Dlaczego PDF nie może osadzić fontu CFF2 wprost?
PDF nie ma slotu na program CFF2. ISO 32000-2 §9.9 wylicza typy osadzonych programów fontowych, które czytnik musi obsłużyć: Type 1 pod /FontFile, TrueType pod /FontFile2, a pod /FontFile3 goły program CFF (Type1C albo CIDFontType0C) albo opakowanie OpenType wokół zarysów TrueType lub CFF. Każdy smak CFF na tej liście znaczy CFF w wersji 1, format parsowany przez czytniki od dwóch dekad
CFF2 to inny format, który przypadkiem dzieli nazwę. Wyrzuca INDEX Name i String, wyrzuca charset, wynosi szerokości glifów z CharStrings i zostawia je hmtx, a dokłada operatory blend, dzięki którym jeden zarys opisuje całą przestrzeń projektową. Parser CFF1, który spotka tabelę CFF2, nie ma podstaw do zgadywania i nic w specyfikacji PDF nie zobowiązuje go do prób. Opakowanie fontu CFF2 w strumień OpenType w nadziei, że jakoś to będzie, to dokładnie ten rodzaj pliku, który renderuje się na jednej maszynie i pada na następnej, więc HotPDF tego nie robi
Konwersja musi być też statyczna. Jak wyjaśnia artykuł o statycznym instancingu fontów zmiennych, font PDF to stały zestaw zarysów ze stałymi metrykami i żaden czytnik nie musi aplikować współrzędnych osi. HotPDF rozwiązuje wariację najpierw i dopiero potem pisze CFF1
Pisanie tekstu natywnego fontem CFF2
Dla kodu aplikacji font CFF2 zachowuje się jak każdy inny zarejestrowany font Unicode: wybierz wartości osi, zarejestruj plik, wybierz rodzinę po nazwie i rysuj. RegisterUnicodeTTF trzyma osobną kopię oryginalnej tabeli name, więc SetFont dopasowuje nazwę rodziny, pełną nazwę, nazwę PostScript albo typograficzną nazwę rodziny i wybiera zamrożone statyczne bajty fontu, bez instalacji w Windows i bez lookupu GDI
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'brochure.pdf';
Pdf.BeginDoc;
// Wartości osi dotyczą fontów zarejestrowanych po tym wywołaniu
Pdf.SetVariableFontAxis('wght', 650);
Pdf.RegisterUnicodeTTF('C:\Fonts\AcmeSerifVF.otf');
// Nazwa rodziny, pełna, PostScript albo typograficznej rodziny z tabeli name
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;
ID glifów pochodzą z zarejestrowanego cmap, a szerokości z zarejestrowanego hmtx. Glif o szerokości faktycznie zerowej zostaje zerowy zamiast spadać na pomiar z GDI, a para surrogate UTF-16 daje dokładnie jedną szerokość, nie dwie. Znaki BMP, znaki z płaszczyzn uzupełniających i syntetyczne nośniki shapingu trzymają per-fontowe mapowania Unicode, więc przełączenie na inny osadzony font albo start nowej strony nie pozwala tamtemu fontowi aktualizować stanu subsetu tego fontu ani pożyczać jego ID glifów
Pomiar idzie za tymi samymi bajtami. THPDFPage.GetWideTextWidth i THPDFPage.GetWideCharAdvances czytają metryki wybranego fontu opartego na bajtach, więc łamanie wierszy i wyrównanie do prawej zgadzają się z tym, co zostanie narysowane. Jeden szczegół kolejności łapie ludzi: zmiany SetVariableFontAxis zrobione po rejestracji dotyczą następnego rejestrowanego fontu, nie już zamrożonej instancji. Drugą grubość dostaniesz, ustawiając nową wartość i rejestrując ponownie
Jak HPDFConvertCFF2ToCFF buduje program z kluczami CID?
HPDFConvertCFF2ToCFF produkuje surowy program CFF1 z kluczami CID z pojedynczym Font DICT, w którym CID równa się oryginalnemu ID glifu dla każdego slotu, łącznie z pustymi i nieużywanymi. Ta tożsamość jest całym projektem. Nazwa fontu, szerokości PDF /W, szerokości CFF i CMap ToUnicode referują tę samą numerację, więc żadna warstwa nie musi tłumaczyć między ID glifów a CID i nic nie może się rozjechać
Po stronie PDF potomek to font CIDFontType0 z kodowaniem Identity-H albo Identity-V, a strumień programu siedzi pod /FontFile3 z /Subtype /CIDFontType0C. Ponieważ CID już równa się GID, potomek nie niesie /CIDToGIDMap, a strumień jest wykluczony z późniejszego haka podmiany strumieni używanego dla zarejestrowanych fontów TrueType. W przeciwieństwie do opakowania /Subtype /OpenType skonwertowany strumień nie podnosi wymagania wersji PDF 1.6
Szerokości potrzebowały małego triku. CharString Type 2 w CFF1 trzyma szerokość jako opcjonalny pierwszy operand względem nominalWidthX, a operandy całkowite są ograniczone do zakresu ze znakiem 16 bitów. Szerokość OpenType to wartość bez znaku 16 bitów, do 65535. HotPDF ustawia nominalWidthX na 32768 i defaultWidthX na 0, potem pisze każdą szerokość wprost jako advance - 32768. Szerokość zerowa staje się -32768, a 65535 staje się 32767, więc cały zakres uint16 jest reprezentowalny bez obcinania choćby jednego glifu
Geometria dostaje tę samą troskę. FontMatrix to [1/UnitsPerEm 0 0 1/UnitsPerEm 0 0], więc font o 2048 jednostkach trzyma natywne współrzędne. Względne ruchy i linie za duże na jeden operand są dzielone na reprezentowalne kroki, a krzywe sześcienne są subdividowane w sposób zachowujący ich kształt. Oryginalne rekordy copyrightu i licencji, oryginalna tabela name i flagi osadzania podróżują dalej w stringu Notice CFF
Wołanie konwertera poza dokumentem
Konwerter to publiczna funkcja w unit HPDFOpenTypeAdvanced pisana w Pascalu, bez zależności od HarfBuzz i bez kroku instalacji fontu systemowego. Daje się używać samodzielnie, na przykład do wstępnej konwersji fontu w czasie buildu albo do zajrzenia, co writer PDF osadzi
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;
Wejście zmienne przechodzi przez statyczny instancing tylko-geometrii przy podanych współrzędnych, obejmujący i zarysy, i szerokości. Statyczny font CFF2 przyjmuje wyłącznie pusty wybór; podanie wartości osi traktuje jako błąd zamiast je zignorować. Wynikiem jest goły program CFF1, nie kontener sfnt, więc nie podawaj go kodowi oczekującemu pliku OpenType
Porażka to pusty TBytes, nie wyjątek, dla oczekiwanych przypadków: wejście, które nie jest CFF2, wybór osi nieprzyjmowany przez instancer, wartości nie-finity takie jak NaN oraz fonty, których OS/2 fsType ustawia osadzanie restricted-license (0x0002) albo bitmap-only (0x0200). Zawsze testuj długość. Wewnątrz EndDoc writer robi to za ciebie i rzuca "StoreFont: CFF2 outline conversion failed", co jest właściwą decyzją: pusty program fontowy w wysłanym PDF-ie jest o wiele gorszy niż widoczny błąd w twoim logu
Budżety i anulowanie dla obciążeń serwerowych
Opcjonalny trzeci parametr, THPDFVariableFontControl, ogranicza pamięć i pozwala anulować długą konwersję. Bez niego konwerter używa domyślnych: limit 128 MiB na wejście i wyjście fontu, skumulowany budżet roboczy 1 GiB oraz limit CFF1 65535 bajtów na CharString
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; // wypływa z HPDFConvertCFF2ToCFF
end;
function TFontJob.Convert(const Source: TBytes): TBytes;
var
Control: THPDFVariableFontControl;
begin
Control := THPDFVariableFontControl.Create(
32 * 1024 * 1024, // maksimum bajtów wyjścia
Int64(256) * 1024 * 1024, // skumulowany budżet roboczy
CheckCancel);
try
// Wybór nil: domyślna instancja fontu zmiennego albo statyczny font CFF2
Result := HPDFConvertCFF2ToCFF(Source, nil, Control);
finally
Control.Free;
end;
end;
Przekroczenia budżetu i anulowanie są zgłaszane wyjątkami, celowo odrębnymi od ścieżki pustego wyniku. Pusta tablica znaczy, że fontu nie da się skonwertować; wyjątek znaczy, że ta próba została zatrzymana. Trzymaj te dwa przypadki osobno w obsłudze błędów, bo ponawianie niekonwertowalnego fontu z większym budżetem nie pomoże
Ile waży osadzony font CFF2?
Główna rzecz do zaplanowania to rozmiar pliku: HotPDF osadza kompletny skonwertowany zestaw glifów dla fontów CFF2, niezależnie od tego, czy EnableFontSubsetting jest włączone. Font z kilkoma tysiącami glifów dokłada wszystkie do każdego dokumentu, który go użyje, nawet jeśli strona pokazuje jeden nagłówek. Dla broszury to akceptowalne. Dla serwera raportów piszącego tysiące małych PDF-ów to realny koszt, który warto zmierzyć przed decyzją
Pokrewne funkcje rozmiarowe podlegają tej samej regule. Skonwertowane fonty CFF2 są osadzane w całości, więc ani kompaktowe subsety fontów włączone przez CompressDocument, ani trwały cache subsetów fontów ich nie dotyczy. Fonty TrueType w tym samym dokumencie są nadal subsetowane i cache'owane jak zwykle. Jedyna korzyść z trzymania każdego glifu to to, że fonty, których licencja zabrania subsetowania, są osadzane w formie tę licencję respektującą
Limity zasobów to limity konwersji, a sam CFF1 kapuje to, co pojedynczy CharString może pomieścić. Glif, którego skonwertowany CharString przekracza 65535 bajtów, albo font przewalający budżet roboczy zawodzi głośno zamiast zostać zapisanym z pustym zarysem. To zamierzony kompromis: głośna porażka na egzotycznym fontcie bije dokument z po cichu brakującymi glifami
Natywny tekst CFF2 i przenośny konwerter CFF1 płyną z komponentem HotPDF Delphi PDF dla Delphi i C++Builder, obok instancingu fontów zmiennych, kolorowych glifów i subsetowania fontów opisywanych gdzie indziej na tym blogu