HotPDF renderuje akcentowane litery z osadzonych fontów poprawnie, obsługując dwa mechanizmy fontowe, które budują jeden glif z innych: glify kompozytowe TrueType, których offsety komponentów muszą być nakładane w jednostkach glifa przed macierzą glif-urządzenie, oraz formę seac CFF operatora endchar, która zszywa literę bazową i akcent po kodzie StandardEncoding. Pomyłka w każdym z nich i é, Å, ü albo cudzysłów klamrowy wychodzi z brakującą częścią, podczas gdy zwykłe litery wokół wyglądają idealnie
Ta ostatnia cecha sprawia, że te błędy są drogie. Raport się renderuje, tekst główny jest w porządku, a tylko "München" ma w sobie dziurę, albo cudzysłów otwiera się pojedynczym apostrofem i luką. Nikt nie podejrzewa renderera. Pierwsza teoria to zawsze uszkodzony font albo zepsuty producent, i zgłoszenie chodzi od biurka do biurka przez tydzień. Ten artykuł przechodzi przez to, co naprawdę się psuje w każdym mechanizmie, jak obchodzi to komponent HotPDF Delphi od v2.768.10 i v2.768.11, oraz pokrewną poprawkę subsetowania z v2.770.83, która trzyma glify kompozytowe w całości, gdy HotPDF zapisuje fonty zamiast je czytać
Dlaczego z glifa PDF znika tylko akcent?
Akcent znika, bo jest jedyną częścią glifa niosącą niezerowy offset. Fonty TrueType i CFF oszczędzają miejsce, używając konturów ponownie: é nie jest rysowane jako nowy kształt, tylko jako "glif e, plus glif akutu przesunięty w górę i w prawo". Litera bazowa prawie zawsze siedzi na offsecie (0, 0), więc każdy błąd w nakładaniu offsetów zostawia ją nietkniętą. Akcent to komponent, który się rusza, więc to on wyląduje w złym miejscu albo nie zostanie narysowany wcale
Wzorzec symptomów to sam w sobie diagnostyka. Jeśli litery bazowe znaków akcentowanych się renderują, a diakrytyki nie, albo para cudzysłowów “ i ” pokazuje się jako jeden znak zamiast dwóch, przestań patrzeć w plik fontu. Zepsuty font zwykle psuje całe glify i psuje je w każdej przeglądarce. Brakujące kawałki składanych glifów w jednym rendererze, podczas gdy Acrobat pokazuje stronę dobrze, wskazują na kod składający komponenty
Jak glify kompozytowe TrueType pozycjonują swoje komponenty?
Glif kompozytowy TrueType pozycjonuje każdy komponent własną małą transformacją afiniczną, wyrażoną w jednostkach fontu, i ta transformacja musi działać na kontur komponentu przed macierzą glif-urządzenie. W tabeli glyf OpenType glif kompozytowy ma ujemną liczbę konturów, po której idzie lista rekordów komponentów. Każdy rekord trzyma słowo flag, indeks glifa komponentu, dwa argumenty będące bajtami albo słowami (ARG_1_AND_2_ARE_WORDS, 0x0001) i opcjonalną skalę: jedną wartość F2Dot14 dla WE_HAVE_A_SCALE (0x0008), dwie dla WE_HAVE_AN_X_AND_Y_SCALE (0x0040) albo pełną macierz 2x2 dla WE_HAVE_A_TWO_BY_TWO (0x0080). Gdy ustawione jest ARGS_ARE_XY_VALUES (0x0002), dwa argumenty to offset dx/dy w jednostkach fontu. MORE_COMPONENTS (0x0020) mówi, że idzie kolejny rekord
Dla każdego komponentu budujesz więc macierz komponentu CM ze części skalującej i offsetu, a potem łączysz ją z M, macierzą mapującą jednostki fontu na piksele urządzenia. Kolejność tej kombinacji to cały błąd. Wewnętrzne mnożenie macierzy w HotPDF używa konwencji wektora kolumnowego, więc iloczyn A i B nakłada najpierw B, potem A. Poprawna kompozycja nakłada najpierw CM, w przestrzeni glifa, a M drugą. Zapisane odwrotnie, M działa pierwsze, a offset CM jest dodawany później, już w pikselach urządzenia i nigdy przeskalowany
// Tylko ilustracja: rekord afiniczny 2D w konwencji wektora kolumnowego,
// gdzie Mul(A, B) mapuje punkt najpierw przez B, a potem przez A
type
TAffine = record
a, b, c, d, e, f: Double;
end;
function Mul(const A, B: TAffine): TAffine;
begin
Result.a := A.a * B.a + A.c * B.b;
Result.b := A.b * B.a + A.d * B.b;
Result.c := A.a * B.c + A.c * B.d;
Result.d := A.b * B.c + A.d * B.d;
Result.e := A.a * B.e + A.c * B.f + A.e;
Result.f := A.b * B.e + A.d * B.f + A.f;
end;
// M : jednostki fontu -> piksele urządzenia (malutka skala, y odwrócone)
// CM : transformacja komponentu; e/f trzymają dx/dy w jednostkach fontu
Sub := Mul(M, CM); // dobrze: offset skaluje się w dół razem z konturem
// Sub := Mul(CM, M); // źle: dx/dy lądują jako surowe piksele urządzenia
Liczby robią z tej awarii coś oczywistego, gdy je zobaczysz. Arial używa ema o 2048 jednostkach, a komponent akutu w é niesie swój offset w tych jednostkach fontu. Przy poprawnej kolejności offset kurczy się razem z konturem do kilku pikseli przy normalnym powiększeniu. Przy odwróconej kolejności surowa wartość w jednostkach fontu jest dodawana jako piksele urządzenia i w testowym renderze HotPDF akcent é z Arial wylądował 243 piksele od swojej litery, daleko poza glifem i zwykle poza czymkolwiek widocznym. Lewy cudzysłów podwójny w Arial to dwie kopie kształtu pojedynczego cudzysłowu, z których druga jest przesunięta, więc renderował się jako samotny ‘ po którym szła pustka
Część liniowa rzadko obnaża błąd. Komponenty akcentów zwykle nie niosą żadnej skali, a jednolita skala i tak jest przemienna z M, więc odwrócony iloczyn obnaża dopiero przesunięcie. Dlatego też szybki test na zwykłym tekście łacińskim przechodzi: odwrócenie siedziało w kodzie przez wiele wydań, zanim v2.768.10 je poprawiło, i pokazywały je tylko komponenty z offsetami
Rekursja i cykle
Glify kompozytowe mogą odwoływać się do innych glifów kompozytowych, więc renderer przechodzi je rekurencyjnie i musi się bronić. HotPDF ogranicza głębokość zagnieżdżenia do 5 poziomów, a łączną liczbę komponentów na glif do 1024, pomija komponent, który wymienia własny indeks glifa, i traktuje rzadko używany tryb dopasowania do punktów (argumenty będące numerami punktów, a nie offsetami) jako offset zerowy. Wrogi albo uszkodzony font może więc stracić komponent, ale nie potrafi zawiesić renderu strony
Czym jest forma seac operatora endchar w CFF?
Forma seac endchar to przestarzały sposób, w jaki charstring CFF Type 2 mówi "ten glif to litera bazowa plus akcent": gdy endchar ma cztery argumenty, są to adx ady bchar achar. Adobe Technical Note #5177 opisuje to w swoim dodatku o zgodności z Type 1. Nazwa pochodzi od operatora seac Type 1 (standard encoding accented character), a subsettingi CFF skonwertowane z fontów Type 1 wciąż używają go intensywnie dla akcentowanych liter łacińskich
Trzy szczegóły wywracają implementatorów. Po pierwsze, bchar i achar to nie identyfikatory glifów. To kody w Adobe StandardEncoding, więc renderer mapuje każdy kod na nazwę glifa, a potem szuka tej nazwy przez charset fontu, by znaleźć GID. Po drugie, Type 2 nie ma argumentu asb, w przeciwieństwie do seac Type 1, które bierze asb adx ady bchar achar. W Type 2 początek akcentu idzie wprost do (adx, ady) względem początku glifa bazowego, bez korekty side-bearing. Po trzecie, fonty CFF z kluczem CID nie mogą w ogóle używać seac, bo nie ma charsetu nazw glifów, przez który dałoby się rozwiązać kody StandardEncoding
Jest jeszcze szerokość. Każdy charstring Type 2 może zaczynać swój pierwszy operator czyszczący stos opcjonalnym argumentem szerokości postępu, a endchar jest jednym z takich operatorów. Zwykły endchar z jednym argumentem niesie szerokość; seac endchar z pięcioma argumentami niesie szerokość, a za nią cztery wartości seac. Reguła polega na liczeniu argumentów względem oczekiwanej stałej liczby, czyli czterech dla seac. Przed v2.768.11 HotPDF traktował każdy endchar jako "weź szerokość i stój", więc glify seac produkowały pusty kontur: ani bazy, ani akcentu, tylko poprawnie wymiarowana luka w linii
// Ilustracja decyzji endchar w interpreterze Type 2.
// Stos trzyma operandy poprzedzające endchar; Count to ich liczba.
if (Count >= 4) and not IsCIDFont then
begin
// 5 operandów: pierwszy to szerokość postępu, reszta to seac
if Count = 5 then
TakeWidthAndShift; // odłóż szerokość, zostaw 4 operandy
if StackDepth = 4 then
begin
BaseGID := GIDForName(StdEncName(Round(Stack[2]))); // bchar
AccentGID := GIDForName(StdEncName(Round(Stack[3]))); // achar
DrawGlyph(BaseGID, Identity);
DrawGlyph(AccentGID, Translate(Stack[0], Stack[1])); // adx, ady
Exit;
end;
end;
// w przeciwnym razie: zwykły endchar, opcjonalna szerokość, zamknij ścieżkę
W prawdziwym interpreterze oba glify komponentów są wykonywane tym samym silnikiem charstringów, macierz akcentu to bieżąca macierz glifa złożona z przesunięciem do (adx, ady), a macierz jest przywracana potem, więc późniejsze glify w linii nie cierpią. Jeśli któraś nazwa nie rozwiązuje się do GID, ten komponent jest pomijany, zamiast wywalić cały glif, co jest pobłażliwym zachowaniem, którego chcesz, gdy subset został przycięty odrobinę za mocno
Sprawdzanie poprawki z własnego kodu Delphi
Najszybszym sposobem weryfikacji obsługi glifów kompozytowych i seac jest renderowanie strony z akcentowanym tekstem z osadzonych fontów przez obie publiczne ścieżki wyjścia: THotPDF.RenderLoadedPageToBitmap dla wyjścia rastrowego i THotPDF.BuildLoadedPageSVG z EmbedFonts ustawionym na True dla wyjścia wektorowego. Oba dzielą ten sam kod konturów glifów, więc oba dostały poprawki z v2.768.10 i v2.768.11. Cały potok jest omówiony w tym, jak HotPDF renderuje osadzone glify TrueType, CFF i Type 3, a opcje bitmap w renderowaniu stron PDF do bitmap w Delphi
uses
System.Classes, Vcl.Graphics, HPDFDoc;
procedure RenderAccentProbe(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
Svg: AnsiString;
Lines: TStringList;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) <= 0 then
Exit;
// Ścieżka rastra: 144 DPI łatwo uwidacznia przesunięte akcenty
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('accent-probe.bmp');
finally
Bmp.Free;
end;
// Ścieżka wektorowa: EmbedFonts = True zapisuje kontury glifów, więc
// składanie kompozytów i seac widać w samych ścieżkach SVG
Svg := Pdf.BuildLoadedPageSVG(0, True);
if Svg <> '' then
begin
Lines := TStringList.Create;
try
Lines.Text := string(Svg);
Lines.SaveToFile('accent-probe.svg');
finally
Lines.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Dokumenty testowe wybieraj świadomie. Chcesz jeden PDF z osadonym subsetem TrueType zawierającym é, Å i cudzysłowy klamrowe oraz jeden z osadzonym subsetem CFF (FontFile3, Type1C) wyprodukowanym z fontu Type 1, bo tam seac wciąż żyje, typowo w starszych wydrukach zecerskich i niektórych workflow drukarskich. Tekst złożony fontami, których producent nie osadził, nie mówi tu nic, bo te glify pochodzą z zainstalowanych fontów Windows, a nie z osadzonego programu fontu opisanego w ISO 32000-1 §9.9
Dlaczego glify kompozytowe łamały się przy subsetowaniu fontów?
Glify kompozytowe łamały się przy subsetowaniu, bo subseter przechodził rekordy komponentów złym krokiem, kiedy komponent niósł skalę albo transformację 2x2, a potem źle czytał indeks glifa następnego komponentu. HotPDF naprawił to w v2.770.83. Subsetowanie to bliźniak błędu renderowania po stronie zapisu: gdy HotPDF osadza subset zainstalowanego fontu TrueType, musi zachować każdy glif, do którego odwołuje się kompozyt, i przenumerować te odwołania na nowe, kompaktowe identyfikatory glifów
Stary walker patrzył tylko na flagę rozmiaru argumentów i flagę więcej-komponentów, więc przesuwał się o 6 albo 8 bajtów na rekord. Ignorował 2, 4 albo 8 bajtów danych transformacji, które dodają flagi skali. Po przeskalowanym komponencie czytany "indeks glifa" to w rzeczywistości były bajty transformacji, więc prawdziwy komponent mógł wypaść z subsetu, a krok przenumerowania mógł zapisać nowy identyfikator glifa prosto w transformację. Wynikiem była akcentowana litera, która renderowała się w niektórych przeglądarkach ze złym akcentem, zniekształconym akcentem albo bez żadnego, i tylko dla fontów skalujących swoje komponenty, dlatego większość testów nigdy go nie złapała
Obok błędu kroku siedziały dwa kolejne problemy. Przenumerowanie indeksu glifa nie jest idempotentne, a zagnieżdżone kompozyty były numerowane dwa razy: raz przez przebieg po każdym oznaczonym kompozycie i jeszcze raz przez rekurencyjne zejście w niego. HotPDF numeruje teraz każdy kompozyt dokładnie raz i napędza krok "których glifów potrzebuje ten kompozyt" z kolejki roboczej, więc cykl kompozytów odwołujących się nawzajem się kończy, indeksy glifów ponad liczbę glifów z maxp fontu są odrzucane, a puste glify nigdy nie są parsowane jako kompozyty
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'accents-subset.pdf';
Pdf.FontEmbedding := True; // osadź glify, które są używane
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 14);
// é, Å, ü i cudzysłowy klamrowe to w Arial glify kompozytowe
Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
'Café, Ångström, München, “quoted”');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Prześlij ten plik w obie strony przez sondę z góry i przez drugą przeglądarkę. Od v2.770.83 każdy komponent każdego glifa kompozytowego przeżywa subsetowanie, włącznie z komponentami za skalą albo transformacją 2x2, więc akcenty renderują się tak samo w HotPDF, Acrobacie i przeglądarkach
Czego obsługa glifów kompozytowych nie pokrywa
Poprawka glifów kompozytowych pokrywa pozycjonowanie offsetowe, czego używa prawie każdy prawdziwy font, i resztę aproksymuje zachowawczo. Kompozyty TrueType z dopasowaniem do punktów, gdzie dwa argumenty nazywają punkty do wyrównania zamiast podawać offset, są rysowane z offsetem zerowym. Seac w fontach CFF z kluczem CID jest ignorowane z założenia, bo specyfikacja zabrania go tam. Seac, którego kod bazy albo akcentu nie ma glifa w przyciętym subsecie, rysuje to, co zdoła znaleźć. Żaden z tych przypadków nie wywala renderera, ale jeśli produkujesz PDF-y do archiwum albo pod PDF/A, bezpieczniejszym ruchem jest osadzanie fontów używających kompozytów offsetowych i charstringów Type 2 bez seac, żeby każdy konsument, nie tylko jeden renderer, widział te same litery
FAQ
Dlaczego akcentowane litery renderują się bez akcentów w moim rendererze PDF?
Litery akcentowane to zwykle glify kompozytowe albo glify CFF seac, a akcent jest jedynym komponentem z offsetem. Renderer, który nakłada offset komponentu po macierzy urządzenia albo ignoruje seac w endchar, wyrzuca albo gubi pozycję akcentu, podczas gdy litera bazowa renderuje się normalnie
Czy HotPDF wspiera formę seac operatora endchar w CFF?
Tak. Od v2.768.11 HotPDF rysuje endchar z czterema argumentami, albo pięcioma z szerokością, jako glif bazowy plus akcent przesunięty do (adx, ady), rozwiązując oba przez nazwy StandardEncoding i charset fontu, zarówno w renderowaniu bitmapowym, jak i eksporcie SVG z osadzonymi fontami
Czy glify kompozytowe są zachowywane, gdy HotPDF robi subset fontów TrueType?
Tak. Od v2.770.83 HotPDF przechodzi transformacje komponentów o 2, 4 albo 8 bajtach poprawnym krokiem, trzyma każdy wskazany komponent w subsecie, numeruje zagnieżdżone kompozyty raz i zatrzymuje się bezpiecznie na cyklicznych albo poza zakresem odwołaniach do glifów
Renderowanie glifów kompozytowych, wsparcie CFF seac i bezpieczne subsetowanie TrueType są częścią HotPDF Delphi PDF Component, natywnej biblioteki VCL dla Delphi i C++Builder, która tworzy, edytuje, ekstrahuje i renderuje dokumenty PDF bez zewnętrznych DLL