Compare commits

...
35 Commits
Author SHA1 Message Date
James R. Barlow cf7c20ca16 v13.4.3 release notes 2022-04-14 20:19:31 -07:00
James R. Barlow b00fe3dc5d pytest.skip() - remove kwarg entirely, to avoid breaking older pytest and not getting warns from newer pytest 2022-04-14 20:15:00 -07:00
James R. Barlow e6aa3a4299 tests: explain why CacheOcrEngine needs lock 2022-04-05 16:16:51 -07:00
James R. Barlow 24f1b57288 Merge branch 'master' of github.com:ocrmypdf/OCRmyPDF 2022-04-05 16:04:45 -07:00
James R. Barlow 43302d7e12 Fix pytest.warns() on older pytest
Thanks @QuLogic
2022-04-05 16:02:50 -07:00
Christopher BeschandGitHub fed0226761 Add Gentoo Language Installation Instructions (#936) 2022-04-05 00:26:24 -07:00
Joseph MorrisandGitHub 27e22b4f07 Update jbig2.rst (#911)
Adding Ubuntu package names for dependencies I needed to save others time. The tequired package for leptonica is particularly confusing since configure just says "Error! Leptonica not detected." and there are multiple leptonica packages
2022-04-05 00:25:28 -07:00
James R. Barlow 79382a6039 Docker: Change base image to debian
Debian has latest qpdf.
2022-04-04 00:40:03 -07:00
James R. Barlow 7788d94c4a v13.4.2 release notes, take 2 2022-04-03 23:58:37 -07:00
James R. Barlow 33bfba8449 ghostscript: match actual reported version 2022-04-03 23:35:30 -07:00
James R. Barlow 1d0584c644 Add advisory note to release notes 2022-04-03 23:04:55 -07:00
James Barlow 84b9d4d021 Speculation: Ghostscript 9.56 new PDF interpreter breaks things 2022-04-03 21:14:19 -07:00
James Barlow 41efd3bf0f Fix Python "3.10" 2022-04-03 21:13:59 -07:00
James Barlow 776ada6713 Upgrade pre-commit and associated tools; various lints 2022-04-03 20:53:01 -07:00
James Barlow f3593c915d ci: test Python 3.10 2022-04-03 19:06:50 -07:00
James Barlow dfe31a2f6d Add lock to certain "with patch" cases
Switch to --use-threads seems to have broken tests that assumed they could
monkeypatch things. Although that's odd, since while we can have multiple
worker threads, we should never have
parallel tests in the same process.
2022-04-03 17:22:04 -07:00
James Barlow 0c43963d69 Fix pytest deprecation warnings 2022-04-03 13:30:58 -07:00
James Barlow f29fe7f23e Fix Pillow deprecation warnings 2022-04-03 13:30:50 -07:00
James R. Barlow 04996caac3 pdfminer 20220319 2022-03-22 22:36:06 -07:00
James R. Barlow 13917c051c Disable oom killer test for --use-threads 2022-03-13 01:02:28 -08:00
James R. Barlow 8182fe9c92 Make --use-threads default and update release notes
Re: issue Hanging on Random Files  #814
2022-03-13 00:37:06 -08:00
James R. Barlow 1950acfbda docs: proofread plugins 2022-02-20 00:56:43 -08:00
James R. Barlow fca6403083 Disallow pikepdf 5.0.0 2022-02-19 01:45:36 -08:00
James R. Barlow c4e2fce1ef v13.4.0 release notes (2) 2022-02-18 23:37:33 -08:00
James R. Barlow 3546479658 optimize: don't deflate JPEGs with fancy DecodeParms settings
This is overly cautious but will do for now.
2022-02-18 23:36:53 -08:00
James R. Barlow 72442fa3d0 Fix error messages when run with pikepdf 5.0.0
Appears that these are spurious errors from qpdf probing the /DecodeParms
dict on images that don't have it.

Not sure exactly why it happens but this can be safely ignored.
2022-02-18 23:35:44 -08:00
James R. Barlow 8f714b1375 v13.4.0 release notes 2022-02-18 23:08:48 -08:00
James R. Barlow cb05c1d122 pdfinfo: a few annotations 2022-02-14 23:31:53 -08:00
James R. Barlow b0ad07bc5f Merge branch 'master' of github.com:ocrmypdf/OCRmyPDF 2022-02-08 00:46:26 -08:00
James R. Barlow 514038d4ec optimize: recognize and produce [/FlateDecode /DCTDecode] images 2022-02-08 00:38:08 -08:00
James R. Barlow 50d76e7f6c optimize: remove comment about issue in Pillow that is now fixed 2022-02-07 22:18:20 -08:00
James R. Barlow 6c78a46285 optimize: remove inaccurate about ICCs
pikepdf will now get the ICC profile out and put it in the JPEG.
2022-02-07 19:42:13 -08:00
James R. Barlow 863d560632 optimize: clarify log message about skipping images with multiple filters 2022-02-06 22:41:10 -08:00
James R. Barlow 73934c854c release notes: typo 2022-02-06 22:39:32 -08:00
rdiezandGitHub 2be8eeec2c Fix spelling of 'ephemeral' (#908) 2022-02-03 10:33:51 -08:00
29 changed files with 314 additions and 95 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# OCRmyPDF
#
FROM ubuntu:21.04 as base
FROM debian:bookworm-slim as base
ENV LANG=C.UTF-8
ENV TZ=UTC
+9 -7
View File
@@ -21,17 +21,19 @@ jobs:
matrix:
include:
- os: ubuntu-18.04
python: 3.7
python: "3.7"
- os: ubuntu-20.04
python: 3.8
python: "3.8"
- os: ubuntu-20.04
python: 3.9
python: "3.9"
- os: ubuntu-20.04
python: "3.10"
- os: ubuntu-latest
python: 3.9
python: "3.9"
- os: ubuntu-latest
python: "pypy-3.7"
- os: ubuntu-latest
python: 3.9
python: "3.9"
tesseract5: true
env:
@@ -120,7 +122,7 @@ jobs:
strategy:
matrix:
os: [macos-latest]
python: ["3.9"]
python: ["3.9", "3.10"]
env:
OS: ${{ matrix.os }}
@@ -175,7 +177,7 @@ jobs:
strategy:
matrix:
os: [windows-latest]
python: ["3.9"]
python: ["3.9", "3.10"]
env:
OS: ${{ matrix.os }}
+6 -6
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
rev: v4.1.0
hooks:
- id: check-case-conflict
- id: check-merge-conflict
@@ -8,26 +8,26 @@ repos:
- id: check-yaml
- id: debug-statements
- repo: https://github.com/pycqa/isort
rev: 5.9.3
rev: 5.10.1
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 21.9b0
rev: 22.3.0
hooks:
- id: black
language_version: python
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.19.0
rev: v1.20.1
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/pyupgrade
rev: v2.29.0
rev: v2.31.1
hooks:
- id: pyupgrade
args: ["--py37-plus"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.910-1
rev: v0.942
hooks:
- id: mypy
additional_dependencies:
+1 -1
View File
@@ -59,7 +59,7 @@ Using the Docker image on the command line
==========================================
**Unlike typical Docker containers**, in this section the OCRmyPDF Docker
container is emphemeral it runs for one OCR job and terminates, just like a
container is ephemeral it runs for one OCR job and terminates, just like a
command line program. We are using Docker to deliver an application (as opposed
to the more conventional case, where a Docker container runs as a server).
+3
View File
@@ -32,6 +32,9 @@ For all other Linux, you must build a JBIG2 encoder from source:
.. _jbig2-lossy:
Dependencies include libtoolize and libleptonica, which on Ubuntu systems
are packaged as libtool and libleptonica-dev.
Lossy mode JBIG2
================
+27
View File
@@ -54,6 +54,33 @@ to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
Gentoo users
============
On Gentoo the package ``app-text/tessdata_fast``, which ``app-text/tesseract`` depends on, handles Tesseract languages.
It accepts USE flags to select what languages should be installed, these can be set in ``/etc/portage/package.use``.
Alternatively one can globally set the `L10N use extension <https://wiki.gentoo.org/wiki/Localization/Guide#L10N>`__ in ``/etc/portage/make.conf``.
This enables these languages for all packages (e.g. including aspell).
.. code-block:: bash
# Display a list of all Tesseract language packs
equery uses app-text/tessdata_fast
# Add English and German language support for Tesseract only
echo 'app-text/tessdata_fast l10n_de l10n_en' >> /etc/portage/package.use
# Add global English and German language support (the `l10n_` from equery has to be omited)
echo L10N="de en" >> /etc/portage/make.conf
# update system to reflect changed USE flags
emerge --update --deep --newuse @world
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 using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
macOS users
===========
+10
View File
@@ -152,6 +152,16 @@ hooks. As such, you cannot "chain" a series of plugin filters together in this
way. Instead, a single hook implementation should be responsible for any such
chaining operations.
Examples
========
* OCRmyPDF's test suite contains several plugins that are used to simulate certain
test conditions.
* `ocrmypdf-papermerge <https://github.com/papermerge/OCRmyPDF_papermerge>`_ is
a production plugin that integrates OCRmyPDF and the Papermerge document
management system.
Custom command line arguments
-----------------------------
+50 -4
View File
@@ -10,7 +10,53 @@ that is, output messages may be improved at any release level, so parsing them
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..
wish to use some of its features for working with PDFs.
The most recent release of OCRmyPDF is |OCRmyPDF PyPI|. Any newer versions
referred to in these notes may exist the main branch but have not been
tagged yet.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
v13.4.3
=======
- Fix error on pytest.skip() with older versions of pytest.
- Documentation updates.
v13.4.2
=======
- Worked around a
`major regression in Ghostscript 9.56.0 <https://bugs.ghostscript.com/show_bug.cgi?id=705187>`__
where **all OCR text is stripped out of the PDF**. It simply removes all text,
even generated by software other than OCRmyPDF. Fortunately, we can ask
Ghostscript 9.56.0 to use its old behavior that worked correctly for our purposes.
Users must avoid the combination (Ghostscript 9.56.0, ocrmypdf <13.4.2) since
older versions of OCRmyPDF have no way of detecting that this particular
version of Ghostscript removes all OCR text.
- Marked pdfminer 20220319 as supported.
- Fixed some deprecation warnings from recent versions of Pillow and pytest.
- Test suite now covers Python 3.10 (Python 3.10 worked fine before, but was not
being tested).
- Docker image now uses debian:bookworm-slim as the base image to fix the Docker
image build.
v13.4.1
=======
- Temporarily make threads rather than processes the default executor worker, due
to a persistent deadlock issue when processes are used. Add a new command line
argument ``--no-use-threads`` to disable this.
v13.4.0
=======
- Fixed test failures when using pikepdf 5.0.0.
- Various improvements to the optimizer. In particular, we now recognize PDF images
that are encoded with both deflate (PNG) and DCT (JPEG), and also produce PDF
with images compressed with deflate and DCT, since this often yields file size
improvements compared to plain DCT.
v13.3.0
=======
@@ -21,8 +67,8 @@ v13.3.0
C library used by unpaper so it cannot be rectified easily.
- We now use better default settings when calling img2pdf.
- We no longer try to optimize images that we failed to save in certain situations.
- We now account for some differences in text output from Tesseract 5 that differs
from Tesseract 4.
- We now account for some differences in text output from Tesseract 5 compared to
Tesseract 4.
- Better handling of Ghostscript producing empty images when attempting to rasterize
page images.
@@ -73,7 +119,7 @@ v13.0.0
Tesseract 5.x has implemented improvements to thresholding, so this feature will be
redundant anyway.
- ``--deskew`` was previous calculated by a Leptonica algorithm. We now use a feature
of Tesseract to term the appropriate the angle to deskew a page. The deskew angle
of Tesseract to find the appropriate the angle to deskew a page. The deskew angle
according to Tesseract may differ from Leptonica's algorithm. At least in theory,
Tesseract's deskew angle is informed by a more complex analysis than Leptonica,
so this should improve results in general. We also use Pillow to perform the
+1 -1
View File
@@ -59,7 +59,7 @@ def do_ocrmypdf(file):
return Response("--sidecar not supported", 501, mimetype='text/plain')
ocrmypdf_args = ["ocrmypdf", *cmd_args, up_file, down_file]
proc = run(ocrmypdf_args, stdout=PIPE, stderr=PIPE, encoding="utf-8")
proc = run(ocrmypdf_args, capture_output=True, encoding="utf-8")
if proc.returncode != 0:
stderr = proc.stderr
return Response(stderr, 400, mimetype='text/plain')
+2 -2
View File
@@ -49,8 +49,8 @@ install_requires =
coloredlogs>=14.0 # strictly optional
img2pdf>=0.3.0,<0.5 # pure Python
packaging>=20
pdfminer.six!=20200720,>=20191110,<=20211012
pikepdf>=4.0.0
pdfminer.six!=20200720,>=20191110,<=20220319
pikepdf!=5.0.0,>=4.0.0
pluggy>=0.13.0,<2
reportlab>=3.5.66
tqdm>=4
+16 -5
View File
@@ -24,6 +24,13 @@ from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
from ocrmypdf.helpers import Resolution
from ocrmypdf.subprocess import get_version, run, run_polling_stderr
# Remove this workaround when we require Pillow >= 10
try:
Transpose = Image.Transpose # type: ignore
except AttributeError:
# Pillow 9 shim
Transpose = Image # type: ignore
log = logging.getLogger(__name__)
missing_gs_error = """
@@ -132,11 +139,11 @@ def rasterize_pdf(
# rotation is a clockwise angle and Image.ROTATE_* is
# counterclockwise so this cancels out the rotation
if rotation == 90:
im = im.transpose(Image.ROTATE_90)
im = im.transpose(Transpose.ROTATE_90)
elif rotation == 180:
im = im.transpose(Image.ROTATE_180)
im = im.transpose(Transpose.ROTATE_180)
elif rotation == 270:
im = im.transpose(Image.ROTATE_270)
im = im.transpose(Transpose.ROTATE_270)
if rotation % 180 == 90:
page_dpi = page_dpi.flip_axis()
im.save(fspath(output_file), dpi=page_dpi)
@@ -210,14 +217,18 @@ def generate_pdfa(
# Older versions of Ghostscript expect a leading slash in
# sColorConversionStrategy, newer ones should not have it. See Ghostscript
# git commit fe1c025d.
strategy = ('/' + strategy) if version() < '9.19' else strategy
gs_version = version()
strategy = ('/' + strategy) if gs_version < '9.19' else strategy
if version() == '9.23':
if gs_version == '9.23':
# 9.23: added JPEG passthrough as a new feature, but with a bug that
# incorrectly formats some images. Fixed as of 9.24. So we disable this
# feature for 9.23.
# https://bugs.ghostscript.com/show_bug.cgi?id=699216
compression_args.append('-dPassThroughJPEGImages=false')
elif gs_version == '9.56.0':
# 9.56.0 breaks our OCR...?
compression_args.append('-dNEWPDF=false')
# nb no need to specify ProcessColorModel when ColorConversionStrategy
# is set; see:
+2 -2
View File
@@ -18,7 +18,7 @@ from decimal import Decimal
from pathlib import Path
from subprocess import PIPE, STDOUT
from tempfile import TemporaryDirectory
from typing import List, Optional, Tuple, Union
from typing import Iterator, List, Optional, Tuple, Union
from PIL import Image
@@ -76,7 +76,7 @@ def _convert_image(im: Image.Image) -> Tuple[Image.Image, bool, str]:
@contextmanager
def _setup_unpaper_io(input_file: Path) -> Tuple[Path, Path, Path]:
def _setup_unpaper_io(input_file: Path) -> Iterator[Tuple[Path, Path, Path]]:
with Image.open(input_file) as im:
if im.width * im.height >= UNPAPER_IMAGE_PIXEL_LIMIT:
raise UnpaperImageTooLargeError(w=im.width, h=im.height)
+11 -2
View File
@@ -18,7 +18,7 @@ from typing import Dict, Iterable, Optional
import img2pdf
import pikepdf
from pikepdf.models.metadata import encode_pdf_date
from PIL import Image, ImageDraw
from PIL import Image, ImageColor, ImageDraw
from ocrmypdf._concurrent import Executor
from ocrmypdf._exec import unpaper
@@ -38,6 +38,13 @@ from ocrmypdf.optimize import optimize
from ocrmypdf.pdfa import generate_pdfa_ps
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
# Remove this workaround when we require Pillow >= 10
try:
BICUBIC = Image.Resampling.BICUBIC # type: ignore
except AttributeError:
# Pillow 9 shim
BICUBIC = Image.BICUBIC # type: ignore
log = logging.getLogger(__name__)
VECTOR_PAGE_DPI = 400
@@ -484,7 +491,9 @@ def preprocess_deskew(input_file: Path, page_context: PageContext):
# According to Pillow docs, .rotate() will automatically use Image.NEAREST
# resampling if image is mode '1' or 'P'
deskewed = im.rotate(
deskew_angle_degrees, resample=Image.BICUBIC, fillcolor='white'
deskew_angle_degrees,
resample=BICUBIC,
fillcolor=ImageColor.getcolor('white', mode=im.mode),
)
deskewed.save(output_file, dpi=dpi)
+1 -1
View File
@@ -41,7 +41,7 @@ log = logging.getLogger(__name__)
def check_platform():
if os.name == 'nt' and sys.maxsize <= 2 ** 32: # pragma: no cover
if os.name == 'nt' and sys.maxsize <= 2**32: # pragma: no cover
# 32-bit interpreter on Windows
log.error(
"You are running OCRmyPDF in a 32-bit (x86) Python interpreter."
+7 -1
View File
@@ -228,7 +228,13 @@ Online documentation is located at:
help=argparse.SUPPRESS,
)
jobcontrol.add_argument(
'--use-threads', action='store_true', help=argparse.SUPPRESS
'--use-threads', action='store_true', default=True, help=argparse.SUPPRESS
)
jobcontrol.add_argument(
'--no-use-threads',
action='store_false',
dest='use_threads',
help=argparse.SUPPRESS,
)
metadata = parser.add_argument_group(
+9 -1
View File
@@ -220,11 +220,19 @@ def check_pdf(input_file: Path) -> bool:
else:
with pdf:
messages = pdf.check()
success = True
for msg in messages:
if 'error' in msg.lower():
log.error(msg)
success = False
elif (
"/DecodeParms: operation for dictionary attempted on object "
"of type null" in msg
):
pass # Ignore/spurious warning
else:
log.warning(msg)
success = False
sio = StringIO()
linearize_msgs = ''
@@ -239,7 +247,7 @@ def check_pdf(input_file: Path) -> bool:
if linearize_msgs:
log.warning(linearize_msgs)
if not messages and not linearize_msgs:
if success and not linearize_msgs:
return True
return False
+105 -28
View File
@@ -8,6 +8,7 @@
import logging
import sys
import tempfile
import threading
from collections import defaultdict
from os import fspath
from pathlib import Path
@@ -23,6 +24,7 @@ from typing import (
Sequence,
Tuple,
)
from zlib import compress
import img2pdf
from pikepdf import (
@@ -31,6 +33,7 @@ from pikepdf import (
Object,
ObjectStreamMode,
Pdf,
PdfError,
PdfImage,
Stream,
UnsupportedImageTypeError,
@@ -78,33 +81,49 @@ def extract_image_filter(
if image.Subtype != Name.Image:
return None
if image.Length < 100:
log.debug(f"Skipping small image, xref {xref}")
log.debug(f"xref {xref}: skipping image with small stream size")
return None
if image.Width < 8 or image.Height < 8: # Issue 732
log.debug(f"Skipping oddly sized image, xref {xref}")
log.debug(f"xref {xref}: skipping image with unusually small dimensions")
return None
pim = PdfImage(image)
if len(pim.filter_decodeparms) > 1:
log.debug(f"Skipping multiply filtered image, xref {xref}")
return None
filtdp = pim.filter_decodeparms[0]
first_filtdp = pim.filter_decodeparms[0]
second_filtdp = pim.filter_decodeparms[1]
if (
len(pim.filter_decodeparms) == 2
and first_filtdp[0] == Name.FlateDecode
and first_filtdp[1].get(Name.Predictor, 1) == 1
and second_filtdp[0] == Name.DCTDecode
and not second_filtdp[1]
):
log.debug(
f"xref {xref}: found image compressed as /FlateDecode /DCTDecode, "
"marked for JPEG optimization"
)
filtdp = pim.filter_decodeparms[1]
else:
log.debug(f"xref {xref}: skipping image with multiple compression filters")
return None
else:
filtdp = pim.filter_decodeparms[0]
if pim.bits_per_component > 8:
log.debug(f"Skipping wide gamut image, xref {xref}")
log.debug(f"xref {xref}: skipping wide gamut image")
return None # Don't mess with wide gamut images
if filtdp[0] == Name.JPXDecode:
log.debug(f"Skipping JPEG2000 image, xref {xref}")
log.debug(f"xref {xref}: skipping JPEG2000 image")
return None # Don't do JPEG2000
if filtdp[0] == Name.CCITTFaxDecode and filtdp[1].get('/K', 0) >= 0:
log.debug(f"Skipping CCITT Group 3 image, xref {xref}")
log.debug(f"xref {xref}: skipping CCITT Group 3 image")
return None # pikepdf doesn't support Group 3 yet
if Name.Decode in image:
log.debug(f"Skipping image with Decode table, xref {xref}")
log.debug(f"xref {xref}: skipping image with Decode table")
return None # Don't mess with custom Decode tables
return pim, filtdp
@@ -172,14 +191,6 @@ def extract_image_generic(
# jpeg_quality_estimate = 117.0 * (bytes_per_pixel ** 0.213)
# if jpeg_quality_estimate < 65:
# return None
# We could get the ICC profile here, but there's no need to look at it
# for quality transcoding
# if icc:
# stream = BytesIO(raw_jpeg.read_raw_bytes())
# iccbytes = icc.read_bytes()
# with Image.open(stream) as im:
# im.save(jpg_name(root, xref), icc_profile=iccbytes)
try:
imgname = root / f'{xref:08d}'
with imgname.open('wb') as f:
@@ -259,9 +270,9 @@ def extract_images(
# Ignore soft masks
smask_xref = Xref(image.SMask.objgen[0])
exclude_xrefs.add(smask_xref)
log.debug(f"Skipping image {smask_xref} because it is an SMask")
log.debug(f"xref {smask_xref}: skipping image because it is an SMask")
include_xrefs.add(xref)
log.debug(f"Treating {xref} as an optimization candidate")
log.debug(f"xref {xref}: treating as an optimization candidate")
if xref not in pageno_for_xref:
pageno_for_xref[xref] = pageno
@@ -273,7 +284,9 @@ def extract_images(
pike=pike, root=root, image=image, xref=xref, options=options
)
except Exception: # pylint: disable=broad-except
log.exception(f"While extracting image xref {xref}, an error occurred")
log.exception(
f"xref {xref}: While extracting this image, an error occurred"
)
errors += 1
else:
if result:
@@ -294,7 +307,7 @@ def extract_images_generic(
pngs.append(xref_ext.xref)
elif xref_ext.ext == '.jpg':
jpegs.append(xref_ext.xref)
log.debug("Optimizable images: JPEGs: %s PNGs: %s", len(jpegs), len(pngs))
log.debug(f"Optimizable images: JPEGs: {len(jpegs)} PNGs: {len(pngs)}")
return jpegs, pngs
@@ -306,7 +319,7 @@ def extract_images_jbig2(pike: Pdf, root: Path, options) -> Dict[int, List[XrefE
group = pageno // options.jbig2_page_group_size
jbig2_groups[group].append(xref_ext)
log.debug("Optimizable images: JBIG2 groups: %s", (len(jbig2_groups),))
log.debug(f"Optimizable images: JBIG2 groups: {len(jbig2_groups)}")
return jbig2_groups
@@ -405,15 +418,11 @@ def convert_to_jbig2(
def _optimize_jpeg(args: Tuple[Xref, Path, Path, int]) -> Tuple[Xref, Optional[Path]]:
xref, in_jpg, opt_jpg, jpeg_quality = args
# This may produce a debug warning from PIL
# DEBUG:PIL.Image:Error closing: 'NoneType' object has no attribute
# 'close'. Seems to be mostly harmless
# https://github.com/python-pillow/Pillow/issues/1144
with Image.open(in_jpg) as im:
im.save(opt_jpg, optimize=True, quality=jpeg_quality)
if opt_jpg.stat().st_size > in_jpg.stat().st_size:
log.debug("xref %s, jpeg, made larger - skip", xref)
log.debug(f"xref {xref}, jpeg, made larger - skip")
opt_jpg.unlink()
return xref, None
return xref, opt_jpg
@@ -440,7 +449,7 @@ def transcode_jpegs(
use_threads=True, # Processes are significantly slower at this task
max_workers=options.jobs,
tqdm_kwargs=dict(
desc="JPEGs",
desc="Recompressing JPEGs",
total=len(jpegs),
unit='image',
disable=not options.progress_bar,
@@ -451,6 +460,73 @@ def transcode_jpegs(
)
def _find_deflatable_jpeg(
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
) -> Optional[XrefExt]:
result = extract_image_filter(pike, root, image, xref)
if result is None:
return None
pim, filtdp = result
if filtdp[0] == Name.DCTDecode and not filtdp[1] and options.optimize >= 1:
return XrefExt(xref, '.memory')
return None
def _deflate_jpeg(args: Tuple[Pdf, threading.Lock, Xref, int]) -> Tuple[Xref, bytes]:
pike, lock, xref, complevel = args
with lock:
xobj = pike.get_object(xref, 0)
try:
data = xobj.read_raw_bytes()
except PdfError:
return xref, b''
compdata = compress(data, complevel)
if len(compdata) >= len(data):
return xref, b''
return xref, compdata
def deflate_jpegs(pike: Pdf, root: Path, options, executor: Executor) -> None:
jpegs = []
for _pageno, xref_ext in extract_images(pike, root, options, _find_deflatable_jpeg):
xref = xref_ext.xref
log.debug(f'xref {xref}: marking this JPEG as deflatable')
jpegs.append(xref)
complevel = 9 if options.optimize == 3 else 6
# Our calls to xobj.write() in finish() need coordination
lock = threading.Lock()
def deflate_args() -> Iterator:
for xref in jpegs:
yield pike, lock, xref, complevel
def finish(result, pbar):
xref, compdata = result
if len(compdata) > 0:
with lock:
xobj = pike.get_object(xref, 0)
xobj.write(compdata, filter=[Name.FlateDecode, Name.DCTDecode])
pbar.update()
executor(
use_threads=True, # We're sharing the pdf directly, must use threads
max_workers=options.jobs,
tqdm_kwargs=dict(
desc="Deflating JPEGs",
total=len(jpegs),
unit='image',
disable=not options.progress_bar,
),
task=_deflate_jpeg,
task_arguments=deflate_args(),
task_finished=finish,
)
def _transcode_png(pike: Pdf, filename: Path, xref: Xref) -> bool:
output = filename.with_suffix('.png.pdf')
with output.open('wb') as f:
@@ -564,6 +640,7 @@ def optimize(
jpegs, pngs = extract_images_generic(pike, root, options)
transcode_jpegs(pike, jpegs, root, options, executor)
deflate_jpegs(pike, root, options, executor)
# if options.optimize >= 2:
# Try pngifying the jpegs
# transcode_pngs(pike, jpegs, jpg_name, root, options)
+6 -5
View File
@@ -25,6 +25,7 @@ from typing import (
Mapping,
NamedTuple,
Optional,
Sequence,
Tuple,
Union,
)
@@ -646,8 +647,8 @@ def _pdf_pageinfo_concurrent(
max_workers,
check_pages,
detailed_analysis=False,
):
pages = [None] * len(pdf.pages)
) -> Sequence[Optional['PageInfo']]:
pages: Sequence[Optional['PageInfo']] = [None] * len(pdf.pages)
def update_pageinfo(result, pbar):
page = result
@@ -918,17 +919,17 @@ class PdfInfo:
self._has_acroform = True
@property
def pages(self):
def pages(self) -> Sequence[Optional[PageInfo]]:
return self._pages
@property
def min_version(self) -> str:
# The minimum PDF is the maximum version that any particular page needs
return max(page.min_version for page in self.pages)
return max(page.min_version for page in self.pages if page)
@property
def has_userunit(self) -> bool:
return any(page.userunit != 1.0 for page in self.pages)
return any(page.userunit != 1.0 for page in self.pages if page)
@property
def has_acroform(self) -> bool:
+4 -4
View File
@@ -132,6 +132,7 @@ def get_progressbar_class():
Here is how OCRmyPDF will use the progress bar:
Example:
pbar_class = pm.hook.get_progressbar_class()
with pbar_class(**tqdm_kwargs) as pbar:
...
@@ -235,9 +236,9 @@ def filter_page_image(page: 'PageContext', image_filename: Path) -> Path:
``image_filename``. The hook may overwrite ``image_filename`` with a new file.
The output image should preserve the same physical unit dimensions, that is
(width * dpi_x, height * dpi_y). That is, if the image is resized, the DPI
``(width * dpi_x, height * dpi_y)``. That is, if the image is resized, the DPI
must be adjusted by the reciprocal. If this is not preserved, the PDF page
will be resized and the OCR layer misaligned. OCRmyPDF does not nothing
will be resized and the OCR layer misaligned. OCRmyPDF does nothing
to enforce these constraints; it is up to the plugin to do sensible things.
OCRmyPDF will create the PDF page based on the image format used (unless the
@@ -399,8 +400,7 @@ def get_ocr_engine() -> OcrEngine:
"""Returns an OcrEngine to use for processing this file.
The OcrEngine may be instantiated multiple times, by both the main process
and child process. As such, it must be obtain store any state in ``options``
or some common location.
and child process.
Note:
This is a :ref:`firstresult hook<firstresult>`.
+4
View File
@@ -4,6 +4,9 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# type: ignore
# Non-Windows mypy now breaks when trying to typecheck winreg
import logging
import os
import shutil
@@ -17,6 +20,7 @@ try:
except ModuleNotFoundError as e:
raise ModuleNotFoundError("This module is for Windows only") from e
log = logging.getLogger(__name__)
T = TypeVar('T')
+1 -2
View File
@@ -134,8 +134,7 @@ def run_ocrmypdf(
p = run(
p_args,
stdout=PIPE,
stderr=PIPE,
capture_output=True,
text=text,
check=False,
)
+19 -4
View File
@@ -50,6 +50,7 @@ import logging
import platform
import re
import shutil
import threading
from functools import partial
from pathlib import Path
from subprocess import PIPE, CalledProcessError, CompletedProcess
@@ -176,26 +177,40 @@ def cached_run(options, run_args, **run_kwargs):
class CacheOcrEngine(TesseractOcrEngine):
# Concurrent threads (with --use-threads) might try to use different parts
# of the OcrEngine, so we need a lock to protect the state of patched
# module whenever it's patched. Should refactor ocrmypdf._exec.tesseract so that
# it does not to be patched at all for testing.
lock = threading.Lock()
@staticmethod
def get_orientation(input_file, options):
with patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)):
with CacheOcrEngine.lock, patch(
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)
):
return TesseractOcrEngine.get_orientation(input_file, options)
@staticmethod
def get_deskew(input_file, options) -> float:
with patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)):
with CacheOcrEngine.lock, patch(
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)
):
return TesseractOcrEngine.get_deskew(input_file, options)
@staticmethod
def generate_hocr(input_file, output_hocr, output_text, options):
with patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)):
with CacheOcrEngine.lock, patch(
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)
):
TesseractOcrEngine.generate_hocr(
input_file, output_hocr, output_text, options
)
@staticmethod
def generate_pdf(input_file, output_pdf, output_text, options):
with patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)):
with CacheOcrEngine.lock, patch(
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)
):
TesseractOcrEngine.generate_pdf(
input_file, output_pdf, output_text, options
)
+2 -4
View File
@@ -24,8 +24,7 @@ def test_fish():
['fish', '-n', 'misc/completion/ocrmypdf.fish'],
check=True,
encoding='utf-8',
stdout=PIPE,
stderr=PIPE,
capture_output=True,
)
assert proc.stderr == '', proc.stderr
except FileNotFoundError:
@@ -41,8 +40,7 @@ def test_bash():
['bash', '-n', 'misc/completion/ocrmypdf.bash'],
check=True,
encoding='utf-8',
stdout=PIPE,
stderr=PIPE,
capture_output=True,
)
assert proc.stderr == '', proc.stderr
except FileNotFoundError:
+1
View File
@@ -13,6 +13,7 @@ from ocrmypdf import ExitCode
from .conftest import run_ocrmypdf_api
@pytest.mark.skipif(True, reason="--use-threads is currently default")
@pytest.mark.skipif(os.name == 'nt', reason="Windows doesn't have SIGKILL")
def test_simulate_oom_killer(resources, no_outpdf):
exitcode = run_ocrmypdf_api(
+2 -4
View File
@@ -630,8 +630,7 @@ def test_compression_preserved(ocrmypdf_exec, resources, image, outpdf):
]
p = run(
p_args,
stdout=PIPE,
stderr=PIPE,
capture_output=True,
stdin=input_stream,
text=True,
check=False,
@@ -691,8 +690,7 @@ def test_compression_changed(ocrmypdf_exec, resources, image, compression, outpd
]
p = run(
p_args,
stdout=PIPE,
stderr=PIPE,
capture_output=True,
stdin=input_stream,
text=True,
check=False,
+1 -1
View File
@@ -81,7 +81,7 @@ def test_jbig2_lossy(lossy, resources, outpdf):
'--image-dpi',
'200',
'--optimize',
3,
'3',
'--jpg-quality',
'50',
'--png-quality',
+3 -5
View File
@@ -173,17 +173,15 @@ def test_stack_abuse():
p = pikepdf.Pdf.new()
stream = pikepdf.Stream(p, b'q ' * 35)
with pytest.warns(None) as record:
with pytest.warns(UserWarning, match="overflowed"):
pdfinfo.info._interpret_contents(stream)
assert 'overflowed' in str(record[0].message)
stream = pikepdf.Stream(p, b'q Q Q Q Q')
with pytest.warns(None) as record:
with pytest.warns(UserWarning, match="underflowed") as record:
pdfinfo.info._interpret_contents(stream)
assert 'underflowed' in str(record[0].message)
stream = pikepdf.Stream(p, b'q ' * 135)
with pytest.warns(None):
with pytest.warns(UserWarning):
with pytest.raises(RuntimeError):
pdfinfo.info._interpret_contents(stream)
+7 -1
View File
@@ -25,6 +25,12 @@ from .conftest import check_ocrmypdf, run_ocrmypdf
# pylintx: disable=unused-variable
# Remove this workaround when we require Pillow >= 10
try:
Transpose = Image.Transpose # type: ignore
except AttributeError:
# Pillow 9 shim
Transpose = Image # type: ignore
RENDERERS = ['hocr', 'sandwich']
@@ -222,7 +228,7 @@ def test_rotate_page_level(image_angle, page_angle, resources, outdir):
with Image.open(fspath(resources / 'typewriter.png')) as im:
if image_angle != 0:
ccw_angle = -image_angle % 360
im = im.transpose(getattr(Image, f'ROTATE_{ccw_angle}'))
im = im.transpose(getattr(Transpose, f'ROTATE_{ccw_angle}'))
im.save(memimg, format='PNG')
memimg.seek(0)
mempdf = BytesIO()
+3 -3
View File
@@ -30,12 +30,12 @@ def test_stdin(ocrmypdf_exec, resources, outpdf):
'--plugin',
'tests/plugins/tesseract_noop.py',
]
run(p_args, stdout=PIPE, stderr=PIPE, stdin=input_stream, check=True)
run(p_args, capture_output=True, stdin=input_stream, check=True)
def test_stdout(ocrmypdf_exec, resources, outpdf):
if 'COV_CORE_DATAFILE' in os.environ:
pytest.skip(msg="Coverage uses stdout")
pytest.skip("Coverage uses stdout")
input_file = str(resources / 'francais.pdf')
output_file = str(outpdf)
@@ -72,7 +72,7 @@ def test_bad_locale(monkeypatch):
)
def test_dev_null(resources):
if 'COV_CORE_DATAFILE' in os.environ:
pytest.skip(msg="Coverage uses stdout")
pytest.skip("Coverage uses stdout")
p = run_ocrmypdf(
resources / 'trivial.pdf',