PDFium Component agrega una capa de texto buscable a las páginas PDF escaneadas desde Delphi mediante ApplyOcrSearchLayer. Renderiza cada página seleccionada, entrega los píxeles a un proveedor de OCR que tú suministras, y escribe las palabras reconocidas de vuelta como objetos de texto invisibles posicionados sobre las palabras del escaneo. La imagen original de la página nunca se decodifica, recodifica ni reemplaza, así que el resultado visual es byte por byte la misma página con la que se empezó
El motor de reconocimiento deliberadamente no forma parte de la biblioteca. PDFium expone renderizado de página, mapeo de coordenadas, carga de fuentes, creación de objetos de texto y modos de renderizado invisible, pero no contiene ningún motor de OCR, y fingir lo contrario significaría empaquetar el producto de reconocimiento de otra empresa dentro de un componente PDF. En su lugar, el reconocimiento vive detrás de la interfaz IPdfOcrProvider: la biblioteca entrega píxeles BGRA de diseño fijo con origen superior, y el proveedor devuelve texto Unicode, valores de confianza y cuadriláteros de palabra
¿Qué es exactamente una capa de texto buscable?
Un PDF escaneado es una fotografía de un documento. El contenido de la página es una sola imagen grande, y no hay nada que seleccionar, buscar, copiar o indexar. Una capa de texto buscable agrega objetos de texto reales encima de esa imagen con el modo de renderizado configurado como invisible, así que los visores no dibujan nada, pero la selección, la búsqueda y la extracción encuentran las palabras exactamente donde aparecen
El posicionamiento es todo el juego. Si el texto invisible queda desplazado unos pocos puntos, los resaltados de selección caen junto a las palabras en lugar de sobre ellas, y copiar un párrafo produce texto en el orden equivocado. Por eso la geometría tiene que provenir de las mismas transformaciones que PDFium usa para renderizar la página, y no de una estimación proporcional
Implementar el proveedor
El contrato del proveedor es un solo método. Recibe un registro de imagen de página que lleva dimensiones, stride, DPI, formato de píxel y los propios bytes de píxel, más un token de cancelación, y devuelve palabras o un mensaje de error:
uses
PDFium;
type
TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
public
function RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
end;
function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
I: Integer;
begin
// Image.Pixels contiene filas BGRA con origen superior de Image.Stride bytes.
// Entrégalas a tu motor y luego llena una entrada por cada palabra reconocida
SetLength(Words, RecognisedCount);
for I := 0 to RecognisedCount - 1 do
begin
Words[I].Text := EngineWordText(I);
Words[I].Confidence := EngineWordConfidence(I); // 0..1
Words[I].Quad := TPdfOcrQuad.FromRectangle(
EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
end;
ErrorMessage := '';
Result := True;
end;
Cuadriláteros en lugar de rectángulos, porque un escaneo rara vez queda perfectamente cuadrado respecto a la página. Una palabra en una página ligeramente rotada ocupa un paralelogramo, y TPdfOcrQuad lleva cuatro puntos de esquina para que las palabras sesgadas y rotadas conserven una región de selección precisa. Los motores que solo reportan cuadros alineados a los ejes pueden usar FromRectangle, que construye el cuadrilátero degenerado
¿Por qué no se pueden escalar las posiciones de palabra de forma proporcional?
Es tentador convertir una coordenada de píxel en una coordenada de página dividiendo entre el ancho de renderizado y multiplicando por el ancho de página. Eso solo funciona para páginas sin rotación, con un CropBox idéntico al MediaBox, y un origen en cero, y bastantes documentos escaneados fallan al menos una de esas condiciones
PDFium Component mapea cada una de las cuatro esquinas del cuadrilátero individualmente mediante FPDF_DeviceToPage, el mismo mapeo que usó el renderizador para producir los píxeles, así que las entradas /Rotate y los cuadros de recorte desplazados se manejan por construcción. La matriz afín del objeto de texto se construye entonces a partir de tres de los puntos mapeados, las esquinas inferior izquierda, inferior derecha y superior izquierda, que es exactamente suficiente para expresar posición, escala, rotación e inclinación
El objeto de texto en sí se crea con tamaño de fuente unitario para que sus límites de fuente reales puedan medirse, y los límites del objeto medidos se mapean luego sobre el cuadrilátero objetivo. Dimensionar con un tamaño de punto adivinado y esperar que coincida con la palabra escaneada se desviaría con cada sustitución de fuente; medir primero hace que el ajuste sea independiente de qué fuente use la capa
Ejecutarlo sobre un documento
El registro de opciones controla la resolución, el filtrado y cada presupuesto. El filtrado por confianza importa más de lo que parece: las palabras basura con baja confianza contaminan los resultados de búsqueda de forma permanente, y a diferencia de un renderizado equivocado, nadie lo nota hasta que una búsqueda devuelve algo sin sentido:
var
Pdf: TPdf;
Options: TPdfOcrOptions;
Report: TPdfOcrReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'scanned-contract.pdf';
Pdf.LoadDocument;
Options := TPdfOcrOptions.Default;
Options.Dpi := 300; // resolución de reconocimiento
Options.MinConfidence := 0.60; // descartar palabras inciertas
Options.SkipPagesWithText := True; // dejar en paz las páginas nativas digitales
Options.ContinueOnError := True; // una página mala no debe detener el trabajo
Options.MaxPixelsPerPage := 40 * 1000 * 1000;
if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
Pdf.SaveAs('scanned-contract-searchable.pdf');
for I := 0 to High(Report.Pages) do
if Report.Pages[I].Status = popsFailed then
Writeln(Format('page %d failed: %s',
[Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
[Report.InsertedWordCount, Report.RejectedWordCount,
Report.SkippedPageCount]));
finally
Pdf.Free;
end;
end;
SkipPagesWithText merece énfasis en archivos mixtos. Un PDF que ya lleva texto real, ya sea nativo digital o previamente procesado, recibe una segunda capa de texto si se le aplica OCR sin distinción, y la duplicación hace que la extracción devuelva cada palabra dos veces. El estado por página popsSkippedExistingText indica exactamente qué páginas se dejaron sin tocar
Presupuestos, cancelación y contención de fallos
Cada cantidad que un documento hostil o simplemente enorme pueda inflar tiene un tope: píxeles por página y en total, palabras por página y en total, y caracteres por palabra. Todos se verifican antes de escribir la página, no después, y la estimación de píxeles se calcula a partir de las dimensiones de la página y el DPI antes de asignar cualquier mapa de bits. Aumentar el DPI de 150 a 300 cuadruplica la memoria por página, así que el tope por página es el parámetro que hay que ajustar primero cuando un trabajo por lotes empieza a fallar con formatos grandes
El token de cancelación atraviesa toda la ruta: el renderizado progresivo, la llamada al proveedor y el bucle de inserción por palabra. Eso significa que un usuario que cancela durante el reconocimiento de un archivo de 400 páginas se detiene dentro de una página en lugar de al final del documento, y el mismo patrón de token usado en otras partes del componente, descrito en renderizado progresivo cancelable, se aplica aquí sin cambios
La contención de fallos es por página. La biblioteca recolecta los descriptores de objeto que insertó en una página y llama a FPDFPage_GenerateContent una sola vez, después de colocar todas las palabras. Si algo falla a mitad de camino, ya sea un error del proveedor o un problema de fuente, los objetos insertados en esa página se eliminan en orden inverso y el contenido de la página se regenera, así que una página fallida vuelve a su estado original en lugar de conservar media capa de texto. El bucle del documento continúa o se detiene entonces según ContinueOnError, y la página activa siempre se restaura
Verificar que la imagen realmente quedó intacta
La verificación más sólida disponible también es la más simple: renderiza la página antes y después de aplicar la capa al mismo tamaño y compara los mapas de bits. Deberían ser idénticos byte por byte, porque el texto invisible no dibuja nada y el flujo de imagen nunca se decodificó. Cualquier diferencia significa que algo distinto de la capa de texto cambió la página
Después de eso, verifica el lado del texto extrayendo del archivo procesado y confirmando que las posiciones de las palabras caen sobre el escaneo. La ruta de extracción es la misma descrita en extracción de texto de documentos PDF, y para una verificación visual rápida de la alineación, renderizar páginas a imágenes como en conversión de páginas PDF a JPEG permite superponer cuadros de palabra sobre el escaneo
El apilado de OCR, el renderizado, la extracción y la edición se ejecutan todos contra el mismo objeto de documento en Delphi, C++Builder y Lazarus; la superficie completa de la API se describe en la página de PDFium Component para Delphi