Teknisk artikel

Att läsa PDF-typsnittsegenskaper med PDFium Component i Delphi

Varje synligt tecken i en PDF bär en referens till det typsnitt som ritade det, och PDFium Component låter dig följa den referensen tillbaka till typsnittsobjektet och läsa vad det vet. Åtkomstenheten är tecknet, inte dokumentet: du väljer ett tecken genom dess index i sidtexten och frågar efter familjenamnet (family name), basnamnet (base name), vikten (weight), den kursiva vinkeln (italic angle) och huruvida det underliggande typsnittet (face) faktiskt bärs inuti filen. Den sista egenskapen är den som de flesta analyser verkligen är ute efter, eftersom ett inbäddat typsnitt reser med dokumentet och ett icke-inbäddat är ett löfte om att läsarens maskin råkar ha samma typsnitt installerat

Komponenten exponerar dessa genom samma TPdf- och TPdfView-objekt som du använder för rendering och textextraktion. Det finns inget separat "typsnittstabell"-objekt att öppna. När en sidas text har tolkats, hänger typsnittsegenskaperna på teckenindexet, och du läser dem en glyf (glyph) i taget. Den designen passar hur PDF lagrar informationen i första hand: en enda sida kan byta typsnitt dussintals gånger, och det enda ärliga svaret på "vilket typsnitt är detta dokument i" är "det beror på vilket tecken du menar."

Att läsa typsnittet bakom ett tecken

Den minsta användbara operationen är att ta ett teckenindex och dumpa allt PDFium kan berätta om dess typsnitt. Varje typsnittsegenskap på TPdf och TPdfView är indexerad efter teckenposition, så indexet träs igenom dem alla. Sidan måste också vara den aktuella sidan för att indexet ska lösas upp mot rätt text, vilket spelar roll när du rör dig förbi sida ett

procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
  Report: TStringList;
  PtSize: Single;
begin
  Report := TStringList.Create;
  try
    PtSize := Pdf.FontSize[CharIndex];

    Report.Add('Character : ' + Pdf.Character[CharIndex]);
    Report.Add('Family    : ' + Pdf.FontFamilyName[CharIndex]);
    Report.Add('Base name : ' + Pdf.FontBaseName[CharIndex]);
    Report.Add('Weight    : ' + IntToStr(Pdf.FontWeight[CharIndex]));
    Report.Add('Italic    : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' deg');
    Report.Add('Size      : ' + FormatFloat('0.0', PtSize) + ' pt');
    Report.Add('Ascent    : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
    Report.Add('Descent   : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
    Report.Add('Embedded  : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));

    ShowMessage(Report.Text);
  finally
    Report.Free;
  end;
end;

Ett par av signaturerna överraskar människor som kommer från andra bibliotek. FontAscent och FontDescent tar två argument, teckenindexet och en punktstorlek, eftersom PDFium rapporterar dessa mått (metrics) i glyfrums-enheter (glyph-space) som bara blir pixlar när du skalar dem med den storlek som texten ställdes in på. Skicka in det värde du redan läst från FontSize[CharIndex] så får du ascent (överhäng) och descent (underhäng) i samma punkter som resten av layouten. Descent kommer tillbaka negativt, eftersom det mäter under baslinjen. Familjenamnet och basnamnet är separata strängar med avsikt: basnamnet är den råa /BaseFont-posten från PDF:en, som ofta bär ett delmängdsprefix (subset prefix) som ABCDEF+, medan familjenamnet är det uppstädade namnet som renderaren löser upp det till

Att förvandla ett klick till ett teckenindex

I en visare vet du sällan indexet i förväg. Användaren klickar på en glyf och du måste översätta pixelkoordinaten till tecknet under den. CharacterIndexAtPos gör exakt det, och tar muspositionen och en tolerans och returnerar indexet för närmaste tecken, eller ett negativt värde när klicket landade på ett vitt utrymme eller en tom sida

procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Index: Integer;
begin
  if not PdfView.Active then
    Exit;

  // 4 px of slack in each direction so a near-miss still hits the glyph.
  Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
  if Index < 0 then
    Exit;                      // clicked between glyphs; leave the panel alone

  PdfView.CurrentCharIndex := Index;
  DescribeFontAt(PdfView.Pdf, Index);
end;

Toleransen är värd att trimma. För snäv (tight) och användarna känner att de måste landa på exakt stammen av en bokstav; för lös och ett klick i en marginal snappar till något avlägset tecken som inte har något att göra med vad de menade. Tre till fem enhetspixlar är en rimlig utgångspunkt för skärmvisning. Det returnerade indexet är in i den tolkade texten på den aktuella sidan, samma indexrymd som varje typsnittsegenskap förväntar sig, så du kan skicka det direkt till rutinen ovan. Att lagra det i CurrentCharIndex är valfritt men bekvämt: vyn behåller det som sin uppfattning om den fokuserade glyfen, vilket är praktiskt om andra delar av gränssnittet vill läsa valet utan att härleda det på nytt

Inbäddning är egenskapen som spelar roll

För det mesta riktiga arbetet är den enda frågan värd att svara på huruvida varje typsnitt är inbäddat. Ett dokument vars alla typsnitt åker inuti det renderas likadant på ett tryckeris RIP, en kollegas bärbara dator och en server utan något gränssnitt alls. Ett dokument som förlitar sig på en icke-inbäddad Helvetica spelar med (gambling) att varenda en av dessa maskiner har ett matchande typsnitt, och när spelet misslyckas ersätter läsaren med något som ligger nära, måtten (metrics) förskjuts (shift), och ett noggrant upplagt formulär flödar om precis tillräckligt för att gå sönder. Att gå igenom sidtexten och gruppera (bucketing) typsnitt efter inbäddningsstatus ger dig det svaret billigt

procedure ReportNonEmbeddedFonts(Pdf: TPdf);
var
  Embedded, External: TStringList;
  I: Integer;
  Name: string;
begin
  Embedded := TStringList.Create;
  External := TStringList.Create;
  try
    Embedded.Sorted := True;
    Embedded.Duplicates := dupIgnore;
    External.Sorted := True;
    External.Duplicates := dupIgnore;

    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      Name := Pdf.FontBaseName[I];
      if Name = '' then
        Continue;              // generated spaces and the like have no font
      if Pdf.FontIsEmbedded[I] then
        Embedded.Add(Name)
      else
        External.Add(Name);
    end;

    if External.Count > 0 then
      ShowMessage(IntToStr(External.Count) +
        ' non-embedded font(s):' + sLineBreak + External.Text)
    else
      ShowMessage('All ' + IntToStr(Embedded.Count) +
        ' font(s) on this page are embedded.');
  finally
    Embedded.Free;
    External.Free;
  end;
end;

Två detaljer håller detta ärligt. Först är CharacterCount per sida, så en granskning av hela dokumentet innebär att ställa in Pdf.PageNumber till varje sida i tur och ordning och köra loopen igen, och sammanfoga (merging) resultaten. För det andra innehåller textlagret genererade tecken, såsom de mellanslag en läsare härleder (infers) mellan ord, och de har inget typsnittsobjekt bakom sig; kontrollen av tomt basnamn hoppar över dem snarare än att logga ett fantom. Basnamnet är rätt nyckel för avduplicering (de-duplication) här eftersom det delmängdsprefix (subset prefix) det bär skiljer två olika delmängder av samma familj, vilket vanligtvis är vad du vill veta

Att dra ut det inbäddade typsnittet

När ett typsnitt är inbäddat kan du läsa dess bytes direkt. FontData returnerar det råa typsnittsprogrammet, samma TrueType- eller CFF-data som PDF:en bär, vilket är tillräckligt för att skriva en fristående typsnittsfil eller för att ta fingeravtryck av typsnittet mot ett känt bibliotek. Den returnerar en tom array när typsnittet inte är inbäddat, så inbäddningskontrollen och längdkontrollen skyddar tillsammans skrivningen

procedure SaveEmbeddedFont(Pdf: TPdf; CharIndex: Integer;
  const OutputFile: string);
var
  Data: TBytes;
  Stream: TFileStream;
begin
  if not Pdf.FontIsEmbedded[CharIndex] then
  begin
    ShowMessage('That glyph''s font is not embedded; nothing to extract.');
    Exit;
  end;

  Data := Pdf.FontData[CharIndex];
  if Length(Data) = 0 then
    Exit;

  Stream := TFileStream.Create(OutputFile, fmCreate);
  try
    Stream.WriteBuffer(Data[0], Length(Data));
  finally
    Stream.Free;
  end;
  ShowMessage('Wrote ' + IntToStr(Length(Data)) + ' bytes.');
end;

Byten är den inbäddade delmängden, inte det ursprungliga återförsäljar-typsnittet (retail font), så det du får tillbaka täcker oftast bara de glyfer som dokumentet faktiskt använde. Det är exakt rätt för kriminalteknik (forensics) och verifiering och dåligt lämpat för återanvändning; en delmängd av Times New Roman som innehåller trettio glyfer är inte ett typsnitt du kan installera och skriva med. Behandla extraktion som ett sätt att inspektera vad som skeppades, inte som ett typsnitts-återställningsverktyg (font-recovery tool). Om du behöver det matchande basnamnet för att märka (label) utdatan, läs FontBaseName[CharIndex] tillsammans med datan, och rensa bort (strip) det inledande delmängdstagget (subset tag) om du vill ha den nakna familjen

Att förstå viktnumret

FontWeight returnerar den numeriska viktklassen, samma skala från 100 till 900 som CSS använder, där 400 är vanlig (regular) och 700 är fet (bold). PDFium rapporterar vad typsnittet än deklarerar, vilket inte alltid är ett jämnt hundratal; ett typsnitt (face) kan tillkännage 350 eller 650, och att behandla allt vid eller över 600 som "tillräckligt fet för att spela roll" håller bättre än att testa för exakt 700. Den kursiva vinkeln (italic angle) är en medföljande signal: ett värde skilt från noll, vanligtvis negativt, betyder att typsnittet är en sned (oblique) eller äkta kursiv (italic) design, och noll betyder upprätt. Tillsammans låter de dig skilja en fetkursiv (bold-italic) körning från en vanlig en utan att rendera någonting, vilket är den sortens kontroll ett preflight-pass eller en tillgänglighetsgranskning vill göra i bulk

Ingen av dessa läsningar kräver en renderad bitmapp. De kommer från det tolkade textlagret, så ett öppet dokument på rätt sida är hela den uppsättning du behöver, vilket gör typsnittsinspektion billig att köra över ett helt arkiv. Om du parar ihop detta med textextraktion ligger samma teckenindex i linje med den text du drar ut, så en glyfs typsnitt och dess Unicode-värde är två läsningar mot ett index. Den tillhörande artikeln om att extrahera text från PDF-dokument med PDFium Component täcker den sidan av textlagret mer i detalj

De typsnittsegenskaper som visas här är en del av PDFium Delphi VCL-komponenten