diff --git a/src/ocrmypdf/pdfinfo/__init__.py b/src/ocrmypdf/pdfinfo/__init__.py index 3d733855..bf89465d 100644 --- a/src/ocrmypdf/pdfinfo/__init__.py +++ b/src/ocrmypdf/pdfinfo/__init__.py @@ -25,11 +25,12 @@ from unittest.mock import Mock import re import xml.etree.ElementTree as ET +from pikepdf import PdfMatrix +import pikepdf + from ..exec import ghostscript from ..helpers import fspath -from pikepdf import PdfMatrix -import pikepdf Colorspace = Enum('Colorspace', 'gray rgb cmyk lab icc index sep devn pattern jpeg2000') @@ -418,7 +419,6 @@ def _find_regular_images(container, contentsinfo): that contains images. Generates images with their DPI at time of drawing. - """ for pdfimage, xobj in _image_xobjects(container): diff --git a/src/ocrmypdf/pdfinfo/layout.py b/src/ocrmypdf/pdfinfo/layout.py new file mode 100644 index 00000000..cda03dd7 --- /dev/null +++ b/src/ocrmypdf/pdfinfo/layout.py @@ -0,0 +1,143 @@ +# © 2018 James R. Barlow: github.com/jbarlow83 +# +# This file is part of OCRmyPDF. +# +# OCRmyPDF is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# OCRmyPDF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OCRmyPDF. If not, see . + +import pdfminer.pdfinterp +import pdfminer.pdfdevice + +from pdfminer.pdfpage import PDFPage + +from pdfminer.utils import matrix2str, bbox2str, fsplit +from pdfminer.pdffont import PDFUnicodeNotDefined +from pdfminer.layout import ( + LTChar, LTContainer, LTLayoutContainer, LTPage, LTTextLine, LAParams +) + +from pdfminer.converter import PDFLayoutAnalyzer + +class LTStateAwareChar(LTChar): + """A subclass of LTChar that tracks text render mode at time of drawing""" + + def __init__(self, matrix, font, fontsize, scaling, rise, text, textwidth, textdisp, textstate, *args): + super().__init__(matrix, font, fontsize, scaling, rise, text, textwidth, textdisp, *args) + self.rendermode = textstate.render + + def is_compatible(self, obj): + """We are only compatible with same rendering mode""" + if not hasattr(obj, 'rendermode'): + return False + return self.rendermode == obj.rendermode + + def __repr__(self): + return ('<%s %s matrix=%s rendermode=%r font=%r adv=%s text=%r>' % + (self.__class__.__name__, bbox2str(self.bbox), + matrix2str(self.matrix), self.rendermode, self.fontname, self.adv, + self.get_text())) + + +class LTStateAwarePage(LTPage): + """A page container that exploits character type information""" + + def __init__(self, pageid, bbox, rotate=0): + LTPage.__init__(self, pageid, bbox, rotate) + + def analyze(self, laparams): + """Analysis taking rendering mode into account + + Looks at visible and invisible characters separately. + Depends on some superclass implementation details... + """ + + objs = self._objs[:] + + # Split into invisible text objects and all others + (invisible_textobjs, other_objs) = fsplit( + lambda obj: getattr(obj, 'rendermode', 0) == 3, self) + + # Analyze all invisible text objects and group them into text lines and + # text boxes + self._objs = invisible_textobjs + LTPage.analyze(self, laparams) + invisible_analyzed = self._objs[:] + + # Analyze all other objects + self._objs = other_objs + LTPage.analyze(self, laparams) + other_analyzed = self._objs[:] + + self._objs = invisible_analyzed + other_analyzed + self.visible = other_analyzed + self.invisible = invisible_analyzed + + +class TextPositionTracker(PDFLayoutAnalyzer): + """A page layout analyzer that pays attention to text visibility""" + + def __init__(self, rsrcmgr, pageno=1, laparams=None): + super().__init__(rsrcmgr, pageno, laparams) + self.textstate = None + self.result = None + + def begin_page(self, page, ctm): + super().begin_page(page, ctm) + self.cur_item = LTStateAwarePage(self.pageno, page.mediabox) + + def end_page(self, page): + assert not self._stack, str(len(self._stack)) + assert isinstance(self.cur_item, LTPage), str(type(self.cur_item)) + if self.laparams is not None: + self.cur_item.analyze(self.laparams) + self.pageno += 1 + self.receive_layout(self.cur_item) + + def render_string(self, textstate, seq, *args): + self.textstate = textstate.copy() + super().render_string(self.textstate, seq, *args) + + def render_char(self, matrix, font, fontsize, scaling, rise, cid, *args): + try: + text = font.to_unichr(cid) + assert isinstance(text, str), str(type(text)) + except PDFUnicodeNotDefined: + text = self.handle_undefined_char(font, cid) + textwidth = font.char_width(cid) + textdisp = font.char_disp(cid) + item = LTStateAwareChar( + matrix, font, fontsize, scaling, rise, text, textwidth, textdisp, self.textstate, *args) + self.cur_item.add(item) + return item.adv + + def handle_undefined_char(self, font, cid): + log.info('undefined: %r, %r', font, cid) + return '(cid:%d)' % cid + + def receive_layout(self, ltpage): + self.result = (ltpage.visible, ltpage.invisible) + + def get_result(self): + return self.result + + +def get_textblocks(infile, pageno): + rman = pdfminer.pdfinterp.PDFResourceManager(caching=True) + dev = TextPositionTracker(rman, laparams=LAParams()) + interp = pdfminer.pdfinterp.PDFPageInterpreter(rman, dev) + + page = PDFPage.get_pages(infile, pagenos=[pageno], maxpages=0) + + interp.process_page(next(page)) + + return dev.get_result()