Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd2770a13 | ||
|
|
7b94129d9e | ||
|
|
d1a0065ef8 | ||
|
|
5a817370fd | ||
|
|
ab0a210763 | ||
|
|
9f800736bc | ||
|
|
c9a83afad6 | ||
|
|
5e14274f10 | ||
|
|
167470b4bd | ||
|
|
f06d3c2ec2 |
+49
-6
@@ -1,20 +1,63 @@
|
|||||||
# OCRmyPDF
|
# OCRmyPDF
|
||||||
#
|
#
|
||||||
# VERSION 4.4.2
|
# VERSION 4.4.2
|
||||||
FROM jbarlow83/ocrmypdf:latest
|
FROM ubuntu:16.10
|
||||||
MAINTAINER James R. Barlow <jim@purplerock.ca>
|
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 add-apt-repository ppa:alex-p/tesseract-ocr
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update \
|
||||||
tesseract-ocr-all
|
&& 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
|
USER docker
|
||||||
|
WORKDIR /home/docker
|
||||||
|
|
||||||
# Must use array form of ENTRYPOINT
|
# Must use array form of ENTRYPOINT
|
||||||
# Non-array form does not append other arguments, because that is "intuitive"
|
# Non-array form does not append other arguments, because that is "intuitive"
|
||||||
|
|||||||
@@ -4,6 +4,20 @@ RELEASE NOTES
|
|||||||
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
|
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
|
||||||
|
|
||||||
|
|
||||||
|
v4.5.1
|
||||||
|
======
|
||||||
|
|
||||||
|
- Fix issue #137, proportions of images with a non-square pixel aspect ratio would be distorted in output for ``--force-ocr`` and some other combinations of flags
|
||||||
|
|
||||||
|
|
||||||
|
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
|
v4.4.2
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|||||||
+20
-2
@@ -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.
|
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
|
.. 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
|
Sample script
|
||||||
"""""""""""""
|
"""""""""""""
|
||||||
|
|||||||
@@ -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-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:
|
For example:
|
||||||
|
|||||||
+9
-10
@@ -272,13 +272,15 @@ def check_options_output(options, log):
|
|||||||
if options.pdf_renderer == 'auto':
|
if options.pdf_renderer == 'auto':
|
||||||
options.pdf_renderer = 'hocr'
|
options.pdf_renderer = 'hocr'
|
||||||
|
|
||||||
if options.pdf_renderer == 'tesseract' and \
|
if options.pdf_renderer in ('tesseract', 'tess4'):
|
||||||
tesseract.version() < '3.04.01' and \
|
if tesseract.version() < '3.05':
|
||||||
os.environ.get('OCRMYPDF_SHARP_TTF', '') != '1':
|
log.warning(
|
||||||
log.warning(
|
"tesseract < 3.05 may corrupt PDF output. "
|
||||||
"Your version of tesseract has problems with PDF output."
|
"--pdf-renderer=tesseract is not recommend.")
|
||||||
" Some PDF viewers will fail to find searchable text.\n"
|
elif tesseract.version() == '4.00.00alpha':
|
||||||
"--pdf-renderer=tesseract is not recommended.")
|
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':
|
if options.debug_rendering and options.pdf_renderer == 'tesseract':
|
||||||
log.info(
|
log.info(
|
||||||
@@ -354,9 +356,6 @@ def check_options(options, log):
|
|||||||
sys.exit(ExitCode.missing_dependency)
|
sys.exit(ExitCode.missing_dependency)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
# Logging
|
# Logging
|
||||||
|
|
||||||
|
|||||||
+130
-37
@@ -89,13 +89,13 @@ def _is_unit_square(shorthand):
|
|||||||
pairwise = zip(values, UNIT_SQUARE)
|
pairwise = zip(values, UNIT_SQUARE)
|
||||||
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
|
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
|
||||||
|
|
||||||
RasterSettings = namedtuple('RasterSettings',
|
XobjectSettings = namedtuple('XobjectSettings',
|
||||||
['name', 'shorthand', 'stack_depth'])
|
['name', 'shorthand', 'stack_depth'])
|
||||||
|
|
||||||
InlineSettings = namedtuple('InlineSettings',
|
InlineSettings = namedtuple('InlineSettings',
|
||||||
['settings', 'shorthand', 'stack_depth'])
|
['settings', 'shorthand', 'stack_depth'])
|
||||||
|
|
||||||
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
|
ContentsInfo = namedtuple('ContentsInfo', ['xobject_settings', 'inline_images'])
|
||||||
|
|
||||||
|
|
||||||
def _normalize_stack(operations):
|
def _normalize_stack(operations):
|
||||||
@@ -114,7 +114,7 @@ def _normalize_stack(operations):
|
|||||||
yield (operands, command)
|
yield (operands, command)
|
||||||
|
|
||||||
|
|
||||||
def _interpret_contents(contentstream):
|
def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
|
||||||
"""Interpret the PDF content stream
|
"""Interpret the PDF content stream
|
||||||
|
|
||||||
The stack represents the state of the PDF graphics stack. We are only
|
The stack represents the state of the PDF graphics stack. We are only
|
||||||
@@ -139,8 +139,8 @@ def _interpret_contents(contentstream):
|
|||||||
|
|
||||||
operations = contentstream.operations
|
operations = contentstream.operations
|
||||||
stack = []
|
stack = []
|
||||||
ctm = _matrix_from_shorthand(UNIT_SQUARE)
|
ctm = _matrix_from_shorthand(initial_shorthand)
|
||||||
image_raster_settings = []
|
xobject_settings = []
|
||||||
inline_images = []
|
inline_images = []
|
||||||
|
|
||||||
for n, op in enumerate(_normalize_stack(operations)):
|
for n, op in enumerate(_normalize_stack(operations)):
|
||||||
@@ -161,10 +161,10 @@ def _interpret_contents(contentstream):
|
|||||||
_matrix_from_shorthand(operands), ctm)
|
_matrix_from_shorthand(operands), ctm)
|
||||||
elif command == b'Do':
|
elif command == b'Do':
|
||||||
image_name = operands[0]
|
image_name = operands[0]
|
||||||
raster = RasterSettings(
|
settings = XobjectSettings(
|
||||||
name=image_name, shorthand=_shorthand_from_matrix(ctm),
|
name=image_name, shorthand=_shorthand_from_matrix(ctm),
|
||||||
stack_depth=len(stack))
|
stack_depth=len(stack))
|
||||||
image_raster_settings.append(raster)
|
xobject_settings.append(settings)
|
||||||
elif command == b'INLINE IMAGE':
|
elif command == b'INLINE IMAGE':
|
||||||
settings = operands['settings']
|
settings = operands['settings']
|
||||||
inline = InlineSettings(
|
inline = InlineSettings(
|
||||||
@@ -173,7 +173,7 @@ def _interpret_contents(contentstream):
|
|||||||
inline_images.append(inline)
|
inline_images.append(inline)
|
||||||
|
|
||||||
return ContentsInfo(
|
return ContentsInfo(
|
||||||
raster_settings=image_raster_settings,
|
xobject_settings=xobject_settings,
|
||||||
inline_images=inline_images)
|
inline_images=inline_images)
|
||||||
|
|
||||||
|
|
||||||
@@ -241,8 +241,8 @@ def _get_dpi(ctm_shorthand, image_size):
|
|||||||
return (dpi_w, dpi_h)
|
return (dpi_w, dpi_h)
|
||||||
|
|
||||||
|
|
||||||
def _find_page_inline_images(page, pageinfo, contentsinfo):
|
def _find_inline_images(contentsinfo):
|
||||||
"Find inline images on the page"
|
"Find inline images in the contentstream"
|
||||||
|
|
||||||
for n, inline in enumerate(contentsinfo.inline_images):
|
for n, inline in enumerate(contentsinfo.inline_images):
|
||||||
image = {}
|
image = {}
|
||||||
@@ -272,20 +272,44 @@ def _find_page_inline_images(page, pageinfo, contentsinfo):
|
|||||||
yield image
|
yield image
|
||||||
|
|
||||||
|
|
||||||
def _find_page_regular_images(page, pageinfo, contentsinfo):
|
def _image_xobjects(container):
|
||||||
"Find images stored in XObject resources"
|
"""Search for all XObject-based images in the container
|
||||||
|
|
||||||
try:
|
Usually the container is a page, but it could also be a Form XObject
|
||||||
page['/Resources']['/XObject']
|
that contains images. Filter out the Form XObjects which are dealt with
|
||||||
except KeyError:
|
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
|
return
|
||||||
for xobj in page['/Resources']['/XObject']:
|
resources = container['/Resources']
|
||||||
# PyPDF2 returns the keys as an iterator
|
if '/XObject' not in resources:
|
||||||
pdfimage = page['/Resources']['/XObject'][xobj]
|
return
|
||||||
if pdfimage['/Subtype'] != '/Image':
|
for xobj in resources['/XObject']:
|
||||||
continue
|
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 = {}
|
||||||
image['name'] = str(xobj)
|
image['name'] = xobj
|
||||||
image['width'] = pdfimage['/Width']
|
image['width'] = pdfimage['/Width']
|
||||||
image['height'] = pdfimage['/Height']
|
image['height'] = pdfimage['/Height']
|
||||||
if '/BitsPerComponent' in pdfimage:
|
if '/BitsPerComponent' in pdfimage:
|
||||||
@@ -329,12 +353,12 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
|
|||||||
|
|
||||||
image['dpi_w'] = image['dpi_h'] = 0
|
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
|
# Loop in case the same image is display multiple times on a page
|
||||||
if raster.name != image['name']:
|
if xobj.name != image['name']:
|
||||||
continue
|
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
|
# At least one PDF in the wild (and test suite) draws an image
|
||||||
# when the graphics stack depth is 0, meaning that the image
|
# when the graphics stack depth is 0, meaning that the image
|
||||||
# gets drawn into a square of 1x1 PDF units (or 1/72",
|
# 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
|
continue
|
||||||
|
|
||||||
dpi_w, dpi_h = _get_dpi(
|
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
|
# When image is used multiple times take the highest DPI it is
|
||||||
# rendered at
|
# rendered at
|
||||||
@@ -358,9 +382,85 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
|
|||||||
yield image
|
yield image
|
||||||
|
|
||||||
|
|
||||||
def _find_page_images(page, pageinfo, contentsinfo):
|
def _find_form_xobject_images(pdf, container, contentsinfo):
|
||||||
yield from _find_page_inline_images(page, pageinfo, contentsinfo)
|
"""Find any images that are in Form XObjects in the container
|
||||||
yield from _find_page_regular_images(page, pageinfo, contentsinfo)
|
|
||||||
|
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):
|
def _page_has_text(pdf, page):
|
||||||
@@ -405,15 +505,8 @@ def _pdf_get_pageinfo(infile, pageno: int):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pageinfo['rotate'] = 0
|
pageinfo['rotate'] = 0
|
||||||
|
|
||||||
try:
|
pageinfo['images'] = [im for im in
|
||||||
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
|
_find_images(pdf, page)]
|
||||||
except AttributeError as e:
|
|
||||||
return pageinfo
|
|
||||||
|
|
||||||
contentsinfo = _interpret_contents(contentstream)
|
|
||||||
pageinfo['images'] = [im for im in _find_page_images(
|
|
||||||
page, pageinfo, contentsinfo)]
|
|
||||||
|
|
||||||
if pageinfo['images']:
|
if pageinfo['images']:
|
||||||
xres = max(image['dpi_w'] for image in pageinfo['images'])
|
xres = max(image['dpi_w'] for image in pageinfo['images'])
|
||||||
yres = max(image['dpi_h'] for image in pageinfo['images'])
|
yres = max(image['dpi_h'] for image in pageinfo['images'])
|
||||||
|
|||||||
+10
-5
@@ -555,16 +555,21 @@ def select_image_layer(
|
|||||||
re_symlink(page_pdf, output_file, log)
|
re_symlink(page_pdf, output_file, log)
|
||||||
else:
|
else:
|
||||||
pageinfo = get_pageinfo(image, context)
|
pageinfo = get_pageinfo(image, context)
|
||||||
dpi = get_page_dpi(pageinfo, options)
|
|
||||||
dpi = float(dpi[0]), float(dpi[1])
|
# We rasterize a square DPI version of each page because most image
|
||||||
layout_fun = img2pdf.get_fixed_dpi_layout_fun(dpi)
|
# processing tools don't support rectangular DPI. Use the square DPI
|
||||||
|
# as it accurately describes the image. It would be possible to
|
||||||
|
# resample the image at this stage back to non-square DPI to more
|
||||||
|
# closely resemble the input, except that the hocr renderer does not
|
||||||
|
# understand non-square DPI. The tess4 renderer would be fine.
|
||||||
|
dpi = get_page_square_dpi(pageinfo, options)
|
||||||
|
layout_fun = img2pdf.get_fixed_dpi_layout_fun((dpi, dpi))
|
||||||
|
|
||||||
with open(image, 'rb') as imfile, \
|
with open(image, 'rb') as imfile, \
|
||||||
open(output_file, 'wb') as pdf:
|
open(output_file, 'wb') as pdf:
|
||||||
rawdata = imfile.read()
|
|
||||||
log.debug('{:4d}: convert'.format(page_number(page_pdf)))
|
log.debug('{:4d}: convert'.format(page_number(page_pdf)))
|
||||||
img2pdf.convert(
|
img2pdf.convert(
|
||||||
rawdata, with_pdfrw=False,
|
imfile, with_pdfrw=False,
|
||||||
layout_fun=layout_fun, outputstream=pdf)
|
layout_fun=layout_fun, outputstream=pdf)
|
||||||
log.debug('{:4d}: convert done'.format(page_number(page_pdf)))
|
log.debug('{:4d}: convert done'.format(page_number(page_pdf)))
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -24,8 +24,9 @@ def is_linux():
|
|||||||
|
|
||||||
@pytest.helpers.register
|
@pytest.helpers.register
|
||||||
def running_in_docker():
|
def running_in_docker():
|
||||||
# Docker creates a file named /.dockerinit
|
# Docker creates a file named /.dockerenv (newer versions) or
|
||||||
return os.path.exists('/.dockerinit')
|
# /.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__))
|
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ under the terms of the license in LICENSE.rst.
|
|||||||
* - epson.pdf
|
* - epson.pdf
|
||||||
- @lowesjam
|
- @lowesjam
|
||||||
- a linearized PDF containing some unusual indirect objects, created by an Epson printer; printout of a Wikipedia article (CC BY-SA)
|
- 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
|
* - francais.pdf
|
||||||
- @jbarlow83
|
- @jbarlow83
|
||||||
- a page containing French accents (diacritics)
|
- a page containing French accents (diacritics)
|
||||||
|
|||||||
Binary file not shown.
@@ -564,6 +564,46 @@ def test_non_square_resolution(renderer, spoof_tesseract_cache,
|
|||||||
assert in_pageinfo[0]['yres'] == out_pageinfo[0]['yres']
|
assert in_pageinfo[0]['yres'] == out_pageinfo[0]['yres']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs math.isclose")
|
||||||
|
@pytest.mark.parametrize('renderer', [
|
||||||
|
'hocr',
|
||||||
|
'tesseract'
|
||||||
|
])
|
||||||
|
def test_convert_to_square_resolution(renderer, spoof_tesseract_cache,
|
||||||
|
resources, outpdf):
|
||||||
|
from math import isclose
|
||||||
|
|
||||||
|
# Confirm input image is non-square resolution
|
||||||
|
in_pageinfo = pdf_get_all_pageinfo(str(resources / 'aspect.pdf'))
|
||||||
|
assert in_pageinfo[0]['xres'] != in_pageinfo[0]['yres']
|
||||||
|
|
||||||
|
# --force-ocr requires means forced conversion to square resolution
|
||||||
|
check_ocrmypdf(
|
||||||
|
resources / 'aspect.pdf', outpdf,
|
||||||
|
'--force-ocr',
|
||||||
|
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||||
|
|
||||||
|
out_pageinfo = pdf_get_all_pageinfo(str(outpdf))
|
||||||
|
|
||||||
|
in_p0, out_p0 = in_pageinfo[0], out_pageinfo[0]
|
||||||
|
|
||||||
|
# Resolution show now be equal
|
||||||
|
assert out_p0['xres'] == out_p0['yres']
|
||||||
|
|
||||||
|
# Page size should match input page size
|
||||||
|
assert isclose(in_p0['width_inches'],
|
||||||
|
out_p0['width_inches'])
|
||||||
|
assert isclose(in_p0['height_inches'],
|
||||||
|
out_p0['height_inches'])
|
||||||
|
|
||||||
|
# Because we rasterized the page to produce a new image, it should occupy
|
||||||
|
# the entire page
|
||||||
|
out_im_w = out_p0['images'][0]['width'] / out_p0['images'][0]['dpi_w']
|
||||||
|
out_im_h = out_p0['images'][0]['height'] / out_p0['images'][0]['dpi_h']
|
||||||
|
assert isclose(out_p0['width_inches'], out_im_w)
|
||||||
|
assert isclose(out_p0['height_inches'], out_im_h)
|
||||||
|
|
||||||
|
|
||||||
def test_image_to_pdf(spoof_tesseract_noop, resources, outpdf):
|
def test_image_to_pdf(spoof_tesseract_noop, resources, outpdf):
|
||||||
check_ocrmypdf(
|
check_ocrmypdf(
|
||||||
resources / 'LinnSequencer.jpg', outpdf, '--image-dpi', '200',
|
resources / 'LinnSequencer.jpg', outpdf, '--image-dpi', '200',
|
||||||
@@ -742,3 +782,9 @@ THIS FILE IS INVALID
|
|||||||
'--tesseract-config', str(cfg_file))
|
'--tesseract-config', str(cfg_file))
|
||||||
assert "parameter not found" in err, "No error message"
|
assert "parameter not found" in err, "No error message"
|
||||||
assert p.returncode == ExitCode.invalid_config
|
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)
|
||||||
|
|||||||
@@ -101,3 +101,10 @@ def test_jpeg(resources, outdir):
|
|||||||
assert pdfimage['enc'] == 'jpeg'
|
assert pdfimage['enc'] == 'jpeg'
|
||||||
assert (pdfimage['dpi_w'] - 150) < 1e-5
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user