HotPDF Delphi Component reconstruye los espacios de palabra y los saltos de línea en THotPDF.ExtractLoadedPageText a partir de la geometría de los glyphs, no de los caracteres de espacio. Un espacio entra cuando el hueco después del propio ancho del glyph supera 0.15 del alto del texto, y una línea nueva arranca solo cuando el origen del texto se mueve en contra de la dirección de escritura más de la mitad del alto del texto. Desde la v2.768.3 el texto de página también incluye texto pintado vía Form XObjects y deja afuera los glyphs fuera del área visible del crop. El resto de este artículo explica por qué cada regla tiene la forma que tiene, porque todas reemplazaron una regla más simple que producía salida plausible pero equivocada en documentos reales
Los síntomas le suenan a cualquiera que haya metido texto de PDF en un índice de búsqueda. Una carátula extrae como PDFReferenceManualNovember4,1998, un formulario fiscal se parte en 156 líneas, una marca de agua diagonal llega de a un carácter por línea, y una prueba recortada encabeza con la slug line de la imprenta que ningún visor muestra jamás. Ninguno de estos archivos está roto. Cada uno usa una manera perfectamente legal de colocar texto que un extractor ingenuo lee mal
¿Por qué el texto extraído de un PDF pierde sus espacios?
El texto extraído pierde espacios porque a un PDF jamás se le exige contenerlos. Un productor puede separar palabras mostrando un carácter de espacio, pero puede igual de bien mover la pluma con un número dentro de un array TJ (ISO 32000-1 §9.4.3) o con un Td fresco (§9.4.2), y la salida de TeX, muchos archivos de Distiller y la mayoría de los layouts justificados hacen exactamente eso. Antes de la v2.766.76, HPDFAssemblePageText solo miraba el movimiento vertical, así que un quiebre de palabra hecho por posicionamiento simplemente desaparecía. El assembler ahora mide, a lo largo de la dirección de escritura del glyph anterior, la distancia desde el fin del propio ancho de ese glyph hasta el origen del glyph actual, e inserta un espacio cuando la distancia supera 0.15 del alto de la caja del glyph actual, medido de ascent a descent en user space. No se agrega espacio cuando alguno de los dos lados ya es blanco, ni entre dos caracteres CJK, porque la justificación estira los ideographs sin que ese estiramiento signifique frontera de palabra. Los records de glyphs exponen la misma geometría, así que puede reproducir la decisión cuando un archivo en particular lo deja perplejo
uses
SysUtils, HPDFDoc, HPDFContentStream;
procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
Glyphs: THPDFGlyphArray;
I: Integer;
Height, Gap: Double;
begin
if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
Exit;
for I := 1 to High(Glyphs) do
begin
// alto de ascent a descent de la caja del glyph, en user space
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// texto horizontal: hueco desde el fin del propio ancho del glyph anterior
Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
if (Height > 0) and (Gap > 0.15 * Height) then
Writeln(Format('U+%.4x gap %.2f height %.2f: space',
[Glyphs[I].Unicode, Gap, Height]));
end;
end;
¿Por qué medir desde el propio ancho del glyph y no desde la posición de la pluma?
HotPDF mide los huecos de palabra desde GlyphEndX / GlyphEndY porque la posición de la pluma después de un glyph ya contiene espaciado que no es un hueco. ISO 32000-1 §9.4.4 define el desplazamiento horizontal como el ancho del glyph por el tamaño de la fuente, más el character spacing Tc, más el word spacing Tw, todo escalado por Tz. BaselineEndX / BaselineEndY guardan ese desplazamiento completo, mientras que GlyphEndX / GlyphEndY guardan solo el advance de la fuente y Tz. La diferencia importa con productores que aprietan el tracking con un Tc negativo y después devuelven el espacio vía un ajuste de TJ después de cada glyph: medido desde la posición de la pluma, la devolución parece un hueco, y el término chino “95后” se extraía como “9 5 后”. El umbral está atado al alto de la caja del glyph en vez del tamaño de Tf por una razón parecida. Los exports de Word suelen escribir 1 Tf y llevar el tamaño real en un Tm escalado, así que Tfs dice 1 mientras el texto mide 10 puntos, y una regla anclada a Tfs trataría distinto a las dos grafías de una misma página
La regla tiene bordes honestos. Un título compuesto con tracking muy suelto, donde el Tc solo abre más de 0.15 del alto del texto entre letras, extrae con un espacio entre cada letra, que es como se ve la página pero probablemente no lo que usted quería indexar. Trozos dibujados fuera de orden sobre una misma línea base producen un hueco negativo y se unen sin espacio. Ninguno de los dos casos es común en cuerpo de texto, y sobre un corpus de test el cambio subió los matches de palabra contra un extractor de referencia en 28 páginas sin bajar ninguno
¿Cuándo arranca HotPDF una línea nueva en el texto extraído?
Desde la v2.766.79, una línea nueva arranca cuando el movimiento desde el origen del glyph anterior al actual, proyectado sobre la normal de la dirección de escritura anterior, supera la mitad del mayor alto de caja de los dos glyphs. La regla anterior comparaba el movimiento Y crudo con la mitad de Tfs, y fallaba en dos direcciones. Con 1 Tf y un Tm escalado el umbral se encogía a media unidad, así que un superíndice levantado por un text rise de 0.4 o el jitter corriente de la línea base partía la línea. La regla además ignoraba la X por completo, así que texto bajo un Tm rotado bajaba escalonado por la página con cada glyph y salía de a un glyph por línea. Proyectar sobre la normal de la dirección hace que los tramos rotados se comporten como horizontales, y tomar el mayor de los dos altos mantiene en una línea a una palabra grande de muestra y su caption chico cuando comparten línea base. En el formulario fiscal mencionado arriba, el conteo de líneas bajó de 156 a 97. El texto vertical en writing mode 1 (§9.7.4.3) sigue un camino aparte: esos glyphs se agrupan en columnas, se leen de derecha a izquierda y de arriba abajo, con un salto de línea en cada cambio de columna
¿Qué texto incluye o deja afuera ExtractLoadedPageText?
ExtractLoadedPageText devuelve el texto que un visor muestra. Desde la v2.766.80 trabaja solo con los glyphs visibles, soltando cada glyph cuyo centro de caja cae fuera de GetLoadedPageVisibleBox, que es el CropBox recortado contra el MediaBox (§14.11.2). Eso elimina slug lines y otras marcas de imprenta compuestas como texto fuera del área de corte. ExtractLoadedPageGlyphs deliberadamente sigue devolviendo cada glyph del content stream de la página, así que puede encontrar ese material cuando lo necesite. El filtro es un test de caja, no un test de visibilidad: texto tapado por un clipping path, pintado en blanco o cubierto por una imagen se extrae igual
var
Pdf: THotPDF;
Glyphs: THPDFGlyphArray;
PageText: UnicodeString;
L, B, R, T: Single;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('trimmed-proof.pdf');
if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
// cada glyph del content stream de la página, slug line incluida
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// solo lo que la página muestra, con el texto de Form XObjects empalmado
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
El texto pintado vía Form XObjects es parte del texto de página desde la v2.768.3. Headers, sellos y marcas de agua viven muy a menudo en forms, y algunos documentos de estándares perdían del 30 al 35 por ciento de sus caracteres antes del cambio. THotPDF.InterpretContentWithForms anota cada Do junto con la CTM vigente, interpreta el form en su /Matrix por esa CTM (§8.10.1), y empalma los glyphs del form en la posición del Do, recursando dentro de forms anidados. Un form sin /Resources propios toma prestados los del stream que lo pinta, como permite §7.8.3. Los glyphs de form cargan TokenIndex = -1, y ExtractLoadedPageGlyphs sigue devolviendo solo glyphs del stream de página, porque search, replace y redaction escriben cambios de vuelta vía TokenIndex y editarían los bytes equivocados si un glyph de form se colara. Dos simplificaciones valen conocer: el texto de form no se recorta contra el /BBox del form, y la recursión se frena a 12 niveles en lugar de detectar ciclos, así que un form malformado que se pinta a sí mismo repite su texto hasta llegar a ese tope
¿Por qué el texto después de un operador Q se decodificaba como basura?
El texto después de Q podía decodificarse mal antes de la v2.766.73 porque el extractor solo guardaba la CTM en el q. Los parámetros de estado de texto, o sea fuente, tamaño, Tc, Tw, Tz, TL, modo de renderizado y rise, pertenecen al estado de gráficos (§9.3.1), así que Q tiene que restaurarlos junto con todo lo demás de la pila (§8.4.2). Un reporte de la industria seleccionaba una fuente Identity-H de dos bytes dentro de q … Q y después mostraba texto WinAnsi de un byte sin Tf propio. El extractor conservaba la fuente interna, leía los leaders y la palabra “Adobe” del índice de contenidos como códigos de dos bytes, y soltaba el 15% de los caracteres de la página. La pila de q/Q del intérprete ahora sostiene el estado de texto completo. Las reglas de extracción descritas aquí aplican a cada página, así que un documento entero puede irse a un archivo en una sola llamada
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// rango vacío = todas las páginas; form feed entre páginas; BOM UTF-8
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
¿Qué API de texto de HotPDF debería usar?
ExtractLoadedPageText se queda en el orden del content stream, que es el default correcto para búsqueda e indexación; la cadena de decodificación que tiene debajo está cubierta en extraer texto de PDFs cargados con HotPDF. Para documentos etiquetados donde importa el orden de autoría, la extracción de texto en orden de estructura camina el árbol de estructura en lugar de adivinar por geometría, y para data atrapada en tablas, la extracción tipada de tablas a través de saltos de página devuelve celdas en lugar de líneas. La referencia completa de la API y una descarga de prueba están en la página de producto de HotPDF Delphi PDF Component