Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0d2948f9a | ||
|
|
cac64b5ed2 | ||
|
|
3692868004 | ||
|
|
064f935699 | ||
|
|
8770fff968 | ||
|
|
82de78b6b0 | ||
|
|
2a52c6dec2 | ||
|
|
2898879be7 | ||
|
|
a9ad805347 | ||
|
|
46d0632fe2 | ||
|
|
ef1e7a814e | ||
|
|
1084724937 | ||
|
|
ecb0109d79 | ||
|
|
504d5776d2 | ||
|
|
ee23976858 | ||
|
|
f559316881 | ||
|
|
084610c242 | ||
|
|
9ff627472b | ||
|
|
956310d1ec | ||
|
|
1a982da442 | ||
|
|
4879a1f0de | ||
|
|
ce66bcc9c8 |
@@ -21,8 +21,12 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**System (please complete the following information):**
|
||||
- OS:
|
||||
- Python version:
|
||||
- Python version:
|
||||
- OCRmyPDF version:
|
||||
|
||||
**Installation**
|
||||
How did you install OCRmyPDF? Did you install it from your operating system's
|
||||
package manager, or using pip?
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.1.0
|
||||
rev: v3.4.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
@@ -12,11 +12,11 @@ repos:
|
||||
hooks:
|
||||
- id: seed-isort-config
|
||||
- repo: https://github.com/pre-commit/mirrors-isort
|
||||
rev: v5.0.5 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases
|
||||
rev: v5.7.0 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 19.10b0
|
||||
rev: 20.8b1
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python
|
||||
|
||||
@@ -5,6 +5,15 @@ API Reference
|
||||
This page summarizes the rest of the public API. Generally speaking this
|
||||
should mainly of interest to plugin developers.
|
||||
|
||||
ocrmypdf
|
||||
========
|
||||
|
||||
.. autoclass:: ocrmypdf.PageContext
|
||||
:members:
|
||||
|
||||
.. autoclass:: ocrmypdf.PdfContext
|
||||
:members:
|
||||
|
||||
ocrmypdf.exceptions
|
||||
===================
|
||||
|
||||
@@ -17,6 +26,9 @@ ocrmypdf.helpers
|
||||
|
||||
.. automodule:: ocrmypdf.helpers
|
||||
:members:
|
||||
:noindex: deprecated
|
||||
|
||||
.. autodecorator:: deprecated
|
||||
|
||||
ocrmypdf.hocrtransform
|
||||
======================
|
||||
|
||||
@@ -520,7 +520,7 @@ DLLs or other Windows patches, and may require a reboot.
|
||||
You may then use ``pip`` to install ocrmypdf. (This can performed by a user or
|
||||
Administrator.):
|
||||
|
||||
* ``pip install ocrmypdf
|
||||
* ``pip install ocrmypdf``
|
||||
|
||||
Chocolatey automatically selects appropriate versions of these applications. If you
|
||||
are installing them manually, please install 64-bit versions of all applications for
|
||||
@@ -533,6 +533,12 @@ override the versions OCRmyPDF selects, you can modify the ``PATH`` environment
|
||||
variable. `Follow these directions <https://www.computerhope.com/issues/ch000549.htm#dospath>`_
|
||||
to change the PATH.
|
||||
|
||||
.. warning::
|
||||
|
||||
As of early 2021, users have reported problems with the Microsoft Store version of
|
||||
Python affected most third party Python packages including OCRmyPDF. Please use
|
||||
Python downloaded from Python.org or Chocolatey as recommended here.
|
||||
|
||||
Windows Subsystem for Linux
|
||||
---------------------------
|
||||
|
||||
|
||||
@@ -65,6 +65,27 @@ similar to ``pytest`` packages such as ``pytest-cov`` (the package) and
|
||||
``ocrmypdf-`` (for the package name on PyPI) and ``ocrmypdf_`` (for the
|
||||
module), just like pytest plugins.
|
||||
|
||||
Setuptools plugins
|
||||
==================
|
||||
|
||||
You can also create a plugin that OCRmyPDF will always automatically load if both are
|
||||
installed in the same virtual environment, using a setuptools entrypoint.
|
||||
|
||||
Your package's ``setup.py`` would need to contain the following, for a plugin
|
||||
named ``ocrmypdf-exampleplugin``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# sample ./setup.py file
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="ocrmypdf-exampleplugin",
|
||||
packages=["exampleplugin"],
|
||||
# the following makes a plugin available to pytest
|
||||
entry_points={"ocrmypdf": ["exampleplugin = exampleplugin.pluginmodule"]},
|
||||
)
|
||||
|
||||
Plugin requirements
|
||||
===================
|
||||
|
||||
|
||||
@@ -12,6 +12,30 @@ may be unreliable. Use the API to depend on precise behavior.
|
||||
The public API may be useful in scripts that launch OCRmyPDF processes or that
|
||||
wish to use some of its features for working with PDFs.
|
||||
|
||||
v11.6.2
|
||||
=======
|
||||
|
||||
- Fixed a regression where the wrong page orientation would be produced when using
|
||||
arguments such as ``--deskew --rotate-pages`` (#730).
|
||||
|
||||
v11.6.1
|
||||
=======
|
||||
|
||||
- Fixed an issue with attempting optimize unusually narrow-width images by excluding
|
||||
these images from optimization (#732).
|
||||
- Remove an obsolete compatibility shim for a version of pikepdf that is no longer
|
||||
supported.
|
||||
|
||||
v11.6.0
|
||||
=======
|
||||
|
||||
- OCRmyPDF will now automatically register plugins from the same virtual environment
|
||||
with an appropriate setuptools entrypoint.
|
||||
- Refactor the plugin manager to remove unnecessary complications and make plugin
|
||||
registration more automatic.
|
||||
- ``PageContext`` and ``PdfContext`` are now formally part of the API, as they
|
||||
should have been, since they were part of ``ocrmypdf.pluginspec``.
|
||||
|
||||
v11.5.0
|
||||
=======
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
- "/media/scan:/input"
|
||||
- "/mnt/scan:/output"
|
||||
environment:
|
||||
- OCR_OUTPUT_DIRECTORY_YEAR_MONT=0
|
||||
- OCR_OUTPUT_DIRECTORY_YEAR_MONTH=0
|
||||
user: "<SET TO YOUR USER ID>:<SET TO YOUR GROUP ID>"
|
||||
entrypoint: python3
|
||||
command: watcher.py
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ["py36", "py37", "py38"]
|
||||
target-version = ["py36", "py37", "py38", "py39"]
|
||||
skip-string-normalization = true
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
from pluggy import HookimplMarker as _HookimplMarker
|
||||
|
||||
from ocrmypdf import helpers, hocrtransform, leptonica, pdfa, pdfinfo
|
||||
from ocrmypdf._jobcontext import PageContext, PdfContext
|
||||
from ocrmypdf._version import PROGRAM_NAME, __version__
|
||||
from ocrmypdf.api import Verbosity, configure_logging, ocr
|
||||
from ocrmypdf.exceptions import (
|
||||
|
||||
@@ -9,10 +9,8 @@
|
||||
|
||||
from contextlib import contextmanager
|
||||
from io import BytesIO
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@@ -10,15 +10,22 @@ import shutil
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from copy import copy
|
||||
from io import IOBase
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
|
||||
from pluggy import PluginManager
|
||||
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
from ocrmypdf.pdfinfo.info import PageInfo
|
||||
|
||||
|
||||
class PdfContext:
|
||||
"""Holds our context for a particular run of the pipeline"""
|
||||
"""Holds the context for a particular run of the pipeline."""
|
||||
|
||||
options: Namespace #: The specified options for processing this PDF.
|
||||
origin: Path #: The filename of the original input file.
|
||||
pdfinfo: PdfInfo #: Detailed data for this PDF.
|
||||
plugin_manager: PluginManager #: PluginManager for processing the current PDF.
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -35,21 +42,33 @@ class PdfContext:
|
||||
self.plugin_manager = plugin_manager
|
||||
|
||||
def get_path(self, name: str) -> Path:
|
||||
"""Generate a ``Path`` for an intermediate file involved in processing.
|
||||
|
||||
The path will be in a temporary folder that is common for all processing
|
||||
of this particular PDF.
|
||||
"""
|
||||
return self.work_folder / name
|
||||
|
||||
def get_page_contexts(self) -> Iterator['PageContext']:
|
||||
"""Get all ``PageContext`` for this PDF."""
|
||||
npages = len(self.pdfinfo)
|
||||
for n in range(npages):
|
||||
yield PageContext(self, n)
|
||||
|
||||
|
||||
class PageContext:
|
||||
"""Holds our context for a page
|
||||
"""Holds our context for a page.
|
||||
|
||||
Must be pickable, so stores only intrinsic/simple data elements or those
|
||||
capable of their serializing themselves via __getstate__.
|
||||
capable of their serializing themselves via ``__getstate__``.
|
||||
"""
|
||||
|
||||
options: Namespace #: The specified options for processing this PDF.
|
||||
origin: Path #: The filename of the original input file.
|
||||
pageno: int #: This page number (zero-based).
|
||||
pageinfo: PageInfo #: Information on this page.
|
||||
plugin_manager: PluginManager #: PluginManager for processing the current PDF.
|
||||
|
||||
def __init__(self, pdf_context: PdfContext, pageno):
|
||||
self.work_folder = pdf_context.work_folder
|
||||
self.origin = pdf_context.origin
|
||||
@@ -59,6 +78,11 @@ class PageContext:
|
||||
self.plugin_manager = pdf_context.plugin_manager
|
||||
|
||||
def get_path(self, name: str) -> Path:
|
||||
"""Generate a ``Path`` for a file that is part of processing this page.
|
||||
|
||||
The path will be based in a common temporary folder and have a prefix based
|
||||
on the page number.
|
||||
"""
|
||||
return self.work_folder / ("%06d_%s" % (self.pageno + 1, name))
|
||||
|
||||
def __getstate__(self):
|
||||
|
||||
@@ -584,7 +584,9 @@ def create_visible_page_jpg(image: Path, page_context: PageContext) -> Path:
|
||||
return output_file
|
||||
|
||||
|
||||
def create_pdf_page_from_image(image: Path, page_context: PageContext):
|
||||
def create_pdf_page_from_image(
|
||||
image: Path, page_context: PageContext, orientation_correction
|
||||
):
|
||||
# We rasterize a square DPI version of each page because most image
|
||||
# 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
|
||||
@@ -595,7 +597,8 @@ def create_pdf_page_from_image(image: Path, page_context: PageContext):
|
||||
|
||||
pageinfo = page_context.pageinfo
|
||||
pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches)
|
||||
if pageinfo.rotation % 180 == 90:
|
||||
effective_rotation = (pageinfo.rotation - orientation_correction) % 360
|
||||
if effective_rotation % 180 == 90:
|
||||
pagesize = pagesize[1], pagesize[0]
|
||||
|
||||
# This create a single page PDF
|
||||
@@ -607,6 +610,7 @@ def create_pdf_page_from_image(image: Path, page_context: PageContext):
|
||||
imfile, with_pdfrw=False, layout_fun=layout_fun, outputstream=pdf
|
||||
)
|
||||
log.debug('convert done')
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
import argparse
|
||||
import importlib
|
||||
import importlib.util
|
||||
import pkgutil
|
||||
import sys
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Callable, List, Tuple, Union
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
import pluggy
|
||||
|
||||
import ocrmypdf.builtin_plugins
|
||||
from ocrmypdf import pluginspec
|
||||
from ocrmypdf.cli import get_parser, plugins_only_parser
|
||||
|
||||
@@ -30,62 +31,69 @@ class OcrmypdfPluginManager(pluggy.PluginManager):
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, *args, setup_func: Callable[[pluggy.PluginManager], None], **kwargs
|
||||
self,
|
||||
*args,
|
||||
plugins: List[Union[str, Path]],
|
||||
builtins: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
self._init_args = args
|
||||
self._setup_func = setup_func
|
||||
self._init_kwargs = kwargs
|
||||
self.__init_args = args
|
||||
self.__init_kwargs = kwargs
|
||||
self.__plugins = plugins
|
||||
self.__builtins = builtins
|
||||
super().__init__(*args, **kwargs)
|
||||
setup_func(self)
|
||||
self.setup_plugins()
|
||||
|
||||
def __getstate__(self):
|
||||
state = dict(
|
||||
_init_args=self._init_args,
|
||||
_setup_func=self._setup_func,
|
||||
_init_kwargs=self._init_kwargs,
|
||||
init_args=self.__init_args,
|
||||
plugins=self.__plugins,
|
||||
builtins=self.__builtins,
|
||||
init_kwargs=self.__init_kwargs,
|
||||
)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__init__(
|
||||
*state['_init_args'],
|
||||
setup_func=state['_setup_func'],
|
||||
**state['_init_kwargs'],
|
||||
*state['init_args'],
|
||||
plugins=state['plugins'],
|
||||
builtins=state['builtins'],
|
||||
**state['init_kwargs'],
|
||||
)
|
||||
|
||||
def setup_plugins(self):
|
||||
self.add_hookspecs(pluginspec)
|
||||
|
||||
def _setup_plugins(
|
||||
pm: pluggy.PluginManager, plugins: List[Union[str, Path]], builtins: bool = True
|
||||
):
|
||||
pm.add_hookspecs(pluginspec)
|
||||
# 1. Register builtins
|
||||
if self.__builtins:
|
||||
for module in pkgutil.iter_modules(ocrmypdf.builtin_plugins.__path__):
|
||||
name = f'ocrmypdf.builtin_plugins.{module.name}'
|
||||
module = importlib.import_module(name)
|
||||
self.register(module)
|
||||
|
||||
all_plugins: List[Union[str, Path]] = []
|
||||
if builtins:
|
||||
all_plugins.extend(
|
||||
[
|
||||
'ocrmypdf.builtin_plugins.ghostscript',
|
||||
'ocrmypdf.builtin_plugins.tesseract_ocr',
|
||||
]
|
||||
)
|
||||
all_plugins.extend(plugins)
|
||||
for name in all_plugins:
|
||||
if isinstance(name, Path) or name.endswith('.py'):
|
||||
# Import by filename
|
||||
module_name = Path(name).stem
|
||||
spec = importlib.util.spec_from_file_location(module_name, name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
else:
|
||||
# Import by dotted module name
|
||||
module = importlib.import_module(name)
|
||||
pm.register(module)
|
||||
# 2. Register setuptools plugins
|
||||
self.load_setuptools_entrypoints('ocrmypdf')
|
||||
|
||||
# 3. Register plugins specified on command line
|
||||
for name in self.__plugins:
|
||||
if isinstance(name, Path) or name.endswith('.py'):
|
||||
# Import by filename
|
||||
module_name = Path(name).stem
|
||||
spec = importlib.util.spec_from_file_location(module_name, name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
else:
|
||||
# Import by dotted module name
|
||||
module = importlib.import_module(name)
|
||||
self.register(module)
|
||||
|
||||
|
||||
def get_plugin_manager(plugins: List[Union[str, Path]], builtins=True):
|
||||
pm = OcrmypdfPluginManager(
|
||||
project_name='ocrmypdf',
|
||||
setup_func=partial(_setup_plugins, plugins=plugins, builtins=builtins),
|
||||
plugins=plugins,
|
||||
builtins=builtins,
|
||||
)
|
||||
return pm
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ def exec_page_sync(page_context: PageContext):
|
||||
if filtered_image:
|
||||
visible_image_out = filtered_image
|
||||
pdf_page_from_image_out = create_pdf_page_from_image(
|
||||
visible_image_out, page_context
|
||||
visible_image_out, page_context, orientation_correction
|
||||
)
|
||||
|
||||
if options.pdf_renderer.startswith('hocr'):
|
||||
|
||||
+15
-9
@@ -17,7 +17,7 @@ from functools import wraps
|
||||
from io import StringIO
|
||||
from math import isclose
|
||||
from pathlib import Path
|
||||
from typing import Any, Sequence, TypeVar
|
||||
from typing import Any, Sequence
|
||||
|
||||
import pikepdf
|
||||
|
||||
@@ -63,8 +63,13 @@ class NeverRaise(Exception):
|
||||
|
||||
|
||||
def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
|
||||
"""
|
||||
Helper function: relinks soft symbolic link if necessary
|
||||
"""Create a symbolic link at ``soft_link_name``, which references ``input_file``.
|
||||
|
||||
Think of this as copying ``input_file`` to ``soft_link_name`` with less overhead.
|
||||
|
||||
Use symlinks safely. Self-linking loops are prevented. On Windows, file copy is
|
||||
used since symlinks may require administrator privileges. An existing link at the
|
||||
destination is removed.
|
||||
"""
|
||||
input_file = os.fspath(input_file)
|
||||
soft_link_name = os.fspath(soft_link_name)
|
||||
@@ -72,8 +77,8 @@ def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
|
||||
# Guard against soft linking to oneself
|
||||
if input_file == soft_link_name:
|
||||
log.warning(
|
||||
"No symbolic link made. You are using "
|
||||
"the original data directory as the working directory."
|
||||
"No symbolic link created. You are using the original data directory "
|
||||
"as the working directory."
|
||||
)
|
||||
return
|
||||
|
||||
@@ -114,7 +119,7 @@ def is_iterable_notstr(thing: Any) -> bool:
|
||||
|
||||
|
||||
def monotonic(L: Sequence) -> bool:
|
||||
"""Does list increase monotonically?"""
|
||||
"""Does this sequence increase monotonically?"""
|
||||
return all(b > a for a, b in zip(L, L[1:]))
|
||||
|
||||
|
||||
@@ -173,7 +178,8 @@ def is_file_writable(test_file: os.PathLike) -> bool:
|
||||
def check_pdf(input_file: Path) -> bool:
|
||||
"""Check if a PDF complies with the PDF specification.
|
||||
|
||||
Checks for proper formatting and proper linearization.
|
||||
Checks for proper formatting and proper linearization. Uses pikepdf (which in
|
||||
turn, uses QPDF) to perform the checks.
|
||||
"""
|
||||
pdf = None
|
||||
try:
|
||||
@@ -217,7 +223,7 @@ def check_pdf(input_file: Path) -> bool:
|
||||
|
||||
|
||||
def clamp(n, smallest, largest): # mypy doesn't understand types for this
|
||||
"""Clamps the value of n to between smallest and largest."""
|
||||
"""Clamps the value of ``n`` to between ``smallest`` and ``largest``."""
|
||||
return max(smallest, min(n, largest))
|
||||
|
||||
|
||||
@@ -235,7 +241,7 @@ def pikepdf_enable_mmap():
|
||||
|
||||
|
||||
def deprecated(func):
|
||||
"""Warn that function is deprecated"""
|
||||
"""Warn that function is deprecated."""
|
||||
|
||||
@wraps(func)
|
||||
def new_func(*args, **kwargs):
|
||||
|
||||
@@ -31,11 +31,10 @@
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from itertools import chain
|
||||
from math import atan, cos, sin
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Union
|
||||
from typing import Any, NamedTuple, Optional, Tuple, Union
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from reportlab.lib.colors import black, cyan, magenta, red
|
||||
@@ -44,7 +43,14 @@ from reportlab.pdfgen.canvas import Canvas
|
||||
|
||||
Element = ElementTree.Element
|
||||
|
||||
Rect = namedtuple('Rect', ['x1', 'y1', 'x2', 'y2'])
|
||||
|
||||
class Rect(NamedTuple): # pylint: disable=inherit-non-class
|
||||
"""A rectangle for managing PDF coordinates."""
|
||||
|
||||
x1: Any
|
||||
y1: Any
|
||||
x2: Any
|
||||
y2: Any
|
||||
|
||||
|
||||
class HocrTransformError(Exception):
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import threading
|
||||
import warnings
|
||||
|
||||
+10
-11
@@ -10,11 +10,9 @@ import sys
|
||||
import tempfile
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
@@ -49,7 +47,7 @@ DEFAULT_PNG_QUALITY = 70
|
||||
Xref = NewType('Xref', int)
|
||||
|
||||
|
||||
class XrefExt(NamedTuple):
|
||||
class XrefExt(NamedTuple): # pylint: disable=inherit-non-class
|
||||
xref: Xref
|
||||
ext: str
|
||||
|
||||
@@ -78,6 +76,9 @@ def extract_image_filter(
|
||||
if image.Length < 100:
|
||||
log.debug(f"Skipping small image, xref {xref}")
|
||||
return None
|
||||
if image.Width < 8 or image.Height < 8: # Issue 732
|
||||
log.debug(f"Skipping oddly sized image, xref {xref}")
|
||||
return None
|
||||
|
||||
pim = PdfImage(image)
|
||||
|
||||
@@ -152,11 +153,6 @@ def extract_image_generic(
|
||||
if pim.bits_per_component == 1:
|
||||
return None
|
||||
|
||||
try:
|
||||
pim.indexed # pikepdf 1.6.3 can't handle [/Indexed [/Array...]]
|
||||
except NotImplementedError:
|
||||
return None
|
||||
|
||||
if filtdp[0] == Name.DCTDecode and options.optimize >= 2:
|
||||
# This is a simple heuristic derived from some training data, that has
|
||||
# about a 70% chance of guessing whether the JPEG is high quality,
|
||||
@@ -212,7 +208,10 @@ def extract_image_generic(
|
||||
|
||||
|
||||
def extract_images(
|
||||
pike: Pdf, root: Path, options, extract_fn: Callable[..., Optional[XrefExt]],
|
||||
pike: Pdf,
|
||||
root: Path,
|
||||
options,
|
||||
extract_fn: Callable[..., Optional[XrefExt]],
|
||||
) -> Iterator[Tuple[int, XrefExt]]:
|
||||
"""Extract image using extract_fn
|
||||
|
||||
@@ -564,10 +563,10 @@ def rewrite_png(pike: Pdf, im_obj: Object, compdata) -> None: # pragma: no cove
|
||||
# ncolors == 0 means we are using a colorspace without a palette
|
||||
if compdata.spp == 1:
|
||||
cs = Name.DeviceGray
|
||||
elif compdata.spp == 3:
|
||||
cs = Name.DeviceRGB
|
||||
elif compdata.spp == 4:
|
||||
cs = Name.DeviceCMYK
|
||||
else: # spp == 3
|
||||
cs = Name.DeviceRGB
|
||||
im_obj.ColorSpace = cs
|
||||
im_obj.write(compdata.read(), filter=Name.FlateDecode, decode_parms=dparms)
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'):
|
||||
|
||||
|
||||
def file_claims_pdfa(filename: Path):
|
||||
"""Determines if the file claims to be PDF/A compliant
|
||||
"""Determines if the file claims to be PDF/A compliant.
|
||||
|
||||
This only checks if the XMP metadata contains a PDF/A marker. It does not
|
||||
do full PDF/A validation.
|
||||
|
||||
@@ -15,7 +15,7 @@ from functools import partial
|
||||
from math import hypot, isclose
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Any, Container, Dict, Iterator, List, Optional, Tuple, Union
|
||||
from typing import Container, Iterator, Optional, Tuple, Union
|
||||
from warnings import warn
|
||||
|
||||
import pikepdf
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# © 2020 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
||||
# copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included
|
||||
# in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"""Tesseract no-op/fixed rotate plugin
|
||||
|
||||
To quickly run tests where getting OCR output is not necessary and we want to test
|
||||
the rotation pipeline.
|
||||
|
||||
In 'hocr' mode, create a .hocr file that specifies no text found.
|
||||
|
||||
In 'pdf' mode, convert the image to PDF using another program.
|
||||
|
||||
In orientation check mode, report 0, 90, 180, 270... based on page number.
|
||||
"""
|
||||
|
||||
import pikepdf
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf import OcrEngine, OrientationConfidence, hookimpl
|
||||
from ocrmypdf.helpers import page_number
|
||||
|
||||
HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name='ocr-system' content='tesseract 4.0.0' />
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "x.tif"; bbox 0 0 {0} {1}; ppageno 0'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 0 1 {0} {1}">
|
||||
<p class='ocr_par' dir='ltr' id='par_1' title="bbox 0 1 {0} {1}">
|
||||
<span class='ocr_line' id='line_1' title="bbox 0 1 {0} {1}"><span class='ocrx_word' id='word_1' title="bbox 0 1 {0} {1}"> </span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
|
||||
class FixedRotateNoopOcrEngine(OcrEngine):
|
||||
@staticmethod
|
||||
def version():
|
||||
return '4.0.0'
|
||||
|
||||
@staticmethod
|
||||
def creator_tag(options):
|
||||
tag = '-PDF' if options.pdf_renderer == 'sandwich' else ''
|
||||
return f"NO-OP {tag} {FixedRotateNoopOcrEngine.version()}"
|
||||
|
||||
def __str__(self):
|
||||
return f"NO-OP {FixedRotateNoopOcrEngine.version()}"
|
||||
|
||||
@staticmethod
|
||||
def languages(options):
|
||||
return {'eng'}
|
||||
|
||||
@staticmethod
|
||||
def get_orientation(input_file, options):
|
||||
page = page_number(input_file)
|
||||
|
||||
angle = ((page - 1) * 90) % 360
|
||||
|
||||
return OrientationConfidence(angle=angle, confidence=99.9)
|
||||
|
||||
@staticmethod
|
||||
def generate_hocr(input_file, output_hocr, output_text, options):
|
||||
with Image.open(input_file) as im, open(
|
||||
output_hocr, 'w', encoding='utf-8'
|
||||
) as f:
|
||||
w, h = im.size
|
||||
f.write(HOCR_TEMPLATE.format(str(w), str(h)))
|
||||
with open(output_text, 'w') as f:
|
||||
f.write('')
|
||||
|
||||
@staticmethod
|
||||
def generate_pdf(input_file, output_pdf, output_text, options):
|
||||
with Image.open(input_file) as im:
|
||||
dpi = im.info['dpi']
|
||||
pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1]
|
||||
ptsize = pagesize[0] * 72, pagesize[1] * 72
|
||||
pdf = pikepdf.new()
|
||||
pdf.add_blank_page(page_size=ptsize)
|
||||
pdf.save(output_pdf, static_id=True)
|
||||
output_text.write_text('')
|
||||
|
||||
|
||||
@hookimpl
|
||||
def get_ocr_engine():
|
||||
return FixedRotateNoopOcrEngine()
|
||||
@@ -173,3 +173,15 @@ def test_multiple_pngs(resources, outdir):
|
||||
inim = next(iter(inpdf.pages[n].images.values()))
|
||||
outim = next(iter(outpdf.pages[n].images.values()))
|
||||
assert len(outim.read_raw_bytes()) < len(inim.read_raw_bytes()), n
|
||||
|
||||
|
||||
def test_optimize_off(resources, outpdf):
|
||||
check_ocrmypdf(
|
||||
resources / 'trivial.pdf',
|
||||
outpdf,
|
||||
'--optimize=0',
|
||||
'--output-type',
|
||||
'pdf',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_noop.py',
|
||||
)
|
||||
|
||||
+50
-11
@@ -6,15 +6,17 @@
|
||||
|
||||
|
||||
from io import BytesIO
|
||||
from math import cos, pi, sin
|
||||
from os import fspath
|
||||
|
||||
import img2pdf
|
||||
import pikepdf
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
|
||||
from ocrmypdf import leptonica
|
||||
from ocrmypdf._exec import ghostscript, tesseract
|
||||
from ocrmypdf._exec import ghostscript
|
||||
from ocrmypdf._plugin_manager import get_plugin_manager
|
||||
from ocrmypdf.helpers import Resolution
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
@@ -249,16 +251,6 @@ def test_rotate_page_level(image_angle, page_angle, resources, outdir):
|
||||
assert check_monochrome_correlation(outdir, reference, 1, out, 1) > 0.2
|
||||
|
||||
|
||||
def test_tesseract_orientation(resources, tmp_path):
|
||||
pix = leptonica.Pix.open(resources / 'crom.png')
|
||||
pix_rotated = pix.rotate_orth(2) # 180 degrees clockwise
|
||||
pix_rotated.write_implied_format(tmp_path / '000001.png')
|
||||
|
||||
tesseract.get_orientation( # Test results of this are unreliable
|
||||
tmp_path / '000001.png', engine_mode='3', timeout=10
|
||||
)
|
||||
|
||||
|
||||
def test_rasterize_rotates(resources, tmp_path):
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
@@ -287,3 +279,50 @@ def test_rasterize_rotates(resources, tmp_path):
|
||||
filter_vector=False,
|
||||
)
|
||||
assert Image.open(img).size == (151, 123), "Image not rotated"
|
||||
|
||||
|
||||
def test_simulated_scan(outdir):
|
||||
canvas = Canvas(
|
||||
fspath(outdir / 'fakescan.pdf'),
|
||||
pagesize=(209.8, 297.6),
|
||||
)
|
||||
|
||||
page_vars = [(2, 36, 250), (91, 170, 240), (179, 190, 36), (271, 36, 36)]
|
||||
|
||||
for n, page_var in enumerate(page_vars):
|
||||
text = canvas.beginText()
|
||||
text.setFont('Helvetica', 20)
|
||||
|
||||
angle, x, y = page_var
|
||||
cos_a, sin_a = cos(angle / 180.0 * pi), sin(angle / 180.0 * pi)
|
||||
|
||||
text.setTextTransform(cos_a, -sin_a, sin_a, cos_a, x, y)
|
||||
text.textOut(f'Page {n + 1}')
|
||||
canvas.drawText(text)
|
||||
canvas.showPage()
|
||||
canvas.save()
|
||||
|
||||
check_ocrmypdf(
|
||||
outdir / 'fakescan.pdf',
|
||||
outdir / 'out.pdf',
|
||||
'--force-ocr',
|
||||
'--deskew',
|
||||
'--rotate-pages',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_debug_rotate.py',
|
||||
)
|
||||
|
||||
with pikepdf.open(outdir / 'out.pdf') as pdf:
|
||||
assert (
|
||||
pdf.pages[1].MediaBox[2] > pdf.pages[1].MediaBox[3]
|
||||
), "Wrong orientation: not landscape"
|
||||
assert (
|
||||
pdf.pages[3].MediaBox[2] > pdf.pages[3].MediaBox[3]
|
||||
), "Wrong orientation: Not landscape"
|
||||
|
||||
assert (
|
||||
pdf.pages[0].MediaBox[2] < pdf.pages[0].MediaBox[3]
|
||||
), "Wrong orientation: Not portrait"
|
||||
assert (
|
||||
pdf.pages[2].MediaBox[2] < pdf.pages[2].MediaBox[3]
|
||||
), "Wrong orientation: Not portrait"
|
||||
|
||||
Reference in New Issue
Block a user