Artículo técnico

Reading PDF Font Properties with PDFium Component in Delphi

Cada carácter visible en un PDF contiene una referencia a la fuente con la que se dibujó, y el PDFium Component le permite seguir esa referencia hasta el objeto de fuente y leer la información disponible. La unidad de acceso es el carácter, no el documento: se selecciona un carácter mediante su índice en el texto de la página y se solicita el nombre de la familia, el nombre base, el grosor (weight), el ángulo de cursiva y si la fuente real está contenida dentro del archivo. Esta última propiedad es la que más interesa en los análisis, ya que una fuente incrustada viaja con el documento, mientras que una no incrustada es una suposición de que el equipo del lector tiene instalada la misma tipografía

El componente expone estas propiedades a través de los mismos objetos TPdf and TPdfView que usted utiliza para el renderizado y la extracción de texto. No hay un objeto "tabla de fuentes" independiente que abrir. Una vez que se ha analizado el texto de la página, las propiedades de la fuente dependen del índice del carácter, y se leen un glifo a la vez. Ese diseño coincide con la forma en que PDF almacena la información originalmente: una sola página puede cambiar de fuente docenas de veces, y la única respuesta correcta a "en qué fuente está este documento" es "depende de a qué carácter se refiera"

Leer la fuente asociada a un carácter

La operación útil más pequeña consiste en tomar un índice de carácter y volcar todo lo que PDFium pueda indicar sobre su fuente. Cada propiedad de fuente en TPdf y TPdfView está indexada por la posición del carácter, por lo que el índice las conecta a todas. La página también debe ser la página actual para que el índice se resuelva frente al texto correcto, lo cual es importante una vez que se avanza más allá 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;

Un par de declaraciones sorprenden a quienes provienen de otras bibliotecas. FontAscent y FontDescent toman dos argumentos, el índice de carácter y un tamaño en puntos, debido a que PDFium reporta esas métricas en unidades de espacio de glifo que solo se convierten en píxeles una vez que las escala por el tamaño asignado al texto. Pase el valor que ya leyó de FontSize[CharIndex] y obtendrá el ascenso (ascent) y descenso (descent) en los mismos puntos que el resto del diseño. El descenso se devuelve como valor negativo, ya que se mide por debajo de la línea base. El nombre de la familia y el nombre base son cadenas separadas intencionalmente: el nombre base es la entrada /BaseFont directa del PDF, que a menudo incluye un prefijo de subconjunto como ABCDEF+, mientras que el nombre de la familia es el nombre limpio que el renderizador 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 usted debe traducir la coordenada de píxeles al carácter situado debajo. CharacterIndexAtPos realiza exactamente eso, tomando la posición del mouse y un rango de tolerancia, y devolviendo el índice del carácter más cercano, o un valor negativo si el clic se realizó sobre un espacio en blanco o 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. Si es demasiado estricta, los usuarios sentirán que deben hacer clic exactamente sobre el trazo de una letra; si es demasiado holgada, un clic en un margen se asociará a algún carácter lejano que no tiene relación con lo que buscaban. De tres a cinco píxeles de dispositivo es un punto de partida razonable para la visualización en pantalla. El índice devuelto corresponde al texto procesado de la página actual, el mismo espacio de índice que esperan todas las propiedades de fuente, por lo que puede pasarlo directamente a la rutina anterior. Almacenarlo en CurrentCharIndex es opcional pero conveniente: la vista lo mantiene como su referencia del glifo enfocado, lo cual resulta útil si otras secciones de la interfaz gráfica desean leer la selección sin tener que volver a calcularla

La incrustación es la propiedad clave

Para la mayoría de los trabajos reales, la única pregunta importante es si cada fuente está incrustada. Un documento cuyas fuentes están todas integradas se renderiza igual en el RIP de una imprenta, en la computadora portátil de un colega y en un servidor sin interfaz gráfica. Un documento que depende de una fuente Helvetica no incrustada asume el riesgo de que cada uno de esos equipos tenga una fuente coincidente, y cuando el supuesto falla, el lector la sustituye por algo similar, las métricas cambian y un diseño de formulario bien estructurado se reorganiza lo suficiente como para dañarse. Recorrer el texto de la página y clasificar las fuentes por su estado de incrustación le proporciona esa respuesta de manera 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;

Dos detalles aclaran el panorama. Primero, CharacterCount es por página, por lo que una auditoría de todo el documento implica configurar Pdf.PageNumber en cada página de forma consecutiva y volver a ejecutar el bucle, combinando los resultados. Segundo, la capa de texto contiene caracteres generados, como los espacios que el lector deduce entre palabras, y estos no tienen un objeto de fuente asociado; la comprobación de nombre base vacío los descarta en lugar de registrar un elemento falso. El nombre base es la clave correcta para la eliminación de duplicados en este caso, debido a que el prefijo del subconjunto que incluye distingue dos subconjuntos diferentes de la misma familia, lo cual suele ser la información requerida

Extraer la fuente incrustada

Cuando una fuente está incrustada, puede leer sus bytes directamente. FontData devuelve el programa de fuentes sin procesar (los mismos datos TrueType o CFF que contiene el PDF), lo cual es suficiente para escribir un archivo de fuente independiente o para contrastar la tipografía con una biblioteca conocida. Devuelve un arreglo vacío si la fuente no está incrustada, por lo que la comprobación de incrustación y la comprobación de longitud 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 corresponden al subconjunto incrustado, no a la fuente comercial original, por lo que lo obtenido normalmente cubre solo los glifos que el documento realmente utilizó. Eso es lo correcto para análisis forenses y de verificación, pero resulta deficiente para la reutilización; un subconjunto de Times New Roman que contenga treinta glifos no es una fuente que usted pueda instalar y con la que pueda escribir. Considere la extracción como una forma de inspeccionar lo que se distribuyó, no como una herramienta de recuperación de fuentes. Si necesita el nombre base correspondiente para etiquetar la salida, lea FontBaseName[CharIndex] junto con los datos, y elimine la etiqueta de subconjunto inicial si desea la familia limpia

Interpretar el valor del grosor

FontWeight devuelve la clase de grosor numérico, la misma escala de 100 a 900 que utiliza CSS, donde 400 es regular y 700 es negrita (bold). FontWeight reporta lo que la fuente declare, que no siempre es una centena redonda; una tipografía puede declarar 350 o 650, y considerar cualquier valor igual o superior a 600 como "suficientemente marcado" resulta más práctico que buscar exactamente 700. El ángulo de cursiva es una señal complementaria: un valor distinto de cero (generalmente negativo) indica que la fuente es un diseño oblicuo o cursiva real, y cero significa vertical. Juntos le permiten distinguir un fragmento en negrita y cursiva de uno normal sin renderizar nada, que es el tipo de comprobación que requiere una verificación previa o una auditoría de accesibilidad en grandes volúmenes de datos

Ninguna de estas lecturas requiere un mapa de bits renderizado. Provienen de la capa de texto analizada, por lo que un documento abierto en la página correcta es toda la configuración necesaria, lo que hace que la inspección de fuentes sea económica de ejecutar en todo un archivo. Si combina esto con la extracción de texto, los mismos índices de caracteres coinciden con el texto extraído, por lo que la fuente de un glifo y su valor Unicode son dos lees frente a un solo índice. El companion artículo en extraer texto de documentos PDF con PDFium Component cubre esa parte de la capa de texto en mayor profundidad

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