Teknisk artikel

Læsning af PDF-skrifttypeegenskaber med PDFium Component i Delphi

Hvert synligt tegn i en PDF bærer en reference til den skrifttype, der tegnede det, og PDFium Component lader dig følge denne reference tilbage til skrifttypeobjektet og læse, hvad det ved. Adgangsenheden er tegnet, ikke dokumentet: du vælger et tegn via dets indeks i sideteksten og beder om familienavnet, basisnavnet, vægten, kursivvinklen, og om den underliggende skrifttype faktisk er gemt i filen. Den sidste egenskab er den, som de fleste analyser i virkeligheden er ude efter, fordi en indlejret skrifttype rejser med dokumentet, og en ikke-indlejret skrifttype er et løfte om, at læserens maskine tilfældigvis har den samme skrifttype installeret

Komponenten eksponerer disse gennem de samme TPdf og TPdfView objekter, du bruger til rendering og tekstudtrækning. Der er intet separat "skrifttypetabel"-objekt at åbne. Når en sides tekst er blevet parset, hænger skrifttypeegenskaberne på tegnindekset, og du læser dem én glyph ad gangen. Dette design passer til, hvordan PDF gemmer informationen i første omgang: en enkelt side kan skifte skrifttype snesevis af gange, og det eneste ærlige svar på "hvilken skrifttype er dette dokument i" er "det afhænger af, hvilket tegn du mener."

Læsning af skrifttypen bag ét tegn

Den mindste nyttige operation er at tage et tegnindeks og dumpe alt, hvad PDFium kan fortælle dig om dets skrifttype. Hver skrifttypeegenskab på TPdf og TPdfView er indekseret efter tegnposition, så indekset går igennem dem alle. Siden skal også være den aktuelle side for at indekset kan løses mod den rigtige tekst, hvilket betyder noget, når du bevæger dig forbi side ét

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;

Et par af signaturerne overrasker folk, der kommer fra andre biblioteker. FontAscent og FontDescent tager to argumenter, tegnindekset og en punktstørrelse, fordi PDFium rapporterer disse metrikker i glyph-space-enheder, der først bliver pixels, når du skalerer dem med den størrelse, teksten blev sat til. Giv den værdi, du allerede har læst fra FontSize[CharIndex], og du får ascent og descent i de samme punkter som resten af layoutet. Descent kommer tilbage negativt, da det måler under grundlinjen. Familienavnet og basisnavnet er separate strenge med vilje: basisnavnet er den rå /BaseFont-post fra PDF'en, som ofte bærer et undergruppepræfiks som ABCDEF+, mens familienavnet er det oprensede navn, som renderen løser det til

At forvandle et klik til et tegnindeks

I en fremviser kender du sjældent indekset på forhånd. Brugeren klikker på en glyph, og du skal oversætte pixelkoordinaten til tegnet under den. CharacterIndexAtPos gør præcis dette; den tager musepositionen og en tolerance og returnerer indekset for det nærmeste tegn, eller en negativ værdi, når klikket landede på et mellemrum eller en tom side

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;

Tolerancen er værd at finjustere. For stram og brugerne føler, at de skal lande på den nøjagtige stamme af et bogstav; for løs og et klik i en margen snapper til et fjernt tegn, der intet har at gøre med det, de mente. Tre til fem enhedspixels er et rimeligt udgangspunkt for skærmvisning. Det returnerede indeks er ind i den parsede tekst på den aktuelle side, det samme indeksrum som hver skrifttypeegenskab forventer, så du kan give det direkte til rutinen ovenfor. At gemme det i CurrentCharIndex er valgfrit, men praktisk: visningen beholder dette som sin opfattelse af den fokuserede glyph, hvilket er praktisk, hvis andre dele af brugergrænsefladen ønsker at læse markeringen uden at udlede den igen

Indlejring er den egenskab, der betyder noget

For det meste rigtige arbejde er det eneste spørgsmål, der er værd at besvare, om hver skrifttype er indlejret. Et dokument, hvis skrifttyper alle er indeni det, renderes ens på et trykkeris RIP, en kollegas bærbare computer og en server helt uden grafisk brugergrænseflade. Et dokument, der læner sig op ad en ikke-indlejret Helvetica, satser på, at hver eneste af disse maskiner har en matchende skrifttype, og når satsningen slår fejl, erstatter læseren den med noget, der ligner, metrikkerne forskydes, og en omhyggeligt layoutet formular ombrydes lige nok til at gå i stykker. At gennemgå sideteksten og inddele skrifttyper efter indlejringsstatus giver dig det svar 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;

To detaljer holder dette ærligt. For det første er CharacterCount pr. side, så en revision af hele dokumentet betyder, at man sætter Pdf.PageNumber til hver side efter tur og kører løkken igen for at flette resultaterne. For det andet indeholder tekstlaget genererede tegn, såsom de mellemrum, en læser udleder mellem ord, og disse har intet skrifttypeobjekt bag sig; det tomme-basisnavn-tjek springer dem over frem for at logge et fantom. Basisnavnet er den rigtige nøgle til de-duplikering her, fordi det undergruppepræfiks, det bærer, adskiller to forskellige undergrupper af den samme familie, hvilket typisk er det, du vil vide

Udtrækning af den indlejrede skrifttype

Når en skrifttype er indlejret, kan du læse dens bytes direkte. FontData returnerer det rå skrifttypeprogram, de samme TrueType- eller CFF-data, som PDF'en bærer, hvilket er nok til at skrive en selvstændig skrifttypefil eller til at fingeraftrykke skrifttypen mod et kendt bibliotek. Det returnerer et tomt array, når skrifttypen ikke er indlejret, så indlejringstjekket og længdetjekket beskytter skrivningen sammen

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;

Bytes'ene er den indlejrede undergruppe, ikke den originale butiksskrifttype, så det, du får tilbage, dækker som regel kun de glyphs, dokumentet faktisk brugte. Det er præcis det rigtige til retsmedicin og verifikation og en dårlig pasform til genbrug; en undergruppe af Times New Roman, der indeholder tredive glyphs, er ikke en skrifttype, du kan installere og skrive med. Behandl udtrækning som en måde at inspicere, hvad der blev leveret, ikke som et værktøj til gendannelse af skrifttyper. Hvis du har brug for det matchende basisnavn til at navngive outputtet, skal du læse FontBaseName[CharIndex] sammen med dataene og fjerne det indledende undergruppetag, hvis du vil have den rene familie

At forstå vægttallet

FontWeight returnerer den numeriske vægtklasse, den samme 100-til-900 skala, som CSS bruger, hvor 400 er almindelig (regular) og 700 er fed (bold). PDFium rapporterer uanset hvad skrifttypen erklærer, hvilket ikke altid er et rundt hundrede; en skrifttype kan annoncere 350 eller 650, og at behandle alt på eller over 600 som "fed nok til at betyde noget" holder bedre end at teste for præcis 700. Kursivvinklen er et ledsagende signal: en værdi, der ikke er nul, normalt negativ, betyder, at skrifttypen har et skråt eller ægte kursivt design, og nul betyder opretstående. Sammen lader de dig kende en fed-kursiv tekst fra en almindelig en uden at rendere noget, hvilket er den slags tjek, en preflight-gennemgang eller en tilgængelighedsrevision ønsker at foretage i store mængder

Ingen af disse læsninger kræver en renderet bitmap. De kommer fra det parsede tekstlag, så et åbent dokument på den rigtige side er alt den opsætning, du behøver, hvilket gør skrifttypeinspektion billig at køre på tværs af et helt arkiv. Hvis du parrer dette med tekstudtrækning, stemmer de samme tegnindekser overens med den tekst, du trækker ud, så en glyphs skrifttype og dens Unicode-værdi er to læsninger mod ét indeks. Ledsageartiklen om udtrækning af tekst fra PDF-dokumenter med PDFium Component dækker den side af tekstlaget mere dybdegående

De skrifttypeegenskaber, der vises her, er en del af PDFium Delphi VCL Component