Compare commits

...
44 Commits
Author SHA1 Message Date
James R. Barlow 59d5b0d1bd v16.1.1 release notes 2024-02-15 16:56:25 -08:00
James R. Barlow 9ec0745ab8 Try pypy3.10 2024-02-14 14:25:13 -08:00
James R. Barlow 3a3635f7f9 Python 3.10 cleanup, manual fixes 2024-02-14 12:48:17 -08:00
James R. Barlow 6a746a1cbb ruff linting/Python 3.10 cleanup 2024-02-14 12:41:51 -08:00
James R. Barlow 906c130f96 Update rust toml settings 2024-02-14 12:32:26 -08:00
James R. Barlow 4a78458821 v16.1.0 release notes 2024-02-12 01:46:21 -08:00
James R. Barlow fddf3ce2f4 Clarify warnings filter 2024-02-12 01:43:47 -08:00
James R. Barlow 353b34e695 Merge branch 'feature/pageboxes' 2024-02-12 01:41:56 -08:00
James R. Barlow 7d63355c3c Use hocr renderer for LTR languages 2024-02-12 01:41:41 -08:00
James R. Barlow 42ff7fc842 Fix handling of pages that are restored to correct orientation with /Rotate
Appears inversion of CTM was incorrect, introduced in commit 9898904
2024-02-12 01:32:26 -08:00
James R. Barlow 26470fe16a Suppress reportlab deprecation warning 2024-02-12 01:17:08 -08:00
James R. Barlow 3b9d4b7f0a Attempt to deal with oddball mediaboxes 2024-02-11 15:34:54 -08:00
James R. Barlow 11f53fe9a9 First cut at propagating page boxes
This would fix the immediate issue, but does not address an offset mediabox.
2024-02-11 15:34:54 -08:00
James R. Barlow 123c0c766f Mention pipx, install --user --upgrade
Closes #1249
2024-02-08 09:42:00 -08:00
James R. Barlow 6a9be2142e Advise Homebrew on Linux for Ubuntu 20.04 2024-02-07 19:52:50 -08:00
James R. Barlow 0bc350f55e Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-02-06 01:28:10 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7a6edf62ba Bump codecov/codecov-action from 3 to 4 (#1247)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 03:55:13 -08:00
James R. Barlow 07b6f06f11 optimize: log images with unclear decode tables 2024-02-01 15:42:40 -08:00
nisbet-hubbardandGitHub 2005f622bb Update gs dependency & instructions for RHEL (#1228)
* Update gs depedency to 9.54

* Add instructions for RHEL

* Update instruction for using python3.11 on RHEL
2024-01-24 10:07:58 -08:00
James R. Barlow cca04fd799 Document ability to remove all OCR 2024-01-23 23:35:43 -08:00
MaxiandGitHub 75bf8e4ba2 Add autotools automake libtool and leptonica requirements (#1230)
These tools are required for a successful install of JBIG2 on Ubuntu
2024-01-09 11:58:17 -08:00
Anish SapkotaandGitHub daabb5b100 Minor english correction in Docs (#1229) 2024-01-08 10:18:26 -08:00
James R. Barlow 035ebea72f Change Windows build to use older Ghostscript 2024-01-07 01:56:38 -08:00
James R. Barlow a499956462 v16.0.4 release notes 2024-01-07 01:39:06 -08:00
James R. Barlow 74d2a156c4 Update cache 2024-01-07 01:35:05 -08:00
James R. Barlow f87fc7b12d gs: check for text-corrupting versions 2024-01-07 01:33:49 -08:00
James R. Barlow 602f5632cb hocr: fix unbound local 2024-01-07 01:33:36 -08:00
James R. Barlow 9fbbcf7599 Merge branch 'feature/hocrdebug' 2024-01-07 01:22:54 -08:00
James R. Barlow 9498f01f59 hocr: Pass text direction for standalone words 2024-01-06 16:21:21 -08:00
James R. Barlow 2c59aca5a1 Fix render_mode ON_TOP when previous content stream does not restore CTM 2024-01-06 16:20:55 -08:00
James R. Barlow 51301d69c9 Change default graft render mode back to underneath
It was mistakenly changed to on top at some point
2024-01-06 16:20:30 -08:00
James R. Barlow 7e608fd1df Use Courier as debug font 2024-01-06 14:24:25 -08:00
James R. Barlow ecc79315df Adjust issue template files description 2024-01-04 00:27:06 -08:00
James R. Barlow 14365d10b8 Skip testing oom killer on Python 3.12
Need to investigate further if there's a safe way to do this test.
2024-01-02 16:28:22 -08:00
James R. Barlow 5e5320020f v16.0.3 release notes 2024-01-02 02:49:54 -08:00
James R. Barlow 103c3e0cd6 Drop Ghostscript CVE warning and adjust version down to 9.54
Most distros have probably now backported the CVE fix. Dropped version to 9.54 so RHEL 9 can use recent ocrmypdf.
2024-01-01 13:02:57 -08:00
Anthony NabilandGitHub 7a1c89edd9 fixed a spelling mistake (#1222) 2023-12-30 15:53:22 -08:00
James R. Barlow a5ff3d2f42 Update instructions about custom language packages
Closes #1218
2023-12-27 16:46:10 -08:00
James R. Barlow b71d16dd96 Draw RTL text with reversed matrix instead of reversing characters 2023-12-24 01:34:03 -08:00
James R. Barlow fd593eb5e9 Reversing character order for RTL helps output 2023-12-24 01:24:44 -08:00
James R. Barlow a0b98abb94 Improve and externalize debug rendering settings 2023-12-24 00:10:09 -08:00
James R. Barlow 18353e1e94 Also fix space rendering 2023-12-24 00:09:47 -08:00
James R. Barlow 9adcad84da Overhaul debug rendering to support possible use of Helvetica for testing 2023-12-23 23:47:35 -08:00
James R. Barlow f2714586d8 Fix upside down glyphless font 2023-12-23 23:44:37 -08:00
82 changed files with 717 additions and 214 deletions
@@ -21,7 +21,7 @@ body:
id: reproduce
attributes:
label: Steps to reproduce
description: Please include steps to reproduce
description: Please include steps to reproduce.
value: |
1. Run ocrmypdf -v1 ...arguments... input.pdf output.pdf
2. Open output.pdf
@@ -31,9 +31,8 @@ body:
id: files
attributes:
label: Files
description: Please attach the input and output files, or any screenshots that may be helpful.
placeholder: |
Drag and drop files here.
description: |
Please attach the input and output files, or any screenshots that may be helpful.
If you cannot provide a test file, we probably won't be able to help with the issue.
PDF is a complex file format, and there may be technical details in the PDF that are
@@ -42,10 +41,11 @@ body:
We understand files may contain personal or sensitive information. Here are some options:
- Try reproducing the issue with a file from the test suite. (See tests/resources)
- Try to create another file in the same way as your private file.
- Encrypt the file to OCRmyPDF's private GPG key.
- Encrypt the file to OCRmyPDF's private GPG key, and then zip the GPG file.
- Use ``qpdf --json yourfile.pdf`` to produce a JSON representation of your file that
omits personal information.
placeholder: |
Drag and drop files here.
- type: dropdown
id: packaging-system
attributes:
+6 -6
View File
@@ -32,8 +32,8 @@ jobs:
- os: ubuntu-latest
python: "3.12"
tesseract5: true
# - os: ubuntu-latest
# python: "pypy3.10"
- os: ubuntu-latest
python: "pypy3.10"
env:
OS: ${{ matrix.os }}
@@ -100,7 +100,7 @@ jobs:
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
env_vars: OS,PYTHON
@@ -158,7 +158,7 @@ jobs:
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
env_vars: OS,PYTHON
@@ -189,7 +189,7 @@ jobs:
- name: Install system packages
run: |
choco install --yes --no-progress --pre tesseract
choco install --yes --no-progress --ignore-checksums ghostscript
choco install --yes --no-progress --ignore-checksums ghostscript --version 9.56.1
- name: Install Python packages
run: |
@@ -201,7 +201,7 @@ jobs:
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
env_vars: OS,PYTHON
+3 -1
View File
@@ -112,7 +112,9 @@ OCRmyPDF is strict about not writing to standard output so that
users can safely use it in a pipeline and produce a valid output
file. A caller application will have to ensure it does not write to
standard output either, if it wants to be compatible with this
behavior and support piping to a file.
behavior and support piping to a file. Another benefit of running
OCRmyPDF in a child process, as recommended above, is that it will
not interfere with the parent process's standard output.
Exceptions
----------
+16 -2
View File
@@ -187,7 +187,7 @@ might remove desirable content, especially from poor quality scans.
background from grayscale or color images. Monochrome images are
ignored. This should not be used on documents that contain color
photos as it may remove them.
- ``--deskew`` will correct pages were scanned at a skewed angle by
- ``--deskew`` will correct pages that were scanned at a skewed angle by
rotating them back into place.
- ``--clean`` uses
`unpaper <https://www.flameeyes.eu/projects/unpaper>`__ to clean up
@@ -245,6 +245,20 @@ if all you want to is to apply image processing or PDF/A conversion.
the case. Use ``--tesseract-non-ocr-timeout`` to control the timeout
for non-OCR operations, if needed.
Remove all text or OCR from my PDF
----------------------------------
This is getting ridiculous, but OCRmyPDF can complete strip all textual
information from a PDF and reconstruct it as a "bag of images" PDF.
.. code-block::
ocrmypdf --tesseract-timeout 0 --force-ocr input.pdf output.pdf
Why would you want to do this? Perhaps you have a PDF where OCR
fails to produce useful results, and just want to get rid of all OCR information.
This command also removes OCR generated by third party tools.
Optimize images without performing OCR
--------------------------------------
@@ -393,4 +407,4 @@ any digital signatures will be invalidated.
OCRmyPDF cannot open documents that are encrypted with a digital certificate.
Versions of OCRmyPDF prior to 14.4.0 would invalidate existing digital signatures
without warning.
without warning.
+50 -32
View File
@@ -139,6 +139,32 @@ from sources <#installing-head-revision-from-sources>`__.
.. _ubuntu-lts-latest:
RHEL 9
------
Prepare the environment by getting Python 3.11:
.. code-block:: bash
dnf install python3.11 python3.11-pip
Then, follow `Requirements for pip and HEAD install <#requirements-for-pip-and-head-install>`__ to instal dependencies:
.. code-block:: bash
dnf install ghostscript tesseract
and build ocrmypdf in virtual environment:
.. code-block:: bash
python3.11 -m venv .venv
To add JBIG2 encoding, see `Installing the JBIG2 encoder <jbig2>`__.
Note Fedora packages for language data haven't been branched for RHEL/EPEL, but you can get traineddata files directly from `tesseract
<https://github.com/tesseract-ocr/tessdata/>`__ and place them in ``/usr/share/tesseract/tessdata``.
Installing the latest version on Ubuntu 22.04 LTS
-------------------------------------------------
@@ -162,37 +188,13 @@ To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 20.04 LTS
----------------
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. To
install a more recent version, uninstall the system-provided version of
ocrmypdf, and install the following dependencies:
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. The
most convenient way to install recent OCRmyPDF on older Ubuntu is to use
Homebrew on Linux (Linuxbrew).
.. code-block:: bash
sudo apt-get -y remove ocrmypdf # remove system ocrmypdf, if installed
sudo apt-get -y update
sudo apt-get -y install \
ghostscript \
icc-profiles-free \
libxml2 \
pngquant \
python3-pip \
tesseract-ocr \
zlib1g
To install ocrmypdf for the system:
.. code-block:: bash
pip3 install ocrmypdf
To install for the current user only:
.. code-block:: bash
export PATH=$HOME/.local/bin:$PATH
pip3 install --user ocrmypdf
To add JBIG2 encoding, see :ref:`jbig2`.
brew install ocrmypdf
Arch Linux (AUR)
----------------
@@ -404,7 +406,7 @@ the following when running in an Administrator command prompt):
* ``choco install --pre tesseract``
* ``choco install pngquant`` (optional)
Either set of commands will install the required software. At the mmoment there is no
Either set of commands will install the required software. At the moment there is no
single command to install Windows.
You may then use ``pip`` to install ocrmypdf. (This can performed by a user or
@@ -540,9 +542,25 @@ try:
pip install --user ocrmypdf
(If the message appears ``Requirement already satisfied: ocrmypdf in...``,
you will need to use ``pip install --user --upgrade ocrmypdf``.)
You should then be able to run ``ocrmypdf --version`` and see that the
latest version was located.
Installing with pipx
====================
Some users may prefer pipx. As with the method above, you will need to
satisfy all non-Python dependencies. Then if pipx is installed, you
can use
.. code-block:: bash
pipx run ocrmypdf
(If not installed, pipx will install first.)
Requirements for pip and HEAD install
-------------------------------------
@@ -553,7 +571,7 @@ manager. ``pip`` cannot provide them.
The following versions are required:
- Python 3.10 or newer
- Ghostscript 9.55 or newer
- Ghostscript 9.54 or newer
- Tesseract 4.1.1 or newer
- jbig2enc 0.29 or newer
- pngquant 2.5 or newer
@@ -671,7 +689,7 @@ To manually install the ``fish`` completion, copy
Note on 32-bit support
======================
Many Python libraries no longer 32-bit binary wheels for Linux. This
Many Python libraries no longer provide 32-bit binary wheels for Linux. This
includes many of the libraries that OCRmyPDF depends on, such as
Pillow. The easiest way to express this to end users is to say we don't
support 32-bit Linux.
@@ -681,4 +699,4 @@ can still install and use OCRmyPDF. A warning message will appear.
In practice, OCRmyPDF may need more than 32-bit memory space to run when
large documents are processed, so there are practical limitations to what
users can accomplish with it. Still, for the common use case of an 32-bit
ARM NAS or Raspberry Pi processing small documents, it should work.
ARM NAS or Raspberry Pi processing small documents, it should work.
+9 -1
View File
@@ -41,7 +41,15 @@ For all other platforms, you would need to build the JBIG2 encoder from source:
Dependencies include libtoolize and libleptonica, which on Ubuntu systems
are packaged as libtool and libleptonica-dev. On Fedora (35) they are packaged
as libtool and leptonica-devel.
as libtool and leptonica-devel. For this to work, please make sure to install
``autotools``, ``automake``, ``libtool`` and ``leptonica`` first if not already
installed.
.. code-block:: bash
[sudo] apt install autotools-dev automake libtool libleptonica-dev
..
Lossy mode JBIG2
================
+31 -13
View File
@@ -18,16 +18,26 @@ Tesseract's documentation also lists the three-letter code for your language.
Some are anglicized, e.g. Spanish is ``spa`` rather than ``esp``, while others
are not, e.g. German is ``deu`` and French is ``fra``.
Language packs (strictly speaking, Tesseract "traineddata" files) generally correspond
to the language in question, but different language packs are used in certain
situations. For German, the "Fraktur" language pack can assist with reading older
materials in the Fraktur typeface family (``deu_frak``). Some communities have changed
their script from Cyrillic to Latin; the Cyrillic version of Uzbek is available
as ``uzb_cyrl`` and the Latin version is ``uzb``.
After you have installed a language pack, you can use it with ``ocrmypdf -l <language>``,
for example ``ocrmypdf -l spa``. For multilingual documents, you can specify
all languages to be expected, e.g. ``ocrmypdf -l eng+fra`` for English and French.
English is assumed by default unless other language(s) are specified.
For Linux users, you can often find packages that provide language
packs:
packs.
Debian and Ubuntu users
=======================
Platform install steps
======================
Debian and Ubuntu (apt)
-----------------------
.. code-block:: bash
@@ -42,8 +52,8 @@ 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``.
Fedora users
============
Fedora
------
.. code-block:: bash
@@ -58,8 +68,8 @@ 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
============
Gentoo
------
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``.
@@ -85,23 +95,31 @@ 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
===========
macOS
-----
You can install additional language packs by
:ref:`installing Tesseract using Homebrew with all language packs <macos-all-languages>`.
Docker users
============
Docker
------
Users of the OCRmyPDF Docker image should install language packs into a
derived Docker image as
:ref:`described in that section <docker-lang-packs>`.
Windows users
=============
Windows
-------
The Tesseract installer provided by Chocolatey currently includes only English language.
To install other languages, download the respective language pack (``.traineddata`` file)
from https://github.com/tesseract-ocr/tessdata/ and place it in
``C:\\Program Files\\Tesseract-OCR\\tessdata`` (or wherever Tesseract OCR is installed).
Custom language packs
=====================
If you have fine-tuned or trained Tesseract and generated custom trained data, you can
copy your ``customlang.traineddata`` file into your Tesseract "tessdata" folder, and
then use the ``-l customlang`` argument to tell OCRmyPDF to pass that language on to
Tesseract.
+34
View File
@@ -30,6 +30,40 @@ OCRmyPDF typically supports the three most recent Python versions.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
v16.1.1
=======
- Fixed PyPy 3.10 support.
v16.1.0
=======
- Improved hOCR renderer is now default for left to right languages.
- Improved handling of rotated pages. Previously, OCR text might be missing for
pages that were rotated with a /Rotate tag on the page entry.
- Improved handling of cropped pages. Previously, in some cases a page with a
crop box would not have its OCR applied correctly and misalignment between
OCR text and visible text coudl occur.
- Documentation improvements, especially installation instructions for less
common platforms.
v16.0.4
=======
- Fixed some issues for left-to-right text with the new hOCR renderer. It is still
not default yet but will be made so soon. Right-to-left text is still in progress.
- Added an error to prevent use of several versions of Ghostscript that seem
corrupt existing text in input PDFs. Newly generated OCR is not affected.
For best results, use Ghostscript 10.02.1 or newer, which contains the fix
for the issue.
v16.0.3
=======
- Changed minimum required Ghostscript to 9.54, to support users of RHEL 9 and its
derivatives, since that is the latest version available there.
- Removed warning message about CVE-2023-43115, on the assumption that most
distributions have backported the patch by now.
v16.0.2
=======
+3 -2
View File
@@ -8,6 +8,7 @@
# Do not enable annotations!
# https://github.com/tiangolo/typer/discussions/598
# from __future__ import annotations
from __future__ import annotations
import json
import logging
@@ -131,7 +132,7 @@ def execute_ocrmypdf(
class HandleObserverEvent(PatternMatchingEventHandler):
def __init__(
def __init__( # noqa: D107
self,
patterns=None,
ignore_patterns=None,
@@ -191,7 +192,7 @@ def main(
bool,
typer.Option(
envvar='OCR_OUTPUT_DIRECTORY_YEAR_MONTH',
help='Create a subdirectory in the output directory for each year and month',
help='Create a subdirectory in the output directory for each year/month',
),
] = False,
on_success_delete: Annotated[
+12 -6
View File
@@ -131,7 +131,11 @@ norecursedirs = ["lib", ".pc", ".git", "venv", "output", "cache", "resources"]
testpaths = ["tests"]
addopts = "-n auto"
markers = ["slow"]
filterwarnings = ["ignore:.*XMLParser.*:DeprecationWarning"]
filterwarnings = [
"ignore:.*XMLParser.*:DeprecationWarning",
"ignore:.*ast.NameConstant.*:DeprecationWarning:reportlab",
"ignore:.*distutils.*:DeprecationWarning:libxmp",
]
[tool.mypy]
@@ -149,7 +153,10 @@ module = [
ignore_missing_imports = true
[tool.ruff]
select = [
target-version = "py310"
[tool.ruff.lint]
"select" = [
"D", # pydocstyle
"E", # pycodestyle
"W", # pycodestyle
@@ -157,16 +164,15 @@ select = [
"I001", # isort
"UP", # pyupgrade
]
target-version = "py310"
[tool.ruff.isort]
[tool.ruff.lint.isort]
known-first-party = ["ocrmypdf"]
required-imports = ["from __future__ import annotations"]
[tool.ruff.pydocstyle]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
"docs/conf.py" = ["D100", "D101", "D105"]
"tests/*.py" = ["D100", "D101", "D102", "D103", "D105"]
"misc/*.py" = ["D103", "D101", "D102"]
+2 -2
View File
@@ -7,8 +7,8 @@ from __future__ import annotations
import threading
from abc import ABC, abstractmethod
from collections.abc import Iterable
from typing import Any, Callable, TypeVar
from collections.abc import Callable, Iterable
from typing import Any, TypeVar
from ocrmypdf._progressbar import NullProgressBar, ProgressBar
+2 -4
View File
@@ -8,19 +8,17 @@ from __future__ import annotations
import logging
import os
import shlex
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from decimal import Decimal
from pathlib import Path
from subprocess import PIPE, STDOUT
from tempfile import TemporaryDirectory
from typing import Union
from packaging.version import Version
from PIL import Image
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
from ocrmypdf.exceptions import SubprocessOutputError
from ocrmypdf.subprocess import get_version, run
# unpaper documentation:
@@ -29,7 +27,7 @@ from ocrmypdf.subprocess import get_version, run
UNPAPER_IMAGE_PIXEL_LIMIT = 256 * 1024 * 1024
DecFloat = Union[Decimal, float]
DecFloat = Decimal | float
log = logging.getLogger(__name__)
+12 -5
View File
@@ -289,9 +289,9 @@ class OcrGrafter:
# Translate the text so it is centered at (0, 0), rotate it there, adjust
# for a size different between initial and text PDF, then untranslate, and
# finally move the lower left corner to match the mediabox. All transforms
# must be premultiplied so they are applied in reverse order here.
ctm = corner @ untranslate @ scale @ rotate @ translate
# finally move the lower left corner to match the mediabox.
ctm = translate @ rotate @ scale @ untranslate @ corner
log.debug("Grafting with ctm %r", ctm)
base_resources = _ensure_dictionary(base_page.obj, Name.Resources)
base_xobjs = _ensure_dictionary(base_resources, Name.XObject)
@@ -311,9 +311,16 @@ class OcrGrafter:
if strip_old_text:
strip_invisible_text(self.pdf_base, base_page)
base_page.contents_coalesce()
if self.render_mode == RenderMode.ON_TOP:
# Add q/Q to ensure content we append is drawn correctly
# Strictly speaking this needs to trace the whole q/Q stack in case
# stack is not balanced.
original = base_page.Contents.read_bytes()
base_page.Contents.write(b'q\n' + original + b'\nQ\n')
base_page.contents_add(
new_text_layer, prepend=self.render_mode == RenderMode.ON_TOP
new_text_layer, prepend=self.render_mode == RenderMode.UNDERNEATH
)
base_page.contents_coalesce()
_update_resources(obj=base_page.obj, font=font, font_key=font_key)
+2 -2
View File
@@ -93,8 +93,8 @@ class PageContext:
state = self.__dict__.copy()
state['options'] = copy(self.options)
if not isinstance(state['options'].input_file, (str, bytes, os.PathLike)):
if not isinstance(state['options'].input_file, str | bytes | os.PathLike):
state['options'].input_file = 'stream'
if not isinstance(state['options'].output_file, (str, bytes, os.PathLike)):
if not isinstance(state['options'].output_file, str | bytes | os.PathLike):
state['options'].output_file = 'stream'
return state
+84 -12
View File
@@ -12,8 +12,9 @@ import re
import sys
from collections.abc import Iterable, Iterator, Sequence
from contextlib import suppress
from io import BytesIO
from pathlib import Path
from shutil import copyfileobj, copystat
from shutil import copyfileobj
from typing import Any, BinaryIO, TypeVar, cast
import img2pdf
@@ -34,7 +35,8 @@ from ocrmypdf.exceptions import (
UnsupportedImageFormatError,
)
from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink
from ocrmypdf.hocrtransform import HocrTransform
from ocrmypdf.hocrtransform import DebugRenderOptions, HocrTransform
from ocrmypdf.hocrtransform._font import Courier
from ocrmypdf.pdfa import generate_pdfa_ps
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
from ocrmypdf.pluginspec import OrientationConfidence
@@ -712,19 +714,29 @@ def create_pdf_page_from_image(
pageinfo = page_context.pageinfo
pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches)
effective_rotation = (pageinfo.rotation - orientation_correction) % 360
if effective_rotation % 180 == 90:
swap_axis = effective_rotation % 180 == 90
if swap_axis:
pagesize = pagesize[1], pagesize[0]
# This create a single page PDF
with open(image, 'rb') as imfile, open(output_file, 'wb') as pdf:
# Create a new single page PDF to hold
bio = BytesIO()
with open(image, 'rb') as imfile:
log.debug('convert')
layout_fun = img2pdf.get_layout_fun(pagesize)
img2pdf.convert(
imfile, layout_fun=layout_fun, outputstream=pdf, **IMG2PDF_KWARGS
imfile,
layout_fun=layout_fun,
outputstream=bio,
engine=img2pdf.Engine.pikepdf,
rotation=img2pdf.Rotation.ifvalid,
)
log.debug('convert done')
# img2pdf does not generate boxes correctly, so we fix them
bio.seek(0)
fix_pagepdf_boxes(bio, output_file, page_context, swap_axis=swap_axis)
output_file = page_context.plugin_manager.hook.filter_pdf_page(
page=page_context, image_filename=image, output_pdf=output_file
)
@@ -741,15 +753,25 @@ def render_hocr_page(hocr: Path, page_context: PageContext) -> Path:
return output_file
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
debug_mode = options.pdf_renderer == 'hocrdebug'
debug_kwargs = {}
if options.pdf_renderer == 'hocrdebug':
debug_kwargs = dict(
debug_render_options=DebugRenderOptions(
render_baseline=True,
render_triangle=True,
render_line_bbox=False,
render_word_bbox=True,
render_paragraph_bbox=False,
render_space_bbox=False,
),
font=Courier(),
)
HocrTransform(
hocr_filename=hocr,
dpi=dpi.to_scalar(), # square
debug=debug_mode,
hocr_filename=hocr, dpi=dpi.to_scalar(), **debug_kwargs # square
).to_pdf(
out_filename=output_file,
image_filename=None,
invisible_text=True if not debug_kwargs else False,
)
return output_file
@@ -769,7 +791,57 @@ def ocr_engine_textonly_pdf(
output_text=output_text,
options=options,
)
return (output_pdf, output_text)
return output_pdf, output_text
def _offset_rect(rect: tuple[float, float, float, float], offset: tuple[float, float]):
"""Offset a rectangle by a given amount."""
return (
rect[0] + offset[0],
rect[1] + offset[1],
rect[2] + offset[0],
rect[3] + offset[1],
)
def fix_pagepdf_boxes(
infile: Path | BinaryIO,
out_file: Path,
page_context: PageContext,
swap_axis: bool = False,
) -> Path:
"""Fix the bounding boxes in a single page PDF.
The single page PDF is created with a normal MediaBox with its lower left corner
at (0, 0). infile is the single page PDF. page_context.mediabox has the original
file's mediabox, which may have a different origin. We needto adjust the other
boxes in the single page PDF to match the effect they had on the original page.
When correcting page rotation, we create a single page PDF that is correctly
rotated instead of an incorrectly rotated and then setting page.Rotate on it.
If rotation is either 90 or 270 degrees, then this function can be called
with swap_axis to swap the X and Y coordinates of all the boxes.
We are not concerned with solving degenerate cases where the boxes overlap or
or express invalid rectangles. We merely pass the boxes, producing a
transformation equivalent to the change made by constructing a new page image.
"""
with pikepdf.open(infile) as pdf:
for page in pdf.pages:
# page.BleedBox = page_context.pageinfo.bleedbox
# page.ArtBox = page_context.pageinfo.artbox
mediabox = page_context.pageinfo.mediabox
offset = mediabox[0], mediabox[1]
cropbox = _offset_rect(page_context.pageinfo.cropbox, offset)
trimbox = _offset_rect(page_context.pageinfo.trimbox, offset)
if swap_axis:
cropbox = cropbox[1], cropbox[0], cropbox[3], cropbox[2]
trimbox = trimbox[1], trimbox[0], trimbox[3], trimbox[2]
page.CropBox = cropbox
page.TrimBox = trimbox
pdf.save(out_file)
return pdf
def generate_postscript_stub(context: PdfContext) -> Path:
+2 -2
View File
@@ -11,13 +11,13 @@ import os
import shutil
import sys
import threading
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from concurrent.futures.process import BrokenProcessPool
from concurrent.futures.thread import BrokenThreadPool
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, NamedTuple, cast
from typing import NamedTuple, cast
import PIL
-1
View File
@@ -80,7 +80,6 @@ def _exec_page_sync(page_context: PageContext) -> PageResult:
page_context
)
ocr_out, text_out = _image_to_ocr_text(page_context, ocr_image_out)
return PageResult(
pageno=page_context.pageno,
pdf_page_from_image=pdf_page_from_image_out,
+7 -7
View File
@@ -14,7 +14,7 @@ from collections.abc import Iterable, Sequence
from enum import IntEnum
from io import IOBase
from pathlib import Path
from typing import AnyStr, BinaryIO, Union
from typing import AnyStr, BinaryIO
from warnings import warn
import pluggy
@@ -28,8 +28,8 @@ from ocrmypdf._validation import check_options
from ocrmypdf.cli import ArgumentParser, get_parser
from ocrmypdf.helpers import is_iterable_notstr
StrPath = Union[Path, AnyStr]
PathOrIO = Union[BinaryIO, StrPath]
StrPath = Path | AnyStr
PathOrIO = BinaryIO | StrPath
# Installing plugins affects the global state of the Python interpreter,
# so we need to use a lock to prevent multiple threads from installing
@@ -169,7 +169,7 @@ def _kwargs_to_cmdline(
# We have a parameter
cmdline.append(f"--{cmd_style_arg}")
if isinstance(val, (int, float)):
if isinstance(val, int | float):
cmdline.append(str(val))
elif isinstance(val, str):
cmdline.append(val)
@@ -201,11 +201,11 @@ def create_options(
defer_kwargs={'progress_bar', 'plugins', 'parser', 'input_file', 'output_file'},
**kwargs,
)
if isinstance(input_file, (BinaryIO, IOBase)):
if isinstance(input_file, BinaryIO | IOBase):
cmdline.append('stream://input_file')
else:
cmdline.append(os.fspath(input_file))
if isinstance(output_file, (BinaryIO, IOBase)):
if isinstance(output_file, BinaryIO | IOBase):
cmdline.append('stream://output_file')
else:
cmdline.append(os.fspath(output_file))
@@ -343,7 +343,7 @@ def ocr( # noqa: D417
if not plugins:
plugins = []
elif isinstance(plugins, (str, Path)):
elif isinstance(plugins, str | Path):
plugins = [plugins]
else:
plugins = list(plugins)
+14 -9
View File
@@ -12,10 +12,10 @@ import queue
import signal
import sys
import threading
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
from contextlib import suppress
from typing import Callable, Union
from typing import Union
from rich.console import Console as RichConsole
@@ -25,8 +25,10 @@ from ocrmypdf._progressbar import RichProgressBar
from ocrmypdf.exceptions import InputFileError
from ocrmypdf.helpers import remove_all_log_handlers
FuturesExecutorClass = Union[type[ThreadPoolExecutor], type[ProcessPoolExecutor]]
Queue = Union[multiprocessing.Queue, queue.Queue]
FuturesExecutorClass = Union[ # noqa: UP007
type[ThreadPoolExecutor], type[ProcessPoolExecutor]
]
Queue = Union[multiprocessing.Queue, queue.Queue] # noqa: UP007
UserInit = Callable[[], None]
WorkerInit = Callable[[Queue, UserInit, int], None]
@@ -128,11 +130,14 @@ class StandardExecutor(Executor):
listener = threading.Thread(target=log_listener, args=(log_queue,))
listener.start()
with self.pbar_class(**progress_kwargs) as pbar, executor_class(
max_workers=max_workers,
initializer=initializer,
initargs=(log_queue, worker_initializer, logging.getLogger("").level),
) as executor:
with (
self.pbar_class(**progress_kwargs) as pbar,
executor_class(
max_workers=max_workers,
initializer=initializer,
initargs=(log_queue, worker_initializer, logging.getLogger("").level),
) as executor,
):
futures = [executor.submit(task, *args) for args in task_arguments]
try:
for future in as_completed(futures):
+12 -9
View File
@@ -17,7 +17,7 @@ log = logging.getLogger(__name__)
# Currently all blacklisted versions are lower than 9.55, so none need to
# be added here. If a future version is blacklisted, add it here.
BLACKLISTED_GS_VERSIONS: frozenset[str] = frozenset()
BLACKLISTED_GS_VERSIONS: frozenset[Version] = frozenset()
@hookimpl
@@ -54,7 +54,7 @@ def check_options(options):
program='gs',
package='ghostscript',
version_checker=ghostscript.version,
need_version='9.55', # Ubuntu 22.04's version
need_version='9.54', # RHEL 9's version; Ubuntu 22.04 has 9.55
)
gs_version = ghostscript.version()
if gs_version in BLACKLISTED_GS_VERSIONS:
@@ -62,13 +62,16 @@ def check_options(options):
f"Ghostscript {gs_version} contains serious regressions and is not "
"supported. Please upgrade to a newer version."
)
if gs_version < Version('10.02.0'):
log.warning(
f"The installed version of Ghostscript {gs_version}, contains a remote "
"code execution security vulnerability. Please upgrade to a newer "
"version. For details see CVE-2023-43115. The issue is not known to "
"affect OCRmyPDF or processing PDFs with Ghostscript, but upgrading "
"Ghostscript is recommended."
if Version('10.0.0') <= gs_version < Version('10.02.1') and (
options.skip_text or options.redo_ocr
):
raise MissingDependencyError(
f"Ghostscript 10.0.0 through 10.02.0 (your version: {gs_version}) "
"contain serious regressions that corrupt PDFs with existing text, "
"such as those processed using --skip-text or --redo-ocr. "
"Please upgrade to a "
"newer version, or use --output-type pdf to avoid Ghostscript, or "
"use --force-ocr to discard existing text."
)
if options.output_type == 'pdfa':
@@ -146,7 +146,11 @@ def check_options(options):
# Decide on what renderer to use
if options.pdf_renderer == 'auto':
options.pdf_renderer = 'sandwich'
if {'ara', 'heb', 'fas', 'per'} & set(options.languages):
log.info("Using sandwich renderer since there is an RTL language")
options.pdf_renderer = 'sandwich'
else:
options.pdf_renderer = 'hocr'
if not tesseract.has_thresholding() and options.tesseract_thresholding != 0:
log.warning(
+2 -2
View File
@@ -6,8 +6,8 @@
from __future__ import annotations
import argparse
from collections.abc import Mapping
from typing import Any, Callable, TypeVar
from collections.abc import Callable, Mapping
from typing import Any, TypeVar
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
from ocrmypdf._version import __version__ as _VERSION
+1 -2
View File
@@ -20,13 +20,12 @@ from __future__ import annotations
import logging
import logging.handlers
import signal
from collections.abc import Iterable, Iterator
from collections.abc import Callable, Iterable, Iterator
from contextlib import suppress
from enum import Enum, auto
from itertools import islice, repeat, takewhile, zip_longest
from multiprocessing import Pipe, Process
from multiprocessing.connection import Connection, wait
from typing import Callable
from ocrmypdf import Executor, hookimpl
from ocrmypdf._concurrent import NullProgressBar
+3 -2
View File
@@ -10,7 +10,7 @@ import multiprocessing
import os
import shutil
import warnings
from collections.abc import Iterable, Sequence
from collections.abc import Callable, Iterable, Sequence
from contextlib import suppress
from decimal import Decimal
from io import StringIO
@@ -19,13 +19,13 @@ from pathlib import Path
from statistics import harmonic_mean
from typing import (
Any,
Callable,
Generic,
TypeVar,
)
import img2pdf
import pikepdf
from deprecation import deprecated
log = logging.getLogger(__name__)
@@ -136,6 +136,7 @@ class Resolution(Generic[T]):
return self._isclose(self.x, other.x) and self._isclose(self.y, other.y)
@deprecated(deprecated_in='15.4.0')
class NeverRaise(Exception):
"""An exception that is never raised."""
+31 -2
View File
@@ -18,7 +18,12 @@ from pikepdf.canvas import Font
log = logging.getLogger(__name__)
class GlyphlessFont(Font):
class EncodableFont(Font):
def text_encode(self, text: str) -> bytes:
raise NotImplementedError()
class GlyphlessFont(EncodableFont):
CID_TO_GID_DATA = zlib.compress(b"\x00\x01" * 65536)
GLYPHLESS_FONT_NAME = 'pdf.ttf'
GLYPHLESS_FONT = (package_files('ocrmypdf.data') / GLYPHLESS_FONT_NAME).read_bytes()
@@ -27,11 +32,14 @@ class GlyphlessFont(Font):
def __init__(self):
pass
def text_width(self, text: str, fontsize: float) -> int:
def text_width(self, text: str, fontsize: float) -> float:
"""Estimate the width of a text string when rendered with the given font."""
# NFKC: split ligatures, combine diacritics
return len(unicodedata.normalize("NFKC", text)) * (fontsize / self.CHAR_ASPECT)
def text_encode(self, text: str) -> bytes:
return text.encode('utf-16be')
def register(self, pdf: Pdf):
"""Register the glyphless font.
@@ -110,3 +118,24 @@ class GlyphlessFont(Font):
font_descriptor.FontFile2 = pdf.make_stream(self.GLYPHLESS_FONT)
cid_font_type2.FontDescriptor = font_descriptor
return basefont
class Courier(EncodableFont):
"""Courier font."""
def text_width(self, text: str, fontsize: float) -> float:
"""Estimate the width of a text string when rendered with the given font."""
return len(text) * fontsize
def text_encode(self, text: str) -> bytes:
return text.encode('pdfdoc', errors='ignore')
def register(self, pdf: Pdf) -> Dictionary:
"""Register the font."""
return pdf.make_indirect(
Dictionary(
BaseFont=Name.Courier,
Type=Name.Font,
Subtype=Name.Type1,
)
)
+37 -22
View File
@@ -27,11 +27,11 @@ from pikepdf.canvas import (
MAGENTA,
RED,
Canvas,
Font,
Text,
TextDirection,
)
from ocrmypdf.hocrtransform._font import EncodableFont as Font
from ocrmypdf.hocrtransform._font import GlyphlessFont
log = logging.getLogger(__name__)
@@ -45,12 +45,12 @@ Element = ElementTree.Element
class DebugRenderOptions:
"""A class for managing rendering options."""
render_paragraph_bbox: bool
render_baseline: bool
render_triangle: bool
render_line_bbox: bool
render_word_bbox: bool
render_space_bbox: bool
render_paragraph_bbox: bool = False
render_baseline: bool = False
render_triangle: bool = False
render_line_bbox: bool = False
render_word_bbox: bool = False
render_space_bbox: bool = False
class HocrTransformError(Exception):
@@ -81,8 +81,21 @@ class HocrTransform:
debug: bool = False,
fontname: Name = Name("/f-0-0"),
font: Font = GlyphlessFont(),
debug_render_options: DebugRenderOptions | None = None,
):
"""Initialize the HocrTransform object."""
if debug:
log.warning("Use debug_render_options instead", DeprecationWarning)
self.render_options = DebugRenderOptions(
render_baseline=debug,
render_triangle=debug,
render_line_bbox=False,
render_word_bbox=debug,
render_paragraph_bbox=False,
render_space_bbox=False,
)
else:
self.render_options = debug_render_options or DebugRenderOptions()
self.dpi = dpi
self.hocr = ElementTree.parse(os.fspath(hocr_filename))
self._fontname = fontname
@@ -103,14 +116,6 @@ class HocrTransform:
self.height = (coords.ury - coords.lly) / (self.dpi / INCH)
# Stop after first div that has page coordinates
break
self.render_options = DebugRenderOptions(
render_baseline=debug,
render_triangle=debug,
render_line_bbox=False,
render_word_bbox=debug,
render_paragraph_bbox=False,
render_space_bbox=False,
)
def _get_element_text(self, element: Element):
"""Return the textual content of the element and its children."""
@@ -215,12 +220,13 @@ class HocrTransform:
if not found_lines:
# Tesseract did not report any lines (just words)
root = self.hocr.find(self._child_xpath('div', 'ocr_page'))
direction = self._get_text_direction(root)
self._do_line(
canvas,
root,
"ocrx_word",
invisible_text,
TextDirection.LTR,
direction,
True,
)
# put the image on the page, scaled to fill the page
@@ -311,8 +317,7 @@ class HocrTransform:
line_box_height = abs(line_box.height) / cos(angle)
fontsize = line_box_height + intercept
text.font(self._fontname, fontsize)
if invisible_text or True:
text.render_mode(3) # Invisible (indicates OCR text)
text.render_mode(3 if invisible_text else 0)
self._debug_draw_baseline(
canvas, line_matrix.inverse().transform(line_box), 0
@@ -362,10 +367,15 @@ class HocrTransform:
self._debug_draw_word_bbox(canvas, box)
# If this word is 0 units wide, our best bet seems to be to suppress this text
if text_direction == TextDirection.RTL:
log.info("RTL: %s", elemtxt)
if font_width > 0:
text.text_transform(Matrix(1, 0, 0, 1, box.llx, 0))
if text_direction == TextDirection.LTR:
text.text_transform(Matrix(1, 0, 0, -1, box.llx, 0))
elif text_direction == TextDirection.RTL:
text.text_transform(Matrix(-1, 0, 0, -1, box.llx + box.width, 0))
text.horiz_scale(100 * box.width / font_width)
text.show(elemtxt.encode('utf-16be'))
text.show(self._font.text_encode(elemtxt))
# Get coordinates of the next word (if there is one)
hocr_next_box = (
@@ -385,11 +395,16 @@ class HocrTransform:
elif text_direction == TextDirection.RTL:
space_box = Rectangle(next_box.urx, box.lly, box.llx, next_box.ury)
self._debug_draw_space_bbox(canvas, space_box)
text.text_transform(Matrix(1, 0, 0, 1, space_box.llx, 0))
space_width = self._font.text_width(' ', fontsize)
if space_width > 0:
if text_direction == TextDirection.LTR:
text.text_transform(Matrix(1, 0, 0, -1, space_box.llx, 0))
elif text_direction == TextDirection.RTL:
text.text_transform(
Matrix(-1, 0, 0, -1, space_box.llx + space_box.width, 0)
)
text.horiz_scale(100 * space_box.width / space_width)
text.show(' '.encode('utf-16be'))
text.show(self._font.text_encode(' '))
def _debug_draw_paragraph_boxes(self, canvas: Canvas, color=CYAN):
"""Draw boxes around paragraphs in the document."""
+9 -3
View File
@@ -11,11 +11,10 @@ import sys
import tempfile
import threading
from collections import defaultdict
from collections.abc import Iterator, MutableSet, Sequence
from collections.abc import Callable, Iterator, MutableSet, Sequence
from os import fspath
from pathlib import Path
from typing import Any, Callable, NamedTuple, NewType
from warnings import warn
from typing import Any, NamedTuple, NewType
from zlib import compress
import img2pdf
@@ -155,6 +154,13 @@ def extract_image_jbig2(
with imgname.open('wb') as f:
ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext))
except NotImplementedError as e:
if '/Decode' in str(e):
log.debug(
f"xref {xref}: skipping image with unsupported Decode table"
)
return None
raise
except UnsupportedImageTypeError:
return None
finally:
+25 -10
View File
@@ -10,9 +10,8 @@ import atexit
import logging
import re
import statistics
import sys
from collections import defaultdict
from collections.abc import Container, Iterable, Iterator, Mapping, Sequence
from collections.abc import Callable, Container, Iterable, Iterator, Mapping, Sequence
from contextlib import contextmanager
from decimal import Decimal
from enum import Enum, auto
@@ -20,7 +19,7 @@ from functools import partial
from math import hypot, inf, isclose
from os import PathLike
from pathlib import Path
from typing import Callable, NamedTuple
from typing import NamedTuple
from warnings import warn
from pdfminer.layout import LTPage, LTTextBox
@@ -854,6 +853,12 @@ class PageInfo:
width_pt = mediabox[2] - mediabox[0]
height_pt = mediabox[3] - mediabox[1]
# self._artbox = [float(d) for d in page.artbox.as_list()]
# self._bleedbox = [float(d) for d in page.bleedbox.as_list()]
self._cropbox = [float(d) for d in page.cropbox.as_list()]
self._mediabox = [float(d) for d in page.mediabox.as_list()]
self._trimbox = [float(d) for d in page.trimbox.as_list()]
check_this_page = pageno in check_pages
if check_this_page and detailed_analysis:
@@ -970,6 +975,21 @@ class PageInfo:
else:
raise ValueError("rotation must be a cardinal angle")
@property
def cropbox(self) -> FloatRect:
"""Return cropbox of page in PDF coordinates."""
return self._cropbox
@property
def mediabox(self) -> FloatRect:
"""Return mediabox of page in PDF coordinates."""
return self._mediabox
@property
def trimbox(self) -> FloatRect:
"""Return trimbox of page in PDF coordinates."""
return self._trimbox
@property
def images(self) -> list[ImageInfo]:
"""Return images."""
@@ -1039,12 +1059,7 @@ class PageInfo:
weights = [area / total_drawn_area for area in image_areas]
# Calculate harmonic mean of DPIs weighted by area
if sys.version_info >= (3, 10):
weighted_dpi = statistics.harmonic_mean(image_dpis, weights)
else:
weighted_dpi = sum(weights) / sum(
weight / dpi for weight, dpi in zip(weights, image_dpis)
)
weighted_dpi = statistics.harmonic_mean(image_dpis, weights)
max_dpi = max(image_dpis)
dpi_average_max_ratio = weighted_dpi / max_dpi
@@ -1155,7 +1170,7 @@ class PdfInfo:
@property
def filename(self) -> str | Path:
"""Return filename of PDF."""
if not isinstance(self._infile, (str, Path)):
if not isinstance(self._infile, str | Path):
raise NotImplementedError("can't get filename from stream")
return self._infile
+2 -2
View File
@@ -5,12 +5,12 @@
from __future__ import annotations
import re
from collections.abc import Mapping
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from math import copysign
from os import PathLike
from pathlib import Path
from typing import Any, Iterator
from typing import Any
from unittest.mock import patch
import pdfminer
+2 -3
View File
@@ -8,12 +8,11 @@ import logging
import os
import re
import sys
from collections.abc import Mapping, Sequence
from collections.abc import Callable, Mapping, Sequence
from contextlib import suppress
from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
from subprocess import run as subprocess_run
from typing import Callable, Union
from packaging.version import Version
@@ -23,7 +22,7 @@ from ocrmypdf.exceptions import MissingDependencyError
log = logging.getLogger(__name__)
Args = Sequence[Union[Path, str]]
Args = Sequence[Path | str]
OsEnviron = os._Environ # pylint: disable=protected-access
+2 -7
View File
@@ -9,18 +9,13 @@ import os
import re
import shutil
import sys
from collections.abc import Iterable, Iterator
from collections.abc import Callable, Iterable, Iterator
from itertools import chain
from pathlib import Path
from typing import Any, Callable, TypeVar
from typing import Any, TypeAlias, TypeVar
from packaging.version import InvalidVersion, Version
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias # pragma: no cover
if sys.platform == 'win32':
# mypy understands 'if sys.platform' better than try/except ModuleNotFoundError
import winreg # pylint: disable=import-error
@@ -0,0 +1,24 @@
i a la Waterman
4 ons linzen
3 liter water
3 uien
bloem, boter
2 kopjes melk
laurier, kruidnagel, kerrie, zout
De linzgen wassen en in-l liter kokend wa-
ter 1 dag laten weken, 2 liter water bij
de linzen voegen, zonder het water waarin
ze geweekt zijn af te gieten, De helft van
de uien bakken met laurier en Kruidnagel.
Alle uien, kerrie en zgout bij de linzen
voegen, Alles aan de kook brengen, Van de
bloem met boter en melk een papje maken en
verder afmaken met de soep, Als de linzen
gfgaar Zijn is de soep klaar.
@@ -0,0 +1,15 @@
Tarnose
Bokale oa
Lehuntze
Mugerre
Milafranga Komunikabideak
BAIONA zeiteninsiie —
7 Trenbideak -----
t\ Basusarri — spmeans:20141004 ae: . _ ~
@@ -0,0 +1 @@
Covfefe is a perfectly cromulent word.
@@ -0,0 +1,24 @@
Linzensoep a la Waterman
4 ons linzen
3 liter water
3 uien
bloem, boter
2 kopjes melk
laurier, kruidnagel, kerrie, zout
De linzgen wassen en in-l liter kokend wa-
ter 1 dag laten weken, 2 liter water bij
de linzen voegen, zonder het water waarin
ze geweekt zijn af te gieten, De helft van
de uien bakken met laurier en Kruidnagel.
Alle uien, kerrie en zgout bij de linzen
voegen, Alles aan de kook brengen, Van de
bloem met boter en melk een papje maken en
verder afmaken met de soep, Als de linzen
gfgaar Zijn is de soep klaar.
@@ -0,0 +1,12 @@
Portez ce vieux whisky au juge
blond qui fume sur son Ile
interieure, a cöte de l'alcöve
ovoide, oU les büches se
consument dans l'ätre, ce qui
ui permet de penser & la
caenogenese de |'etre dont il
est question dans la cause
ambigu& entendue a MoY, dans
un capharnaüm qui, pense-t-il,
diminue ca et la la qualit& de son
ceuvre.
@@ -0,0 +1,22 @@
600
500 .
400 _ EB fp ys
300 —f— EN / ~/ Y
Tg ANA a3
100 - nee -eemiae
0-+—-* a ee ee ee eee
S @ s > oO © ve S ) @ + & & “A 5 oo
Se ° Ps as ge eS € x ro NS Po e? & s AS
a a © FF SF HY HK SK BM ee sO
e < Na : > cy xs eS @ Ww a) Oo
ee S&S FF SF HK S e © 4
~ & e
3 x
—¢—Support
—H— No vote[note 1]
ir Oppose
—=—Net[note 2]
re Percentage [note 3]
+12
View File
@@ -71,3 +71,15 @@
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.6", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/poster.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.6", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.6", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/poster.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/francais.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__--oem__1__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/trivial.pdf", "args": ["-l", "eng", "--oem", "1", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/trivial.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/2400dpi.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__thresholding_method=1__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/trivial.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "-c", "thresholding_method=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__thresholding_method=2__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/trivial.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "-c", "thresholding_method=2", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/graph_ocred.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/poster.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "5.3.2", "system": "Linux", "python": "3.11.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/poster.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
@@ -0,0 +1,118 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
© Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
synthesizers!
© Ultra-fast 3!” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
e Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
¢ Optional SMPTE time code synchronization.
¢ Optional remote control.
Recording a Sequence
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spot-recording. To overdub a new part,
select a different track and start recording—while you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
sustain pedal, and program changes!
Editing
To erase a wrong note, simply hold ERASE and press
the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specific points within a sequence,
Additional Features
simply use LOCATE, FAST FORWARD, or REWIND to
find the desired bar number, then start recording.
The INSERT/COPY function allows you to move bars
from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections.
Creating a Song
One way to create a song is to record each track all the
way through (up to 999 bars). Another way is to record
each basic section (verse, chorus, etc.) in individual
sequences, then use the CREATE SONG function to “chain”
them together. CREATE SONG will then automatically
copy all the parts into a new sequence. If desired, you can
even set the last few bars to repeat infinitely, for a fadeout.
Composition Without Compromise
The technology you use should never be so complex that
it interferes with the creative process. Thats precisely why
the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
¢ Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
® Non-destructive recording—existing notes are not erased while recording.
© Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
¢ Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
@ Will sync to standard LinnDrum or Linn 9000 sync tone.
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
¢ TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
e Any TIME SIGNATURE may be used, and may be changed within a song.
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
-1
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
from pathlib import Path
from subprocess import CalledProcessError
from unittest.mock import patch
+4
View File
@@ -4,6 +4,7 @@
from __future__ import annotations
import os
import platform
import pytest
@@ -13,6 +14,9 @@ from .conftest import run_ocrmypdf_api
@pytest.mark.skipif(os.name == 'nt', reason="Windows doesn't have SIGKILL")
@pytest.mark.skipif(
platform.python_version_tuple() >= ('3', '12'), reason="can deadlock due to fork"
)
def test_simulate_oom_killer(multipage, no_outpdf):
exitcode = run_ocrmypdf_api(
multipage,
+1 -1
View File
@@ -17,7 +17,7 @@ from PIL import Image
import ocrmypdf
from ocrmypdf._exec import tesseract
from ocrmypdf.exceptions import ExitCode, MissingDependencyError, OutputFileAccessError
from ocrmypdf.exceptions import ExitCode, MissingDependencyError
from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
from ocrmypdf.subprocess import get_version
+4 -3
View File
@@ -176,9 +176,10 @@ def test_multiple_pngs(resources, outdir):
)
mock.assert_called()
with pikepdf.open(outdir / 'in.pdf') as inpdf, pikepdf.open(
outdir / 'out.pdf'
) as outpdf:
with (
pikepdf.open(outdir / 'in.pdf') as inpdf,
pikepdf.open(outdir / 'out.pdf') as outpdf,
):
for n in range(len(inpdf.pages)):
inim = next(iter(inpdf.pages[n].images.values()))
outim = next(iter(outpdf.pages[n].images.values()))
+53 -29
View File
@@ -4,10 +4,10 @@
from __future__ import annotations
import operator
import warnings
from io import BytesIO
from math import cos, pi, sin
from os import fspath
from subprocess import run
import img2pdf
import pikepdf
@@ -22,10 +22,6 @@ from ocrmypdf.pdfinfo import PdfInfo
from .conftest import check_ocrmypdf, run_ocrmypdf_api
warnings.filterwarnings(
"ignore", category=DeprecationWarning, module="reportlab.lib.rl_safe_eval"
)
# pylintx: disable=unused-variable
RENDERERS = ['hocr', 'sandwich']
@@ -215,34 +211,35 @@ def test_rotate_deskew_ocr_timeout(resources, outdir):
assert cmp > 0.95
def make_rotate_test(imagefile, outdir, prefix, image_angle, page_angle):
memimg = BytesIO()
with Image.open(fspath(imagefile)) as im:
if image_angle != 0:
ccw_angle = -image_angle % 360
im = im.transpose(getattr(Image.Transpose, f'ROTATE_{ccw_angle}'))
im.save(memimg, format='PNG')
memimg.seek(0)
mempdf = BytesIO()
img2pdf.convert(
memimg.read(),
layout_fun=img2pdf.get_fixed_dpi_layout_fun((200, 200)),
outputstream=mempdf,
**IMG2PDF_KWARGS,
)
mempdf.seek(0)
with pikepdf.open(mempdf) as pdf:
pdf.pages[0].Rotate = page_angle
target = outdir / f'{prefix}_{image_angle}_{page_angle}.pdf'
pdf.save(target)
return target
@pytest.mark.slow
@pytest.mark.parametrize('page_angle', (0, 90, 180, 270))
@pytest.mark.parametrize('image_angle', (0, 90, 180, 270))
def test_rotate_page_level(image_angle, page_angle, resources, outdir, caplog):
def make_rotate_test(prefix, image_angle, page_angle):
memimg = BytesIO()
with Image.open(fspath(resources / 'typewriter.png')) as im:
if image_angle != 0:
ccw_angle = -image_angle % 360
im = im.transpose(getattr(Image.Transpose, f'ROTATE_{ccw_angle}'))
im.save(memimg, format='PNG')
memimg.seek(0)
mempdf = BytesIO()
img2pdf.convert(
memimg.read(),
layout_fun=img2pdf.get_fixed_dpi_layout_fun((200, 200)),
outputstream=mempdf,
**IMG2PDF_KWARGS,
)
mempdf.seek(0)
with pikepdf.open(mempdf) as pdf:
pdf.pages[0].Rotate = page_angle
target = outdir / f'{prefix}_{image_angle}_{page_angle}.pdf'
pdf.save(target)
return target
reference = make_rotate_test('ref', 0, 0)
test = make_rotate_test('test', image_angle, page_angle)
reference = make_rotate_test(resources / 'typewriter.png', outdir, 'ref', 0, 0)
test = make_rotate_test(resources, outdir, 'test', image_angle, page_angle)
out = test.with_suffix('.out.pdf')
exitcode = run_ocrmypdf_api(
@@ -258,6 +255,33 @@ def test_rotate_page_level(image_angle, page_angle, resources, outdir, caplog):
assert compare_images_monochrome(outdir, reference, 1, out, 1) > 0.2
@pytest.mark.slow
@pytest.mark.parametrize('page_rotate_angle', (0, 90, 180, 270))
def test_page_rotate_tag(page_rotate_angle, resources, outdir, caplog):
# Check that pages that have an image that is misrotated but restored to
# correct rotation with a /Rotate will be processed correct and yield text.
test = make_rotate_test(
resources / 'crom.png', outdir, 'test', -page_rotate_angle, page_rotate_angle
)
out = test.with_suffix('.out.pdf')
exitcode = run_ocrmypdf_api(
test,
out,
'-O0',
)
assert exitcode == 0, caplog.text
def pdftotext(filename):
return (
run(['pdftotext', '-enc', 'UTF-8', filename, '-'], capture_output=True)
.stdout.strip()
.decode('utf-8')
)
test_text = pdftotext(out)
assert 'is a' in test_text, test_text
def test_rasterize_rotates(resources, tmp_path):
pm = get_plugin_manager([])
+1 -2
View File
@@ -7,10 +7,9 @@ from math import isclose
import pytest
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.pdfinfo import PdfInfo
from .conftest import check_ocrmypdf, run_ocrmypdf_api
from .conftest import check_ocrmypdf
# pylint: disable=redefined-outer-name