Technický článek

Vložení chybějících písem do existujících PDF pro PDF/A v Delphi

losLab PDF Library dokáže vložit chybějící programy písem již načteného PDF pomocí jediného volání: EmbedMissingFonts projde každý slovník písem v dokumentu, vyhledá odpovídající nainstalované systémové písmo podle jeho názvu BaseFont a zapíše program písma zpět do souboru. Pro týmy opravující dokumenty třetích stran, které selhávají při validaci PDF/A kvůli vkládání písem, je to oprava, díky níž chyba preflightu 00030 zmizí

Tento scénář je skličující a velmi běžný. Archivační linka přijímá soubory PDF od dodavatelů, zákazníků nebo skenovací služby; dokumenty se správně vykreslují na každém stole v budově; a poté validátor PDF/A odmítne celou dávku se stejnou stížností opakovanou jednou pro každý soubor: alespoň jedno písmo není vloženo. Nikdo na začátku řetězce soubory znovu nevygeneruje, takže je linka musí opravit. Tento článek se zabývá touto cestou opravy. Navazuje na článek o preflightu, který se zabývá detekcí porušení norem PDF/A a PDF/UA: tento článek vám napoví, které dokumenty jsou poškozené, a ten aktuální opravuje nejčastější způsob jejich poškození

Proč PDF/A vyžaduje vložení každého písma?

Norma ISO 19005-1 §6.3.4 vyžaduje, aby každé písmo použité v vyhovujícím dokumentu mělo svůj program písma uložený uvnitř souboru, protože celým příslibem PDF/A je reprodukovatelnost: dokument se musí vykreslit identicky na stroji za padesát let, který nesdílí žádná písma se strojem, na němž byl vytvořen. Nevložené písmo je instrukcí pro vyhledání písma Arial někde v zobrazovacím systému a postoj normy je takový, že „někde v zobrazovacím systému“ nepředstavuje archivační záruku. Jakékoli glyfy, metriky a pokrytí má náhradní písmo, to čtenář dostane, a nemusí to být to, co viděl autor

Historickým viníkem je konvence Standard 14. PDF 1.0 slibovalo, že každý prohlížeč bude obsahovat Helvetica, Times, Courier, Symbol a ZapfDingbats, takže generátory se naučily na tato písma odkazovat jménem a nic nevkládat, a třicet let nástrojů dělá přesně totéž. Knihovna losLab PDF Library bere tento požadavek natolik vážně, že v režimu vytváření PDF/A je volání AddStandardFont záměrně prázdnou operacijí (no-op): knihovna nedodává programy písem Standard 14, nemůže vložit to, co nemá, a odmítá zapsat nevložený odkaz do dokumentu, který deklaruje shodu s normou. Vrátí hodnotu 0 bez výběru písma, takže dokument PDF/A musí místo toho použít AddTrueTypeFont s vkládáním a jakýkoli požadavek na Embed=0 je při aktivním režimu PDF/A tiše povýšen na Embed=1. To je strana zápisu. Obtížnějším problémem je strana čtení: dokument, který již napsal někdo jiný, plný slovníků písem, které jste nevytvořili

Jak EmbedMissingFonts opravuje načtený dokument?

losLab PDF Library opravuje písma na místě, místo aby je znovu sestavovala. Když generátor PDF zapíše nevložené TrueType písmo, slovník FontDescriptor, který vytvoří, je již kompletní: FontName, FontBBox, Flags, Ascent, Descent, StemV, vše je přítomno. Jediné, co jej odlišuje od vloženého písma, je absence jedné položky, a to odkazu na datový proud /FontFile2 obsahující skutečný program písma. Funkce EmbedMissingFonts se tedy nedotýká slovníku písma, kódování, pole šířek ani žádného datového proudu obsahu, který na písmo odkazuje názvem prostředku. Přečte odpovídající program písma ze systému, zkomprimuje jej do nového objektu streamu a přidá jediný odkaz /FontFile2 (nebo /FontFile3 pro písma CIDFontType0) do již existujícího FontDescriptoru. Vše, na co stránky dokumentu ukazují, zůstává přesně tam, kde to bylo, což činí tuto operaci bezpečnou pro spuštění na souborech, které nemáte pod kontrolou

Pokrytí zahrnuje obě architektury písem, s nimiž se v praxi setkáte: jednoduchá TrueType písma a kompozitní písma Type0/CID, tedy druhy vytvářené pro texty CJK a moderní kódování Unicode. Průchod záměrně prochází každý slovník Font v objektovém stromu dokumentu, místo aby se spoléhal na procházení prostředků stránku po stránce, takže jsou zachycena i písma odkazovaná z poznámek nebo sdílená napříč stránkami. Rozhraní API tvoří jedno volání nad načteným dokumentem

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');

    // Walks every Font dictionary; returns how many fonts
    // gained a font program. Fonts whose program cannot be
    // found on the system are skipped, not failed.
    Repaired := PDF.EmbedMissingFonts;
    Writeln(Format('%d font program(s) embedded', [Repaired]));

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

Jeden detail stojí za zmínku, protože vysvětluje, proč porovnávání názvů funguje lépe než naivní porovnávání řetězců: knihovna před vyhledáním normalizuje názvy BaseFont. Prefixy podsad (vzor ABCDEF+ skládající se ze šesti velkých písmen a znaménka plus) are odstraněny, přípony ve stylu PostScriptu jako ArialMT se převedou na Arial a soubory TrueType Collection jsou detekovány a rozbaleny, takže se řez písma nacházející se uvnitř .ttc stále správně vloží

Ověření opravy pomocí zprávy o preflightu

CreatePreflightReport je krok ověření a smyčka se tím uzavírá: stejný audit, který soubor označil za chybný, by měl být ten, který jej schválí. Kód chyby 00030 je nález hloubkového auditu PDF/A, který říká: „Nejméně jedno písmo není vloženo (chybí FontFile/FontFile2/FontFile3)“, a je hlášen pro soubor jako celek, takže i jediné přehlédnuté písmo udržuje chybu aktivní. Spusťte zprávu nad zdrojovým souborem, proveďte opravu, uložte ji a znovu ji spustte nad výstupem

function HasFontEmbeddingViolation(PDF: TPDFlib;
  const FileName: string): Boolean;
var
  Report: string;
begin
  // ComplianceTests = 1 selects the PDF/A checks
  Report := PDF.CreatePreflightReport(FileName, '', 1, 0);
  Result := Pos('00030', Report) > 0;
end;

Chcete-li získat pohled na jednotlivá písma spíše než celkový verdikt nad souborem, načtěte opravený dokument znovu a proveďte enumeraci: FindFonts následované SelectFont a GetFontIsEmbedded hlásí stav vložení písmo po písmu, což je správný nástroj, když dávková úloha potřebuje zaznamenat, který konkrétní řez v kterém souboru nebylo možné opravit. Stejný vzor enumerace se objevuje v článku o extrahování textu, obrázků a písem z načtených PDF, kde slouží k extrakci namísto opravy

Co se stane, když písmo není v systému nainstalováno?

Funkce EmbedMissingFonts přeskočí každé písmo, jehož program nemůže najít, a toto přeskočení nahlásí prostřednictvím své návratové hodnoty: pokud se vrátí nižší počet než počet nevložených písem, které jste napočítali, rozdíl představují písma, která systém nemá. Jedná se o korektní režim selhání a je lepší než alternativy, protože vytvoření náhradního programu pro písmo pojmenované v dokumentu by změnilo vykreslování, což je přesně to, co archivační oprava nesmí nikdy udělat. Pro tyto případy poskytuje losLab PDF Library metodu EmbedFontProgramFromFile, která vloží volajícím dodaný soubor .ttf nebo .otf do pojmenovaného písma, takže linka může distribuovat podniková písma, u nichž se očekává výskyt, a záměrně se k nim vracet

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
        // Try the installed system font first, then fall back
        // to a font file shipped alongside the application
        if PDF.EmbedFontProgram(PDF.FontName) = 0 then
          PDF.EmbedFontProgramFromFile(PDF.FontName,
            'fonts\CorporateSans.ttf');
  end;
end;

Dva limity je třeba uvést na rovinu. Za prvé, písma Type1 nejsou v současné implementaci opravována: jejich položka /FontFile vyžaduje třísegmentovou strukturu PFB s explicitními klíči délky a knihovna je raději přeskočí, než aby zapsala poškozený datový proud; v moderních dokumentech jsou vzácná, ale objevují se ve starých archivech. Za druhé, vkládání písma je licenční úkon. Oprávnění k vkládání TrueType písma patří jeho tvůrci a tým provádějící opravy, který vkládat licencované programy písem do dokumentů opouštějících organizaci, by měl mít potvrzeno, zda to licence k písmům skutečně umožňují. Knihovna udělá to, o co požádáte; zda o to můžete požádat, je otázka pro vaše právní oddělení, nikoli pro váš kompilátor

Vložení je nutné, nikoli však dostačující

Oprava písem odstraní pouze chybu 00030 a nic jiného. Dokument, který selže při validaci PDF/A z důvodu šifrování, chybějících metadat XMP, na zařízení závislého barevného prostoru bez OutputIntent nebo na chybějících mapách ToUnicode, selže i po vložení všech písem, a proto oprava patří spíše do smyčky řízené preflightem než aby ji nahrazovala. Spusťte úplnou zprávu, opravte to, co jmenuje, a nechte zprávu, aby vám řekla, kdy máte hotovo. Existuje také nákladová dimenze: kompletní program písma CJK má velikost v megabajtech, takže vložení několika z nich může malý dokument dramaticky zvětšit. Protiváhou je vytváření podsad (subsetting), kterému se věnuje článek o optimalizaci velikosti souborů PDF a vytváření podsad písem, což zredukuje každý vložený program pouze na glyfy, které dokument skutečně vykresluje

Zamezení regrese u nových dokumentů

Metoda SetEmbedAllFonts je preventivní částí téže funkce: ochrana na straně zápisu, která brání vašemu vlastnímu kódu v vytváření dokumentů, které tento článek opravuje. Pokud je aktivní SetEmbedAllFonts(1), jakékoli následné volání AddTrueTypeFont požadující Embed=0 je povýšeno na vložený odkaz, což rozšiřuje na každý dokument záruku, kterou již vynucuje režim PDF/A. Ovlivňuje písma přidaná po volání, nikoli písma již obsažená v načteném souboru, takže rozdělení úloh je jasné: SetEmbedAllFonts pro dokumenty, které vytváříte, a EmbedMissingFonts pro dokumenty, které dědíte

PDF.NewDocument;
PDF.SetEmbedAllFonts(1);
// From here on, AddTrueTypeFont(Name, 0) behaves
// like AddTrueTypeFont(Name, 1): no non-embedded
// reference can reach the output file

Obě poloviny, ochrana na straně zápisu i cesta načíst-opravit-uložit, jsou součástí knihovny losLab PDF Library pro Delphi, C# a VB.NET, společně s modulem preflightu, který ověřuje výsledek; stránka produktu obsahuje kompletní referenční příručku API pro písma včetně volání pro vkládání a vytváření podsad pro jednotlivá písma