Teknisk artikel

PDF-typsnitt och text: Varför glyfer blir till rutor

En PDF som ser perfekt ut på din maskin och renderas som en rad tomma rutor på någon annans är det vanligaste typsnittsfelet i dokumentprogramvara, och det betyder nästan aldrig att texten är fel. Tecknen är intakta, kodningen är bra, glyferna är helt enkelt inte där. Vad som ändrades mellan de två maskinerna är vilka typsnitt operativsystemet hade installerat, och skillnaden mellan en portabel fil och en skör (fragile) sådan är ett beslut som togs när sidan skrevs: huruvida typsnittet reste inuti PDF:en eller antogs finnas i andra änden

Att förstå varför det händer, och varför ett separat fel producerar sökbart utseende text som kopieras ut som nonsens, innebär att titta på hur PDF lagrar text. Den lagrar inte meningar. Den lagrar glyfkoder plus ett typsnittsprogram plus tabeller som mappar den ena till den andra, och varje renderings- eller extraktionsbugg lever i ett glapp mellan de tre. Det som följer är en rundtur i det maskineriet, grundat i ISO 32000, med Delphi-anropen som styr det där de spelar roll

Tecken, koder och glyfer är tre olika saker

Ordförrådet brukar förvirra folk eftersom vardagstal slår ihop tre distinkta idéer till ordet "bokstav". Ett tecken är en abstrakt skrivenhet, idén om stort A, identifierat i Unicode som U+0041. En glyf är en ritad form, konturen av kurvor och stammar som ett specifikt typsnitt använder för att avbilda det tecknet. Mellan dem sitter koden: byten eller byten i innehållsströmmen som berättar för visaren vilken glyf i det aktuella typsnittet som ska målas

PDF fungerar med koder. När en innehållsström visar en sträng är de byten index in i det aktiva typsnittet, inte Unicode. Typsnittets kodning avgör att en kod på 65 betyder "rita glyfen som arkiverats under 65", och ingenting i den operationen vet att resultatet ser ut som ett A för en människa. Det är det som gör att PDF renderas identiskt överallt där den kan hitta glyferna, och det är också varför extraktion är ett separat problem från visning: ritning behöver bara kod-till-glyf, läsning behöver kod-till-Unicode, och det är två olika tabeller som kan vara oense eller saknas oberoende av varandra

De typsnittstyper du faktiskt kommer att möta

ISO 32000 definierar flera typer av typsnittsordböcker, och i praktiken använder ett dokument du tar emot eller genererar en av tre. Att veta vilken du tittar på förklarar det mesta av vad som kan gå fel

Type 1 är Adobes ursprungliga PostScript-konturformat, byggt av kubiska Bezier-kurvor. De fjorton standardtypsnitten varje konform visare måste tillhandahålla, familjerna Helvetica, Times, Courier, Symbol och ZapfDingbats, är Type 1, och en typsnittsordbok som namnger en av dem kan lagligt utelämna typsnittsprogrammet. Det är det enda fallet där det är säkert att lämna ett typsnitt oinbäddat (unembedded) enligt specifikationen snarare än av tur. För alla andra Type 1-typsnitt måste programmet vara inbäddat eller så ersätter visaren det med något, vanligtvis ett metriskt liknande men synligt annorlunda typsnitt

TrueType använder kvadratiska kurvor och kom från Apple- och Microsoft-världen. Det är vad de flesta systemtypsnitt är, och vad du oftast kommer att bädda in. Ett enkelt TrueType-typsnitt i PDF är begränsat till en-byte-koder, så ett sådant typsnitt kan adressera som mest 256 glyfer åt gången. Det taket är det strukturella skälet till att CJK och andra stora skript inte kan köras på ett enkelt typsnitt

Type 0, det sammansatta (composite) eller CID-nycklade typsnittet, är svaret på den begränsningen. Det använder fler-byte-koder och en CMap för att dirigera dem genom ett underordnat CIDFont, vars konturer i sig antingen är TrueType eller CFF/Type 1. Detta är den enda typsnittstypen som kan bära tusentals glyfer, så varje PDF som innehåller kinesiska, japanska, koreanska, eller en bred flerspråkig blandning använder Type 0 oavsett om författaren tänkte på det eller inte. Utbytet är komplexitet: fler rörliga delar, varav fler måste vara korrekta för både rendering och extraktion

Ett TrueType-typsnitt renderat i 12, 18, 24 och 36 punkter i en PDF, vilket visar att en enda inbäddad kontur skalas till valfri storlek

En detalj bakom den bilden driver filstorleken. Ett typsnitt är ett bibliotek av konturer, inte bitmappar av fast storlek, så samma inbäddade program betjänar varje punktstorlek på sidan. Skalning är en transform som tillämpas vid ritningstillfället, vilket är anledningen till att en rubrik och dess brödtext delar ett inbäddat utseende och varför inbäddningskostnaden är per typsnitt, inte per storlek

Inbäddning är skillnaden mellan portabel och skör

Inbäddning innebär att typsnittsprogrammet, själva konturdata, skrivs in i PDF:en som en ström. En läsare på en maskin som aldrig har hört talas om ditt typsnitt läser dessa konturer direkt från filen och ritar exakta glyfer. Hoppa över inbäddning och du slår vad om att destinationen har ett typsnitt med samma namn; när den inte har det faller visaren tillbaka på en ersättning. För standard fjorton är den ersättningen definierad och godartad. För allt annat varierar det från en nära miss i ett annat typsnitt till tomm-ruta-resultatet (empty-box) när ingen ersättning täcker skriptet alls

Med HotPDF är kontrollen en enda egenskap, inställd innan dokumentet öppnas. FontEmbedding säger åt biblioteket att packa in typsnitten det ritar med in i filen:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Compression := cmFlateDecode;
    Pdf.FontEmbedding := True;          // outlines travel inside the file
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Calibri', [], 11);
    Pdf.CurrentPage.TextOut(72, 760, 0, 'This renders the same on a machine without Calibri.');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Ordningen är inte kosmetisk. BeginDoc är där HotPDF binder upp dokumentstrukturen, så FontEmbedding måste vara sant innan det anropet. Tilldela det efteråt och det finns inget fel, ingen varning, bara en fil som tyst gick ut utan sina typsnitt. Det är den värsta typen av bugg: den passerar alla tester på utvecklarens maskin, där typsnittet råkar vara installerat, och dyker bara upp på en kunds, där det inte är det

Inbäddning är också där licensiering möter teknik. Ett typsnittsprogram bär flaggor som beskriver om det får bäddas in fritt, endast för förhandsgranskning, eller inte alls. Att respektera de flaggorna är ditt ansvar, inte renderarens, och "det fungerade" är inte detsamma som "det var tillåtet"

Subsetting (delmängder): bädda bara in de glyfer du använde

Full inbäddning skriver in hela typsnittsprogrammet i filen. Ett stort CJK TrueType-typsnitt kan bli på flera megabyte, och att bädda in det i sin helhet för att visa ett dussin tecken är slösaktigt på ett sätt som förvärras över ett flersidigt dokument. Subsetting (delmängdskapande) löser detta genom att bara skriva de glyfer dokumentet refererar till, och byter sedan namn på typsnittet med en sexbokstavstagg och ett plustecken, formen ABCDEF+Calibri i vilken som helst lista över typsnitt för delmängds-PDF:er, så att en läsare aldrig blandar ihop det partiella typsnittet med ett fullt systemtypsnitt med samma namn

För de flesta genererade dokument är subsetting rätt standardval. Det håller filstorleken proportionell till innehållet snarare än till källtypsnittet, vilket är viktigast för de stora flerspråkiga typsnitten som annars skulle dominera filen. Det enda förbehållet är att en delmängd bara innehåller det som användes vid skapandet. Om en nedströms process försöker lägga till text i ett delmängdstypsnitt senare, kanske de glyfer den behöver inte finns i filen, en verklig begränsning för inkrementell redigering av någon annans PDF

Unicode-typsnitt och problemet med CJK-rutor

När texten inte är vanligt latinskt skript tar den enkla typsnittsvägen slut, och lösningen är att uttryckligen registrera ett Unicode-kapabelt typsnitt och låta HotPDF bygga ett Type 0-typsnitt från det. RegisterUnicodeTTF laddar en TrueType-fil via sökväg; därefter är det registrerade namnet användbart i SetFont som vilket annat som helst:

Pdf.FontEmbedding := True;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansCJKsc-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('NotoSansCJKsc-Regular', [], 14);
Pdf.CurrentPage.TextOut(72, 720, 0, '你好,世界 こんにちは 안녕하세요');
Pdf.EndDoc;

Två saker avgör detta (make or break). Typsnittet måste täcka skripten i strängen: ett TrueType-typsnitt med enbart latinska tecken kommer inte att odla fram kinesiska glyfer för att du bad om det, och resultatet blir tomma rutor igen, den här gången för att glyfen verkligen inte existerar i det typsnittet. Och inbäddning måste förbli påslaget, för att ett Type 0-typsnitt som satts ihop från en registrerad TTF är meningslöst för en läsare som inte kan hitta konturerna. För blandat innehåll är det hållbara valet ett typsnitt med bred täckning, där Noto- och Arial Unicode MS-familjerna är de vanliga svaren, inbäddade och som delmängder

Skript från höger till vänster och komplexa skript lägger till ett formningslager (shaping layer) ovanpå täckningen. HotPDF exponerar RtLTextOut för arabiska och hebreiska, vilket hanterar den riktningsmässiga omordningen så att du skickar logisk ordning och låter biblioteket lägga ut det. Att få arabiska rätt är täckning plus formning plus riktning, tre separata saker, och en ruta där kan betyda att vilken som helst av dem misslyckades

ToUnicode-tabellen: där kopiera-klistra in lever

Allt ovan rör ritning. Extraktion är spegelbilden och misslyckas av sina egna skäl. En visare renderar en sida med hjälp av typsnittets kod-till-glyf-mappning, men när en användare markerar text och kopierar den, måste visaren förvandla samma koder tillbaka till Unicode. Den omvända mappningen är ToUnicode CMap, en valfri ström som bifogas typsnittet

När den finns där och är korrekt kommer kopierad text ut som de rätta tecknen. När den saknas eller är fel, eller typsnittet delmängdsskapades med anpassade glyfkoder och ingen ToUnicode skrevs, ser sidan perfekt ut och klippboken fylls med skräp: glyfkoder läses som om de vore Unicode, vilket de inte är för en anpassad delmängd. Det är därför ett skannat dokument med ett OCR-textlager kan vara sökbart medan en digitalt född PDF från en slarvig generator inte är det. Rendering och extraktion ritar från olika tabeller, så en fil kan tillfredsställa den ena och misslyckas med den andra. Om extraktion spelar roll för din output, behandla en korrekt ToUnicode-karta som ett krav, och verifiera den genom att kopiera text ur ett prov i stället för att lita på att den finns där

Hur man diagnostiserar en typsnittsbugg snabbt

Feltypen berättar för dig var du ska leta. Tomma rutor på en annan maskin betyder nästan alltid ett typsnitt som inte var inbäddat, så kontrollera inbäddning först och glyftäckning sedan. Rutor som dyker upp även på din egen maskin pekar på täckning: typsnittet innehåller inte det skriptet, oavsett inbäddning. Text som renderas korrekt men kopieras ut som nonsens är ett ToUnicode-problem, inte ett renderingsproblem, och att pilla med typsnitt eller inbäddning kommer inte att fixa det eftersom ritningen aldrig var trasig. För att läsa en färdig fil, öppna den i Acrobat och titta på Dokumentegenskaper, Typsnitt: en hälsosam post visar typen, säger Inbäddat eller Inbäddad delmängd och namnger kodningen. Ett typsnitt som borde vara inbäddat och inte är det meddelar sig självt där innan en kund gör det

Inget av detta är exotiskt när uppdelningen mellan tecken, kod och glyf är tydlig. Bädda in de typsnitt du ritar med, gör delmängder av de stora, sträck dig efter ett Unicode-typsnitt och RegisterUnicodeTTF i samma ögonblick som text lämnar latin, och behåll en korrekt ToUnicode-karta om någon ska extrahera texten. Få de sakerna rätt och rutorna slutar att visas. För den omgivande mekaniken visar anatomin av en minimal PDF var typsnittsordboken sitter i objekt-trädet, och genomgången av dokumentstruktur täcker hur resurser delas över sidor

Anropen SetFont, FontEmbedding och RegisterUnicodeTTF som visas här är en del av HotPDF-komponenten för Delphi och C++Builder