Starbucks
Starbucks

Reputation: 1568

PDFMiner Extraction for Single Words - LTText LTTextBox

I am generating word x,y coordinates with PDFMiner in the below example, however the results are on a line by line basis. How can I split each word from another word, rather splitting groups of words line by line (see example below). I have tried several of the arguments in the PDFMiner tutorial. LTTextBox and LTText were both tried. Moreover, I cannot use beginning and end offsets normally used in text analytics.

This PDF is a good example, this is used in the code below.

http://www.africau.edu/images/default/sample.pdf

from pdfminer.layout import LAParams, LTTextBox, LTText
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFPageInterpreter, PDFResourceManager
from pdfminer.converter import PDFPageAggregator

#Imports Searchable PDFs and prints x,y coordinates
fp = open('C:\sample.pdf', 'rb')
manager = PDFResourceManager()
laparams = LAParams()
dev = PDFPageAggregator(manager, laparams=laparams)
interpreter = PDFPageInterpreter(manager, dev)
pages = PDFPage.get_pages(fp)

for page in pages:
    print('--- Processing ---')
    interpreter.process_page(page)
    layout = dev.get_result()
    for lobj in layout:
        if isinstance(lobj, LTText):
            x, y, text = lobj.bbox[0], lobj.bbox[3], lobj.get_text()
            print('At %r is text: %s' % ((x, y), text))

This returns the x,y coordinates for the searchable PDF as demonstrated below:

--- Processing ---
At (57.375, 747.903) is text: A Simple PDF File
At (69.25, 698.098) is text: This is a small demonstration .pdf file -
At (69.25, 674.194) is text: just for use in the Virtual Mechanics tutorials. More text. And more 
 text. And more text. And more text. And more text.

Wanted result (the coordinates are proxy for demonstration):

--- Processing ---
At (57.375, 747.903) is text: A
At (69.25, 698.098) is text: Simple
At (69.25, 674.194) is text: PDF
At (69.25, 638.338) is text: File

Upvotes: 3

Views: 5500

Answers (2)

SIDDHARTH SAHANI
SIDDHARTH SAHANI

Reputation: 94

An alternate package the readers may want to try is pdfparser that's built on Poppler too (using Cyton bindings) and happens to be more optimised in performance

                           pdfreader      pdfminer  speed-up factor

tiny document (half page)    0.033s         0.121s     3.6 x
small document (5 pages)     0.141s         0.810s     5.7 x
medium document (55 pages)   1.166s        10.524s     9.0 x
large document (436 pages)  10.581s       108.095s    10.2 x

Apart from being faster it's error handling is also better, and resolved a couple of issues where Pdfminer stuggles

import pdfparser.poppler as pdf
import sys

d=pdf.Document(sys.argv[1])

print('No of pages', d.no_of_pages)
for p in d:
    print('Page', p.page_no, 'size =', p.size)
    for f in p:
        print(' '*1,'Flow')
        for b in f:
            print(' '*2,'Block', 'bbox=', b.bbox.as_tuple())
            for l in b:
                print(' '*3, l.text.encode('UTF-8'), '(%0.2f, %0.2f, %0.2f, %0.2f)'% l.bbox.as_tuple())
                #assert l.char_fonts.comp_ratio < 1.0
                for i in range(len(l.text)):
                    print(l.text[i].encode('UTF-8'), '(%0.2f, %0.2f, %0.2f, %0.2f)'% l.char_bboxes[i].as_tuple(),\
                        l.char_fonts[i].name, l.char_fonts[i].size, l.char_fonts[i].color,)
                print()

As you can clearly see the source code is the shortest, yet still provides all necessary data, including font colour, font-size, font-family.

More importantly, you get the words straight away in the blocks 9just a level above the characters). Avoids the space checking logic, one has to use with Pdfminer.

Upvotes: 1

miwin
miwin

Reputation: 1215

With PDFMiner, after going through each line (as you already did), you may only go through each character in the line.

I did this with the code below, while trying to record the x, y of the first character per word and setting up a condition to split the words at each LTAnno (e.g. \n ) or .get_text() == ' ' empty space.

from pdfminer.layout import LAParams, LTTextBox, LTText, LTChar, LTAnno
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFPageInterpreter, PDFResourceManager
from pdfminer.converter import PDFPageAggregator

#Imports Searchable PDFs and prints x,y coordinates
fp = open('C:\sample.pdf', 'rb')
manager = PDFResourceManager()
laparams = LAParams()
dev = PDFPageAggregator(manager, laparams=laparams)
interpreter = PDFPageInterpreter(manager, dev)
pages = PDFPage.get_pages(fp)

for page in pages:
    print('--- Processing ---')
    interpreter.process_page(page)
    layout = dev.get_result()
    x, y, text = -1, -1, ''
    for textbox in layout:
        if isinstance(textbox, LTText):
          for line in textbox:
            for char in line:
              # If the char is a line-break or an empty space, the word is complete
              if isinstance(char, LTAnno) or char.get_text() == ' ':
                if x != -1:
                  print('At %r is text: %s' % ((x, y), text))
                x, y, text = -1, -1, ''     
              elif isinstance(char, LTChar):
                text += char.get_text()
                if x == -1:
                  x, y, = char.bbox[0], char.bbox[3]    
    # If the last symbol in the PDF was neither an empty space nor a LTAnno, print the word here
    if x != -1:
      print('At %r is text: %s' % ((x, y), text))

The output looks as follows

At (64.881, 747.903) is text: A
At (90.396, 747.903) is text: Simple
At (180.414, 747.903) is text: PDF
At (241.92, 747.903) is text: File

Perhaps you can optimize the conditions to detect the words for your requirements and liking. (e.g. cut punctuation marks .!? at the end of words)

Upvotes: 4

Related Questions