HotPDF vykresluje písmena s diakritikou z vložených fontů správně tím, že zvládá dva fontové mechanismy, které staví jeden glyph z jiných: kompozitní glyphy TrueType, jejichž komponentové odsady se musí aplikovat v jednotkách glyphu před maticí glyph-do-zařízení, a seac formu endchar v CFF, která sešije základní písmeno a akcent dohromady podle kódu StandardEncoding. Pokazíte-li kterýkoli z nich, é, Å, ü nebo vlnitá uvozovka vyleze bez kousku, zatímco obyčejná písmena kolem vypadají dokonale
Právě ten poslední detail dělá tyhle chyby drahé. Report se vyrenderuje, tělo textu je v pořádku a jen „München“ má v sobě díru, nebo uvozovka se otevře apostrofem a mezerou. Nikdo nepodezřívá renderer. První teorie je vždycky poškozený font nebo špatný producent a ticket putuje týden. Tenhle článek projde tím, co se v každém mechanismu doopravdy kazí, jak to komponenta HotPDF Delphi řeší od v2.768.10 a v2.768.11, a související opravou subsettování ve v2.770.83, která drží kompozitní glyphy v celku, když HotPDF fonty zapisuje místo čtení
Proč z glyphu v PDF mizí jen akcent?
Akcent mizí, protože je jedinou částí glyphu, která nese nenulovou odsadu. Fonty TrueType i CFF šetří místo znovupoužitím obrysů: é se nekreslí jako nový tvar, ale jako „glyph e plus glyph čárky posunutý nahoru a doprava“. Základní písmeno sedí téměř vždy na odsadě (0, 0), takže jakákoli chyba v aplikování odsad ho nechá netknutý. Akcent je komponenta, která se hýbe, takže je to komponenta, která skončí na špatném místě nebo se nikdy nenakreslí
Vzor příznaků je sám o sobě diagnostika. Pokud se základní písmena znaků s diakritikou renderují a diakritika ne, nebo pokud párová interpunkce jako „ a “ ukazuje jako jedna značka místo dvou, přestaňte se koukat do souboru fontu. Rozbitý font obvykle rozbije celé glyphy a rozbije je pro každý prohlížeč. Chybějící kusy složených glyphů v jednom rendereru, zatímco Acrobat ukazuje stránku v pořádku, ukazují na kód, který skládá komponenty
Jak kompozitní glyphy TrueType umisťují své komponenty?
Kompozitní glyph TrueType umísťuje každou komponentu svou malou afinní transformací vyjádřenou v jednotkách fontu a ta transformace musí působit na obrys komponenty, než to udělá matice glyph-do-zařízení. V tabulce glyf OpenType má kompozitní glyph záporný počet obrysů, následovaný seznamem záznamů komponent. Každý záznam drží slovo příznaků, index glyphu komponenty, dva argumenty, které jsou byty nebo slova (ARG_1_AND_2_ARE_WORDS, 0x0001), a volitelné škálování: jednu hodnotu F2Dot14 pro WE_HAVE_A_SCALE (0x0008), dvě pro WE_HAVE_AN_X_AND_Y_SCALE (0x0040) nebo plnou matici 2x2 pro WE_HAVE_A_TWO_BY_TWO (0x0080). Je-li nastaveno ARGS_ARE_XY_VALUES (0x0002), jsou dva argumenty odsada dx/dy v jednotkách fontu. MORE_COMPONENTS (0x0020) říká, že následuje další záznam
Pro každou komponentu si tedy postavíte komponentovou matici CM ze škálovací části a odsady a pak ji zkombinujete s M, maticí, která mapuje jednotky fontu na pixely zařízení. Pořadí té kombinace je celá chyba. Interní násobení matic v HotPDF používá konvenci sloupcových vektorů, takže produkt A a B aplikuje nejdřív B a pak A. Správná kompozice aplikuje nejdřív CM, v prostoru glyphu, a pak M. Napsáno naopak, běží nejdřív M a odsada CM se přičte potom, už v pixelech zařízení a nikdy se neškáluje
// Jen ilustrace: záznam 2D afiny s konvencí sloupcových vektorů,
// kde Mul(A, B) mapuje bod nejdřív přes B a pak přes 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 : jednotky fontu -> pixely zařízení (drobné škálování, y převráceno)
// CM : transformace komponenty; e/f drží dx/dy v jednotkách fontu
Sub := Mul(M, CM); // správně: odsada se zmenší spolu s obrysem
// Sub := Mul(CM, M); // špatně: dx/dy přistanou jako syrové pixely zařízení
Čísla udělají ze selhání očividnou věc, jakmile je uvidíte. Arial používá em o 2048 jednotkách a akcentová komponenta é nese svou odsadu v těchto jednotkách fontu. Se správným pořadím se ta odsada zmenší spolu s obrysem na pár pixelů při běžném přiblížení. V obráceném pořadí se syrová hodnota v jednotkách fontu přičte jako pixely zařízení a v testovacím renderu HotPDF skončil akcent é z Arialu 243 pixelů od svého písmene, daleko mimo glyph a obvykle mimo cokoli viditelného. Levná dvojitá uvozovka v Arialu jsou dvě kopie tvaru apostrofu, druhá posunutá, takže se renderovala jako osamocená ‘ následovaná prázdným místem
Lineární část chybu odhalí jen zřídka. Akcentové komponenty obvykle nenesou žádné škálování a jednotné škálování s M stejně komutuje, takže obrácený produkt prozradí jen posunutí. Proto taky projde rychlý test s obyčejným latinským textem: obrácení sedělo v kódu přes spoustu vydání, než to opravila v2.768.10, a ukázaly ho jen komponenty s odsadami
Rekurze a cykly
Kompozitní glyphy mohou odkazovat na jiné kompozitní glyphy, takže je renderer prochází rekurzivně a musí se bránit. HotPDF zastropuje hloubku zanoření na 5 úrovní a celkový počet komponent na glyph na 1024, přeskočí komponentu, která jmenuje vlastní index glyphu, a bere málo používaný mód párování bodů (argumenty, které jsou čísla bodů místo odsad) jako nulovou odsadu. Nepřátelský nebo poškozený font tak může o komponentu přijít, ale nemůže zavěsit render stránky
Co je seac forma CFF endchar?
Seac forma endchar je zavržený způsob, jak charstring CFF Type 2 řekne „tahle glyph je základní písmeno plus akcent“: když má endchar čtyři argumenty, jsou to adx ady bchar achar. Popisuje ho Adobe Technical Note #5177 v příloze o kompatibilitě s Type 1. Název pochází z operátoru seac Type 1 (standard encoding accented character) a CFF subsety konvertované z fontů Type 1 ho pro latinská písmena s diakritikou pořád používají hojně
Tři detaily shodí implementátory. Zapatře, bchar a achar nejsou glyph ID. Jsou to kódy v Adobe StandardEncoding, takže renderer mapuje každý kód na název glyphu a pak ten název vyhledá přes charset fontu, aby našel GID. Za druhé, Type 2 nemá argument asb, na rozdíl od seac Type 1, který bere asb adx ady bchar achar. V Type 2 jde počátek akcentu přímo na (adx, ady) relativně k počátku základního glyphu, bez korekce side-bearing. Za třetí, CID-keyed CFF fonty nesmí seac používat vůbec, protože neexistuje charset názvů glyphů, přes který by se rozluštily kódy StandardEncoding
Pak je tu šířka. Každý charstring Type 2 může začít svůj první operátor čistící zásobník volitelným argumentem advance width a endchar je jedním z těch operátorů. Obyčejný endchar s jedním argumentem nese šířku; seac endchar s pěti argumenty nese šířku následovanou čtyřmi hodnotami seac. Pravidlo je počítat argumenty proti očekávanému pevnému počtu, který je pro seac čtyři. Před v2.768.11 bralo HotPDF každý endchar jako „vezmi šířku a skonči“, takže seac glyphy produkovaly prázdný obrys: žádný základ, žádný akcent, jen správně velká mezera v řádce
// Ilustrace rozhodnutí endchar v interpretu Type 2.
// Stack drží operandy předcházející endchar; Count říká kolik.
if (Count >= 4) and not IsCIDFont then
begin
// 5 operandů: první je advance width, zbytek je seac
if Count = 5 then
TakeWidthAndShift; // hoď šířku pryč, nech 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;
// jinak: obyčejný endchar, volitelná šířka, zavři cestu
V reálném interpretu se obě komponentové glyphy vykonávají stejným charstring enginem, matice akcentu je aktuální matice glyphu složená s posunutím na (adx, ady) a matice se potom obnoví, takže pozdější glyphy na řádce nejsou postižené. Nerozluští-li se kterýkoli název na GID, ta komponenta se přeskočí místo selhat celým glyphem, což je shovívavé chování, které chcete, když byl subset ostříhaný trochu moc agresivně
Ověření opravy z vlastního kódu v Delphi
Nejrychlejší cesta, jak ověřit zacházení s kompozitními glyphy a seac, je vyrenderovat stránku s textem s diakritikou z vložených fontů oběma veřejnými výstupními cestami: THotPDF.RenderLoadedPageToBitmap pro rastrový výstup a THotPDF.BuildLoadedPageSVG s EmbedFonts nastaveným na True pro vektorový výstup. Obě sdílejí týž kód obrysů glyphů, takže obě dostaly opravy v2.768.10 a v2.768.11. Celou pipeline rozebírá jak HotPDF renderuje vložené glyphy TrueType, CFF a Type 3 a volby bitmapy vykreslování PDF stránek do bitmap v 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;
// Rastrová cesta: 144 DPI dělá z přemístěných akcentů snadno viditelné
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('accent-probe.bmp');
finally
Bmp.Free;
end;
// Vektorová cesta: EmbedFonts = True zapisuje obrysy glyphů, takže
// je skládání kompozitů a seac vidět přímo v cestách 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;
Testovací dokumenty vybírejte vědomě. Chcete jedno PDF s vloženým TrueType subsetem obsahujícím é, Å a vlnité dvojité uvozovky a jedno s vloženým CFF subsetem (FontFile3, Type1C) vyrobeným z fontu Type 1, kde seac pořád žije, typicky ve starším sazebním výstupu a některých tiskových tocích. Text nasázený fonty, které producent nevložil, vám tady neřekne nic, protože ty glyphy pocházejí z nainstalovaných fontů Windows místo z vloženého fontového programu popsaného v ISO 32000-1 §9.9
Proč se kompozitní glyphy rozbíjely při subsettování fontů?
Kompozitní glyphy se rozbíjely při subsettování, protože subsetter procházel záznamy komponent špatným krokem, kdykoli komponenta nesla škálování nebo transformaci 2x2, a pak špatně četl index glyphu další komponenty. HotPDF to opravil ve v2.770.83. Subsetting je zapisovací dvojče renderovací chyby: když HotPDF vkládá subset nainstalovaného TrueType fontu, musí zachovat každý glyph, na který kompozit odkazuje, a přečíslovat ty odkazy na nová, kompaktní glyph ID
Starý procházející kód koukal jen na příznak velikosti argumentů a příznak more-components, takže postupoval 6 nebo 8 bajtů na záznam. Ignoroval 2, 4 nebo 8 bajtů transformačních dat, která přidávají příznaky škálování. Po škálované komponentě byl další „index glyphu“, který přečetl, doopravdy transformační bajty, takže skutečná komponenta mohla zůstat mimo subset a přečíslovací krok mohl zapsat nové glyph ID přímo do transformace. Výsledkem bylo písmeno s diakritikou, které se v některých prohlížečích renderovalo se špatným akcentem, deformovaným akcentem nebo žádným, a jen u fontů, které své komponenty škálují, proto to většina testů nikdy nechytla
Vedle chyby kroku seděly dva problémy navíc. Přečíslování indexu glyphu není idempotentní a vnořené kompozity se přečíslovaly dvakrát: jednou průchodem přes každý označený kompozit a podruhé rekurzivním sestupem do něj. HotPDF teď přečíslovává každý kompozit přesně jednou a krok „které glyphy tenhle kompozit potřebuje“ řídí z pracovní fronty, takže cyklus kompozitů odkazujících navzájem se ukončí, indexy glyphů nad počtem glyphů v maxp fontu se odmítnou a prázdné glyphy se nikdy neparsují jako kompozity
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'accents-subset.pdf';
Pdf.FontEmbedding := True; // vlož použité glyphy
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 14);
// é, Å, ü i vlnité uvozovky jsou v Arialu všechny kompozitní glyphy
Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
'Café, Ångström, München, “quoted”');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Prohněte ten soubor zkouškou výše a druhým prohlížečem. Od v2.770.83 přežije subsettování každá komponenta každého kompozitního glyphu, včetně komponent za škálováním nebo transformací 2x2, takže akcenty se renderují stejně v HotPDF, Acrobatu i prohlížečích
Co zacházení s kompozitními glyphy nepokrývá
Oprava kompozitních glyphů pokrývá umísťování přes odsady, což používá téměř každý reálný font, a zbytek aproximuje konzervativně. Kompozity TrueType s párováním bodů, kde dva argumenty jmenují body k zarovnání místo dát odsadu, se kreslí s nulovou odsadou. Seac v CID-keyed CFF fontech se ignoruje záměrně, protože ho norma tam zakazuje. Seac, jehož kód základu nebo akcentu nemá v ostříhaném subsetu glyph, nakreslí, co najde. Žádný z těchto případů neshodí renderer, ale produkujete-li PDF pro archivaci nebo PDF/A, bezpečnější krok je vkládat fonty, které používají kompozity přes odsady a charstringy Type 2 bez seac, aby každý konzument, ne jen jeden renderer, viděl táž písmena
FAQ
Proč se písmena s diakritikou renderují bez akcentů v mém PDF rendereru?
Písmena s diakritikou bývají kompozitní glyphy nebo glyphy CFF seac a akcent je jediná komponenta s odsadou. Renderer, který aplikuje odsadu komponenty za maticí zařízení, nebo ignoruje seac v endchar, zahodí nebo přemístí akcent, zatímco základní písmeno se renderuje normálně
Podporuje HotPDF seac formu CFF endchar?
Ano. Od v2.768.11 kreslí HotPDF endchar se čtyřmi argumenty, nebo pěti včetně šířky, jako základní glyph plus akcent posunutý na (adx, ady), přičemž oba rozluští přes názvy StandardEncoding a charset fontu, pro rastrové renderování i export SVG s vloženými fonty
Zachovávají se kompozitní glyphy, když HotPDF subsetuje fonty TrueType?
Ano. Od v2.770.83 HotPDF správně prochází transformace komponent o 2, 4 nebo 8 bajtech, drží každou odkazovanou komponentu v subsetu, přečíslovává vnořené kompozity jednou a bezpečně se zastaví na cyklických nebo mimo rozsahových odkazech na glyphy
Vykreslování kompozitních glyphů, podpora CFF seac a bezpečný subsetting TrueType jsou součástí HotPDF Delphi PDF Component, nativní VCL knihovny pro Delphi a C++Builder, která vytváří, upravuje, extrahuje a renderuje PDF dokumenty bez externích DLL