Compare commits

...
9 Commits
Author SHA1 Message Date
James R. Barlow 7b94129d9e v4.5 notes 2017-02-14 13:03:48 -08:00
James R. Barlow d1a0065ef8 Create test case for Form XObjects 2017-02-14 12:51:15 -08:00
James R. Barlow 5a817370fd Warn more strongly about —pdf-renderer tesseract until fix is widely propagated 2017-02-14 11:33:07 -08:00
James R. Barlow ab0a210763 Update dockerfile.tess4 yet again
Installing Tess4 PPA over Tess3 proved too much pain, so sever the link
between this and the jbarlow83/ocrmypdf image, starting each from
scratch. Also the complete set of language packs proves too much - the
build seems likely to fail when trying to install so many.
2017-02-14 11:32:05 -08:00
James R. Barlow 9f800736bc Fix running_in_docker() check failing on newer Docker
This test has to work to ensure spoof/tesseract_cache.py has a writable
directory to put cache into. Otherwise those tests fail.
2017-02-13 02:16:06 -08:00
James R. Barlow c9a83afad6 Improve batch processing examples 2017-02-13 02:14:32 -08:00
James R. Barlow 5e14274f10 pageinfo: learn to extract image information from Form XObjects 2017-02-11 16:48:59 -08:00
James R. Barlow 167470b4bd Re-fix Dockerfile.tess4
[ci skip]
2017-02-10 15:43:49 -08:00
James R. Barlow f06d3c2ec2 Fix tesseract 3.04 on tesseract 4 on image
[skip ci]
2017-02-10 08:38:22 -08:00
11 changed files with 237 additions and 58 deletions
+49 -6
View File
@@ -1,20 +1,63 @@
# OCRmyPDF
#
# VERSION 4.4.2
FROM jbarlow83/ocrmypdf:latest
FROM ubuntu:16.10
MAINTAINER James R. Barlow <jim@purplerock.ca>
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common python-software-properties \
python3-wheel \
python3-reportlab \
python3-venv \
ghostscript \
qpdf \
poppler-utils \
unpaper \
libffi-dev
RUN add-apt-repository ppa:alex-p/tesseract-ocr
RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr-all
RUN apt-get update \
&& apt-get autoremove -y \
&& apt-get install -y --no-install-recommends \
tesseract-ocr \
tesseract-ocr-eng \
tesseract-ocr-fra \
tesseract-ocr-deu \
tesseract-ocr-spa \
tesseract-ocr-por \
tesseract-ocr-ara \
tesseract-ocr-rus \
tesseract-ocr-chi-sim
RUN apt-get autoremove -y && apt-get clean -y
RUN python3 -m venv --system-site-packages /appenv
# This installs the latest binary wheel instead of the code in the current
# folder. Installing from source will fail, apparently because cffi needs
# build-essentials (gcc) to do a source installation
# (i.e. "pip install ."). It's unclear to me why this is the case.
RUN . /appenv/bin/activate; \
pip install --upgrade pip \
&& pip install ocrmypdf
# Now copy the application in, mainly to get the test suite.
# Do this now to make the best use of Docker cache.
COPY . /application
RUN . /appenv/bin/activate; \
pip install -r /application/test_requirements.txt
# Remove the junk, including the source version of application since it was
# already installed
RUN rm -rf /tmp/* /var/tmp/* /root/* /application/ocrmypdf \
&& apt-get autoremove -y \
&& apt-get autoclean -y
RUN useradd docker \
&& mkdir /home/docker \
&& chown docker:docker /home/docker
USER docker
WORKDIR /home/docker
# Must use array form of ENTRYPOINT
# Non-array form does not append other arguments, because that is "intuitive"
+8
View File
@@ -4,6 +4,14 @@ RELEASE NOTES
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
v4.5
====
- Exotic PDFs containing "Form XObjects" are now supported (issue #134; PDF reference manual 8.10), and images they contain are taken into account when determining the resolution for rasterizing
- The Tesseract 4 Docker image no longer includes all languages, because it took so long to build something would tend to fail
- OCRmyPDF now warns about using ``--pdf-renderer tesseract`` with Tesseract 3.04 or lower due to issues with Ghostscript corrupting the OCR text in these cases
v4.4.2
======
+20 -2
View File
@@ -10,11 +10,29 @@ Consider using the excellent `GNU Parallel <https://www.gnu.org/software/paralle
Both ``parallel`` and ``ocrmypdf`` will try to use all available processors. To maximize parallelism without overloading your system with processes, consider using ``parallel -j 2`` to limit parallel to running two jobs at once.
This command will run all ocrmypdf all files named ``*.pdf`` in the current directory and write them to the previous created ``output/`` folder.
This command will run all ocrmypdf all files named ``*.pdf`` in the current directory and write them to the previous created ``output/`` folder. It will not search subdirectories.
The ``--tag`` argument tells parallel to print the filename as a prefix whenever a message is printed, so that one can trace any errors to the file that produced them.
.. code-block:: bash
parallel -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
parallel --tag -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
Directory trees
---------------
This will walk through a directory tree and run OCR on all files in place, printing the output in a way that makes
.. code-block:: bash
find . --printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
This only runs one ``ocrmypdf`` process at a time. This variation uses ``find`` to create a directory list and ``parallel`` to parallelize runs of ``ocrmypdf``, again updating files in place.
.. code-block:: bash
find . -name '*.pdf' | parallel --tag -j 2 ocrmypdf '{}' '{}'
Sample script
"""""""""""""
+2 -1
View File
@@ -46,7 +46,8 @@ Assuming you have a Docker engine running, you can download one of the three ava
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
| ocrmypdf-polyglot | ``docker pull jbarlow83/ocrmypdf-polyglot`` | As above, with all available language packs. |
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
| ocrmypdf-tess4 | ``docker pull jbarlow83/ocrmypdf-tess4`` | Latest ocrmypdf with Tesseract 4.00.00alpha and all language packs. |
| ocrmypdf-tess4 | ``docker pull jbarlow83/ocrmypdf-tess4`` | Latest ocrmypdf with Tesseract 4.00.00alpha and English, French, German, |
| | | Spanish, Portuguese, Chinese Simplified, Arabic and Russian (the top 8). |
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
For example:
+9 -10
View File
@@ -272,13 +272,15 @@ def check_options_output(options, log):
if options.pdf_renderer == 'auto':
options.pdf_renderer = 'hocr'
if options.pdf_renderer == 'tesseract' and \
tesseract.version() < '3.04.01' and \
os.environ.get('OCRMYPDF_SHARP_TTF', '') != '1':
log.warning(
"Your version of tesseract has problems with PDF output."
" Some PDF viewers will fail to find searchable text.\n"
"--pdf-renderer=tesseract is not recommended.")
if options.pdf_renderer in ('tesseract', 'tess4'):
if tesseract.version() < '3.05':
log.warning(
"tesseract < 3.05 may corrupt PDF output. "
"--pdf-renderer=tesseract is not recommend.")
elif tesseract.version() == '4.00.00alpha':
log.warning(
"tesseract 4.00.00alpha may corrupt PDF output. "
"--pdf-renderer={tesseract,tess4} is not recommend.")
if options.debug_rendering and options.pdf_renderer == 'tesseract':
log.info(
@@ -354,9 +356,6 @@ def check_options(options, log):
sys.exit(ExitCode.missing_dependency)
# ----------
# Logging
+130 -37
View File
@@ -89,13 +89,13 @@ def _is_unit_square(shorthand):
pairwise = zip(values, UNIT_SQUARE)
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
RasterSettings = namedtuple('RasterSettings',
XobjectSettings = namedtuple('XobjectSettings',
['name', 'shorthand', 'stack_depth'])
InlineSettings = namedtuple('InlineSettings',
['settings', 'shorthand', 'stack_depth'])
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
ContentsInfo = namedtuple('ContentsInfo', ['xobject_settings', 'inline_images'])
def _normalize_stack(operations):
@@ -114,7 +114,7 @@ def _normalize_stack(operations):
yield (operands, command)
def _interpret_contents(contentstream):
def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
"""Interpret the PDF content stream
The stack represents the state of the PDF graphics stack. We are only
@@ -139,8 +139,8 @@ def _interpret_contents(contentstream):
operations = contentstream.operations
stack = []
ctm = _matrix_from_shorthand(UNIT_SQUARE)
image_raster_settings = []
ctm = _matrix_from_shorthand(initial_shorthand)
xobject_settings = []
inline_images = []
for n, op in enumerate(_normalize_stack(operations)):
@@ -161,10 +161,10 @@ def _interpret_contents(contentstream):
_matrix_from_shorthand(operands), ctm)
elif command == b'Do':
image_name = operands[0]
raster = RasterSettings(
settings = XobjectSettings(
name=image_name, shorthand=_shorthand_from_matrix(ctm),
stack_depth=len(stack))
image_raster_settings.append(raster)
xobject_settings.append(settings)
elif command == b'INLINE IMAGE':
settings = operands['settings']
inline = InlineSettings(
@@ -173,7 +173,7 @@ def _interpret_contents(contentstream):
inline_images.append(inline)
return ContentsInfo(
raster_settings=image_raster_settings,
xobject_settings=xobject_settings,
inline_images=inline_images)
@@ -241,8 +241,8 @@ def _get_dpi(ctm_shorthand, image_size):
return (dpi_w, dpi_h)
def _find_page_inline_images(page, pageinfo, contentsinfo):
"Find inline images on the page"
def _find_inline_images(contentsinfo):
"Find inline images in the contentstream"
for n, inline in enumerate(contentsinfo.inline_images):
image = {}
@@ -272,20 +272,44 @@ def _find_page_inline_images(page, pageinfo, contentsinfo):
yield image
def _find_page_regular_images(page, pageinfo, contentsinfo):
"Find images stored in XObject resources"
def _image_xobjects(container):
"""Search for all XObject-based images in the container
try:
page['/Resources']['/XObject']
except KeyError:
Usually the container is a page, but it could also be a Form XObject
that contains images. Filter out the Form XObjects which are dealt with
elsewhere.
Generate a sequence of tuples (image, xobj container), where container,
where xobj is the name of the object and image is the object itself,
since the object does not know its own name.
"""
if '/Resources' not in container:
return
for xobj in page['/Resources']['/XObject']:
# PyPDF2 returns the keys as an iterator
pdfimage = page['/Resources']['/XObject'][xobj]
if pdfimage['/Subtype'] != '/Image':
continue
resources = container['/Resources']
if '/XObject' not in resources:
return
for xobj in resources['/XObject']:
candidate = resources['/XObject'][xobj]
if candidate['/Subtype'] == '/Image':
image = candidate
yield (image, xobj)
def _find_regular_images(container, contentsinfo):
"""Find images stored in the container's /Resources /XObject
Usually the container is a page, but it could also be a Form XObject
that contains images.
Generates images with their DPI at time of drawing.
"""
for pdfimage, xobj in _image_xobjects(container):
image = {}
image['name'] = str(xobj)
image['name'] = xobj
image['width'] = pdfimage['/Width']
image['height'] = pdfimage['/Height']
if '/BitsPerComponent' in pdfimage:
@@ -329,12 +353,12 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
image['dpi_w'] = image['dpi_h'] = 0
for raster in contentsinfo.raster_settings:
for xobj in contentsinfo.xobject_settings:
# Loop in case the same image is display multiple times on a page
if raster.name != image['name']:
if xobj.name != image['name']:
continue
if raster.stack_depth == 0 and _is_unit_square(raster.shorthand):
if xobj.stack_depth == 0 and _is_unit_square(xobj.shorthand):
# At least one PDF in the wild (and test suite) draws an image
# when the graphics stack depth is 0, meaning that the image
# gets drawn into a square of 1x1 PDF units (or 1/72",
@@ -343,7 +367,7 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
continue
dpi_w, dpi_h = _get_dpi(
raster.shorthand, (image['width'], image['height']))
xobj.shorthand, (image['width'], image['height']))
# When image is used multiple times take the highest DPI it is
# rendered at
@@ -358,9 +382,85 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
yield image
def _find_page_images(page, pageinfo, contentsinfo):
yield from _find_page_inline_images(page, pageinfo, contentsinfo)
yield from _find_page_regular_images(page, pageinfo, contentsinfo)
def _find_form_xobject_images(pdf, container, contentsinfo):
"""Find any images that are in Form XObjects in the container
The container may be a page, or a parent Form XObject.
"""
if '/Resources' not in container:
return
resources = container['/Resources']
if '/XObject' not in resources:
return
for xobj in resources['/XObject']:
candidate = resources['/XObject'][xobj]
if candidate['/Subtype'] != '/Form':
continue
form_xobject = candidate
for settings in contentsinfo.xobject_settings:
if settings.name != xobj:
continue
# Find images once for each time this Form XObject is drawn.
# This could be optimized to cache the multiple drawing events
# but in practice both Form XObjects and multiple drawing of the
# same object are both very rare.
ctm_shorthand = settings.shorthand
yield from _find_images(pdf, form_xobject, ctm_shorthand)
def _find_images(pdf, container, shorthand=None):
"""Find all individual instances of images drawn in the container
Usually the container is a page, but it may also be a Form XObject.
On a typical page images are stored inline or as regular images
in an XObject.
Form XObjects may include inline images, XObject images,
and recursively, other Form XObjects; and also vector drawing commands.
Every instance of an image being drawn somewhere is flattened and
treated as a unique image, since if the same image is drawn multiple times
on one page it may be drawn at differing resolutions, and our objective
is to find the resolution at which the page can be rastered without
downsampling.
"""
if container.get('/Type') == '/Page':
# For a /Page the content stream is attached to the page's /Contents
page = container
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
initial_shorthand = shorthand or UNIT_SQUARE
elif container.get('/Type') == '/XObject' and \
container['/Subtype'] == '/Form':
# For a Form XObject that content stream is attached to the XObject
contentstream = pypdf.pdf.ContentStream(container, pdf)
# Set the CTM to the state it was when the "Do" operator was
# encountered that is drawing this instance of the Form XObject
ctm = _matrix_from_shorthand(shorthand or UNIT_SQUARE)
# A Form XObject may provide its own matrix to map form space into
# user space. Get this if one exists
form_matrix = _matrix_from_shorthand(
container.get('/Matrix', UNIT_SQUARE))
# Concatenate form matrix with CTM to ensure CTM is correct for
# drawing this instance of the XObject
ctm = matrix_mult(form_matrix, ctm)
initial_shorthand = _shorthand_from_matrix(ctm)
else:
return
contentsinfo = _interpret_contents(contentstream, initial_shorthand)
yield from _find_inline_images(contentsinfo)
yield from _find_regular_images(container, contentsinfo)
yield from _find_form_xobject_images(pdf, container, contentsinfo)
def _page_has_text(pdf, page):
@@ -405,15 +505,8 @@ def _pdf_get_pageinfo(infile, pageno: int):
except KeyError:
pageinfo['rotate'] = 0
try:
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
except AttributeError as e:
return pageinfo
contentsinfo = _interpret_contents(contentstream)
pageinfo['images'] = [im for im in _find_page_images(
page, pageinfo, contentsinfo)]
pageinfo['images'] = [im for im in
_find_images(pdf, page)]
if pageinfo['images']:
xres = max(image['dpi_w'] for image in pageinfo['images'])
yres = max(image['dpi_h'] for image in pageinfo['images'])
+3 -2
View File
@@ -24,8 +24,9 @@ def is_linux():
@pytest.helpers.register
def running_in_docker():
# Docker creates a file named /.dockerinit
return os.path.exists('/.dockerinit')
# Docker creates a file named /.dockerenv (newer versions) or
# /.dockerinit (older) -- this is undocumented, not an offical test
return os.path.exists('/.dockerenv') or os.path.exists('/.dockerinit')
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
+3
View File
@@ -64,6 +64,9 @@ under the terms of the license in LICENSE.rst.
* - epson.pdf
- @lowesjam
- a linearized PDF containing some unusual indirect objects, created by an Epson printer; printout of a Wikipedia article (CC BY-SA)
* - formxobject.pdf
- @jbarlow83
- hand-crafted exotic PDF containing an image inside a Form XObject
* - francais.pdf
- @jbarlow83
- a page containing French accents (diacritics)
Binary file not shown.
+6
View File
@@ -742,3 +742,9 @@ THIS FILE IS INVALID
'--tesseract-config', str(cfg_file))
assert "parameter not found" in err, "No error message"
assert p.returncode == ExitCode.invalid_config
def test_form_xobject(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(resources / 'formxobject.pdf', outpdf,
'--force-ocr',
env=spoof_tesseract_noop)
+7
View File
@@ -101,3 +101,10 @@ def test_jpeg(resources, outdir):
assert pdfimage['enc'] == 'jpeg'
assert (pdfimage['dpi_w'] - 150) < 1e-5
def test_form_xobject(resources):
filename = resources / 'formxobject.pdf'
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
pdfimage = pdfinfo[0]['images'][0]
assert pdfimage['width'] == 50