PDFium Component añade 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 usted mismo suministra, y escribe las palabras reconocidas de vuelta como objetos de texto invisibles posicionados sobre las palabras del escaneado. La imagen de página original nunca se decodifica, se vuelve a codificar ni se sustituye, así que el resultado visual es, byte a byte, la página con la que se empezó
El motor de reconocimiento no forma parte de la biblioteca de forma deliberada. PDFium expone el renderizado de páginas, la traslación de coordenadas, la carga de fuentes, la creación de objetos de texto y los modos de renderizado invisibles, 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 reside detrás de la interfaz IPdfOcrProvider: la biblioteca pasa píxeles BGRA de disposición fija 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 única imagen grande, y no hay nada que seleccionar, buscar, copiar o indexar. Una capa de texto buscable añade objetos de texto reales encima de esa imagen con el modo de renderizado establecido en invisible, de modo 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 lo es todo. 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 usa PDFium para renderizar la página, en lugar de una estimación proporcional
Implementar el proveedor
El contrato del proveedor consiste en un único método. Recibe un registro de imagen de página que lleva las dimensiones, el stride, el DPI, el 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éguelas a su motor y después rellene 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 escaneado rara vez está perfectamente alineado con 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 inclinadas y rotadas conserven una región de selección precisa. Los motores que solo informan de cuadros alineados con los ejes pueden usar FromRectangle, que construye el cuadrilátero degenerado
¿Por qué no se pueden escalar las posiciones de las palabras de forma proporcional?
Resulta tentador convertir una coordenada de píxel en una coordenada de página dividiendo por 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 muchos documentos escaneados incumplen al menos una de esas condiciones
PDFium Component traslada cada una de las cuatro esquinas del cuadrilátero individualmente mediante FPDF_DeviceToPage, la misma correspondencia que usó el renderizador para producir los píxeles, así que las entradas /Rotate y los cuadros de recorte desplazados se gestionan por construcción. La matriz afín del objeto de texto se construye entonces a partir de tres de los puntos trasladados, las esquinas inferior izquierda, inferior derecha y superior izquierda, que es exactamente lo necesario para expresar posición, escala, rotación e inclinación
El propio objeto de texto se crea con un tamaño de fuente unitario para poder medir sus límites de fuente reales, y los límites de objeto medidos se trasladan después al cuadrilátero objetivo. Dimensionar con un tamaño de punto adivinado y esperar que coincida con la palabra escaneada iría a la deriva 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 todos los presupuestos. El filtrado por confianza importa más de lo que parece: las palabras basura con confianza baja contaminan los resultados de búsqueda de forma permanente, y a diferencia de un renderizado incorrecto, nadie se da cuenta hasta que una búsqueda devuelve sinsentidos:
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 intactas las páginas nativamente digitales
Options.ContinueOnError := True; // una página defectuosa 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 un énfasis especial en archivos mixtos. Un PDF que ya lleva texto real, ya sea nativamente digital o procesado previamente, recibe una segunda capa de texto si se le aplica OCR a ciegas, 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 intactas
Presupuestos, cancelación y contención de fallos
Toda cantidad que un documento hostil o simplemente enorme pueda inflar tiene un límite: píxeles por página y en total, palabras por página y en total, y caracteres por palabra. Todos se comprueban antes de escribir la página, no después, y la estimación de píxeles se calcula a partir de las dimensiones de página y el DPI antes de reservar ningún mapa de bits. Aumentar el DPI de 150 a 300 cuadruplica la memoria por página, así que el límite 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 recorre toda la vía: 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 el renderizado progresivo cancelable, se aplica aquí sin cambios
La contención de fallos es por página. La biblioteca recopila los identificadores de objeto que insertó en una página y llama a FPDFPage_GenerateContent una vez, tras 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, de modo que una página fallida vuelve a su estado original en lugar de quedarse con 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 comprobación más sólida disponible es también la más sencilla: renderice la página antes y después de aplicar la capa al mismo tamaño y compare los mapas de bits. Deberían ser idénticos byte a 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
A continuación, verifique el lado del texto extrayendo del archivo procesado y confirmando que las posiciones de las palabras caen sobre el escaneado. La vía de extracción es la misma descrita en extraer texto de documentos PDF, y para una comprobación visual rápida de la alineación, renderizar páginas a imágenes como en convertir páginas PDF a JPEG permite superponer cuadros de palabra sobre el escaneado
La superposición 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