Saltar al contenido

Reemplazar texto dentro de un archivo PDF usando iText

Bienvenido a nuestra web, en este lugar vas a hallar la solucíon que necesitas.

Como ya se ha mencionado en comentarios y respuestas, PDF no es un formato diseñado para edición de texto. Es un formato final y la información sobre el flujo del texto, su diseño e incluso su asignación a Unicode es opcional.

Por lo tanto, incluso asumiendo que la información opcional sobre el mapeo de glifos a Unicode esté presente, el enfoque de esta tarea con iText podría parecer un poco insatisfactorio: primero se determinaría la posición del texto en cuestión usando una estrategia de extracción de texto personalizada, luego continuaría eliminando el contenido actual de todo en esa posición utilizando el PdfCleanUpProcessory, finalmente, dibuje el texto de reemplazo en el espacio.

En esta respuesta, presentaría una clase de ayuda que permite combinar los dos primeros pasos, encontrar y eliminar el texto existente, con la ventaja de que, de hecho, solo el texto es removido, no también cualquier gráficos de fondo, etc. como en el caso de PdfCleanUpProcessor redacción. Además, el ayudante devuelve las posiciones del texto eliminado, lo que permite sellar el reemplazo en el mismo.

La clase de ayuda se basa en el PdfContentStreamEditor presentado en esta respuesta anterior. Sin embargo, use la versión de esta clase en github, ya que la clase original se ha mejorado un poco desde su concepción.

los SimpleTextRemover La clase de ayuda ilustra lo que es necesario para eliminar correctamente el texto de un PDF. En realidad, está limitado en algunos aspectos:

  • Solo reemplaza el texto en los flujos de contenido de la página real.

    Para reemplazar también texto en XObjects incrustados, uno tiene que iterar a través de los recursos de XObject de la página respectiva en cuestión de forma recursiva y también aplicarles el editor.

  • Es “simple” de la misma manera SimpleTextExtractionStrategy es: Asume que el texto que muestra las instrucciones debe aparecer en el contenido en orden de lectura.

    Para trabajar también con flujos de contenido para los que el orden es diferente y las instrucciones deben estar ordenadas, y esto implica que todas las instrucciones entrantes y la información de procesamiento relevante deben almacenarse en caché hasta el final de la página, no solo unas pocas instrucciones a la vez. Luego, la información de renderizado se puede clasificar, las secciones para eliminar se pueden identificar en la información de render ordenada, las instrucciones asociadas se pueden manipular y las instrucciones eventualmente se pueden almacenar.

  • No intenta identificar espacios entre los glifos que representan visualmente un espacio en blanco mientras que en realidad no hay ningún glifo.

    Para identificar los huecos, el código debe ampliarse para comprobar si dos glifos consecutivos se siguen exactamente entre sí o si hay un hueco o un salto de línea.

  • Al calcular el espacio para dejar donde se elimina un glifo, aún no tiene en cuenta el espacio entre caracteres y palabras.

    Para mejorar esto, se debe mejorar el cálculo del ancho del glifo.

Sin embargo, teniendo en cuenta el extracto de su ejemplo de su flujo de contenido, estas restricciones probablemente no lo obstaculicen.

public class SimpleTextRemover extends PdfContentStreamEditor 
    public SimpleTextRemover() 
        super (new SimpleTextRemoverListener());
        ((SimpleTextRemoverListener)getRenderListener()).simpleTextRemover = this;
    

    /**
     * 

Removes the string to remove from the given page of the * document in the PDF reader the given PDF stamper works on.

*

The result is a list of glyph lists each of which represents * a match can can be queried for position information.

*/ public List> remove(PdfStamper pdfStamper, int pageNum, String toRemove) throws IOException if (toRemove.length() == 0) return Collections.emptyList(); this.toRemove = toRemove; cachedOperations.clear(); elementNumber = -1; pendingMatch.clear(); matches.clear(); allMatches.clear(); editPage(pdfStamper, pageNum); return allMatches; /** * Adds the given operation to the cached operations and checks * whether some cached operations can meanwhile be processed and * written to the result content stream. */ @Override protected void write(PdfContentStreamProcessor processor, PdfLiteral operator, List operands) throws IOException cachedOperations.add(new ArrayList<>(operands)); while (process(processor)) cachedOperations.remove(0); /** * Removes any started match and sends all remaining cached * operations for processing. */ @Override public void finalizeContent() pendingMatch.clear(); try while (!cachedOperations.isEmpty()) if (!process(this)) // TODO: Should not happen, so warn System.err.printf("Failure flushing operation %s; dropping.n", cachedOperations.get(0)); cachedOperations.remove(0); catch (IOException e) throw new ExceptionConverter(e); /** * Tries to process the first cached operation. Returns whether * it could be processed. */ boolean process(PdfContentStreamProcessor processor) throws IOException if (cachedOperations.isEmpty()) return false; List operands = cachedOperations.get(0); PdfLiteral operator = (PdfLiteral) operands.get(operands.size() - 1); String operatorString = operator.toString(); if (TEXT_SHOWING_OPERATORS.contains(operatorString)) return processTextShowingOp(processor, operator, operands); super.write(processor, operator, operands); return true; /** * Tries to processes a text showing operation. Unless a match * is pending and starts before the end of the argument of this * instruction, it can be processed. If the instructions contains * a part of a match, it is transformed to a TJ operation and * the glyphs in question are replaced by text position adjustments. * If the original operation had a side effect (jump to next line * or spacing adjustment), this side effect is explicitly added. */ boolean processTextShowingOp(PdfContentStreamProcessor processor, PdfLiteral operator, List operands) throws IOException PdfObject object = operands.get(operands.size() - 2); boolean isArray = object instanceof PdfArray; PdfArray array = isArray ? (PdfArray) object : new PdfArray(object); int elementCount = countStrings(object); // Currently pending glyph intersects parameter of this operation -> cannot yet process if (!pendingMatch.isEmpty() && pendingMatch.get(0).elementNumber < processedElements + elementCount) return false; // The parameter of this operation is subject to a match -> copy as is if (matches.size() == 0 /** * Counts the strings in the given argument, itself a string or * an array containing strings and non-strings. */ int countStrings(PdfObject textArgument) if (textArgument instanceof PdfArray) int result = 0; for (PdfObject object : (PdfArray)textArgument) if (object instanceof PdfString) result++; return result; else return textArgument instanceof PdfString ? 1 : 0; /** * Writes side effects of a text showing operation which is going to be * replaced by a TJ operation. Side effects are line jumps and changes * of character or word spacing. */ void writeSideEffect(PdfContentStreamProcessor processor, PdfLiteral operator, List operands) throws IOException switch (operator.toString()) case """: super.write(processor, OPERATOR_Tw, Arrays.asList(operands.get(0), OPERATOR_Tw)); super.write(processor, OPERATOR_Tc, Arrays.asList(operands.get(1), OPERATOR_Tc)); case "'": super.write(processor, OPERATOR_Tasterisk, Collections.singletonList(OPERATOR_Tasterisk)); /** * Writes a TJ operation with the given array unless array is empty. */ void writeTJ(PdfContentStreamProcessor processor, PdfArray array) throws IOException if (!array.isEmpty()) List operands = Arrays.asList(array, OPERATOR_TJ); super.write(processor, OPERATOR_TJ, operands); /** * Analyzes the given text render info whether it starts a new match or * finishes / continues / breaks a pending match. This method is called * by the @link SimpleTextRemoverListener registered as render listener * of the underlying content stream processor. */ void renderText(TextRenderInfo renderInfo) elementNumber++; int index = 0; for (TextRenderInfo info : renderInfo.getCharacterRenderInfos()) int matchPosition = pendingMatch.size(); pendingMatch.add(new Glyph(info, elementNumber, index)); if (!toRemove.substring(matchPosition, matchPosition + info.getText().length()).equals(info.getText())) reduceToPartialMatch(); if (pendingMatch.size() == toRemove.length()) matches.add(new ArrayList<>(pendingMatch)); allMatches.add(new ArrayList<>(pendingMatch)); pendingMatch.clear(); index++; /** * Reduces the current pending match to an actual (partial) match * after the addition of the next glyph has invalidated it as a * whole match. */ void reduceToPartialMatch() outer: while (!pendingMatch.isEmpty()) pendingMatch.remove(0); int index = 0; for (Glyph glyph : pendingMatch) if (!toRemove.substring(index, index + glyph.text.length()).equals(glyph.text)) continue outer; index++; break; String toRemove = null; final List> cachedOperations = new LinkedList<>(); int elementNumber = -1; int processedElements = 0; final List pendingMatch = new ArrayList<>(); final List> matches = new ArrayList<>(); final List> allMatches = new ArrayList<>(); /** * Render listener class used by @link SimpleTextRemover as listener * of its content stream processor ancestor. Essentially it forwards * @link TextRenderInfo events and ignores all else. */ static class SimpleTextRemoverListener implements RenderListener @Override public void beginTextBlock() @Override public void renderText(TextRenderInfo renderInfo) simpleTextRemover.renderText(renderInfo); @Override public void endTextBlock() @Override public void renderImage(ImageRenderInfo renderInfo) SimpleTextRemover simpleTextRemover = null; /** * Value class representing a glyph with information on * the displayed text and its position, the overall number * of the string argument of a text showing instruction * it is in and the index at which it can be found therein, * and the width to use as text position adjustment when * replacing it. Beware, the width does not yet consider * character and word spacing! */ public static class Glyph public Glyph(TextRenderInfo info, int elementNumber, int index) text = info.getText(); ascent = info.getAscentLine(); base = info.getBaseline(); descent = info.getDescentLine(); this.elementNumber = elementNumber; this.index = index; this.width = info.getFont().getWidth(text); public final String text; public final LineSegment ascent; public final LineSegment base; public final LineSegment descent; final int elementNumber; final int index; final float width; final PdfLiteral OPERATOR_Tasterisk = new PdfLiteral("T*"); final PdfLiteral OPERATOR_Tc = new PdfLiteral("Tc"); final PdfLiteral OPERATOR_Tw = new PdfLiteral("Tw"); final PdfLiteral OPERATOR_Tj = new PdfLiteral("Tj"); final PdfLiteral OPERATOR_TJ = new PdfLiteral("TJ"); final static List TEXT_SHOWING_OPERATORS = Arrays.asList("Tj", "'", """, "TJ"); final static Glyph[] EMPTY_GLYPH_ARRAY = new Glyph[0];

(Clase auxiliar SimpleTextRemover)

Puedes usarlo así:

PdfReader pdfReader = new PdfReader(SOURCE);
PdfStamper pdfStamper = new PdfStamper(pdfReader, RESULT_STREAM);
SimpleTextRemover remover = new SimpleTextRemover();

System.out.printf("ntest.pdf - Testn");
for (int i = 1; i <= pdfReader.getNumberOfPages(); i++)

    System.out.printf("Page %d:n", i);
    List> matches = remover.remove(pdfStamper, i, "Test");
    for (List match : matches) 
        Glyph first = match.get(0);
        Vector baseStart = first.base.getStartPoint();
        Glyph last = match.get(match.size()-1);
        Vector baseEnd = last.base.getEndPoint();
        System.out.printf("  Match from (%3.1f %3.1f) to (%3.1f %3.1f)n", baseStart.get(I1), baseStart.get(I2), baseEnd.get(I1), baseEnd.get(I2));
    


pdfStamper.close();

(Prueba RemovePageTextContent testRemoveTestFromTest)

con la siguiente salida de consola para mi archivo de prueba:

test.pdf - Test
Page 1:
  Match from (134,8 666,9) to (177,8 666,9)
  Match from (134,8 642,0) to (153,4 642,0)
  Match from (172,8 642,0) to (191,4 642,0)

y las apariciones de “Prueba” que faltan en esas posiciones en el PDF de salida.

En lugar de generar las coordenadas de coincidencia, puede usarlas para dibujar texto de reemplazo en la posición en cuestión.

Reseñas y valoraciones

Eres capaz de asentar nuestro análisis añadiendo un comentario y dejando una puntuación te estamos agradecidos.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)


Tags :

Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *