Coordenadas de texto al eliminar de PDFBox

7 minutos de lectura

avatar de usuario
Samuel Diella

Estoy tratando de extraer texto con coordenadas de un archivo pdf usando PDFBox.

Mezclé algunos métodos/información que se encuentra en Internet (también stackoverflow), pero el problema que tengo con las coordenadas no parece ser correcto. Cuando trato de usar coordenadas para dibujar un rectángulo encima de tex, por ejemplo, el rect se pinta en otro lugar.

Este es mi código (no juzgues el estilo, fue escrito muy rápido solo para probar)

TextLine.java

    import java.util.List;
    import org.apache.pdfbox.text.TextPosition;

    /**
     *
     * @author samue
     */
    public class TextLine {
        public List<TextPosition> textPositions = null;
        public String text = "";
    }

myStripper.java

    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    import org.apache.pdfbox.pdmodel.PDDocument;
    import org.apache.pdfbox.pdmodel.PDPage;
    import org.apache.pdfbox.text.PDFTextStripper;
    import org.apache.pdfbox.text.TextPosition;

    /*
     * To change this license header, choose License Headers in Project Properties.
     * To change this template file, choose Tools | Templates
     * and open the template in the editor.
     */

    /**
     *
     * @author samue
     */
    public class myStripper extends PDFTextStripper {
        public myStripper() throws IOException
        {
        }

        @Override
        protected void startPage(PDPage page) throws IOException
        {
            startOfLine = true;
            super.startPage(page);
        }

        @Override
        protected void writeLineSeparator() throws IOException
        {
            startOfLine = true;
            super.writeLineSeparator();
        }

        @Override
        public String getText(PDDocument doc) throws IOException
        {
            lines = new ArrayList<TextLine>();
            return super.getText(doc);
        }

        @Override
        protected void writeWordSeparator() throws IOException
        {
            TextLine tmpline = null;

            tmpline = lines.get(lines.size() - 1);
            tmpline.text += getWordSeparator();

            super.writeWordSeparator();
        }


        @Override
        protected void writeString(String text, List<TextPosition> textPositions) throws IOException
        {
            TextLine tmpline = null;

            if (startOfLine) {
                tmpline = new TextLine();
                tmpline.text = text;
                tmpline.textPositions = textPositions;
                lines.add(tmpline);
            } else {
                tmpline = lines.get(lines.size() - 1);
                tmpline.text += text;
                tmpline.textPositions.addAll(textPositions);
            }

            if (startOfLine)
            {
                startOfLine = false;
            }
            super.writeString(text, textPositions);
        }

        boolean startOfLine = true;
        public ArrayList<TextLine> lines = null;

    }

haga clic en evento en el botón AWT

 private void jButton1MouseClicked(java.awt.event.MouseEvent evt) {                                      
    // TODO add your handling code here:
    try {
        File file = new File("C:\\Users\\samue\\Desktop\\mwb_I_201711.pdf");
        PDDocument doc = PDDocument.load(file);

        myStripper stripper = new myStripper();

        stripper.setStartPage(1); // fix it to first page just to test it
        stripper.setEndPage(1);
        stripper.getText(doc);

        TextLine line = stripper.lines.get(1); // the line i want to paint on

        float minx = -1;
        float maxx = -1;

        for (TextPosition pos: line.textPositions)
        {
            if (pos == null)
                continue;

            if (minx == -1 || pos.getTextMatrix().getTranslateX() < minx) {
                minx = pos.getTextMatrix().getTranslateX();
            }
            if (maxx == -1 || pos.getTextMatrix().getTranslateX() > maxx) {
                maxx = pos.getTextMatrix().getTranslateX();
            }
        }

        TextPosition firstPosition = line.textPositions.get(0);
        TextPosition lastPosition = line.textPositions.get(line.textPositions.size() - 1);

        float x = minx;
        float y = firstPosition.getTextMatrix().getTranslateY();
        float w = (maxx - minx) + lastPosition.getWidth();
        float h = lastPosition.getHeightDir();

        PDPageContentStream contentStream = new PDPageContentStream(doc, doc.getPage(0), PDPageContentStream.AppendMode.APPEND, false);

        contentStream.setNonStrokingColor(Color.RED);
        contentStream.addRect(x, y, w, h);
        contentStream.fill();
        contentStream.close();

        File fileout = new File("C:\\Users\\samue\\Desktop\\pdfbox.pdf");
        doc.save(fileout);
        doc.close();
    } catch (Exception ex) {

    }
}                                     

¿cualquier sugerencia? ¿Qué estoy haciendo mal?

  • No he entendido tu código (debo irme a la cama ahora). Tenga en cuenta que en PDF, y = 0 es inferior, no superior. Aquí hay un ejemplo que puede ayudar a entender cómo usar las coordenadas de extracción de texto: svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/…

    –Tilman Hausherr

    6 sep 2017 a las 20:17

  • ¿Has probado a usar el PDPageContentStream constructor con otro argumento booleano resetContext y poniéndolo en true?

    – mkl

    06/09/2017 a las 23:32

  • sí, sé que 0 es la parte inferior, esta es la razón por la que usé .getTextMatrix().getTranslateY() en lugar de getY() o getYDirAdj(). He intentado usar resetContext pero sin ayuda. ahora voy a ver ese código fuente, lo actualizaré, gracias

    – Samuel Diella

    7 sep 2017 a las 14:07


  • Acabo de probar su código y funcionó correctamente en un PDF de muestra (bueno, solo cubrió el texto desde la línea de base hacia arriba, pero eso es de esperar). Por lo tanto, hay algo diferente en los archivos PDF en los que los rectángulos están pintado en otro lugar. Supongo que eso se arreglaría si usaras el PDPageContentStream Señalé hacia en mi comentario anterior. Sin embargo, no puedo estar seguro sin un PDF de muestra en el que haya observado el problema. Por lo tanto, comparta un PDF de muestra.

    – mkl

    7 sep 2017 a las 14:50


  • Probé el código en tu enlace. Está funcionando, pero está pintando formas en un PNG, no rectángulos en la transmisión. Estoy tratando de convertir formas en rectángulos, pero tengo algunas dificultades ^^” de todos modos, el PDF es este: descargar-a.akamaihd.net/files/media_mwb/b7/mwb_I_201711.pdf supongo que es algo sobre fuentes o transformación de fuentes

    – Samuel Diella

    7 sep 2017 a las 23:07


Este es solo otro caso del exceso PdfTextStripper normalización de coordenadas. Al igual que tú, había pensado que al usar TextPosition.getTextMatrix() (en lugar de getX() y getY) uno obtendría las coordenadas reales, pero no, incluso estos valores de matriz deben corregirse (al menos en PDFBox 2.0.x, no he verificado 1.8.x) porque la matriz se multiplica por una traducción que hace la esquina inferior izquierda del cuadro de cultivo el origen.

Por lo tanto, en su caso (en el que la parte inferior izquierda del cuadro de recorte no es el origen), debe corregir los valores, por ejemplo, reemplazando

        float x = minx;
        float y = firstPosition.getTextMatrix().getTranslateY();

por

        PDRectangle cropBox = doc.getPage(0).getCropBox();

        float x = minx + cropBox.getLowerLeftX();
        float y = firstPosition.getTextMatrix().getTranslateY() + cropBox.getLowerLeftY();

En lugar de

sin corrección

ahora obtienes

con corrección x,y

Obviamente, sin embargo, también tendrá que corregir un poco la altura. Esto se debe a la forma en que PdfTextStripper determina la altura del texto:

    // 1/2 the bbox is used as the height todo: why?
    float glyphHeight = bbox.getHeight() / 2;

(desde showGlyph(...) en LegacyPDFStreamEnginela clase padre de PdfTextStripper)

Si bien el cuadro delimitador de la fuente suele ser demasiado grande, la mitad de él a menudo no es suficiente.

  • ¡Gracias! Esta es la solución al problema, de verdad gracias! De todos modos, estoy tratando de hacer algo más basado en glifos individuales, cuando termine lo publicaré. ¡Gracias de nuevo!

    – Samuel Diella

    8 sep 2017 a las 11:42


  • Si alguna respuesta “es la solución al problema”acéptelo (haga clic en la marca en la esquina superior izquierda).

    – mkl

    8 sep 2017 a las 11:51

  • lo siento, no me di cuenta 😛

    – Samuel Diella

    8 sep 2017 a las 12:15

  • ¿@SamueleDiella encontró una solución para eso? Estoy enfrentando el mismo problema. Por favor, ayúdame si tienes algo al respecto.

    – Mukesh Methaniya

    26 de marzo de 2018 a las 6:12

  • @MukeshMethaniya Como puede leer en el comentario de Samuele, la resolución fue usar las adaptaciones presentadas en mi respuesta.

    – mkl

    26 de marzo de 2018 a las 8:27

El siguiente código funcionó para mí:

    // Definition of font baseline, ascent, descent: https://en.wikipedia.org/wiki/Ascender_(typography)
    //
    // The origin of the text coordinate system is the top-left corner where Y increases downward.
    // TextPosition.getX(), getY() return the baseline.
    TextPosition firstLetter = textPositions.get(0);
    TextPosition lastLetter = textPositions.get(textPositions.size() - 1);

    // Looking at LegacyPDFStreamEngine.showGlyph(), ascender and descender heights are calculated like
    // CapHeight: https://stackoverflow.com/a/42021225/14731
    float ascent = firstLetter.getFont().getFontDescriptor().getAscent() / 1000 * lastLetter.getFontSize();
    Point topLeft = new Point(firstLetter.getX(), firstLetter.getY() - ascent);

    float descent = lastLetter.getFont().getFontDescriptor().getDescent() / 1000 * lastLetter.getFontSize();
    // Descent is negative, so we need to negate it to move downward.
    Point bottomRight = new Point(lastLetter.getX() + lastLetter.getWidth(),
        lastLetter.getY() - descent);

    float descender = lastLetter.getFont().getFontDescriptor().getDescent() / 1000 * lastLetter.getFontSize();
    // Descender height is negative, so we need to negate it to move downward
    Point bottomRight = new Point(lastLetter.getX() + lastLetter.getWidth(),
        lastLetter.getY() - descender);

En otras palabras, estamos creando un cuadro delimitador desde el ascendente de la fuente hasta su descendente.

Si desea representar estas coordenadas con el origen en la esquina inferior izquierda, consulte https://stackoverflow.com/a/28114320/14731 para obtener más detalles. Deberá aplicar una transformación como esta:

contents.transform(new Matrix(1, 0, 0, -1, 0, page.getHeight()));

  • Lamentablemente, el ascenso / descenso, etc., no siempre son confiables. Si necesita límites exactos, vea los rectángulos cian en el DrawPrintTextLocations.java ejemplo.

    –Tilman Hausherr

    19 de julio de 2019 a las 10:12

  • @TilmanHausherr Ese es un código excelente. ¿Por qué esta funcionalidad no está integrada directamente en la API de PDFBox? Es decir, ¿por qué se encuentra en el código de ejemplo en lugar de ser un método en (digamos) TextPosition?

    – gili

    19 de julio de 2019 a las 16:20

  • Estaba pensando en agregarlo en el código LegacyPDFStreamEngine. Sin embargo, muchos resultados de extracción de texto están cambiando. Y tampoco es perfecto, no funcionará para las fuentes type3. Y por último: tantas cosas que hacer… como responder a las preguntas de los usuarios, corregir errores, etc.

    –Tilman Hausherr

    19 de julio de 2019 a las 18:10

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad