Articolo tecnico

Leggere le Proprietà dei Caratteri PDF con PDFium Component in Delphi

Ogni carattere visibile in un PDF porta con sé un riferimento al font che lo ha disegnato, e PDFium Component ti permette di seguire quel riferimento fino all'oggetto del font e leggere le sue informazioni. L'unità di accesso è il carattere, non il documento: scegli un carattere tramite il suo indice nel testo della pagina e chiedi il nome della famiglia, il nome base, il peso, l'angolo del corsivo e se il font sottostante è effettivamente presente all'interno del file. Quest'ultima proprietà è quella a cui puntano realmente gran parte delle analisi, perché un font incorporato viaggia insieme al documento mentre uno non incorporato è una promessa che la macchina del lettore abbia installato il medesimo tipo di carattere

Il componente le espone attraverso gli stessi oggetti TPdf e TPdfView che usi per il rendering e l'estrazione del testo. Non c'è alcun oggetto separato "tabella dei font" da aprire. Una volta analizzato il testo di una pagina, le proprietà del font si legano all'indice del carattere, e si possono leggere un glifo alla volta. Tale architettura corrisponde al modo in cui il PDF memorizza l'informazione in partenza: una singola pagina può cambiare font decine di volte e l'unica vera risposta a "che font usa questo documento" è "dipende da quale carattere intendi"

Leggere il font dietro a un carattere

L'operazione utile più semplice è prendere l'indice di un carattere e stampare tutto ciò che PDFium può dirti sul suo font. Ogni proprietà del font in TPdf e TPdfView è indicizzata per posizione del carattere, cosicché l'indice si propaga in tutti essi. La pagina deve inoltre essere la pagina corrente affinché l'indice si riferisca al testo corretto, il che è essenziale non appena si va oltre la pagina uno

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;

Un paio di firme stupiscono chi proviene da altre librerie. FontAscent e FontDescent richiedono due argomenti, l'indice del carattere e una dimensione in punti, perché PDFium riporta quelle metriche in unità di spazio del glifo che diventano pixel soltanto se le ridimensioni rispetto alla dimensione in cui il testo è stato impostato. Passa il valore che hai già letto da FontSize[CharIndex] e otterrai la salita (ascent) e la discesa (descent) negli stessi punti dell'intero layout. La discesa viene restituita come numero negativo, dal momento che si misura al di sotto della linea di base. Il nome della famiglia e il nome base sono stringhe separate intenzionalmente: il nome base è la voce grezza /BaseFont del PDF, la quale porta spesso un prefisso di sottoinsieme come ABCDEF+, mentre il nome della famiglia è il nome ripulito a cui il rendering lo risolve

Trasformare un clic in un indice di carattere

In un visualizzatore raramente si conosce l'indice in anticipo. L'utente clicca su un glifo e tu devi tradurre la coordinata in pixel nel carattere sottostante. CharacterIndexAtPos fa esattamente quello, prendendo la posizione del mouse e un margine di tolleranza, restituendo l'indice del carattere più vicino, o un valore negativo se il clic è avvenuto su uno spazio vuoto o sulla pagina vuota

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;

Vale la pena perfezionare la tolleranza. Troppo severa e gli utenti avranno la sensazione di dover centrare esattamente il tratto di una lettera; troppo larga e un clic nel margine si collegherà a qualche carattere lontano che non c'entra niente con le loro intenzioni. Da tre a cinque pixel fisici del dispositivo sono un punto di partenza ragionevole per la visualizzazione su schermo. L'indice restituito fa riferimento al livello di testo analizzato della pagina corrente, il medesimo spazio indicizzato previsto da ogni proprietà del font, per cui puoi passarlo direttamente alla routine vista sopra. Salvarlo in CurrentCharIndex è opzionale ma pratico: la vista lo conserva come la sua nozione del glifo a fuoco, il che è comodo qualora altre parti della UI volessero leggere la selezione senza ricalcolarla

L'incorporazione è la proprietà che conta

Per gran parte del lavoro reale, l'unica domanda a cui valga la pena rispondere è se ciascun font sia incorporato. Un documento i cui font viaggiano all'interno di esso viene riprodotto allo stesso modo sul RIP di una copisteria, sul portatile di un collega e su un server senza alcuna GUI. Un documento che poggia su un Helvetica non incorporato scommette che tutte queste macchine abbiano installato un font corrispondente, e quando questa scommessa non paga il lettore sostituisce con qualcosa di simile, le metriche slittano, e un modulo impaginato attentamente salta quanto basta per rompersi. Percorrere il testo della pagina e raggruppare i font per stato di incorporazione ti restituisce quella risposta a basso costo

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;

Due dettagli mantengono questo approccio corretto. Primo, CharacterCount si intende per pagina, perciò un controllo sull'intero documento implica l'impostazione di Pdf.PageNumber ad ogni pagina consecutivamente, rieseguendo il ciclo e fondendo i risultati. Secondo, il livello del testo contiene caratteri generati, come gli spazi che un lettore presume tra le parole, e questi non presentano alcun oggetto font alla base; il controllo sul nome base vuoto li salta piuttosto che loggare un fantasma. Il nome base costituisce la chiave adatta per la de-duplicazione in questo caso, giacché il prefisso di sottoinsieme che include differenzia due differenti sottoinsiemi della stessa famiglia, il che di norma è ciò che interessa scoprire

Estrarre il font incorporato

Quando un font è incorporato, puoi leggere direttamente i suoi byte. FontData restituisce il programma di font grezzo, il medesimo dato TrueType o CFF contenuto nel PDF, il che basta per scrivere un file di font autonomo o per effettuare l'identificazione (fingerprinting) del font contro una libreria nota. Questo restituisce un array vuoto qualora il font non sia incorporato, di conseguenza il controllo di incorporazione e il controllo della lunghezza proteggono assieme la scrittura

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;

I byte consistono nel sottoinsieme incorporato, non nel font al dettaglio originale, dunque ciò che si ottiene solitamente copre unicamente i glifi impiegati dal documento nel concreto. È precisamente corretto per scopi legali o di verifica e una cattiva soluzione per il riutilizzo; un sottoinsieme di Times New Roman contenente trenta glifi non è un font che si può installare e usare per scrivere. Considera l'estrazione come un modo per ispezionare ciò che è stato distribuito, e non come uno strumento per il ripristino dei font. Qualora serva il nome di base associato per dare un'etichetta al risultato, leggi FontBaseName[CharIndex] insieme al dato e togli il tag iniziale del sottoinsieme se si desidera la semplice famiglia del font

Dare un senso al numero del peso

FontWeight restituisce la classe di peso numerico, la stessa scala da 100 a 900 utilizzata nei CSS, dove 400 corrisponde a regolare e 700 a grassetto. PDFium riporta qualunque cosa il font dichiari, che non è sempre un centinaio arrotondato; un tipo di carattere può annunciare 350 o 650 e trattare qualunque elemento a partire da 600 in su come "abbastanza grassetto da importare" si rivela migliore del verificare l'esatto 700. L'angolo corsivo è un segnale gemello: un valore diverso da zero, spesso negativo, significa che il font ha un design obliquo o vero corsivo, e zero denota dritto. Unitamente, permettono di distinguere un blocco di testo grassetto-corsivo da uno regolare senza la necessità di elaborare il rendering, e tale è il genere di verifica che un passaggio di preflight o un audit di accessibilità intende eseguire su vasta scala

Nessuna di queste letture esige una bitmap di rendering. Vengono estratte dal layer di testo analizzato, sicché un documento aperto alla giusta pagina è il solo preparativo occorrente, cosa che rende l'ispezione dei font alquanto economica da avviare lungo tutto l'archivio. Se unisci tale operazione all'estrazione del testo, i medesimi indici dei caratteri si allineano col testo da ricavare, per cui il font di un glifo e il suo valore Unicode scaturiscono da due letture rivolte al medesimo indice. L'articolo complementare sull'estrazione di testo dai documenti PDF con PDFium Component prende in esame quell'ambito del livello testuale in modo più dettagliato

Le proprietà del font qui presentate fanno parte del PDFium Delphi VCL Component