Tehnički članak

Ugradnja nedostajućih fontova u PDF/A dokumente u Delphiju

losLab PDF Library može programe fontova koji nedostaju u već učitanom PDF-u ugraditi jednim pozivom: EmbedMissingFonts prolazi kroz svaki rječnik fontova u dokumentu, po nazivu BaseFont pronalazi odgovarajući instalirani sustavski font i zapisuje program fonta natrag u datoteku. Timovima koji popravljaju tuđe dokumente što padaju na PDF/A provjeri ugradnje fontova, ovo je popravak koji uklanja preflight pogrešku 00030

Scenarij je obeshrabrujuće čest. Cjevovod za unos u arhivu prima PDF-ove od dobavljača, kupaca ili servisa za skeniranje; dokumenti se uredno prikazuju na svakom radnom mjestu u zgradi; a onda PDF/A validator odbije cijelu seriju uz istu primjedbu ponovljenu jednom po datoteci: barem jedan font nije ugrađen. Nitko uzvodno neće ponovno generirati datoteke, pa ih cjevovod mora popraviti. Ovaj članak pokriva taj put popravka. Pratilac je članka o preflightu, koji obrađuje otkrivanje kršenja PDF/A i PDF/UA: onaj tekst govori koji su dokumenti pokvareni, a ovaj popravlja najčešći način na koji jesu

Zašto PDF/A traži da svaki font bude ugrađen?

ISO 19005-1 §6.3.4 traži da svaki font koji sukladan dokument koristi nosi svoj program fonta unutar datoteke, jer je cijelo obećanje PDF/A-a ponovljivost: dokument se mora prikazati jednako i na stroju za pedeset godina koji s proizvodnim strojem ne dijeli nijedan font. Font koji nije ugrađen uputa je da se Arial negdje potraži na sustavu za pregled, a stav standarda jest da „negdje na sustavu za pregled“ nije arhivsko jamstvo. Kakve god glifove, metrike i pokrivenost zamjenski font ima, to je ono što čitatelj dobiva, i to možda nije ono što je autor vidio

Povijesni je krivac konvencija Standard 14. PDF 1.0 obećao je da svaki preglednik isporučuje Helveticu, Times, Courier, Symbol i ZapfDingbats, pa su generatori naučili te fontove referencirati po nazivu i ne ugrađivati ništa, a trideset godina alata i danas radi upravo to. losLab PDF Library taj zahtjev shvaća dovoljno ozbiljno da je u načinu izrade PDF/A dokumenata AddStandardFont namjerno bez učinka: biblioteka ne isporučuje programe fontova iz skupa Standard 14, ne može ugraditi ono što nema i odbija u dokument koji tvrdi da je sukladan zapisati referencu na neugrađeni font. Vraća 0 bez odabira fonta, pa PDF/A dokument umjesto toga mora koristiti AddTrueTypeFont s ugradnjom, a svaki zahtjev Embed=0 tiho se promiče u Embed=1 dok je PDF/A način aktivan. To je strana zapisivanja. Teži je problem strana čitanja: dokument koji je netko drugi već napisao, pun rječnika fontova koje niste vi stvorili

Kako EmbedMissingFonts popravlja učitani dokument?

losLab PDF Library popravlja fontove na mjestu umjesto da ih iznova gradi. Kada generator PDF-a zapiše TrueType font bez ugradnje, rječnik FontDescriptor koji pritom nastaje već je potpun: FontName, FontBBox, Flags, Ascent, Descent, StemV, sve je prisutno. Jedino što ga dijeli od ugrađenog fonta izostanak je jednog unosa, reference na tok /FontFile2 koja drži stvarni program fonta. Zato EmbedMissingFonts ne dira rječnik fonta, kodiranje, polje širina ni ijedan tok sadržaja koji font referencira po nazivu resursa. Čita odgovarajući program fonta sa sustava, sažima ga u novi objekt toka i u FontDescriptor koji je ondje već bio dodaje jednu referencu /FontFile2 (ili /FontFile3 za fontove CIDFontType0). Sve na što stranice dokumenta pokazuju ostaje točno gdje je bilo, a upravo to čini operaciju sigurnom za pokretanje nad datotekama koje ne kontrolirate

Dijagram PDF Library for Delphi rječnika FontDescriptor prije i nakon poziva EmbedMissingFonts: svi su unosi već prisutni i dodaje se samo tok FontFile2 koji je nedostajao, preuzet iz instaliranog sustavskog fonta
EmbedMissingFonts dodaje jednu referencu na tok u deskriptor koji je već potpun

Pokrivenost obuhvaća obje arhitekture fontova na koje ćete u praksi naići: jednostavne TrueType fontove i složene Type0/CID fontove, one koji nastaju za CJK tekst i moderni Unicode izlaz. Obilazak namjerno nabraja svaki rječnik Font u stablu objekata dokumenta umjesto da se oslanja na prolazak kroz resurse stranicu po stranicu, pa se pokupe i fontovi na koje se pozivaju bilješke ili koji se dijele među stranicama. API je jedan poziv nad učitanim dokumentom

var
  PDF: TPDFlib;
  Repaired: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('supplier-invoice.pdf', '') <> 1 then
      raise Exception.Create('Could not load PDF');

    // Prolazi kroz svaki rječnik Font; vraća koliko je fontova
    // dobilo program fonta. Fontovi čiji se program ne može
    // pronaći na sustavu preskaču se, ne javljaju pogrešku.
    Repaired := PDF.EmbedMissingFonts;
    Writeln(Format('%d font program(s) embedded', [Repaired]));

    PDF.SaveToFile('supplier-invoice-repaired.pdf');
  finally
    PDF.Free;
  end;
end;

Jedan detalj vrijedi znati jer objašnjava zašto podudaranje naziva radi bolje od naivne usporedbe nizova: biblioteka normalizira nazive BaseFont prije nego što ih potraži. Prefiksi podskupa (uzorak ABCDEF+ od šest velikih slova i znaka plus) uklanjaju se, sufiksi u PostScript stilu poput ArialMT razrješavaju se u Arial, a datoteke TrueType Collection prepoznaju se i raspakiravaju, pa se rez fonta koji živi unutar .ttc datoteke svejedno ispravno ugradi

Provjera popravka izvještajem preflighta

CreatePreflightReport je korak provjere, a petlja je namjerno zatvorena: ista revizija koja je datoteku osudila trebala bi je i osloboditi. Šifra pogreške 00030 nalaz je dubinske PDF/A revizije koji glasi „At least one font is not embedded (FontFile/FontFile2/FontFile3 missing)“, a prijavljuje se za datoteku kao cjelinu, pa je jedan previđeni font održava na životu. Pokrenite izvještaj nad izvornom datotekom, popravite, spremite i pokrenite ga ponovno nad izlazom

PDF Library for Delphi: zatvorena petlja popravka u kojoj CreatePreflightReport prijavi šifru pogreške 00030, EmbedMissingFonts ugradi prijavljene fontove, SaveToFile zapiše popravljeni PDF, a drugo pokretanje izvještaja potvrdi da serija prolazi
Prvo pokrenite izvještaj preflighta, pustite ga da vodi popravak, a zatim neka ista revizija ovjeri izlaz
function HasFontEmbeddingViolation(PDF: TPDFlib;
  const FileName: string): Boolean;
var
  Report: string;
begin
  // ComplianceTests = 1 odabire provjere za PDF/A
  Report := PDF.CreatePreflightReport(FileName, '', 1, 0);
  Result := Pos('00030', Report) > 0;
end;

Za pogled po pojedinom fontu umjesto presude nad cijelom datotekom, ponovno učitajte popravljeni dokument i nabrojite ih: FindFonts, a zatim SelectFont i GetFontIsEmbedded, daju status ugradnje font po font, što je pravi alat kada skupna obrada mora zabilježiti točno koji se rez u kojoj datoteci nije dao popraviti. Isti se obrazac nabrajanja pojavljuje u članku o izvlačenju teksta, slika i fontova iz učitanih PDF-ova, gdje hrani izvlačenje umjesto popravka

Što se događa kada font nije instaliran na sustavu?

EmbedMissingFonts preskače svaki font čiji program ne može pronaći i to preskakanje prijavljuje povratnom vrijednošću: ako je broj manji od broja neugrađenih fontova koje ste prebrojali, razlika su fontovi kojih sustav nema. To je pošten način neuspjeha i bolji je od svih alternativa, jer bi izmišljanje zamjenskog programa za font imenovan u dokumentu promijenilo prikaz, a upravo to arhivski popravak nikada ne smije učiniti. Za takve slučajeve losLab PDF Library nudi EmbedFontProgramFromFile, koji u imenovani font ugrađuje .ttf ili .otf datoteku koju pozivatelj dostavi, pa cjevovod može uz sebe nositi korporativne fontove na koje očekuje naići i namjerno se osloniti na njih

PDF Library for Delphi tok odlučivanja koji nabraja svaki rječnik fonta, normalizira nazive BaseFont prema instaliranom sustavu, pronađene programe ugrađuje pozivom EmbedFontProgram ili se vraća na EmbedFontProgramFromFile, te prijavljuje broj popravljenih fontova
Normalizacija naziva čini pretragu po sustavu popustljivom, a povratni broj drži preskočene rezove vidljivima
var
  I, FontID: Integer;
begin
  PDF.FindFonts;
  for I := 1 to PDF.FontCount do
  begin
    FontID := PDF.GetFontID(I);
    if (FontID > 0) and (PDF.SelectFont(FontID) = 1) then
      if PDF.GetFontIsEmbedded = 0 then
        // Najprije pokušaj s instaliranim sustavskim fontom, a
        // zatim s datotekom fonta isporučenom uz aplikaciju
        if PDF.EmbedFontProgram(PDF.FontName) = 0 then
          PDF.EmbedFontProgramFromFile(PDF.FontName,
            'fonts\CorporateSans.ttf');
  end;
end;

Dvije granice zaslužuju da se izgovore izravno. Prvo, Type1 fontovi u trenutnoj se izvedbi ne popravljaju: njihov unos /FontFile traži trodijelnu PFB strukturu s izričitim ključevima duljine, pa ih biblioteka radije preskoči nego da zapiše deformiran tok; rijetki su u modernim dokumentima, ali se u starim arhivima pojavljuju. Drugo, ugradnja fonta je licencni čin. Dopuštenja za ugradnju TrueType fonta pripadaju njegovoj slovolivnici, pa bi kod cjevovoda koji licencirane programe fontova trpa u dokumente što napuštaju organizaciju netko trebao potvrditi da licencije fontova to doista dopuštaju. Biblioteka će učiniti ono što tražite; smijete li tražiti, pitanje je za vaš pravni odjel, a ne za vaš kompajler

Ugradnja je nužna, ali nije dovoljna

Popravak fontova uklanja pogrešku 00030 i ništa drugo. Dokument koji na PDF/A pada zbog enkripcije, nedostajućih XMP metapodataka, prostora boja ovisnog o uređaju bez zapisa OutputIntent ili izostalih ToUnicode mapa i dalje će padati nakon što se ugrade svi fontovi, i zato popravak pripada unutar petlje koju vodi preflight, a ne umjesto nje. Pokrenite potpun izvještaj, popravite ono što imenuje i pustite izvještaj da vam kaže kada ste gotovi. Postoji i troškovna strana: potpun program CJK fonta ide u megabajte, pa ugradnja nekoliko njih može dramatično napuhati mali dokument. Protuteža je izrada podskupa, obrađena u članku o optimizaciji veličine PDF datoteke i izradi podskupa fontova, koja svaki ugrađeni program svodi na glifove koje dokument doista prikazuje

Kako spriječiti da novi dokumenti opet zakažu

SetEmbedAllFonts preventivna je polovica iste značajke: zaštita na strani zapisivanja koja sprječava da vaš vlastiti kod proizvede dokumente kakve ovaj članak popravlja. Dok je SetEmbedAllFonts(1) aktivan, svaki sljedeći poziv AddTrueTypeFont koji traži Embed=0 promiče se u ugrađenu referencu, čime se na svaki dokument proširuje jamstvo koje PDF/A način već nameće. Djeluje na fontove dodane nakon poziva, a ne na fontove koji su već u učitanoj datoteci, pa je podjela posla čista: SetEmbedAllFonts za dokumente koje sami stvarate, EmbedMissingFonts za dokumente koje nasljeđujete

PDF.NewDocument;
PDF.SetEmbedAllFonts(1);
// Odsad se AddTrueTypeFont(Name, 0) ponaša
// kao AddTrueTypeFont(Name, 1): nijedna neugrađena
// referenca ne može doći do izlazne datoteke

Obje polovice, zaštita na strani zapisivanja i put učitaj-popravi-spremi, dio su biblioteke losLab PDF Library za Delphi, C# i VB.NET, uz mehanizam preflighta koji provjerava rezultat; stranica proizvoda nosi potpunu referencu API-ja za fontove, uključujući pozive za ugradnju i izradu podskupa po pojedinom fontu