HotPDF Delphi Component, desde la v2.768.15, elimina los caracteres duplicados que el fake bold y los logos sobreimpresos dejan en el texto extraído de un PDF. Cuando un productor pinta el mismo glifo dos veces a una fracción de punto de distancia, ExtractLoadedPageText, ExtractLoadedPageTextLayout y ExtractLoadedPageTextColumns leen ahora ese carácter una vez, así que "Bold words" ya no sale como "Bold wordsBold words"
Este es de esos bugs que parecen un fallo de decodificación y no lo son. Cada glifo se 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 del artículo explica la regla que usa HotPDF, los umbrales que hay detrás, dónde se diferencia a propósito de otras herramientas de extracción, y un segundo bug de extracción, no relacionado, del mismo tren de releases que también cortaba texto al final de línea
¿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 las dos copias son operaciones reales de mostrado de texto. Cuando una familia tipográfica no tiene cara bold, o la herramienta de autoría simplemente no se molesta, muchos generadores emulan la negrita mostrando la misma cadena con Tj o TJ, empujando la posición del texto unas décimas 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 flag que diga "esta es decoración"
Los logos sobreimpresos dan en el mismo problema desde otro ángulo. Un logotipo puede pintarse una vez como relleno y otra como trazo o 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, por lo que el resultado dice "Bold wordsBold words" en lugar de "BBoolldd". Antes de la v2.768.15, el ensamblado de texto de HotPDF conservaba las dos copias, y el duplicado rompía lo que a la mayoría de callers de verdad les importa: una búsqueda de "Bold words" en 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 mostrado de texto que produce un único juego 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 mismo carácter pintado cuando llevan el mismo valor Unicode, tienen la misma dirección de escritura, están dentro de un 5% de diferencia de tamaño y se sitúan casi superpuestos: no más de 0.1 del tamaño del glifo de separación a lo largo de la línea base y no más de 0.2 del tamaño a lo ancho. 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 título y una letra pequeña de pie de foto que casualmente se solapan no son el mismo texto, así que una diferencia de tamaño descarta el par
- 0.1 del tamaño a lo largo de la línea base. Los desplazamientos del fake bold son de unas décimas de punto, unos pocos por ciento de un tamaño de texto normal. Dos letras reales adyacentes, incluso una "ll" muy kernada, están separadas por aproximadamente un advance width completo, un orden de magnitud más que esta tolerancia
- 0.2 del tamaño a lo ancho de la línea base. 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 línea siguiente
- Gana el primero pintado. Conservar el glifo anterior preserva el orden de lectura de la primera corrida, así que el texto superviviente sigue fluyendo como lo escribió el productor
Las distancias se miden en el marco de línea base del propio glifo en lugar de la X e Y en bruto de la página, así que el texto rotado y el texto sobre una línea base inclinada usan las mismas tolerancias que las líneas horizontales corrientes. El tamaño usado es el del glifo tal como está pintado en la página, derivado de su quad, no el operando de tamaño dado a Tf; eso importa para documentos que ponen Tf a 1 y escalan todo a través de la matriz de texto, donde un tamaño de fuente en bruto dejaría todas las tolerancias inútiles
Los espacios nunca son candidatos. Los code points hasta U+0020, el espacio de no ruptura U+00A0 y el espacio ideográfico U+3000 se saltan, porque los glifos en blanco no llevan tinta visible que duplicar y descartar uno fundiría palabras
Mantener la comprobación rápida en páginas densas
La comprobación de duplicados en HotPDF corre en un tiempo de 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 todos los demás sería correcto e inutilizable: una página densa de letra pequeña puede llevar fácilmente decenas de miles de glifos, y la comprobación corre en cada llamada de extracción de texto
La rutina interna ordena los glifos sin espacios por valor Unicode, luego por origen X y luego por orden de pintado, usando un merge sort estable. Tras 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 línea base, así que un par sobre una línea base ligeramente inclinada sigue cayendo dentro antes de que corra la prueba precisa en el marco de la línea base. Un caso sutil: si el escaneo desde el glifo A se topa con un glifo coincidente pintado antes que A, A es la copia, así que A se descarta y el escaneo se para, dejando que el glifo anterior continúe la comparación con lo que venga después. Esa regla es lo que hace que "gana el primero pintado" 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 otro, se reconocen igualmente como un carácter. La contrapartida es que un glifo cuyo código resuelve a Unicode 0 no tiene nada fiable que comparar, que es una razón más para vigilar el contador de no resueltos que cubre extraer texto de PDF cargados con HotPDF
¿Qué llamadas de extracción de HotPDF eliminan los glifos duplicados?
Todas las llamadas que producen texto en HotPDF eliminan duplicados, y la llamada de glifos en bruto no. La desduplicación vive en THotPDF.ExtractVisibleLoadedPageGlyphs, el paso que también descarta los glifos fuera de la caja visible de la página, y la extracción de texto plano, por columnas, por layout y semántica leen todos sus glifos de ahí. THotPDF.ExtractLoadedPageGlyphs sigue devolviendo cada glifo que pinta el content stream, sobreimpresos incluidos, porque los callers que construyen resaltado, redacción o herramientas forenses necesitan el registro completo de lo dibujado
Para trabajo de texto ordinario nada cambia en el código que llama. El mismo bucle que extraía texto duplicado antes de la v2.768.15 extrae ahora 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 por layout y por columnas se beneficia igual, ya que una corrida duplicada solía empujar el resto de una línea hacia los lados en la rejilla de caracteres. ExtractLoadedPageTextColumns conserva su multiplicador de hueco opcional; el filtro de duplicados corre antes de la división por 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 supera 3x el advance mediano del glifo
if Pdf.ExtractLoadedPageTextColumns(0, Columns, 3.0) then
TFile.WriteAllText('page1-columns.txt', Columns, TEncoding.UTF8);
end;
Si quieres saber si una página contiene texto sobreimpreso en absoluto, compara las dos llamadas de glifos. Ten en cuenta que la diferencia también cuenta glifos que caen fuera del crop box, así que trátala como un límite superior de duplicados y no como una 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 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 a propósito con otras herramientas
HotPDF descarta duplicados exactos que otras herramientas de extracción conservan, y esa diferencia es intencional. Cuando el cambio de la v2.768.15 se contrastó 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 sobre un glifo idéntico es invisible como carácter aparte, así que leerlo una vez es lo que la página muestra realmente
El límite honesto corre en la otra dirección. Un fake bold que desplaza la segunda pasada más de 0.1 del tamaño del glifo a lo largo de la línea base no se caza, y tampoco una pasada de sombra dibujada a un tamaño notablemente distinto. Esas maquetaciones son raras en la práctica, porque el efecto visual deja de parecer negrita en cuanto el desplazamiento se hace tan grande, pero si ves texto duplicado de un título decorativo, compara los valores X de los glifos de ExtractLoadedPageGlyphs antes de asumir que el filtro está roto. El filtro tampoco hace nada con duplicados que no están co-localizados geométricamente, como una cabecera corrida repetida en una capa oculta en otro punto de la página; esa es una cuestión de contenido, no de sobreimpresión
¿Por qué se cortaban los finales de línea en texto con fuentes CID?
Un bug aparte corregido en HotPDF v2.768.12 perdía los finales de línea en texto compuesto con fuentes con claves CID (Type 0) cuyo array de anchos /W no estaba ordenado por CID. ISO 32000-1 §9.7.4.3 define el formato del array /W pero no exige que sus entradas vayan 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 en silencio entrada ordenada. En un array desordenado la mayoría de las búsquedas fallaban y caían en el ancho por defecto /DW, que son 1000 unidades, un em completo. El texto latino en tal fuente se maquetaba por tanto 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 caían 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 revientaba y nada se reportaba, que es lo que hacía difícil verlo
HotPDF ordena ahora las entradas de /W con un merge sort estable justo después de leerlas, así que cuando dos entradas se solapan gana la que se escribió primero, igual que se lee el array en orden de archivo. El renderizador de página nunca estuvo afectado, porque guarda su propia copia de los datos de anchos y los recorre linealmente, y el array de anchos vertical /W2 también se recorre linealmente. Esa división conviene recordarla cuando una página se ve perfecta pero se extrae mal: render y extracción en una biblioteca PDF suelen compartir mucho menos código del que esperarías
FAQ
¿Elimina HotPDF letras repetidas como la doble l de "fill"?
No. Dos letras reales adyacentes están a unos un advance width de distancia, mientras que HotPDF solo funde caracteres idénticos que estén dentro de 0.1 del tamaño del glifo a lo largo de la línea base y 0.2 a lo ancho, así que las repeticiones legítimas se conservan
¿Cómo obtengo todos los glifos, incluidos los duplicados sobreimpresos?
Llama a 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 desduplicado y visible
¿Qué versión de HotPDF corrige el texto duplicado por fake bold?
HotPDF v2.768.15 añadió 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 perdidos en fuentes CID con un array /W desordenado
Ambos arreglos vienen en el componente HotPDF Delphi PDF actual, que extrae texto de PDF cargados en Delphi y C++Builder junto a sus funciones de creación, edición y render de PDF