Artículo técnico

Buscar y reemplazar texto en un PDF existente con Delphi

HotPDF Component puede buscar y reemplazar texto dentro de un PDF existente desde Delphi y C++Builder. SearchLoadedPageText y SearchLoadedDocumentText localizan cada ocurrencia de una cadena con precisión a nivel de glifo, y ReplaceLoadedPageText y ReplaceLoadedDocumentText reescriben los bytes coincidentes en su lugar, siempre que cada carácter de reemplazo se pueda codificar de nuevo a través de la fuente original, una restricción física que este artículo trata con honestidad en lugar de ocultarla en una nota al pie

La solicitud detrás de esta característica siempre es trivial. Una empresa cambia de nombre y tres mil facturas archivadas todavía llevan el nombre antiguo. Una plantilla de contrato se envió con la fecha de vencimiento del año pasado. Se retiró un código de producto y cada hoja de datos que lo menciona necesita el código sucesor en su lugar. En un procesador de textos, cada uno de estos es un trabajo de treinta segundos. En un PDF, es un problema realmente difícil y comprender por qué marca la diferencia entre usar bien la API y presentar un informe de error que en realidad es una cita de la especificación

¿Por qué es tan difícil reemplazar texto en un PDF?

Reemplazar texto en un PDF es difícil porque una página PDF no contiene texto editable: contiene glifos posicionados. Según el modelo de visualización de texto de la norma ISO 32000-1 §9.4, un flujo de contenido dirige operadores como Tj y TJ que pintan secuencias de códigos de caracteres en coordenadas establecidas por la matriz de texto. Esos códigos no son Unicode; son índices en cualquier codificación que declare la fuente de la página, y el mapeo de regreso a caracteres legibles puede residir en un CMap /ToUnicode, una matriz de diferencias de codificación o una cadena de mapeo CID. No hay ningún objeto de párrafo, ningún flujo de texto y ninguna garantía de que una palabra visual se almacene siquiera como una única cadena

El reemplazo añade una segunda capa de dificultad además de la decodificación: debe saber exactamente qué bytes del flujo original produjeron cada glifo, de modo que pueda empalmar nuevos bytes precisamente en ese tramo y en ningún otro lugar. Un extractor de texto puede permitirse desechar las posiciones de los bytes una vez que ha obtenido el Unicode. Un reemplazador no puede. Por eso HotPDF dividió el trabajo en dos lanzamientos: la versión v2.251.0 construyó la capa de búsqueda y seguimiento de desplazamientos, y la versión v2.252.0 construyó la capa de reescritura sobre ella

Buscar texto: búsqueda a nivel de glifo con seguimiento de desplazamiento de bytes

El método SearchLoadedDocumentText de HotPDF encuentra cada ocurrencia de una búsqueda comparándola con la secuencia de glifos Unicode decodificada de cada página, no con bytes de flujo sin procesar, por lo que un acierto es un acierto independientemente de cómo lo haya codificado la fuente. La infraestructura subyacente se introdujo en la versión v2.251.0: el tokenizador del flujo de contenido registra un tramo de bytes StartOfs/EndOfs para cada operando de cadena (incluidos sus delimitadores ( ) o < >) y cada glifo decodificado lleva un trío TokenIndex/ItemIndex/ByteOffset que apunta de regreso al operando exacto, al elemento de la matriz TJ y a la unidad de código que lo produjo. El mismo intérprete de glifos potencia la API de extracción descrita en el artículo sobre la extracción de texto de un PDF cargado en Delphi; la búsqueda simplemente conserva el origen que la extracción desecha

Cada coincidencia se devuelve como un registro THPDFTextMatch que lleva el índice de página, el rango de glifo inclusivo, el origen X/Y del espacio de usuario y el ancho del acierto, el token de origen y el índice del elemento, y el texto coincidente en sí. Eso es suficiente para controlar una superposición de resaltado, una interfaz de usuario de revisión o el paso de reemplazo. Una búsqueda que no encuentra nada devuelve una matriz vacía en lugar de fallar, por lo que el patrón de llamada sigue siendo simple

var
  Pdf: THotPDF;
  Matches: THPDFTextMatchArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
    begin
      if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
        for I := 0 to Length(Matches) - 1 do
          WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
            [Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
             Matches[I].Text]));
    end;
  finally
    Pdf.Free;
  end;
end;

Una elección de diseño deliberada merece una nota. Cuando CaseSensitive es False, la comparación convierte mayúsculas y minúsculas solo para caracteres ASCII, por diseño: la conversión de mayúsculas y minúsculas de Unicode completo se comporta de manera diferente en las cadenas de herramientas de Delphi 5 a XE que admite HotPDF, y una API de búsqueda que encuentra diferentes coincidencias según qué compilador construyó su aplicación es peor que una con un límite documentado y predecible. Para textos comerciales latinos (nombres, códigos, fechas), la conversión ASCII cubre los casos prácticos

Reemplazar texto: codificación inversa y empalme quirúrgico

ReplaceLoadedDocumentText, añadido en HotPDF v2.252.0, vuelve a escribir cada ocurrencia de una búsqueda ejecutando el mecanismo de decodificación al revés. La función HPDFEncodeUnicode es la inversa del decodificador de códigos de caracteres: recorre la misma cadena de estrategias a la inversa (búsqueda de bfchar y bfrange /ToUnicode, mapeo de CID de flujo de codificación, mapeos de identidad de Tipo 0 y las tablas predefinidas WinAnsi y MacRoman) para convertir cada carácter de reemplazo de nuevo en los bytes de código de carácter que espera la fuente original. Luego, los bytes codificados de nuevo se serializan en un literal de cadena bien formado o una cadena hexadecimal, reflejando las propias reglas de escape del tokenizador para que el viaje de ida y vuelta de análisis → reserialización sea estable

El empalme en sí es quirúrgico en lugar de masivo. Solo el rango de bytes de código cubierto por la coincidencia se reemplaza dentro del operando de cadena; los bytes no coincidentes en el mismo operando, el espacio en blanco entre tokens y cada operador circundante se conservan literalmente, byte por byte. Reemplazar bca dentro de abcabc produce a + reemplazo + bc, no un operando dañado. Los reemplazos pueden ser más cortos o más largos que la búsqueda; el literal se vuelve a serializar y la longitud /Length del flujo se actualiza, y cada flujo /Contents de una página con múltiples flujos se procesa de forma aislada para que la página se mantenga bien formada

var
  Pdf: THotPDF;
  ReplaceCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
    begin
      if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
        True, ReplaceCount) then
        WriteLn(Format('%d operand rewrites performed', [ReplaceCount]));
      Pdf.SaveLoadedDocument('contract-final.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Tenga en cuenta lo que la API no hace: no vuelve a componer la página. PDF no tiene reflujo, por lo que un reemplazo que es visualmente más ancho que el original simplemente ocupará más espacio horizontal y puede saturar lo que se pintó a su derecha. Las sustituciones de igual o similar longitud (fechas, cadenas de versión, números de pieza, correcciones de nombres) son el punto ideal. La reformulación completa pertenece al documento de origen, no al PDF

¿Por qué no se puede reemplazar texto con caracteres que el subconjunto de fuentes nunca incluyó?

No se puede reemplazar texto con un carácter que el subconjunto de fuentes incrustado nunca incluyó, porque la secuencia de bytes que seleccionaría ese carácter simplemente no existe en las tablas de mapeo de la fuente. Cuando un productor de PDF incrusta una fuente de subconjunto, su CMap /ToUnicode y las estructuras de codificación cubren solo los glifos que el documento original realmente utilizó. HPDFEncodeUnicode solo puede revertir un mapeo que esté presente: si el documento nunca contuvo la letra E en esa fuente, no hay código de carácter para E al que revertir. Esta es una propiedad física del archivo, no una limitación de ninguna biblioteca en particular; ninguna herramienta puede evocar un mapeo de glifos que nunca fue incrustado

HotPDF maneja el fallo de manera conservadora. Si algún carácter del reemplazo no se puede codificar de nuevo, se omite toda la ocurrencia de la búsqueda; sin excepciones, sin texto parcial inservible, y la ocurrencia simplemente no se cuenta en ReplaceCount. La consecuencia práctica: verifique ReplaceCount frente al recuento de coincidencias de una búsqueda previa y trate una diferencia como una señal. En el ejemplo de la fecha anterior, el dígito 6 must aparecer en algún lugar del texto del documento en esa misma fuente para que la reescritura tenga éxito; probable en una factura, pero nunca garantizado en general. Cuando los caracteres que necesita simplemente no están disponibles y el objetivo es eliminar texto confidencial en lugar de reformularlo, la eliminación real de contenido es la mejor herramienta de todos modos; consulte el artículo sobre redacción y reestructuración de archivos PDF cargados en Delphi para esa ruta

var
  Matches: THPDFTextMatchArray;
  Expected, Replaced: Integer;
begin
  Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
  Expected := Length(Matches);
  Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
  if Replaced < Expected then
    WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
      'from the font subset, or match spans multiple operands',
      [Expected - Replaced]));
end;

La segunda condición de omisión en ese mensaje es el otro límite documentado: una búsqueda que abarca múltiples operandos de cadena (por ejemplo, Hello dividido en elementos [(He)(llo)] TJ) es encontrada por la búsqueda, porque esta coincide con la secuencia de glifos decodificada, pero el reemplazo la omite, porque reescribir a través de los límites de los operandos requeriría fusionar tramos de bytes adyacentes. Buscar y luego verificar hace que ambos límites sean visibles en lugar de silenciosos

¿Qué cambia en el archivo al guardar?

Un flujo /Contents reemplazado se guarda sin comprimir. Los flujos comprimidos con FlateDecode se descomprimen para su edición, y cuando HotPDF escribe los bytes reconstruidos, elimina la entrada /Filter del flujo y actualiza la longitud /Length en lugar de volver a comprimir. El PDF resultante es completamente válido y se representa con normalidad en los visores habituales; la contrapartida es un archivo más grande para cada flujo editado. Para una canalización por lotes que procesa miles de documentos, planifique ese crecimiento o ejecute una pasada de compresión independiente en una fase posterior. Cómo interactúan los objetos reescritos con la estructura de referencias cruzadas del documento al guardar es su propio tema, cubierto en el artículo sobre flujos de objetos y actualizaciones incrementales en HotPDF

Todo lo demás en el archivo se deja intacto. Los flujos no tocados conservan su compresión, las fuentes y las imágenes no se reescriben, y el empalme a nivel de operando significa que incluso los flujos editados difieren del original solo donde coincidió un acierto. Ese conservadurismo es deliberado: cuanto más reescribe una biblioteca de un documento cargado, más oportunidades tiene de romper una peculiaridad del productor que no anticipó

La búsqueda y reemplazo de texto se une a la extracción, redacción y representación de páginas en el conjunto de herramientas de documentos cargados de HotPDF, todo impulsado por el mismo intérprete de flujo de contenido y disponible desde Delphi 5 hasta las versiones actuales de RAD Studio sin dependencias externas. La referencia completa de la API y la descarga de prueba están en la página del producto HotPDF Component