Teknisk artikkel

PDFlibPas: automatisk skriftfallback for CJK og emoji

PDFlibPas løser tegn den valgte skriften ikke kan tegne, ved å søke gjennom en fallback-kjede av installerte skrifter, klynge for klynge, samtidig som forming og bidireksjonal rekkefølge bevares. Du aktiverer det med SetAutomaticFontFallback, utvider kjeden med AddFontFallback, og bare fallback-skriftene som faktisk brukes i utdataene, blir bygget inn i filen

Problemet dette løser, møter enhver dokumentgenerator første gang et kundenavn kommer i et skriftsystem malskriften aldri var laget for. Feilen er stille, og det er nettopp det som gjør den kostbar

Hvorfor forsvinner ustøttet tekst i stedet for å gi en feilmelding?

Fordi PDF ikke har noe begrep om en skrift som ikke kan tegne et tegn. En enkel skrift kobler bytekoder til glyffnavn gjennom en koding; en sammensatt skrift kobler koder gjennom et CMap til glyffindekser. Ber du om en glyff skriften ikke inneholder, får du glyffindeks null, .notdef, som de fleste skrifter tegner som ingenting eller som en tom boks. Filen er strukturelt gyldig, tekstoperatoren er velformet, og siden rendres. Det er bare tomt der navnet skulle stått

Ingenting i ISO 32000-1 krever at en produsent legger merke til dette. En generator som skriver tekst uten å sjekke dekning, produserer en teknisk konform PDF som stille har mistet innhold, og tapet dukker opp på en kundes skjerm uker senere. Dette er grunnen til at fallback-funksjonen og rapporten for manglende glyffer leveres sammen: å løse det som kan løses, er bare halve jobben, og å rapportere det som ikke kunne løses, er den andre halvparten

Fallback skjer per klynge, ikke per kodepunkt

Granularitet er detaljen som skiller en fungerende implementasjon fra en tilsynelatende god en. Tekst er ikke en sekvens av uavhengige tegn. En devanagari-stavelse, en emoji med en hudtonemodifikator, en grunnbokstav med diakritiske merker: hver av dem er én klynge som må rendres av én skrift, fordi formingsavgjørelsene inni den avhenger av tabeller i den spesifikke skriften

PDFlibPas løser klynger, så en klynge som en fallback-skrift dekker, tegnes fullstendig av den skriften. Å dele midt i en klynge og tegne halvparten fra primærskriften og halvparten fra en fallback ville gitt et teknisk sett til stede, men visuelt ødelagt resultat, som man kan hevde er verre enn det tomme rommet du startet med. Rekkefølgen bevares også, slik at en fallback inne i en høyre-til-venstre-sekvens ikke omorganiserer teksten rundt; den samme mekanismen ligger under den vertikale layouten beskrevet i vertikal skriving for japansk og kinesisk

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.SetAutomaticFontFallback(1);

    // Søkerekkefølge: første treff vinner, så plasser de bredeste skriftene sist
    Lib.AddFontFallback('Microsoft YaHei');   // Forenklet kinesisk
    Lib.AddFontFallback('Meiryo');            // Japansk
    Lib.AddFontFallback('Segoe UI Symbol');
    Lib.AddFontFallback('Segoe UI Emoji');

    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);

    Lib.AddTrueTypeFont('Arial', 1);          // 1 = bygg inn skriften
    Lib.SetTextSize(11);
    Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
    Lib.DrawText(72, 700, 'Delivery status: on time');

    Lib.SaveToFile('invoice.pdf');
  finally
    Lib.Free;
  end;
end;

Sett opp kjeden med omhu. Løsningen bruker den første skriften som dekker klyngen, så en bred pan-Unicode-skrift plassert først vil vinne over nesten alt, og de nøye utvalgte skriftsystemspesifikke skriftene dine blir aldri konsultert. Sett de spesifikke skriftene først og fangnetts-skriften sist

Rapportere eller avbryte: hvilken feil vil du ha?

SetMissingGlyphPolicy tar imot PDF_MISSING_GLYPH_REPORT, standardvalget som er kompatibelt bakover, eller PDF_MISSING_GLYPH_ABORT. Under rapporteringspolicyen fortsetter tekstoperasjonen, uløselige kodepunkter droppes som før, og hver av dem registreres. Under avbruddspolicyen avvises tekstoperasjonen før noe innhold skrives, og LastErrorCode settes til 521

Velg ut fra hva dokumentet skal brukes til. En batch med interne rapporter bør fortsette å rendre og logge hullene, fordi en litt ufullstendig rapport i dag er bedre enn ingen rapport i det hele tatt. En juridisk bindende kontrakt, en faktura eller noe som helst med et navn på, bør avbrytes, fordi et stille droppet tegn i et partsnavn er en feil du vil oppdage i din egen prosess snarere enn i en tvist. Avbruddspolicyen feiler før noe skrives, så ingen halvferdig innholdsstrøm blir liggende igjen

var
  Lib: TPDFlib;
  Report: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
    // ... bygg dokumentet ...

    if Lib.DrawText(72, 660, CustomerName) <> 1 then
      if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
      begin
        Report := Lib.GetMissingGlyphReportJSON;
        // {"valid":false,"policy":1,"eventCount":1,"events":[
        //   {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
        //    "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
        //    "fontType":"TrueType","operation":"DrawText"}]}
        EscalateToOperator(Report);
      end;
  finally
    Lib.Free;
  end;
end;

Rapporten er bevisst maskinlesbar og begrenset. Hver hendelse inneholder siden, UTF-16-indeksen inne i strengen, kodepunktet i både numerisk og U+XXXX-form, skriften som ble valgt, dens type og operasjonen som traff problemet, slik at en supportsak kan navngi det nøyaktige tegnet i stedet for å beskrive et symptom. Sporeren beholder de 256 siste hendelsene, som er nok til å diagnostisere et dokument og lite nok til at en patologisk kjøring ikke kan gjøre diagnostikk til et minneproblem

Måling og tegning må stemme overens

Breddemåling bruker de samme klyngebevisste fallback-avgjørelsene som tegning. Dette høres opplagt ut og er nettopp det de fleste hjemmesnekrede fallback-lag gjør feil: de flikker på tegnestien, lar målingen bli værende på primærskriften, og hver tekstboks, høyrejustering og tabellkolonne ender opp med å bli beregnet fra bredder som ikke stemmer med det som faktisk ble rendret

Fordi begge stiene deler samme løsning, opptar en streng som måles før den tegnes, nøyaktig den bredden den ble målt til, inkludert fallback-sekvensene. Det er dette som gjør det trygt å slå på fallback globalt i stedet for bare på stedene du har kontrollert manuelt

Bare det du faktisk brukte, blir bygget inn

Fallback-skrifter bygges inn lat: en skrift i kjeden som aldri løste en klynge, bidrar med ingenting til utdataene. Et dokument som inneholder ett kinesisk tegn og 5000 latinske, bærer ikke en full CJK-skrift; det bærer det subsetting-prosessen produserte for den ene glyffen, som er atferden beskrevet i filstørrelsesoptimalisering og skriftsubsetting

Denne latskapen gjør det billig å konfigurere en bred kjede. Registrer skriftene dokumentsettet ditt kan trenge på tvers av alle språk du betjener, og hver enkelt PDF betaler bare for det den faktisk brukte. For dokumenter du ikke selv genererte, der de manglende skriftene allerede finnes inne i en eksisterende fil, er reparasjonsveien en annen og dekkes i å bygge inn manglende skrifter i en eksisterende PDF

Ett forbehold ved utrulling er verdt å si rett ut: fallback løses mot skrifter installert på maskinen som kjører koden. En server uten installerte CJK-skrifter har ingenting å falle tilbake til, og rapporten forteller deg dette allerede på det første dokumentet i stedet for etter den første klagen. Send med skriftene du er avhengig av, og bekreft lisensieringen for å bygge dem inn

PDFlibPas er et PDF-bibliotek for Delphi, C++Builder og Lazarus med matchende DLL- og ActiveX-grensesnitt, så API-ene for fallback og manglende glyffer er tilgjengelige også for kallere utenfor Pascal. Full dokumentasjon finnes på PDFlibPas sin side for Delphi PDF-bibliotek