Artículo técnico

Corregir texto duplicado por fake bold al extraer PDF

HotPDF Delphi Component, desde v2.768.15, elimina los caracteres duplicados que el fake bold y los logos sobreimpresos dejan en el texto PDF extraído. Cuando un productor pinta el mismo glifo dos veces a una fracción de punto de distancia, ExtractLoadedPageText, ExtractLoadedPageTextLayout y ExtractLoadedPageTextColumns ahora leen ese carácter una vez, así que "Bold words" ya no sale como "Bold wordsBold words"

Este es uno de esos bugs que parece una falla de decodificación y no lo es. Cada glifo decodifica correctamente; el texto de verdad está dos veces en la página. El arreglo tiene que decidir cuál de dos glifos perfectamente válidos es la copia, y hacerlo sin comerse letras repetidas legítimas como la doble l de "fill" o las celdas idénticas de una tabla. El resto de este artículo explica la regla que usa HotPDF, los umbrales detrás de ella, dónde difiere deliberadamente de otras herramientas de extracción, y un segundo bug de extracción, no relacionado, del mismo tren de releases que también recortaba texto al final de las líneas

¿Por qué el fake bold produce texto duplicado en la extracción de PDF?

El fake bold duplica el texto extraído porque el productor dibuja la cadena dos veces y ambas copias son operaciones reales de mostrar texto. Cuando una familia de fuentes no tiene cara bold, o la herramienta de autoría simplemente no se molesta, muchos generadores emulan el bold mostrando la misma cadena con Tj o TJ, empujando la posición del texto unos décimos de punto a la derecha, y mostrándola otra vez. En pantalla las dos copias se funden en un trazo más grueso. En el content stream hay dos corridas completas de códigos de carácter, e ISO 32000-1 §9.4 no le da al extractor ninguna bandera que diga "esta es decoración"

Los logos sobreimpresos golpean el mismo problema desde otro ángulo. Un logotipo puede pintarse una vez como relleno y otra como trazo o como una sombra ligeramente desplazada, usando la misma fuente y los mismos caracteres. Un extractor ingenuo que recorre el content stream en orden de pintado emite toda la primera corrida y luego toda la segunda, que es por lo que el resultado lee "Bold wordsBold words" en lugar de "BBoolldd". Antes de v2.768.15, el ensamblado de texto de HotPDF conservaba ambas copias, y el duplicado rompía lo que a la mayoría de los callers realmente les importa: una búsqueda de "Bold words" sobre el texto de la página podía fallar porque el límite de palabra entre las dos corridas había desaparecido

El modo real de renderizado de texto 2 (relleno y luego trazo, ISO 32000-1 §9.3.6) no tiene este problema, porque es una sola operación de mostrar texto que produce un solo conjunto de glifos. Los duplicados solo aparecen cuando el productor emite el texto dos veces

¿Cómo decide HotPDF qué glifo es el duplicado?

HotPDF trata dos glifos como un solo carácter pintado cuando cargan el mismo valor Unicode, tienen la misma dirección de escritura, están dentro de un 5% de diferencia de tamaño y se sientan casi uno encima del otro: a no más de 0.1 del tamaño del glifo a lo largo de la baseline y a no más de 0.2 del tamaño del glifo a través de ella. De cada par así, el glifo pintado primero se conserva y el posterior se descarta. Esos son los valores de la implementación v2.768.15, y cada uno está ahí por una razón

  • Mismo Unicode, misma dirección. Solo caracteres idénticos pueden ser duplicados, y un glifo horizontal nunca se compara con uno vertical (WMode 1)
  • Tamaño dentro del 5%. Una letra grande de encabezado y una letra pequeña de pie de foto que casualmente se traslapan no son el mismo texto, así que un desajuste de tamaño descarta al par
  • 0.1 del tamaño a lo largo de la baseline. Los desplazamientos de fake bold son de unos décimos de punto, unos pocos por ciento de un tamaño de texto normal. Dos letras reales adyacentes, incluso una "ll" apretadamente kerned, están separadas por aproximadamente un ancho de avance completo, un orden de magnitud más que esta tolerancia
  • 0.2 del tamaño a través de la baseline. Las sombras y las pasadas de contorno suelen desplazarse hacia abajo además de a la derecha, así que la tolerancia vertical es más holgada, pero sigue muy por debajo de la distancia a la siguiente línea
  • El primero pintado gana. Conservar el glifo anterior preserva el orden de lectura de la primera corrida, así que el texto sobreviviente fluye como el productor lo escribió

Las distancias se miden en el marco de baseline del propio glifo en lugar de la X y Y crudas de la página, así que el texto rotado y el texto sobre una baseline inclinada usan las mismas tolerancias que las líneas horizontales ordinarias. El tamaño usado es el del glifo tal como quedó pintado en la página, derivado de su quad, no el operando de tamaño dado a Tf; eso importa para documentos que fijan Tf en 1 y escalan todo a través de la matriz de texto, donde un tamaño de fuente crudo volvería inútil cada tolerancia

Los espacios nunca son candidatos. Los code points hasta U+0020, el espacio sin salto U+00A0 y el espacio ideográfico U+3000 se omiten, porque los glifos en blanco no cargan tinta visible que duplicar y descartar uno fusionaría palabras

Mantener la verificación rápida en páginas densas

La verificación de duplicados en HotPDF corre en tiempo aproximadamente n log n, no n al cuadrado, porque ordena primero y solo compara glifos dentro de una ventana pequeña. Comparar cada glifo con cada otro glifo sería correcto e inutilizable: una página densa de letra chica puede cargar fácilmente decenas de miles de glifos, y la verificación corre en cada llamada de extracción de texto

La rutina interna ordena los glifos que no son espacio por valor Unicode, luego por origen X, luego por orden de pintado, usando un merge sort estable. Después de ordenar, cada duplicado potencial de un glifo queda inmediatamente después de él en el orden, así que el escaneo solo avanza mientras el valor Unicode coincide y la diferencia de X se mantiene dentro de 0.25 del tamaño del glifo. La ventana de 0.25 es deliberadamente más ancha que la tolerancia de 0.1 a lo largo de la baseline, así que un par sobre una baseline ligeramente inclinada aún cae dentro antes de que corra la prueba precisa en el marco de baseline. Un caso sutil: si el escaneo desde el glifo A encuentra un glifo coincidente que fue pintado antes que A, A es la copia, así que A se descarta y el escaneo se detiene, dejando que el glifo anterior continúe la comparación con lo que venga más adelante. Esa regla es la que hace que "el primero pintado gana" se cumpla para triples golpes además de pares

La comparación es por valor Unicode, no por fuente ni código de carácter. Dos glifos que mapean al mismo Unicode a través de fuentes distintas, por ejemplo una pasada de relleno en un subset incrustado y una pasada de trazo en otra, siguen reconociéndose como un carácter. El reverso es que un glifo cuyo código resuelve a Unicode 0 no tiene nada confiable con qué comparar, que es una razón más para vigilar el conteo de no resueltos cubierto en extracción de texto de PDFs cargados con HotPDF

¿Qué llamadas de extracción de HotPDF eliminan glifos duplicados?

Todas las llamadas que producen texto en HotPDF eliminan duplicados, y la llamada cruda de glifos no. La deduplicación vive en THotPDF.ExtractVisibleLoadedPageGlyphs, el paso que además descarta los glifos fuera de la caja visible de la página, y la extracción de texto plano, de columnas, de layout y de texto semántico todos leen sus glifos de ahí. THotPDF.ExtractLoadedPageGlyphs sigue devolviendo cada glifo que el content stream pinta, sobreimpresiones incluidas, porque los callers que construyen resaltado, redacción o herramientas forenses necesitan el registro completo de lo que fue dibujado

Para trabajo de texto ordinario nada cambia en el código que llama. El mismo bucle que extraía texto duplicado antes de v2.768.15 ahora extrae cada palabra una vez

var
  Pdf: THotPDF;
  PageCount, I: Integer;
  PageText: UnicodeString;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('brochure.pdf');
    for I := 0 to PageCount - 1 do
      if Pdf.ExtractLoadedPageText(I, PageText) then
        if Pos('Bold words', PageText) > 0 then
          Writeln('Found on page ', I + 1);
  finally
    Pdf.Free;
  end;
end;

La salida de layout y de columnas se beneficia igual, ya que una corrida duplicada solía empujar el resto de una línea hacia un lado en la cuadrícula de caracteres. ExtractLoadedPageTextColumns conserva su multiplicador de hueco opcional; el filtro de duplicados corre antes de la partición en columnas, así que no interactúa con ese ajuste

var
  Grid, Columns: UnicodeString;
begin
  if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
    TFile.WriteAllText('page1-layout.txt', Grid, TEncoding.UTF8);
  // Las columnas se parten donde un hueco excede 3x el avance mediano del glifo
  if Pdf.ExtractLoadedPageTextColumns(0, Columns, 3.0) then
    TFile.WriteAllText('page1-columns.txt', Columns, TEncoding.UTF8);
end;

Si quiere saber si una página contiene texto sobreimpreso en absoluto, compare las dos llamadas de glifos. Tenga en cuenta que la diferencia también cuenta glifos que caen fuera del crop box, así que trátela como cota superior de duplicados y no como cifra exacta

var
  AllGlyphs, Visible: THPDFGlyphArray;
begin
  if Pdf.ExtractLoadedPageGlyphs(0, AllGlyphs) and
     Pdf.ExtractVisibleLoadedPageGlyphs(0, Visible) then
    Writeln(Format('%d painted, %d read as text (%d overprinted or off-page)',
      [Length(AllGlyphs), Length(Visible),
       Length(AllGlyphs) - Length(Visible)]));
end;

Las reglas de espaciado de palabras y de saltos de línea que convierten estos glifos en texto no cambian con el filtro; están cubiertas en detalle en cómo HotPDF coloca espacios de palabra y saltos de línea en el texto de página

Dónde la regla discrepa deliberadamente con otras herramientas

HotPDF descarta duplicados exactos que algunas otras herramientas de extracción conservan, y esa diferencia es intencional. Cuando el cambio de v2.768.15 se verificó contra un corpus de referencia, un pie de página pintado dos veces en exactamente la misma posición se extrajo una vez en HotPDF y dos veces en un extractor de referencia. Un glifo pintado encima de un glifo idéntico es invisible como carácter separado, así que leerlo una vez es lo que la página realmente muestra

El límite honesto corre en la otra dirección. Un fake bold que desplaza la segunda pasada por más de 0.1 del tamaño del glifo a lo largo de la baseline no se atrapa, y tampoco una pasada de sombra dibujada a un tamaño notablemente distinto. Esos layouts son raros en la práctica, porque el efecto visual deja de parecer bold una vez que el desplazamiento se hace tan grande, pero si ve texto duplicado de un encabezado decorativo, compare los valores X de los glifos de ExtractLoadedPageGlyphs antes de asumir que el filtro está roto. El filtro tampoco hace nada por duplicados que no están geométricamente co-localizados, como un encabezado corrido repetido en una capa oculta en otra parte de la página; eso es una cuestión de contenido, no una sobreimpresión

¿Por qué se cortaban los finales de línea en texto con fuentes CID?

Un bug separado corregido en HotPDF v2.768.12 tiraba los finales de línea en texto compuesto con fuentes de claves CID (Type 0) cuyo arreglo de anchos /W no estaba ordenado por CID. ISO 32000-1 §9.7.4.3 define el formato del arreglo /W pero no exige que sus entradas estén en orden ascendente de CID, y algunos generadores las escriben en el orden en que los glifos se usaron por primera vez en el documento

El decodificador de extracción de texto buscaba los anchos con búsqueda binaria, que asume silenciosamente entrada ordenada. Sobre un arreglo sin orden la mayoría de las búsquedas fallaban y caían de vuelta al ancho predeterminado /DW, que son 1000 unidades, un em completo. El texto latino en tal fuente quedaba dispuesto como si cada letra fuera de ancho completo, las líneas corrían muy pasado el borde derecho de la página, y los glifos que aterrizaban fuera de la caja visible se descartaban, así que los finales de línea desaparecían del texto extraído y de la búsqueda. Nada crasheaba y nada se reportaba, que es lo que lo hacía difícil de detectar

HotPDF ahora ordena las entradas de /W con un merge sort estable justo después de leerlas, así que cuando dos entradas se traslapan la escrita primero sigue ganando, igual que como el arreglo se lee en orden de archivo. El renderizador de páginas nunca se vio afectado, porque mantiene su propia copia de los datos de anchos y los escanea linealmente, y el arreglo de anchos verticales /W2 también se escanea linealmente. Esa división vale recordar cuando una página se ve perfecta pero extrae mal: render y extracción en una biblioteca PDF a menudo comparten mucho menos código del que uno esperaría

Preguntas frecuentes

¿HotPDF elimina letras repetidas como la doble l de "fill"?

No. Dos letras reales adyacentes están a aproximadamente un ancho de avance de distancia, mientras que HotPDF solo fusiona caracteres idénticos que están dentro de 0.1 del tamaño del glifo a lo largo de la baseline y 0.2 a través de ella, así que las repeticiones legítimas se conservan

¿Cómo obtengo cada glifo, incluidos los duplicados sobreimpresos?

Llame THotPDF.ExtractLoadedPageGlyphs, que devuelve cada glifo que la página pinta en orden de pintado. THotPDF.ExtractVisibleLoadedPageGlyphs y todas las llamadas de extracción de texto devuelven el conjunto deduplicado y visible

¿Qué versión de HotPDF corrige el texto duplicado por fake bold?

HotPDF v2.768.15 agregó la eliminación de glifos duplicados a la extracción de texto, layout, columnas y texto semántico, y v2.768.12 corrigió los finales de línea faltantes en fuentes CID con un arreglo /W sin ordenar

Ambas correcciones vienen en el componente HotPDF Delphi PDF actual, que extrae texto de PDFs cargados en Delphi y C++Builder junto con sus funciones de creación, edición y render de PDF