Technical Article

Leer propiedades de fuentes PDF con PDFium VCL en Delphi

Cada carácter visible en un PDF lleva una referencia a la fuente que lo dibujó, y PDFium VCL permite seguir esa referencia hasta el objeto de fuente y leer lo que contiene. La unidad de acceso es el carácter, no el documento: se selecciona un carácter por su índice en el texto de la página y se consulta el nombre de familia, el nombre base, el peso, el ángulo de cursiva y si la tipografía subyacente está realmente incluida en el fichero. Esa última propiedad es la que la mayoría de los análisis realmente buscan, porque una fuente incrustada viaja con el documento y una no incrustada es una promesa de que la máquina del lector tiene instalada la misma tipografía.

El componente expone estos datos a través de los mismos objetos TPdf y TPdfView que se usan para el renderizado y la extracción de texto. No hay un objeto separado de "tabla de fuentes" que abrir. Una vez analizado el texto de una página, las propiedades de fuente cuelgan del índice de carácter y se leen glifo a glifo. Ese diseño se adapta a cómo PDF almacena la información: una sola página puede cambiar de fuente decenas de veces, y la única respuesta honesta a "en qué fuente está este documento" es "depende del carácter al que te refieras".

Leer la fuente detrás de un carácter

La operación mínima útil es tomar un índice de carácter y volcar todo lo que PDFium puede decirte sobre su fuente. Cada propiedad de fuente en TPdf y TPdfView está indexada por posición de carácter, por lo que el índice atraviesa todas ellas. La página también debe ser la página actual para que el índice se resuelva contra el texto correcto, lo que importa en cuanto se pasa de la primera 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;

Algunas de las firmas sorprenden a quienes vienen de otras bibliotecas. FontAscent y FontDescent toman dos argumentos, el índice de carácter y un tamaño en puntos, porque PDFium reporta esas métricas en unidades de espacio de glifo que solo se convierten en píxeles al escalarlas por el tamaño al que se estableció el texto. Pasa el valor que ya leíste de FontSize[CharIndex] y obtendrás ascenso y descenso en los mismos puntos que el resto del diseño. El descenso vuelve negativo, ya que mide por debajo de la línea base. El nombre de familia y el nombre base son cadenas separadas intencionadamente: el nombre base es la entrada /BaseFont sin procesar del PDF, que a menudo lleva un prefijo de subconjunto como ABCDEF+, mientras que el nombre de familia es el nombre limpio al que el renderizador lo resuelve.

Convertir un clic en un índice de carácter

En un visor rara vez se conoce el índice de antemano. El usuario hace clic en un glifo y hay que traducir la coordenada de píxel al carácter que hay debajo. CharacterIndexAtPos hace exactamente eso: toma la posición del ratón y una tolerancia y devuelve el índice del carácter más cercano, o un valor negativo cuando el clic cayó en un espacio en blanco o en una página vacía.

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 ajustar la tolerancia. Demasiado pequeña y los usuarios sienten que tienen que acertar exactamente en el trazo de una letra; demasiado grande y un clic en un margen salta a algún carácter lejano que no tiene nada que ver con lo que pretendían. De tres a cinco píxeles de dispositivo es un punto de partida razonable para la visualización en pantalla. El índice devuelto apunta al texto analizado de la página actual, el mismo espacio de índice que espera cada propiedad de fuente, por lo que se puede pasar directamente a la rutina anterior. Almacenarlo en CurrentCharIndex es opcional pero conveniente: la vista lo mantiene como su noción del glifo enfocado, lo que resulta útil si otras partes de la interfaz quieren leer la selección sin volver a derivarla.

La incrustación es la propiedad que importa

Para la mayoría del trabajo real, la única pregunta que vale la pena responder es si cada fuente está incrustada. Un documento cuyas fuentes están todas dentro se renderiza igual en el RIP de una imprenta, en el portátil de un compañero y en un servidor sin interfaz gráfica. Un documento que depende de una Helvetica no incrustada está apostando a que todas esas máquinas tienen instalada una tipografía equivalente, y cuando la apuesta falla el lector sustituye algo parecido, las métricas cambian y un formulario cuidadosamente maquetado se refluye lo justo para romperse. Recorrer el texto de la página y clasificar las fuentes por estado de incrustación da esa respuesta a bajo coste.

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;

Hay dos detalles que mantienen esto honesto. Primero, CharacterCount es por página, por lo que una auditoría de todo el documento implica establecer Pdf.PageNumber en cada página por turno, volver a ejecutar el bucle y fusionar los resultados. Segundo, la capa de texto contiene caracteres generados, como los espacios que un lector infiere entre palabras, y esos no tienen ningún objeto de fuente detrás; la comprobación de nombre base vacío los omite en lugar de registrar un fantasma. El nombre base es la clave correcta para la deduplicación aquí, porque el prefijo de subconjunto que lleva distingue dos subconjuntos diferentes de la misma familia, que es normalmente lo que se quiere saber.

Extraer la tipografía incrustada

Cuando una fuente está incrustada se pueden leer sus bytes directamente. FontData devuelve el programa de fuente sin procesar, los mismos datos TrueType o CFF que lleva el PDF, lo que es suficiente para escribir un fichero de fuente independiente o para identificar la tipografía frente a una biblioteca conocida. Devuelve un array vacío cuando la fuente no está incrustada, por lo que la comprobación de incrustación y la comprobación de longitud juntas protegen la escritura.

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;

Los bytes son el subconjunto incrustado, no la fuente comercial original, por lo que lo que se obtiene generalmente solo cubre los glifos que el documento utilizó realmente. Eso es exactamente correcto para análisis forense y verificación, y una mala elección para la reutilización; un subconjunto de Times New Roman que contiene treinta glifos no es una fuente que se pueda instalar y usar para escribir. Trata la extracción como una forma de inspeccionar lo que se distribuyó, no como una herramienta de recuperación de fuentes. Si se necesita el nombre base correspondiente para etiquetar la salida, lee FontBaseName[CharIndex] junto con los datos y elimina la etiqueta de subconjunto inicial si se quiere la familia sin prefijo.

Interpretar el número de peso

FontWeight devuelve la clase de peso numérica, la misma escala de 100 a 900 que usa CSS, donde 400 es regular y 700 es negrita. PDFium reporta lo que declare la fuente, que no siempre es una centena redonda; una tipografía puede anunciar 350 o 650, y tratar cualquier valor en o por encima de 600 como "suficientemente negrita para importar" funciona mejor que comprobar exactamente 700. El ángulo de cursiva es una señal complementaria: un valor distinto de cero, normalmente negativo, significa que la tipografía es un diseño oblicuo o cursiva real, y cero significa vertical. Juntos permiten distinguir un fragmento en negrita-cursiva de uno regular sin renderizar nada, que es el tipo de comprobación que una pasada de preflight o una auditoría de accesibilidad quiere hacer en masa.

Ninguna de estas lecturas requiere un mapa de bits renderizado. Provienen de la capa de texto analizado, por lo que un documento abierto en la página correcta es toda la configuración que se necesita, lo que hace que la inspección de fuentes sea barata de ejecutar en un archivo completo. Si se combina con la extracción de texto, los mismos índices de carácter se alinean con el texto extraído, por lo que la fuente de un glifo y su valor Unicode son dos lecturas contra un único índice. El artículo complementario sobre extracción de texto de documentos PDF con PDFium VCL cubre ese lado de la capa de texto con más profundidad.

Las propiedades de fuente mostradas aquí forman parte del PDFium Delphi VCL Component.