Compare commits

...
24 Commits
Author SHA1 Message Date
James R. Barlow 14a85f9473 Fix pinned dependencies 2020-11-03 04:12:47 -08:00
James R. Barlow d22a1b3367 v11.3.2 release notes (2)
Since we never tagged it, fix other things.
2020-11-03 02:03:25 -08:00
James R. Barlow b913e5dfef ghostscript: don't repeat log in debug
Subprocess already does this for us.
2020-11-03 01:45:06 -08:00
James R. Barlow dd8a5a4c72 Fix log domain names
ocrmypdf.subprocess.subprocess.ghostscript -> ocrmypdf.subprocess.ghostscript
2020-11-03 01:44:35 -08:00
James R. Barlow 36e9a54f02 Remove extraneous page rotation
This was added in commit b5ccbfd but seems to have been ill-advised.
2020-11-03 01:34:28 -08:00
James R. Barlow 3707af3b74 Change pdf.root to pdf.Root 2020-11-03 01:30:31 -08:00
James R. Barlow ced7ad9164 unpaper: round off DPI 2020-11-03 01:14:57 -08:00
James R. Barlow 54bbbfdeb3 Fix UnboundLocalError when considering ImageMasks for optimization
Uncovered by test file in issue 667, although unrelated to that issue.
2020-11-03 01:08:14 -08:00
James R. Barlow 7f73a6ed1e Some Python 3.9 fixes 2020-11-03 00:45:47 -08:00
James R. Barlow dce206d3dc Fix pre-commit for Py3.9 2020-11-03 00:20:25 -08:00
James R. Barlow 9304c856cf Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2020-11-02 02:47:36 -08:00
James R. Barlow e5df98cbdf v11.3.2 release notes 2020-11-02 02:43:32 -08:00
James R. Barlow 19bf3aeb00 api: improve typing 2020-11-02 02:33:34 -08:00
James R. Barlow e86be0031c unpaper: fix process output handling
With the ocrmypdf.subprocess wrapper, logging the output here
is redundant and loses the page number context.
2020-11-02 01:07:41 -08:00
James R. Barlow 6425977998 unpaper: use pnm instead of png
Some users reported problems with PNG recently; try PNM.

Fixes #665
Fixes #667
2020-11-02 01:05:56 -08:00
James R. Barlow d57df2d980 subprocess: support programs that write their messages to stdout 2020-11-02 01:00:59 -08:00
James R. Barlow 664d0c7969 Document configure_debug_logging 2020-11-02 00:59:00 -08:00
James R. Barlow a354663ee1 Fix typo in API documentation 2020-11-02 00:58:28 -08:00
Graham MilnandGitHub b21b048ec4 Add macOS brew language support (#615)
Note `brew` command for installing additional languages on macOS.
2020-10-30 01:09:06 -07:00
James R. Barlow 709c65b41a v11.3.1 release notes 2020-10-27 23:11:11 -07:00
James R. Barlow 67f99c5bb7 Endorse pdfminer.six 20201018 2020-10-27 23:09:45 -07:00
James R. Barlow d55e673d9c Fix warning about --pdfa-image-compression argument at wrong times
Closes #663
2020-10-27 23:09:45 -07:00
James R. Barlow 21b90d2d14 Endorse pikepdf 2.x 2020-10-27 23:09:45 -07:00
Edward BettsandGitHub 2def7e3392 Use % for percentage in string format (#643) 2020-10-27 23:09:14 -07:00
17 changed files with 130 additions and 91 deletions
+1 -1
View File
@@ -19,5 +19,5 @@ repos:
rev: 19.10b0 rev: 19.10b0
hooks: hooks:
- id: black - id: black
language_version: python3.8 language_version: python
exclude: ^src/ocrmypdf/lib/_leptonica.py exclude: ^src/ocrmypdf/lib/_leptonica.py
+3
View File
@@ -92,6 +92,9 @@ apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified lan
# Arch Linux users # Arch Linux users
pacman -S tesseract-data-eng tesseract-data-deu # Example: Install the English and German language packs pacman -S tesseract-data-eng tesseract-data-deu # Example: Install the English and German language packs
# brew macOS users
brew install tesseract-lang
``` ```
You can then pass the `-l LANG` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple languages can be requested. You can then pass the `-l LANG` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple languages can be requested.
+1 -2
View File
@@ -156,9 +156,8 @@ stages:
# versionSpec: "$(python.version)" # versionSpec: "$(python.version)"
- bash: | - bash: |
brew update brew update
brew unlink python@2
brew upgrade python brew upgrade python
echo "Using Python `python3 --version`" echo "Using `python3 --version`"
displayName: "Update brew and Python" displayName: "Update brew and Python"
- bash: | - bash: |
brew install \ brew install \
+23
View File
@@ -12,6 +12,29 @@ 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 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. wish to use some of its features for working with PDFs.
v11.3.2
=======
- Explicitly require pikepdf 2.0.0 or newer when running on Python 3.9. (There are
concerns about the stability of pybind11 2.5.x with Python 3.9, which is used in
pikepdf 1.x.)
- Fixed another issue related to page rotation.
- Fixed an issue where image marked as image masks were not properly considered
as optimization candidates.
- On some systems, unpaper seems to be unable to process the PNGs we offer it
as input. We now convert the input to PNM format, which unpaper always accepts.
Fixes #665 and #667.
- DPI sent to unpaper is now rounded to a more reasonable number of decimal digits.
- Debug and error messages from unpaper were being suppressed.
- Some documentation tweaks.
v11.3.1
=======
- Declare support for new versions: pdfminer.six 20201018 and pikepdf 2.x
- Fix warning related to ``--pdfa-image-compression`` that appears at the wrong
time.
v11.3.0 v11.3.0
======= =======
+7 -7
View File
@@ -1,12 +1,12 @@
# requirements.txt can be used to replicate the developer's build environment # requirements.txt can be used to replicate the developer's build environment
# setup.py lists a separate set of requirements that are looser to simplify # setup.py lists a separate set of requirements that are looser to simplify
# installation # installation
cffi == 1.14.0 cffi == 1.14.3
coloredlogs == 14.0 # technically optional coloredlogs == 14.0 # technically optional
img2pdf == 0.3.6 img2pdf == 0.4.0
pdfminer.six == 20200517 pdfminer.six == 20201018
pikepdf == 1.16.1 pikepdf == 2.0.0
pluggy == 0.13.1 pluggy == 0.13.1
Pillow == 7.1.2 Pillow == 8.0.1
reportlab == 3.5.42 reportlab == 3.5.55
tqdm == 4.46.1 tqdm == 4.51.0
+3 -3
View File
@@ -63,7 +63,6 @@ setup(
python_requires=' >= 3.6', python_requires=' >= 3.6',
setup_requires=[ # can be removed whenever we can drop pip 9 support setup_requires=[ # can be removed whenever we can drop pip 9 support
'cffi >= 1.9.1', # to build the leptonica module 'cffi >= 1.9.1', # to build the leptonica module
'pytest-runner', # to enable python setup.py test
'setuptools_scm', # so that version will work 'setuptools_scm', # so that version will work
'setuptools_scm_git_archive', # enable version from github tarballs 'setuptools_scm_git_archive', # enable version from github tarballs
], ],
@@ -73,8 +72,9 @@ setup(
'cffi >= 1.9.1', # must be a setup and install requirement 'cffi >= 1.9.1', # must be a setup and install requirement
'coloredlogs >= 14.0', # strictly optional 'coloredlogs >= 14.0', # strictly optional
'img2pdf >= 0.3.0, < 0.5', # pure Python, so track HEAD closely 'img2pdf >= 0.3.0, < 0.5', # pure Python, so track HEAD closely
'pdfminer.six >= 20191110, != 20200720, <= 20200726', 'pdfminer.six >= 20191110, != 20200720, <= 20201018',
'pikepdf >= 1.14.0, < 2', "pikepdf >= 1.14.0, < 3 ; python_version < '3.9'",
"pikepdf >= 2.0.0 ; python_version >= '3.9'",
'Pillow >= 7.0.0', 'Pillow >= 7.0.0',
'pluggy >= 0.13.0, < 1.0', 'pluggy >= 0.13.0, < 1.0',
'reportlab >= 3.3.0', # oldest released version with sane image handling 'reportlab >= 3.3.0', # oldest released version with sane image handling
-2
View File
@@ -122,8 +122,6 @@ def rasterize_pdf(
stderr = p.stderr.decode(errors='replace') stderr = p.stderr.decode(errors='replace')
if _gs_error_reported(stderr): if _gs_error_reported(stderr):
log.error(stderr) log.error(stderr)
elif stderr:
log.debug(stderr)
with Image.open(BytesIO(p.stdout)) as im: with Image.open(BytesIO(p.stdout)) as im:
if rotation is not None: if rotation is not None:
+26 -31
View File
@@ -55,21 +55,21 @@ def _setup_unpaper_io(tmpdir: Path, input_file: Path) -> Tuple[Path, Path]:
"Failed to convert image to a supported format." "Failed to convert image to a supported format."
) from e ) from e
if im_modified or input_file.suffix != '.png': if im_modified or input_file.suffix != '.pnm':
input_png = tmpdir / 'input.png' input_pnm = tmpdir / 'input.pnm'
im.save(input_png, format='PNG', compress_level=1) im.save(input_pnm, format='PPM')
else: else:
# No changes, PNG input, just use the file we already have # No changes, PNG input, just use the file we already have
input_png = input_file input_pnm = input_file
output_pnm = tmpdir / f'output{suffix}' output_pnm = tmpdir / f'output{suffix}'
return input_png, output_pnm return input_pnm, output_pnm
def run(input_file, output_file, dpi, mode_args): def run(input_file, output_file, dpi, mode_args):
args_unpaper = ['unpaper', '-v', '--dpi', str(dpi)] + mode_args args_unpaper = ['unpaper', '-v', '--dpi', str(round(dpi, 6))] + mode_args
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
input_png, output_pnm = _setup_unpaper_io(Path(tmpdir), input_file) input_pnm, output_pnm = _setup_unpaper_io(Path(tmpdir), input_file)
# To prevent any shenanigans from accepting arbitrary parameters in # To prevent any shenanigans from accepting arbitrary parameters in
# --unpaper-args, we: # --unpaper-args, we:
@@ -78,31 +78,26 @@ def run(input_file, output_file, dpi, mode_args):
# 3) append absolute paths for the input and output file # 3) append absolute paths for the input and output file
# This should ensure that a user cannot clobber some other file with # This should ensure that a user cannot clobber some other file with
# their unpaper arguments (whether intentionally or otherwise) # their unpaper arguments (whether intentionally or otherwise)
args_unpaper.extend([os.fspath(input_png), os.fspath(output_pnm)]) args_unpaper.extend([os.fspath(input_pnm), os.fspath(output_pnm)])
external_run(
args_unpaper,
close_fds=True,
check=True,
universal_newlines=True,
stderr=STDOUT, # unpaper writes logging output to stdout and stderr
stdout=PIPE, # and cannot send file output to stdout
cwd=tmpdir,
logs_errors_to_stdout=True,
)
try: try:
proc = external_run( with Image.open(output_pnm) as imout:
args_unpaper, imout.save(output_file, dpi=(dpi, dpi))
check=True, except (FileNotFoundError, OSError):
close_fds=True, raise SubprocessOutputError(
universal_newlines=True, "unpaper: failed to produce the expected output file. "
stderr=STDOUT, # unpaper writes logging output to stdout and stderr + " Called with: "
cwd=tmpdir, # and cannot send file output to stdout + str(args_unpaper)
stdout=PIPE, ) from None
)
except CalledProcessError as e:
log.debug(e.stderr)
raise e from e
else:
log.debug(proc.stderr)
try:
with Image.open(output_pnm) as imout:
imout.save(output_file, dpi=(dpi, dpi))
except (FileNotFoundError, OSError):
raise SubprocessOutputError(
"unpaper: failed to produce the expected output file. "
+ " Called with: "
+ str(args_unpaper)
) from None
def validate_custom_args(args: str): def validate_custom_args(args: str):
-3
View File
@@ -536,9 +536,6 @@ def create_ocr_image(image: Path, page_context: PageContext):
# Pillow requires integer DPI # Pillow requires integer DPI
dpi = tuple(round(coord) for coord in im.info['dpi']) dpi = tuple(round(coord) for coord in im.info['dpi'])
if page_context.pageinfo.rotation != 0:
log.info(f"Rotating {page_context.pageinfo.rotation}")
im = im.rotate(page_context.pageinfo.rotation)
im.save(output_file, dpi=dpi) im.save(output_file, dpi=dpi)
return output_file return output_file
+8 -1
View File
@@ -307,7 +307,14 @@ class NeverRaise(Exception):
pass # pylint: disable=unnecessary-pass pass # pylint: disable=unnecessary-pass
def configure_debug_logging(log_filename, prefix=''): def configure_debug_logging(log_filename, prefix: str = ''):
"""
Create a debug log file at a specified location.
Arguments:
log_filename: Where to the put the log file.
prefix: The logging domain prefix that should be sent to the log.
"""
log_file_handler = logging.FileHandler(log_filename, delay=True) log_file_handler = logging.FileHandler(log_filename, delay=True)
log_file_handler.setLevel(logging.DEBUG) log_file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter( formatter = logging.Formatter(
+3 -3
View File
@@ -213,12 +213,12 @@ def check_options_optimizing(options):
def check_options_advanced(options): def check_options_advanced(options):
if options.pdfa_image_compression != 'auto' and options.output_type.startswith( if options.pdfa_image_compression != 'auto' and not options.output_type.startswith(
'pdfa' 'pdfa'
): ):
log.warning( log.warning(
"--pdfa-image-compression argument has no effect when " "--pdfa-image-compression argument only applies when "
"--output-type is not 'pdfa', 'pdfa-1', or 'pdfa-2'" "--output-type is one of 'pdfa', 'pdfa-1', or 'pdfa-2'"
) )
+6 -5
View File
@@ -10,7 +10,7 @@ import os
import sys import sys
from enum import IntEnum from enum import IntEnum
from pathlib import Path from pathlib import Path
from typing import BinaryIO, Iterable, Union from typing import AnyStr, BinaryIO, Iterable, Optional, Union
from warnings import warn from warnings import warn
from ocrmypdf._logging import PageNumberFilter, TqdmConsole from ocrmypdf._logging import PageNumberFilter, TqdmConsole
@@ -26,7 +26,8 @@ except ModuleNotFoundError:
coloredlogs = None coloredlogs = None
PathOrIO = Union[BinaryIO, os.PathLike, str, bytes] StrPath = Union[os.PathLike, AnyStr]
PathOrIO = Union[BinaryIO, StrPath]
class Verbosity(IntEnum): class Verbosity(IntEnum):
@@ -202,7 +203,7 @@ def ocr( # pylint: disable=unused-argument
language: Iterable[str] = None, language: Iterable[str] = None,
image_dpi: int = None, image_dpi: int = None,
output_type=None, output_type=None,
sidecar: os.PathLike = None, sidecar: Optional[StrPath] = None,
jobs: int = None, jobs: int = None,
use_threads: bool = None, use_threads: bool = None,
title: str = None, title: str = None,
@@ -239,7 +240,7 @@ def ocr( # pylint: disable=unused-argument
user_words: os.PathLike = None, user_words: os.PathLike = None,
user_patterns: os.PathLike = None, user_patterns: os.PathLike = None,
fast_web_view: float = None, fast_web_view: float = None,
plugins: Iterable[Union[str, Path]] = None, plugins: Iterable[StrPath] = None,
keep_temporary_files: bool = None, keep_temporary_files: bool = None,
progress_bar: bool = None, progress_bar: bool = None,
**kwargs, **kwargs,
@@ -261,7 +262,7 @@ def ocr( # pylint: disable=unused-argument
read. read.
output_file: If a :class:`pathlib.Path`, ``str`` or ``bytes``, this is output_file: If a :class:`pathlib.Path`, ``str`` or ``bytes``, this is
interpreted as file system path to the output file. If the object interpreted as file system path to the output file. If the object
appears to be a writable stream (with methods such as ``.read()`` and appears to be a writable stream (with methods such as ``.write()`` and
``.seek()``), the output will be written to this stream. If ``.seek()``), the output will be written to this stream. If
``output_file`` is ``"-"``, the output will be written to ``sys.stdout`` ``output_file`` is ``"-"``, the output will be written to ``sys.stdout``
(provided that standard output does not seem to be a terminal device). (provided that standard output does not seem to be a terminal device).
+24 -20
View File
@@ -115,25 +115,29 @@ def extract_image_jbig2(
and filtdp[0] != Name.JBIG2Decode and filtdp[0] != Name.JBIG2Decode
and jbig2enc.available() and jbig2enc.available()
): ):
try: # Save any colorspace associated with the image, so that we
# Save any colorspace associated with the image, so that we # will export a pure 1-bit PNG with no palette or ICC profile.
# will export a pure 1-bit PNG with no palette or ICC profile. # Showing the palette or ICC to jbig2enc will cause it to perform
# Showing the palette or ICC to jbig2enc will cause it to perform # colorspace transform to 1bpp, which will conflict the palette or
# colorspace transform to 1bpp, which will conflict the palette or # ICC if it exists.
# ICC if it exists. colorspace = pim.obj.get(pikepdf.Name.ColorSpace, None)
colorspace = pim.obj.ColorSpace if colorspace is not None or pim.image_mask:
# Set to DeviceGray temporarily; we already in 1 bpc. try:
pim.obj.ColorSpace = pikepdf.Name.DeviceGray # Set to DeviceGray temporarily; we already in 1 bpc.
imgname = root / f'{xref:08d}' pim.obj.ColorSpace = pikepdf.Name.DeviceGray
with imgname.open('wb') as f: imgname = root / f'{xref:08d}'
ext = pim.extract_to(stream=f) with imgname.open('wb') as f:
imgname.rename(imgname.with_suffix(ext)) ext = pim.extract_to(stream=f)
except pikepdf.UnsupportedImageTypeError: imgname.rename(imgname.with_suffix(ext))
return None except pikepdf.UnsupportedImageTypeError:
finally: return None
# Restore image colorspace after temporarily setting it to DeviceGray finally:
pim.obj.ColorSpace = colorspace # Restore image colorspace after temporarily setting it to DeviceGray
return XrefExt(xref, ext) if colorspace is not None:
pim.obj.ColorSpace = colorspace
else:
del pim.obj.ColorSpace
return XrefExt(xref, ext)
return None return None
@@ -613,7 +617,7 @@ def optimize(input_file: Path, output_file: Path, context, save_settings) -> Non
) )
ratio = input_size / output_size ratio = input_size / output_size
savings = 1 - output_size / input_size savings = 1 - output_size / input_size
log.info(f"Optimize ratio: {ratio:.2f} savings: {(100 * savings):.1f}%") log.info(f"Optimize ratio: {ratio:.2f} savings: {(savings):.1%}")
if savings < 0: if savings < 0:
log.info("Image optimization did not improve the file - discarded") log.info("Image optimization did not improve the file - discarded")
+4 -4
View File
@@ -818,12 +818,12 @@ class PdfInfo:
check_pages=check_pages, check_pages=check_pages,
detailed_analysis=detailed_analysis, detailed_analysis=detailed_analysis,
) )
self._needs_rendering = pdf.root.get('/NeedsRendering', False) self._needs_rendering = pdf.Root.get('/NeedsRendering', False)
self._has_acroform = False self._has_acroform = False
if '/AcroForm' in pdf.root: if '/AcroForm' in pdf.Root:
if len(pdf.root.AcroForm.get('/Fields', [])) > 0: if len(pdf.Root.AcroForm.get('/Fields', [])) > 0:
self._has_acroform = True self._has_acroform = True
elif '/XFA' in pdf.root.AcroForm: elif '/XFA' in pdf.Root.AcroForm:
self._has_acroform = True self._has_acroform = True
@property @property
+17 -5
View File
@@ -25,13 +25,21 @@ from ocrmypdf.exceptions import MissingDependencyError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def run(args, *, env=None, **kwargs): def run(args, *, env=None, logs_errors_to_stdout=False, **kwargs):
"""Wrapper around :py:func:`subprocess.run` """Wrapper around :py:func:`subprocess.run`
The main purpose of this wrapper is to log subprocess output in an orderly The main purpose of this wrapper is to log subprocess output in an orderly
fashion that indentifies the responsible subprocess. An additional fashion that indentifies the responsible subprocess. An additional
task is that this function goes to greater lengths to find possible Windows task is that this function goes to greater lengths to find possible Windows
locations of our dependencies when they are not on the system PATH. locations of our dependencies when they are not on the system PATH.
Arguments should be identical to ``subprocess.run``, except for following:
Arguments:
logs_errors_to_stdout: If True, indicates that the process writes its error
messages to stdout rather than stderr, so stdout should be logged
if there is an error. If False, stderr is logged. Could be used with
stderr=STDOUT, stdout=PIPE for example.
""" """
if not env: if not env:
env = os.environ env = os.environ
@@ -43,25 +51,29 @@ def run(args, *, env=None, **kwargs):
args = _fix_windows_args(program, args, env) args = _fix_windows_args(program, args, env)
log.debug("Running: %s", args) log.debug("Running: %s", args)
process_log = log.getChild('subprocess.' + os.path.basename(program)) process_log = log.getChild(os.path.basename(program))
if sys.version_info < (3, 7) and os.name == 'nt': if sys.version_info < (3, 7) and os.name == 'nt':
# Can't use close_fds=True on Windows with Python 3.6 or older # Can't use close_fds=True on Windows with Python 3.6 or older
# https://bugs.python.org/issue19575, etc. # https://bugs.python.org/issue19575, etc.
kwargs['close_fds'] = False kwargs['close_fds'] = False
stderr = None stderr = None
stderr_name = 'stderr' if not logs_errors_to_stdout else 'stdout'
try: try:
proc = subprocess_run(args, env=env, **kwargs) proc = subprocess_run(args, env=env, **kwargs)
except CalledProcessError as e: except CalledProcessError as e:
stderr = getattr(e, 'stderr', None) stderr = getattr(e, stderr_name, None)
raise raise
else: else:
stderr = getattr(proc, 'stderr', None) stderr = getattr(proc, stderr_name, None)
finally: finally:
if process_log.isEnabledFor(logging.DEBUG) and stderr: if process_log.isEnabledFor(logging.DEBUG) and stderr:
with suppress(AttributeError, UnicodeDecodeError): with suppress(AttributeError, UnicodeDecodeError):
stderr = stderr.decode('utf-8', 'replace') stderr = stderr.decode('utf-8', 'replace')
process_log.debug("stderr = %s", stderr) if logs_errors_to_stdout:
process_log.debug("stdout/stderr = %s", stderr)
else:
process_log.debug("stderr = %s", stderr)
return proc return proc
+2 -2
View File
@@ -302,8 +302,8 @@ def test_kodak_toc(resources, outpdf):
p = pikepdf.open(outpdf) p = pikepdf.open(outpdf)
if pikepdf.Name.First in p.root.Outlines: if pikepdf.Name.First in p.Root.Outlines:
assert isinstance(p.root.Outlines.First, pikepdf.Dictionary) assert isinstance(p.Root.Outlines.First, pikepdf.Dictionary)
def test_metadata_fixup_warning(resources, outdir, caplog): def test_metadata_fixup_warning(resources, outdir, caplog):
+2 -2
View File
@@ -137,9 +137,9 @@ def test_report_file_size(tmp_path, caplog):
caplog.clear() caplog.clear()
waste_of_space = b'Dummy' * 5000 waste_of_space = b'Dummy' * 5000
pdf.root.Dummy = waste_of_space pdf.Root.Dummy = waste_of_space
pdf.save(in_) pdf.save(in_)
pdf.root.Dummy2 = waste_of_space + waste_of_space pdf.Root.Dummy2 = waste_of_space + waste_of_space
pdf.save(out) pdf.save(out)
with patch('ocrmypdf._validation.jbig2enc.available', return_value=True), patch( with patch('ocrmypdf._validation.jbig2enc.available', return_value=True), patch(