Teknisk artikel

Styra PDF-typsnittsersättning i Delphi med PDFium

PDFium Component låter en Delphi-applikation avgöra vilka typsnittsbyte som används när en PDF refererar till ett typsnitt den inte bäddar in. ConfigureSystemFontProvider installerar en implementation av IPdfSystemFontProvider som tar emot varje typsnittsmappningsförfrågan PDFium gör, komplett med typsnittsnamn, vikt, kursivflagga, teckenuppsättning och stigningsfamilj, och svarar med de TrueType-, TrueType Collection- eller OpenType-byte som ska användas

Detta finns eftersom icke-inbäddade typsnitt är ett renderingslotteri. En PDF som namnger Arial och inte bäddar in något renderas med Arial på en arbetsstation, med en metriskt kompatibel ersättning på en Linux-server, och med vad host-mapparen hittar på en låst containeravbild. Samma faktura ser olika ut på var och en, radbrytningar flyttas, och en kund tar emot ett dokument som inte matchar den arkiverade kopian

Varför inte bara installera typsnitten på servern?

Ibland är det svaret, och när det är det, ta det. Men det misslyckas i tre vanliga situationer. Licensiering kan förbjuda att installera ett typsnitt på en server för automatiserad rendering. Containeravbilder byggs om ofta och ett typsnitt installerat för hand försvinner vid nästa driftsättning. Och reglerade arbetsflöden behöver att renderingsstacken är reproducerbar från artefakter under versionskontroll, vilket en maskinomfattande typsnittsinstallation inte är

En leverantör adresserar alla tre genom att flytta beslutet in i din applikation. Typsnitt levereras som resurser du kontrollerar, mappningspolicyn är kod du kan granska, och samma binär renderar identiskt överallt eftersom inget beror på vad som råkar vara installerat

Att installera en leverantör

Konfiguration måste ske innan biblioteket laddas. PDFium accepterar en systemtypsnittsinfostruktur vid initiering och behåller handtag den delar ut efteråt, så att byta leverantör medan dokument är öppna skulle göra typsnittshandtag PDFium fortfarande håller ogiltiga; komponenten avvisar det helt istället för att låta det korrumpera en rendering:

uses
  PDFium;

type
  TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
  public
    function ResolveFont(const Request: TPdfSystemFontRequest;
      out Font: TPdfSystemFontData): Boolean;
  end;

function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
  out Font: TPdfSystemFontData): Boolean;
var
  Path: string;
begin
  // Deterministisk mappning: typsnittsnamn plus vikt och kursiv avgör
  // vilken fil vi levererar för denna förfrågan
  Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
    Request.Italic, Request.Charset);
  Result := Path <> '';
  if not Result then
    Exit;
  Font.FaceName := Request.FaceName;
  Font.FontData := LoadFileBytes(Path);   // kompletta sfnt- eller TTC-byte
  Font.Charset := Request.Charset;
  Font.TTCIndex := 0;                     // index inuti en samling
end;

var
  Policy: TPdfSystemFontPolicy;
begin
  Policy := TPdfSystemFontPolicy.Default;
  Policy.AllowDefaultFallback := False;   // värden avgör allt
  Policy.AllowFaceSubstitution := False;  // avvisa ett annat typsnittsnamn
  Policy.MaxFontBytes := 32 * 1024 * 1024;
  Policy.MaxCacheEntries := 64;

  ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
  // Ladda biblioteket och öppna dokument först nu
end;

Nedmontering körs i omvänd ordning: leverantören kopplas loss från PDFium först, sedan avlastas biblioteket. Att hoppa över frånkopplingen lämnar inhemska typsnittshandtag pekande på Pascal-objekt som strax ska frigöras, vilket är den klassiska nedstängningsåtkomstöverträdelsen i kod som blandar referensräknade gränssnitt med ett C-bibliotek

Vad policyflaggorna egentligen avgör

AllowDefaultFallback är växeln mellan två driftlägen. Med den avstängd misslyckas helt enkelt en förfrågan leverantören avböjer, vilket är vad du vill medan du bevisar att varje typsnitt i ett underlag är redovisat: varje lucka blir synlig omedelbart istället för att kittas över. Med den påslagen delegeras olösta förfrågningar till mapparen som returneras av FPDF_GetDefaultSystemFontInfo, medan omvärlden fortfarande ser ett enhetligt handtagsomslag, med typsnittsnamn, teckenuppsättning, tabelldata och typsnittsborttagning korrekt dirigerade efter ursprung

AllowFaceSubstitution styr om en leverantör får svara med ett annat typsnittsnamn än det som begärdes. Att stänga av den gör ersättning till ett explicit beslut snarare än en olycka, vilket spelar roll när ett dokument namnger ett typsnitt vars mått skiljer sig tillräckligt för att ändra paginering

Komponenten validerar varje leverantörssvar innan det når PDFium: tom data avvisas, överstora typsnitt avvisas mot MaxFontBytes, TTC-indexet kontrolleras, och enskilda sfnt-tabeller serveras från typsnittskatalogen när PDFium frågar efter en tabell snarare än efter hela filen. Den sistnämnda förmågan betyder att en leverantör kan lämna över en komplett typsnittsfil och låta komponenten svara på tabellnivåförfrågningar, istället för att exponera råa Pascal-objekt över C-ABI:et

Cachning utan hängande typsnittsdata

Typsnittsmappningsförfrågningar upprepas konstant under rendering, så svar cachas med en nyckel som täcker varje typsnittsvalsparameter, evikteras enligt begränsad senast-använd-ordning. Finessen är livstid: PDFium kan fortfarande läsa byten i ett typsnitt vars cacheinlägg just evikterats

Cachen lagrar referensräknade dynamiska arrayer och varje inhemskt handtag håller sin egen ögonblicksbild, så eviktion släpper en referens snarare än att frigöra minne i bruk. Borttagningsåterropet frisläpper handtaget och underhåller ett aktivt antal. Praktiskt betyder detta att MaxCacheEntries kan justeras för minne utan någon risk att dra bort data under en pågående rendering

Anropas leverantören på min tråd?

Nej, inte nödvändigtvis. PDFium kan anropa mapparen från sina egna arbetartrådar, så en implementation måste vara trådsäker. Delade räknare, cachen och konfigurationsövervakning skyddas var och en inuti komponenten av sitt eget kritiska avsnitt, men koden inuti ResolveFont är din att göra säker

Den säkraste formen är en leverantör som inte rör något föränderligt delat tillstånd: läs från en tabell byggd vid uppstart, ladda byte från en fil eller en resurs, returnera. Om en uppslagning behöver en egen delad cache, skydda den. Och håll undantag inuti din implementation, eftersom ett Pascal-undantag aldrig får linda sig upp genom PDFium-stacken; komponenten fångar vid C-ABI-gränsen och omvandlar till ett misslyckande eller en valfri standardreserv, men att förlita sig på det som normalt kontrollflöde kostar prestanda och döljer buggar. Trådningsregler för resten av komponenten följer samma principer som de i renderlåsdisciplin

Att bevisa mappningen i produktion

Statistik gör typsnittsersättning från gissningsarbete till något du kan hävda med säkerhet. GetSystemFontProviderStatistics rapporterar om en leverantör är konfigurerad och installerad, hur många mappningsförfrågningar som gjordes, och hur de tillgodosågs, uppdelat i cacheträffar, leverantörsträffar och standardreservträffar, tillsammans med avvisade svar, misslyckade förfrågningar, aktiva handtag och cachade typsnitt:

var
  Stats: TPdfSystemFontStatistics;
begin
  Stats := GetSystemFontProviderStatistics;
  Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
    [Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
     Stats.DefaultFallbackHits]));
  Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
    [Stats.RejectedProviderResponses, Stats.FailedRequests,
     Stats.ActiveHandles, Stats.CachedFonts]));

  // I en konformitetskörning med reserv avstängd betyder varje
  // reservträff eller misslyckad förfrågan att ett dokument refererade
  // till ett typsnitt vi inte levererar
  if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
    raise Exception.Create('unmapped font encountered - update the font set');
end;

Ett stigande antal RejectedProviderResponses är signalen att en leverantör svarar med data policyn vägrar, vanligtvis en överstor fil eller ett ersatt typsnittsnamn, och det är värt att larma på eftersom dessa förfrågningar tyst degraderas till reserv eller misslyckande. För att diagnostisera vilka typsnitt ett dokument faktiskt behöver innan du bygger mappningstabellen listar inspektionsvägen i analys av PDF-typsnittsegenskaper inbäddade och icke-inbäddade typsnitt per dokument

Typsnittsförsörjning, rendering och textextraktion delar samma biblioteksinstans över Delphi, C++Builder och Lazarus; driftsättningsdetaljer beskrivs på sidan för PDFium Component för Delphi