Artigo Técnico

Lendo as Propriedades da Fonte do PDF com o PDFium Component no Delphi

Cada caractere visível em um PDF carrega uma referência à fonte que o desenhou, e o PDFium Component permite seguir essa referência até o objeto da fonte e ler o que ele sabe. A unidade de acesso é o caractere, não o documento: você escolhe um caractere por seu índice no texto da página e pergunta pelo nome da família, o nome base, o peso, o ângulo do itálico e se o tipo de letra subjacente é realmente carregado dentro do arquivo. Essa última propriedade é o que a maioria das análises realmente procura, porque uma fonte incorporada viaja com o documento, e uma não incorporada é apenas uma promessa de que a máquina do leitor, por acaso, tem a mesma fonte instalada

O componente os expõe por meio dos mesmos objetos TPdf e TPdfView que você usa para renderização e extração de texto. Não há um objeto "tabela de fontes" (font table) separado para abrir. Depois que o texto de uma página é analisado, as propriedades da fonte dependem do índice do caractere, e você as lê um glifo por vez. Esse design se adequa à forma como o PDF armazena as informações em primeiro lugar: uma única página pode alternar as fontes dezenas de vezes, e a única resposta honesta para "em qual fonte está esse documento" é "depende de qual caractere você quer dizer."

Lendo a fonte por trás de um caractere

A menor operação útil é pegar o índice de um caractere e descarregar tudo o que o PDFium pode lhe informar sobre sua fonte. Cada propriedade de fonte em TPdf e TPdfView é indexada pela posição do caractere, portanto, o índice é encadeado através de todas elas. A página também deve ser a página atual para que o índice seja resolvido em relação ao texto certo, o que importa quando você passa da primeira página

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;

Algumas das assinaturas surpreendem as pessoas que vêm de outras bibliotecas. FontAscent e FontDescent recebem dois argumentos, o índice do caractere e um tamanho em pontos, porque o PDFium relata essas métricas em unidades de espaço de glifos, que só se tornam pixels depois que você as dimensiona pelo tamanho em que o texto foi definido. Passe o valor que você já leu de FontSize[CharIndex] e você obterá ascensão e descensão nos mesmos pontos que o restante do layout. A descensão retorna negativa, pois é medida abaixo da linha de base. O nome da família e o nome base são strings separadas de propósito: o nome base é a entrada bruta /BaseFont do PDF, muitas vezes carregando um prefixo de subconjunto como ABCDEF+, enquanto o nome da família é o nome limpo para o qual o renderizador o resolve

Transformando um clique em um índice de caractere

Em um visualizador você raramente sabe o índice antecipadamente. O usuário clica em um glifo e você precisa traduzir a coordenada de pixels para o caractere debaixo dele. O CharacterIndexAtPos faz exatamente isso, ele recebe a posição do mouse e uma tolerância e retorna o índice do caractere mais próximo, ou um valor negativo quando o clique atingir um espaço em branco ou página vazia

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 a pena ajustar a tolerância. Se for muito rígida, os usuários sentem que precisam acertar exatamente na haste de uma letra; se for muito frouxa, um clique na margem os prende a algum caractere distante que não tem nada a ver com o que pretendiam. Três a cinco pixels do dispositivo é um ponto de partida razoável para a visualização na tela. O índice retornado vai para o texto analisado da página atual, o mesmo espaço de índice que cada propriedade de fonte espera, então você pode passá-lo diretamente para a rotina acima. Armazená-lo em CurrentCharIndex é opcional, mas conveniente: a visualização mantém isso como sua noção do glifo focado, o que é útil se outras partes da interface de usuário quiserem ler a seleção sem derivá-la novamente

Incorporação é a propriedade que importa

Para a maior parte do trabalho real, a única questão que vale a pena responder é se cada fonte está incorporada. Um documento cujas fontes estão todas dentro dele é renderizado da mesma forma no RIP de uma agência de impressão, no laptop de um colega e em um servidor sem nenhuma GUI. Um documento que depende de uma Helvetica não incorporada está apostando que cada uma dessas máquinas tem uma fonte correspondente, e quando a aposta falha, o leitor substitui algo próximo, as métricas mudam e um formulário cuidadosamente desenhado reflui o suficiente para quebrar. Percorrer o texto da página e classificar as fontes por status de incorporação oferece essa resposta de forma econômica

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;

Dois detalhes mantêm isso honesto. Primeiro, o CharacterCount é por página, portanto, uma auditoria em todo o documento significa definir Pdf.PageNumber para cada página, em seguida, e executar o loop novamente, mesclando os resultados. Em segundo lugar, a camada de texto contém caracteres gerados, como os espaços que um leitor deduz entre as palavras, e eles não têm objeto de fonte por trás deles; a verificação de nome base vazio os pula em vez de registrar um fantasma. O nome base é a chave certa para a eliminação de duplicatas (de-duplication) aqui, pois o prefixo de subconjunto que carrega distingue dois subconjuntos diferentes da mesma família, que normalmente é o que você quer saber

Retirando a fonte incorporada

Quando uma fonte está incorporada, você pode ler seus bytes diretamente. FontData retorna o programa da fonte bruto, os mesmos dados TrueType ou CFF que o PDF transporta, o que é suficiente para escrever um arquivo de fonte autônomo ou fazer o reconhecimento (fingerprint) do tipo de fonte em uma biblioteca conhecida. Ele retorna um array vazio quando a fonte não está incorporada, portanto, a verificação da incorporação e a verificação do comprimento juntas protegem a gravação

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;

Os bytes são o subconjunto incorporado, não a fonte de varejo original, então o que você obtém geralmente abrange apenas os glifos que o documento realmente usou. Isso é exatamente o que é ideal para investigações forenses e verificações, e pouco adequado para reutilização; um subconjunto de Times New Roman que contém trinta glifos não é uma fonte que você possa instalar e usar. Trate a extração como uma forma de inspecionar o que foi enviado, e não como uma ferramenta de recuperação de fontes. Se você precisar do nome base correspondente para rotular a saída, leia FontBaseName[CharIndex] juntamente com os dados e remova a tag de subconjunto inicial se quiser a família sem alterações

Entendendo o número do peso

FontWeight retorna a classe de peso numérico, a mesma escala de 100 a 900 que o CSS usa, onde 400 é normal e 700 é negrito. O PDFium informa o que a fonte declarar, o que nem sempre é uma centena redonda; uma fonte pode anunciar 350 ou 650, e tratar qualquer coisa igual ou superior a 600 como "suficientemente em negrito para ser importante" se sustenta melhor do que testar exatamente para 700. O ângulo itálico é um sinal de acompanhamento: um valor diferente de zero, geralmente negativo, significa que a fonte é um design itálico verdadeiro ou oblíquo e zero significa que é normal. Juntos, eles permitem que você diferencie uma sequência em negrito-itálico de uma normal sem renderizar nada, o que é o tipo de verificação que um preflight ou uma auditoria de acessibilidade pretende realizar em massa

Nenhuma dessas leituras exige um bitmap renderizado. Eles vêm da camada de texto analisada, portanto, um documento aberto na página certa é toda a configuração que você precisa, o que torna a inspeção de fontes econômica para ser executada em um arquivo inteiro. Se você for aliar a isso a extração de texto, os mesmos índices de caracteres se alinham com o texto que você extrai, portanto, a fonte de um glifo e seu valor Unicode são duas leituras de um índice. O artigo associado sobre extrair textos de documentos PDF usando PDFium Component abrange esse lado da camada de texto com maior profundidade

As propriedades de fonte mostradas aqui fazem parte do PDFium Delphi VCL Component