Using PDFbox to determine the coordinates of words in a document

Viewed 50739

I'm using PDFbox to extract the coordinates of words/strings in a PDF document, and have so far had success determining the position of individual characters. this is the code thus far, from the PDFbox doc:

package printtextlocations;

import java.io.*;
import org.apache.pdfbox.exceptions.InvalidPasswordException;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.util.PDFTextStripper;
import org.apache.pdfbox.util.TextPosition;

import java.io.IOException;
import java.util.List;

public class PrintTextLocations extends PDFTextStripper {

    public PrintTextLocations() throws IOException {
        super.setSortByPosition(true);
    }

    public static void main(String[] args) throws Exception {

        PDDocument document = null;
        try {
            File input = new File("C:\\path\\to\\PDF.pdf");
            document = PDDocument.load(input);
            if (document.isEncrypted()) {
                try {
                    document.decrypt("");
                } catch (InvalidPasswordException e) {
                    System.err.println("Error: Document is encrypted with a password.");
                    System.exit(1);
                }
            }
            PrintTextLocations printer = new PrintTextLocations();
            List allPages = document.getDocumentCatalog().getAllPages();
            for (int i = 0; i < allPages.size(); i++) {
                PDPage page = (PDPage) allPages.get(i);
                System.out.println("Processing page: " + i);
                PDStream contents = page.getContents();
                if (contents != null) {
                    printer.processStream(page, page.findResources(), page.getContents().getStream());
                }
            }
        } finally {
            if (document != null) {
                document.close();
            }
        }
    }

    /**
     * @param text The text to be processed
     */
    @Override /* this is questionable, not sure if needed... */
    protected void processTextPosition(TextPosition text) {
        System.out.println("String[" + text.getXDirAdj() + ","
                + text.getYDirAdj() + " fs=" + text.getFontSize() + " xscale="
                + text.getXScale() + " height=" + text.getHeightDir() + " space="
                + text.getWidthOfSpace() + " width="
                + text.getWidthDirAdj() + "]" + text.getCharacter());
    }
}

This produces a series of lines containing the position of each character, including spaces, that looks like this:

String[202.5604,41.880127 fs=1.0 xscale=13.98 height=9.68814 space=3.8864403 width=9.324661]P

Where 'P' is the character. I have not been able to find a function in PDFbox to find words, and I am not familiar enough with Java to be able to accurately concatenate these characters back into words to search through even though the spaces are also included. Has anyone else been in a similar situation, and if so how did you approach it? I really only need the coordinate of the first character in the word so that parts simplified, but as to how I'm going to match a string against that kind of output is beyond me.

5 Answers

For those who still need assistance, this is what I used in my code and should be useful to start with. It uses PDFBox 2.0.16

public class PDFTextLocator extends PDFTextStripper {
    
    private static String key_string;
    private static float x;
    private static float y;
    
    public PDFTextLocator() throws IOException {
        x = -1;
        y = -1;
    }
    
    /**
     * Takes in a PDF Document, phrase to find, and page to search and returns the x,y in float array
     * @param document
     * @param phrase
     * @param page
     * @return
     * @throws IOException
     */
    public static float[] getCoordiantes(PDDocument document, String phrase, int page) throws IOException {
        key_string = phrase;
        PDFTextStripper stripper = new PDFTextLocator();
        stripper.setSortByPosition(true);
        stripper.setStartPage(page);
        stripper.setEndPage(page);
        stripper.writeText(document, new OutputStreamWriter(new ByteArrayOutputStream()));
        y = document.getPage(page).getMediaBox().getHeight()-y;
        
        return new float[]{x,y};
    }
    
    /**
     * Override the default functionality of PDFTextStripper.writeString()
     */
    @Override
    protected void writeString(String string, List<TextPosition> textPositions) throws IOException {
        if(string.contains(key_string)) {
            TextPosition text = textPositions.get(0);
            if(x == -1) {
                x = text.getXDirAdj();
                y = text.getYDirAdj();
            }        
        }
    }
}

Below is the Maven dependency details,

<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>2.0.16</version>
</dependency>

I got this working using the IKVM conversion PDFBox.NET 1.8.9. in C# and .NET.

I finally figured out the character (glyph) coordinates are private to the .NET assembly, but can be accessed using System.Reflection.

I posted a full example of getting the coordinates of WORDS and drawing them back on images of PDF's using SVG and HTML here: https://github.com/tsamop/PDF_Interpreter

For the example below you need PDFbox.NET: http://www.squarepdf.net/pdfbox-in-net, and include references to it in your project.

It took me quite a while to figure it out, so I really hope it saves someone else time!!

If you just need to know where to look for the characters & coordinates, a very abridged version would be:

    using System;
using System.Reflection;

using org.apache.pdfbox.pdmodel;
using org.apache.pdfbox.util;

  // to test run pdfTest.RunTest(@"C:\temp\test_2.pdf");

class pdfTest
{
    //simple example for getting character (gliph) coordinates out of a pdf doc.
    // a more complete example is here:   https://github.com/tsamop/PDF_Interpreter
    public static void RunTest(string sFilename)
    {
    //probably a better way to get page count, but I cut this out of a bigger project.
    PDDocument oDoc = PDDocument.load(sFilename);
        object[] oPages = oDoc.getDocumentCatalog().getAllPages().toArray();

        int iPageNo = 0; //1's based!!
        foreach (object oPage in oPages)
        {
            iPageNo++;

            //feed the stripper a page.
            PDFTextStripper tStripper = new PDFTextStripper();
            tStripper.setStartPage(iPageNo);
            tStripper.setEndPage(iPageNo);
            tStripper.getText(oDoc);

            //This gets the "charactersByArticle" private object in PDF Box.
            FieldInfo charactersByArticleInfo = typeof(PDFTextStripper).GetField("charactersByArticle", BIndingFlags.NonPublic | BindingFlags.Instance);
            object charactersByArticle = charactersByArticleInfo.GetValue(tStripper);

            object[] aoArticles = (object[])charactersByArticle.GetField("elementData");

            foreach (object oArticle in aoArticles)
            {
                if (oArticle != null)
                {

                    //THE CHARACTERS within the article
                    object[] aoCharacters = (object[])oArticle.GetField("elementData");

                    foreach (object oChar in aoCharacters)
                    {

                        /*properties I caulght using reflection:
                         * endX, endY, font, fontSize, fontSizePt, maxTextHeight, pageHeight, pageWidth, rot, str textPos, unicodCP, widthOfSpace, widths, wordSpacing, x, y
                         * 
                         */
                        if (oChar != null)
                        {
                            //this is a really quick test.  
                            // for a more complete solution that pulls the characters into words and displays the word positions on the page, try this:  https://github.com/tsamop/PDF_Interpreter
                            //the Y's appear to be the bottom of the char?
                            double mfMaxTextHeight = Convert.ToDouble(oChar.GetField("maxTextHeight")); //I think this is the height of the character/word

                            char mcThisChar = oChar.GetField("str").ToString().ToCharArray()[0];
                            double mfX = Convert.ToDouble(oChar.GetField("x"));
                            double mfY = Convert.ToDouble(oChar.GetField("y")) - mfMaxTextHeight;

                            //CALCULATE THE OTHER SIDE OF THE GLIPH
                            double mfWidth0 = ((Single[])oChar.GetField("widths"))[0];
                            double mfXend = mfX + mfWidth0; // Convert.ToDouble(oChar.GetField("endX"));

                            //CALCULATE THE BOTTOM OF THE GLIPH.
                            double mfYend = mfY + mfMaxTextHeight; //  Convert.ToDouble(oChar.GetField("endY"));

                            double mfPageHeight = Convert.ToDouble(oChar.GetField("pageHeight"));
                            double mfPageWidth = Convert.ToDouble(oChar.GetField("pageWidth"));

                            System.Diagnostics.Debug.Print(@"add some stuff to test {0}, {1}, {2}", mcThisChar, mfX, mfY);

                        }
                    }

                }
            }

        }
    }
}



using System.Reflection;

/// <summary>
/// To deal with the Java interface hiding necessary properties! ~mwr
/// </summary>
public static class GetField_Extension
{
    public static object GetField(this object randomPDFboxObject, string sFieldName)
    {
            FieldInfo itemInfo = randomPDFboxObject.GetType().GetField(sFieldName, BindingFlags.NonPublic | BindingFlags.Instance);
            return itemInfo.GetValue(randomPDFboxObject);
    }

}
Related