Technisch artikel

Ontbrekende lettertypen insluiten in bestaande PDF's voor PDF/A in Delphi

losLab PDF Library kan de ontbrekende lettertypeprogramma's van een reeds geladen PDF insluiten met een enkele aanroep: EmbedMissingFonts doorloopt elk lettertypewoordenboek in het document, zoekt het bijbehorende geïnstalleerde systeemlettertype op basis van de BaseFont-naam en schrijft het lettertypeprogramma terug in het bestand. Voor teams die documenten van derden repareren die de PDF/A-validatie op lettertype-insluiting niet doorstaan, is dit de oplossing die preflight-fout 00030 doet verdwijnen

Dit scenario komt helaas vaak voor. Een archiefverwerkingspijplijn ontvangt PDF's van leveranciers, klanten of een scanbureau; the documenten worden op elk bureau in het gebouw prima weergegeven; en vervolgens keurt de PDF/A-validator de hele batch af met dezelfde klacht die eenmaal per bestand wordt herhaald: er is ten minste één lettertype niet ingesloten. Niemand stroomopwaarts zal de bestanden opnieuw genereren, dus moet de pijplijn ze repareren. Dit artikel behandelt dat reparatiepad. Het is de aanvulling op het preflight-artikel, dat het detecteren van PDF/A- en PDF/UA-schendingen behandelt: dat deel vertelt u welke documenten kapot zijn, dit deel repareert de meest voorkomende manier waarop ze kapot zijn

Waarom vereist PDF/A dat elk lettertype is ingesloten?

ISO 19005-1 §6.3.4 vereist dat elk lettertype dat door een conform document wordt gebruikt, zijn lettertypeprogramma in het bestand meedraagt, omdat de belofte van PDF/A volledig draait om reproduceerbaarheid: het document moet over vijftig jaar identiek worden weergegeven op een machine die geen lettertypen deelt met de machine die het heeft geproduceerd. Een niet-ingesloten lettertype is een instructie om ergens op het weergavesysteem naar Arial te zoeken, en het standpunt van de standaard is dat "ergens op het weergavesysteem" geen archiveringsgarantie is. Welke glyphs, statistieken en dekking het vervangende lettertype ook heeft, dat is wat de lezer te zien krijgt, en het kan afwijken van wat de auteur zag

De historische boosdoener is de Standard 14-conventie. PDF 1.0 beloofde dat elke viewer Helvetica, Times, Courier, Symbol en ZapfDingbats meelevert, dus leerden generatoren om naar die lettertypen te verwijzen op naam en niets in te sluiten, en dertig jaar aan tools doet nog steeds precies dat. losLab PDF Library neemt deze vereiste zo serieus dat in de PDF/A-aanmaakmodus AddStandardFont bewust een no-op is: de bibliotheek levert de Standard 14-lettertypeprogramma's niet mee, kan niet insluiten wat ze niet heeft, en weigert een niet-ingesloten verwijzing te schrijven naar een document dat beweert conform te zijn. Het retourneert 0 zonder een lettertype te selecteren, dus moet een PDF/A-document in plaats daarvan AddTrueTypeFont met insluiting gebruiken, en elk verzoek voor Embed=0 wordt stilletjes gepromoveerd tot Embed=1 terwijl de PDF/A-modus actief is. Dat is de kant van de schrijver. Het moeilijkere probleem is de kant van de lezer: een document dat iemand anders al heeft geschreven, vol met lettertypewoordenboeken die u niet hebt gemaakt

Hoe repareert EmbedMissingFonts een geladen document?

losLab PDF Library repareert lettertypen ter plaatse in plaats van ze opnieuw op te bouwen. Wanneer een PDF-generator een niet-ingesloten TrueType-lettertype schrijft, is het geproduceerde FontDescriptor-woordenboek al compleet: FontName, FontBBox, Flags, Ascent, Descent, StemV, allemaal aanwezig. Het enige dat het scheidt van een ingesloten lettertype is de afwezigheid van één item, de streamverwijzing /FontFile2 die het werkelijke lettertypeprogramma bevat. Dus EmbedMissingFonts raakt het lettertypewoordenboek, de codering, de breedtetabel of enige inhoudsstroom die naar het lettertype verwijst via de resourcenaam niet aan. Het leest het overeenkomstige lettertypeprogramma van het systeem, comprimeert het in een nieuw streamobject en voegt een enkele /FontFile2-verwijzing (of /FontFile3 voor CIDFontType0-lettertypen) toe aan de FontDescriptor die er al staat. Alles waarnaar de pagina's van het document verwijzen blijft precies waar het was, wat de bewerking veilig maakt om uit te voeren op bestanden die u niet beheert

Diagram van PDF Library for Delphi met een FontDescriptor-woordenboek voor en na EmbedMissingFonts: elke entry al aanwezig en alleen de ontbrekende FontFile2-stream toegevoegd vanuit het geïnstalleerde systeemfont
EmbedMissingFonts voegt één streamverwijzing toe aan een descriptor die al compleet is

De dekking omvat beide lettertype-architecturen die u in de praktijk tegenkomt: eenvoudige TrueType-lettertypen en samengestelde Type0/CID-lettertypen, het soort dat wordt geproduceerd voor CJK-tekst en moderne Unicode-uitvoer. De doorloop doorloopt bewust elk Font-woordenboek in de objectstructuur van het document in plaats van te vertrouwen op een paginaspecifieke doorloop van bronnen, zodat lettertypen waarnaar wordt verwezen vanuit annotaties of die over pagina's worden gedeeld ook worden meegenomen. De API is een enkele aanroep op het geladen document

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

    // Doorloopt elk Font-dictionary; retourneert hoeveel lettertypen
    // een lettertypeprogramma hebben gekregen. Lettertypen waarvan het programma niet
    // op het systeem kan worden gevonden, worden overgeslagen, niet als fout gemarkeerd.
    Repaired := PDF.EmbedMissingFonts;
    Writeln(Format('%d font program(s) embedded', [Repaired]));

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

Eén detail is het weten waard, omdat het verklaart waarom het matchen op naam beter werkt dan een naïeve stringvergelijking: de bibliotheek normaliseert BaseFont-namen voordat ze worden opgezocht. Subsetprefixen (het patroon ABCDEF+ van zes hoofdletters en een plusteken) worden weggehaald, PostScript-achtige achtervoegsels zoals ArialMT lossen op naar Arial, en TrueType Collection-bestanden worden herkend en uitgepakt, zodat een lettertype dat in een .ttc-bestand zit alsnog correct wordt ingesloten

De reparatie verifiëren met een preflight-rapport

CreatePreflightReport is de verificatiestap, en de cirkel is bewust rond: dezelfde audit die het bestand heeft afgekeurd, moet ook degene zijn die het goedkeurt. Foutcode 00030 is de PDF/A-deep-auditbevinding die luidt "Er is ten minste één lettertype niet ingesloten (FontFile/FontFile2/FontFile3 ontbreekt)", en dit wordt gerapporteerd voor het bestand als geheel, dus een enkele over het hoofd gezien lettertype houdt de fout in stand. Voer het rapport uit op het bronbestand, repareer, sla op en voer het opnieuw uit op het uitvoerbestand

PDF Library for Delphi: Gesloten herstellus waarin CreatePreflightReport foutcode 00030 vlagt, EmbedMissingFonts de gevlagde fonts inbedt, SaveToFile de gerepareerde PDF wegschrijft en een tweede rapportrun verifieert dat de batch slaagt
Draai eerst het preflight-rapport, laat het de reparatie aansturen en laat dezelfde audit daarna de uitvoer certificeren
function HasFontEmbeddingViolation(PDF: TPDFlib;
  const FileName: string): Boolean;
var
  Report: string;
begin
  // ComplianceTests = 1 selecteert de PDF/A-controles
  Report := PDF.CreatePreflightReport(FileName, '', 1, 0);
  Result := Pos('00030', Report) > 0;
end;

Laad het gerepareerde document opnieuw en doorloop het voor een weergave per lettertype in plaats van een oordeel per bestand: FindFonts gevolgd door SelectFont en GetFontIsEmbedded rapporteert de insluitingsstatus lettertype voor lettertype, wat het juiste hulpmiddel is wanneer een batchtaak exact moet loggen welk lettertype in welk bestand niet kon worden gerepareerd. Hetzelfde enumeratiepatroon verschijnt in het artikel over het extraheren van tekst, afbeeldingen en lettertypen uit geladen PDF's, waar het dient voor extractie in plaats van reparatie

Wat gebeurt er als het lettertype niet op het systeem is geïnstalleerd?

EmbedMissingFonts slaat elk lettertype over waarvan het het programma niet kan vinden, en rapporteert de overslaande actie via de retourwaarde: als de telling lager uitvalt dan het aantal niet-ingesloten lettertypen dat u hebt geteld, is het verschil de lettertypen die het systeem niet heeft. Dit is de eerlijke foutmodus en deze is beter dan de alternatieven, omdat het bedenken van een vervangend programma voor een lettertype dat in het document wordt genoemd de weergave zou veranderen, wat een archiveringsreparatie juist nooit mag doen. Voor deze gevallen biedt losLab PDF Library EmbedFontProgramFromFile, waarmee een door de aanroeper geleverd .ttf- of .otf-bestand in het genoemde lettertype wordt ingesloten, zodat een pijplijn de huisstijllettertypen kan meesturen die hij verwacht tegen te komen en daar bewust op kan terugvallen

Beslissingsstroom van PDF Library for Delphi die elk fontwoordenboek opsomt, BaseFont-namen normaliseert tegen het geïnstalleerde systeem, gevonden fontprogramma's inbedt met EmbedFontProgram of terugvalt op EmbedFontProgramFromFile, en het gerepareerde aantal rapporteert
Naamnormalisatie maakt de systeemopzoeking vergevingsgezind, en het teruggegeven aantal houdt overgeslagen lettertypen zichtbaar
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
        // Probeer eerst het geïnstalleerde systeemlettertype, val anders
        // terug op een lettertypebestand dat met de toepassing wordt meegeleverd
        if PDF.EmbedFontProgram(PDF.FontName) = 0 then
          PDF.EmbedFontProgramFromFile(PDF.FontName,
            'fonts\CorporateSans.ttf');
  end;
end;

Twee grenzen moeten duidelijk worden aangegeven. Ten eerste worden Type1-lettertypen in de huidige implementatie niet gerepareerd: hun /FontFile-item vereist de driesegmenten-PFB-structuur met expliciete lengtesleutels, en de bibliotheek slaat ze over in plaats van een misvormde stream te schrijven; ze zijn zeldzaam in moderne documenten, maar ze komen voor in oude archieven. Ten tweede is het insluiten van een lettertype een licentiehandeling. De insluitingsrechten van een TrueType-lettertype behoren toe aan de leverancier, en een reparatiepijplijn die gelicentieerde lettertypeprogramma's stopt in documenten die de organisatie verlaten, moet door iemand laten controleren of de lettertyplicenties dit daadwerkelijk toestaan. De bibliotheek doet wat u vraagt; of u dat mag vragen is een vraag voor uw juridische afdeling, niet voor uw compiler

Insluiten is noodzakelijk, maar niet voldoende

Het repareren van lettertypen lost alleen fout 00030 op en niets anders. Een document dat mislukt voor PDF/A op basis van versleuteling, ontbrekende XMP-metadata, een apparaatafhankelijke kleurruimte zonder een OutputIntent, of ontbrekende ToUnicode-tabellen, zal nog steeds mislukken nadat elk lettertype is ingesloten. Daarom hoort de reparatie thuis in een preflight-gestuurde lus in plaats van deze te vervangen. Voer het volledige rapport uit, repareer wat het aangeeft en laat het rapport u vertellen wanneer u klaar bent. Er is ook een kostenaspect: een volledig CJK-lettertypeprogramma kan oplopen tot megabytes, dus het insluiten van meerdere van deze programma's kan een klein document drastisch vergroten. Het tegengewicht is subsetting, behandeld in het artikel over PDF-bestandsgrootte-optimalisatie en lettertype-subsetting, dat elk ingesloten programma beperkt tot de glyphs die het document daadwerkelijk weergeeft

Voorkomen dat nieuwe documenten achteruitgaan

SetEmbedAllFonts is de helft ter voorkoming van dezelfde functie: een beveiliging aan de schrijverszijde die voorkomt dat uw eigen code de documenten produceert die in dit artikel worden gerepareerd. Als SetEmbedAllFonts(1) actief is, wordt elke volgende AddTrueTypeFont-aanroep die vraagt om Embed=0 gepromoveerd tot een ingesloten verwijzing, wat de garantie die de PDF/A-modus al afdwingt uitbreidt naar elk document. Het heeft invloed op lettertypen die na de aanroep worden toegevoegd, niet op lettertypen die al in een geladen bestand aanwezig zijn, dus de taakverdeling is duidelijk: SetEmbedAllFonts voor de documenten die u maakt, EmbedMissingFonts voor de documenten die u erft

PDF.NewDocument;
PDF.SetEmbedAllFonts(1);
// Vanaf hier gedraagt AddTrueTypeFont(Name, 0) zich
// als AddTrueTypeFont(Name, 1): geen niet-ingesloten
// verwijzing kan het uitvoerbestand bereiken

Beide helften, de beveiliging aan de schrijverszijde en het laad-repareer-bewaar-pad, maken deel uit van losLab PDF Library voor Delphi, C# en VB.NET, samen met de preflight-engine die het resultaat verifieert; de productpagina bevat de volledige referentie van de lettertype-API, inclusief de aanroepen voor insluiting en subsetting per lettertype