Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a78458821 | ||
|
|
fddf3ce2f4 | ||
|
|
353b34e695 | ||
|
|
7d63355c3c | ||
|
|
42ff7fc842 | ||
|
|
26470fe16a | ||
|
|
3b9d4b7f0a | ||
|
|
11f53fe9a9 | ||
|
|
123c0c766f | ||
|
|
6a9be2142e | ||
|
|
0bc350f55e | ||
|
|
7a6edf62ba | ||
|
|
07b6f06f11 | ||
|
|
2005f622bb | ||
|
|
cca04fd799 | ||
|
|
75bf8e4ba2 | ||
|
|
daabb5b100 | ||
|
|
035ebea72f | ||
|
|
a499956462 | ||
|
|
74d2a156c4 | ||
|
|
f87fc7b12d | ||
|
|
602f5632cb | ||
|
|
9fbbcf7599 | ||
|
|
9498f01f59 | ||
|
|
2c59aca5a1 | ||
|
|
51301d69c9 | ||
|
|
7e608fd1df | ||
|
|
ecc79315df | ||
|
|
b71d16dd96 | ||
|
|
fd593eb5e9 | ||
|
|
a0b98abb94 | ||
|
|
18353e1e94 | ||
|
|
9adcad84da | ||
|
|
f2714586d8 |
@@ -21,7 +21,7 @@ body:
|
|||||||
id: reproduce
|
id: reproduce
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to reproduce
|
label: Steps to reproduce
|
||||||
description: Please include steps to reproduce
|
description: Please include steps to reproduce.
|
||||||
value: |
|
value: |
|
||||||
1. Run ocrmypdf -v1 ...arguments... input.pdf output.pdf
|
1. Run ocrmypdf -v1 ...arguments... input.pdf output.pdf
|
||||||
2. Open output.pdf
|
2. Open output.pdf
|
||||||
@@ -31,9 +31,8 @@ body:
|
|||||||
id: files
|
id: files
|
||||||
attributes:
|
attributes:
|
||||||
label: Files
|
label: Files
|
||||||
description: Please attach the input and output files, or any screenshots that may be helpful.
|
description: |
|
||||||
placeholder: |
|
Please attach the input and output files, or any screenshots that may be helpful.
|
||||||
Drag and drop files here.
|
|
||||||
|
|
||||||
If you cannot provide a test file, we probably won't be able to help with the issue.
|
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
|
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:
|
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 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.
|
- 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
|
- Use ``qpdf --json yourfile.pdf`` to produce a JSON representation of your file that
|
||||||
omits personal information.
|
omits personal information.
|
||||||
|
placeholder: |
|
||||||
|
Drag and drop files here.
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: packaging-system
|
id: packaging-system
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.xml
|
files: ./coverage.xml
|
||||||
env_vars: OS,PYTHON
|
env_vars: OS,PYTHON
|
||||||
@@ -158,7 +158,7 @@ jobs:
|
|||||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.xml
|
files: ./coverage.xml
|
||||||
env_vars: OS,PYTHON
|
env_vars: OS,PYTHON
|
||||||
@@ -189,7 +189,7 @@ jobs:
|
|||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: |
|
run: |
|
||||||
choco install --yes --no-progress --pre tesseract
|
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
|
- name: Install Python packages
|
||||||
run: |
|
run: |
|
||||||
@@ -201,7 +201,7 @@ jobs:
|
|||||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.xml
|
files: ./coverage.xml
|
||||||
env_vars: OS,PYTHON
|
env_vars: OS,PYTHON
|
||||||
|
|||||||
+3
-1
@@ -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
|
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
|
file. A caller application will have to ensure it does not write to
|
||||||
standard output either, if it wants to be compatible with this
|
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
|
Exceptions
|
||||||
----------
|
----------
|
||||||
|
|||||||
+16
-2
@@ -187,7 +187,7 @@ might remove desirable content, especially from poor quality scans.
|
|||||||
background from grayscale or color images. Monochrome images are
|
background from grayscale or color images. Monochrome images are
|
||||||
ignored. This should not be used on documents that contain color
|
ignored. This should not be used on documents that contain color
|
||||||
photos as it may remove them.
|
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.
|
rotating them back into place.
|
||||||
- ``--clean`` uses
|
- ``--clean`` uses
|
||||||
`unpaper <https://www.flameeyes.eu/projects/unpaper>`__ to clean up
|
`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
|
the case. Use ``--tesseract-non-ocr-timeout`` to control the timeout
|
||||||
for non-OCR operations, if needed.
|
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
|
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.
|
OCRmyPDF cannot open documents that are encrypted with a digital certificate.
|
||||||
|
|
||||||
Versions of OCRmyPDF prior to 14.4.0 would invalidate existing digital signatures
|
Versions of OCRmyPDF prior to 14.4.0 would invalidate existing digital signatures
|
||||||
without warning.
|
without warning.
|
||||||
|
|||||||
+48
-30
@@ -139,6 +139,32 @@ from sources <#installing-head-revision-from-sources>`__.
|
|||||||
|
|
||||||
.. _ubuntu-lts-latest:
|
.. _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
|
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 LTS
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. To
|
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. The
|
||||||
install a more recent version, uninstall the system-provided version of
|
most convenient way to install recent OCRmyPDF on older Ubuntu is to use
|
||||||
ocrmypdf, and install the following dependencies:
|
Homebrew on Linux (Linuxbrew).
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
sudo apt-get -y remove ocrmypdf # remove system ocrmypdf, if installed
|
brew install ocrmypdf
|
||||||
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`.
|
|
||||||
|
|
||||||
Arch Linux (AUR)
|
Arch Linux (AUR)
|
||||||
----------------
|
----------------
|
||||||
@@ -540,9 +542,25 @@ try:
|
|||||||
|
|
||||||
pip install --user ocrmypdf
|
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
|
You should then be able to run ``ocrmypdf --version`` and see that the
|
||||||
latest version was located.
|
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
|
Requirements for pip and HEAD install
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
@@ -553,7 +571,7 @@ manager. ``pip`` cannot provide them.
|
|||||||
The following versions are required:
|
The following versions are required:
|
||||||
|
|
||||||
- Python 3.10 or newer
|
- Python 3.10 or newer
|
||||||
- Ghostscript 9.55 or newer
|
- Ghostscript 9.54 or newer
|
||||||
- Tesseract 4.1.1 or newer
|
- Tesseract 4.1.1 or newer
|
||||||
- jbig2enc 0.29 or newer
|
- jbig2enc 0.29 or newer
|
||||||
- pngquant 2.5 or newer
|
- pngquant 2.5 or newer
|
||||||
@@ -671,7 +689,7 @@ To manually install the ``fish`` completion, copy
|
|||||||
Note on 32-bit support
|
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
|
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
|
Pillow. The easiest way to express this to end users is to say we don't
|
||||||
support 32-bit Linux.
|
support 32-bit Linux.
|
||||||
|
|||||||
+9
-1
@@ -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
|
Dependencies include libtoolize and libleptonica, which on Ubuntu systems
|
||||||
are packaged as libtool and libleptonica-dev. On Fedora (35) they are packaged
|
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
|
Lossy mode JBIG2
|
||||||
================
|
================
|
||||||
|
|||||||
@@ -30,6 +30,27 @@ OCRmyPDF typically supports the three most recent Python versions.
|
|||||||
|
|
||||||
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
|
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
|
||||||
|
|
||||||
|
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
|
v16.0.3
|
||||||
=======
|
=======
|
||||||
|
|||||||
+5
-1
@@ -131,7 +131,11 @@ norecursedirs = ["lib", ".pc", ".git", "venv", "output", "cache", "resources"]
|
|||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
addopts = "-n auto"
|
addopts = "-n auto"
|
||||||
markers = ["slow"]
|
markers = ["slow"]
|
||||||
filterwarnings = ["ignore:.*XMLParser.*:DeprecationWarning"]
|
filterwarnings = [
|
||||||
|
"ignore:.*XMLParser.*:DeprecationWarning",
|
||||||
|
"ignore:.*ast.NameConstant.*:DeprecationWarning:reportlab",
|
||||||
|
"ignore:.*distutils.*:DeprecationWarning:libxmp",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -289,9 +289,9 @@ class OcrGrafter:
|
|||||||
|
|
||||||
# Translate the text so it is centered at (0, 0), rotate it there, adjust
|
# 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
|
# for a size different between initial and text PDF, then untranslate, and
|
||||||
# finally move the lower left corner to match the mediabox. All transforms
|
# finally move the lower left corner to match the mediabox.
|
||||||
# must be premultiplied so they are applied in reverse order here.
|
ctm = translate @ rotate @ scale @ untranslate @ corner
|
||||||
ctm = corner @ untranslate @ scale @ rotate @ translate
|
log.debug("Grafting with ctm %r", ctm)
|
||||||
|
|
||||||
base_resources = _ensure_dictionary(base_page.obj, Name.Resources)
|
base_resources = _ensure_dictionary(base_page.obj, Name.Resources)
|
||||||
base_xobjs = _ensure_dictionary(base_resources, Name.XObject)
|
base_xobjs = _ensure_dictionary(base_resources, Name.XObject)
|
||||||
@@ -311,9 +311,16 @@ class OcrGrafter:
|
|||||||
|
|
||||||
if strip_old_text:
|
if strip_old_text:
|
||||||
strip_invisible_text(self.pdf_base, base_page)
|
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(
|
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)
|
_update_resources(obj=base_page.obj, font=font, font_key=font_key)
|
||||||
|
|||||||
+83
-11
@@ -12,6 +12,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from collections.abc import Iterable, Iterator, Sequence
|
from collections.abc import Iterable, Iterator, Sequence
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copyfileobj, copystat
|
from shutil import copyfileobj, copystat
|
||||||
from typing import Any, BinaryIO, TypeVar, cast
|
from typing import Any, BinaryIO, TypeVar, cast
|
||||||
@@ -34,7 +35,8 @@ from ocrmypdf.exceptions import (
|
|||||||
UnsupportedImageFormatError,
|
UnsupportedImageFormatError,
|
||||||
)
|
)
|
||||||
from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink
|
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.pdfa import generate_pdfa_ps
|
||||||
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
|
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
|
||||||
from ocrmypdf.pluginspec import OrientationConfidence
|
from ocrmypdf.pluginspec import OrientationConfidence
|
||||||
@@ -712,19 +714,29 @@ def create_pdf_page_from_image(
|
|||||||
pageinfo = page_context.pageinfo
|
pageinfo = page_context.pageinfo
|
||||||
pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches)
|
pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches)
|
||||||
effective_rotation = (pageinfo.rotation - orientation_correction) % 360
|
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]
|
pagesize = pagesize[1], pagesize[0]
|
||||||
|
|
||||||
# This create a single page PDF
|
# Create a new single page PDF to hold
|
||||||
with open(image, 'rb') as imfile, open(output_file, 'wb') as pdf:
|
bio = BytesIO()
|
||||||
|
with open(image, 'rb') as imfile:
|
||||||
log.debug('convert')
|
log.debug('convert')
|
||||||
|
|
||||||
layout_fun = img2pdf.get_layout_fun(pagesize)
|
layout_fun = img2pdf.get_layout_fun(pagesize)
|
||||||
img2pdf.convert(
|
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')
|
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(
|
output_file = page_context.plugin_manager.hook.filter_pdf_page(
|
||||||
page=page_context, image_filename=image, output_pdf=output_file
|
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
|
return output_file
|
||||||
|
|
||||||
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
|
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(
|
HocrTransform(
|
||||||
hocr_filename=hocr,
|
hocr_filename=hocr, dpi=dpi.to_scalar(), **debug_kwargs # square
|
||||||
dpi=dpi.to_scalar(), # square
|
|
||||||
debug=debug_mode,
|
|
||||||
).to_pdf(
|
).to_pdf(
|
||||||
out_filename=output_file,
|
out_filename=output_file,
|
||||||
image_filename=None,
|
image_filename=None,
|
||||||
|
invisible_text=True if not debug_kwargs else False,
|
||||||
)
|
)
|
||||||
return output_file
|
return output_file
|
||||||
|
|
||||||
@@ -769,7 +791,57 @@ def ocr_engine_textonly_pdf(
|
|||||||
output_text=output_text,
|
output_text=output_text,
|
||||||
options=options,
|
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:
|
def generate_postscript_stub(context: PdfContext) -> Path:
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ def _exec_page_sync(page_context: PageContext) -> PageResult:
|
|||||||
page_context
|
page_context
|
||||||
)
|
)
|
||||||
ocr_out, text_out = _image_to_ocr_text(page_context, ocr_image_out)
|
ocr_out, text_out = _image_to_ocr_text(page_context, ocr_image_out)
|
||||||
|
|
||||||
return PageResult(
|
return PageResult(
|
||||||
pageno=page_context.pageno,
|
pageno=page_context.pageno,
|
||||||
pdf_page_from_image=pdf_page_from_image_out,
|
pdf_page_from_image=pdf_page_from_image_out,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Currently all blacklisted versions are lower than 9.55, so none need to
|
# 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.
|
# 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
|
@hookimpl
|
||||||
@@ -62,6 +62,17 @@ def check_options(options):
|
|||||||
f"Ghostscript {gs_version} contains serious regressions and is not "
|
f"Ghostscript {gs_version} contains serious regressions and is not "
|
||||||
"supported. Please upgrade to a newer version."
|
"supported. Please upgrade to a newer version."
|
||||||
)
|
)
|
||||||
|
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':
|
if options.output_type == 'pdfa':
|
||||||
options.output_type = 'pdfa-2'
|
options.output_type = 'pdfa-2'
|
||||||
|
|||||||
@@ -146,7 +146,11 @@ def check_options(options):
|
|||||||
|
|
||||||
# Decide on what renderer to use
|
# Decide on what renderer to use
|
||||||
if options.pdf_renderer == 'auto':
|
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:
|
if not tesseract.has_thresholding() and options.tesseract_thresholding != 0:
|
||||||
log.warning(
|
log.warning(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from typing import (
|
|||||||
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
import pikepdf
|
import pikepdf
|
||||||
|
from deprecation import deprecated
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ class Resolution(Generic[T]):
|
|||||||
return self._isclose(self.x, other.x) and self._isclose(self.y, other.y)
|
return self._isclose(self.x, other.x) and self._isclose(self.y, other.y)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(deprecated_in='15.4.0')
|
||||||
class NeverRaise(Exception):
|
class NeverRaise(Exception):
|
||||||
"""An exception that is never raised."""
|
"""An exception that is never raised."""
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ from pikepdf.canvas import Font
|
|||||||
log = logging.getLogger(__name__)
|
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)
|
CID_TO_GID_DATA = zlib.compress(b"\x00\x01" * 65536)
|
||||||
GLYPHLESS_FONT_NAME = 'pdf.ttf'
|
GLYPHLESS_FONT_NAME = 'pdf.ttf'
|
||||||
GLYPHLESS_FONT = (package_files('ocrmypdf.data') / GLYPHLESS_FONT_NAME).read_bytes()
|
GLYPHLESS_FONT = (package_files('ocrmypdf.data') / GLYPHLESS_FONT_NAME).read_bytes()
|
||||||
@@ -27,11 +32,14 @@ class GlyphlessFont(Font):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
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."""
|
"""Estimate the width of a text string when rendered with the given font."""
|
||||||
# NFKC: split ligatures, combine diacritics
|
# NFKC: split ligatures, combine diacritics
|
||||||
return len(unicodedata.normalize("NFKC", text)) * (fontsize / self.CHAR_ASPECT)
|
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):
|
def register(self, pdf: Pdf):
|
||||||
"""Register the glyphless font.
|
"""Register the glyphless font.
|
||||||
|
|
||||||
@@ -110,3 +118,24 @@ class GlyphlessFont(Font):
|
|||||||
font_descriptor.FontFile2 = pdf.make_stream(self.GLYPHLESS_FONT)
|
font_descriptor.FontFile2 = pdf.make_stream(self.GLYPHLESS_FONT)
|
||||||
cid_font_type2.FontDescriptor = font_descriptor
|
cid_font_type2.FontDescriptor = font_descriptor
|
||||||
return basefont
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ from pikepdf.canvas import (
|
|||||||
MAGENTA,
|
MAGENTA,
|
||||||
RED,
|
RED,
|
||||||
Canvas,
|
Canvas,
|
||||||
Font,
|
|
||||||
Text,
|
Text,
|
||||||
TextDirection,
|
TextDirection,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ocrmypdf.hocrtransform._font import EncodableFont as Font
|
||||||
from ocrmypdf.hocrtransform._font import GlyphlessFont
|
from ocrmypdf.hocrtransform._font import GlyphlessFont
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -45,12 +45,12 @@ Element = ElementTree.Element
|
|||||||
class DebugRenderOptions:
|
class DebugRenderOptions:
|
||||||
"""A class for managing rendering options."""
|
"""A class for managing rendering options."""
|
||||||
|
|
||||||
render_paragraph_bbox: bool
|
render_paragraph_bbox: bool = False
|
||||||
render_baseline: bool
|
render_baseline: bool = False
|
||||||
render_triangle: bool
|
render_triangle: bool = False
|
||||||
render_line_bbox: bool
|
render_line_bbox: bool = False
|
||||||
render_word_bbox: bool
|
render_word_bbox: bool = False
|
||||||
render_space_bbox: bool
|
render_space_bbox: bool = False
|
||||||
|
|
||||||
|
|
||||||
class HocrTransformError(Exception):
|
class HocrTransformError(Exception):
|
||||||
@@ -81,8 +81,22 @@ class HocrTransform:
|
|||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
fontname: Name = Name("/f-0-0"),
|
fontname: Name = Name("/f-0-0"),
|
||||||
font: Font = GlyphlessFont(),
|
font: Font = GlyphlessFont(),
|
||||||
|
debug_render_options: DebugRenderOptions | None = None,
|
||||||
):
|
):
|
||||||
"""Initialize the HocrTransform object."""
|
"""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.dpi = dpi
|
||||||
self.hocr = ElementTree.parse(os.fspath(hocr_filename))
|
self.hocr = ElementTree.parse(os.fspath(hocr_filename))
|
||||||
self._fontname = fontname
|
self._fontname = fontname
|
||||||
@@ -103,14 +117,6 @@ class HocrTransform:
|
|||||||
self.height = (coords.ury - coords.lly) / (self.dpi / INCH)
|
self.height = (coords.ury - coords.lly) / (self.dpi / INCH)
|
||||||
# Stop after first div that has page coordinates
|
# Stop after first div that has page coordinates
|
||||||
break
|
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):
|
def _get_element_text(self, element: Element):
|
||||||
"""Return the textual content of the element and its children."""
|
"""Return the textual content of the element and its children."""
|
||||||
@@ -215,12 +221,13 @@ class HocrTransform:
|
|||||||
if not found_lines:
|
if not found_lines:
|
||||||
# Tesseract did not report any lines (just words)
|
# Tesseract did not report any lines (just words)
|
||||||
root = self.hocr.find(self._child_xpath('div', 'ocr_page'))
|
root = self.hocr.find(self._child_xpath('div', 'ocr_page'))
|
||||||
|
direction = self._get_text_direction(root)
|
||||||
self._do_line(
|
self._do_line(
|
||||||
canvas,
|
canvas,
|
||||||
root,
|
root,
|
||||||
"ocrx_word",
|
"ocrx_word",
|
||||||
invisible_text,
|
invisible_text,
|
||||||
TextDirection.LTR,
|
direction,
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
# put the image on the page, scaled to fill the page
|
# put the image on the page, scaled to fill the page
|
||||||
@@ -311,8 +318,7 @@ class HocrTransform:
|
|||||||
line_box_height = abs(line_box.height) / cos(angle)
|
line_box_height = abs(line_box.height) / cos(angle)
|
||||||
fontsize = line_box_height + intercept
|
fontsize = line_box_height + intercept
|
||||||
text.font(self._fontname, fontsize)
|
text.font(self._fontname, fontsize)
|
||||||
if invisible_text or True:
|
text.render_mode(3 if invisible_text else 0)
|
||||||
text.render_mode(3) # Invisible (indicates OCR text)
|
|
||||||
|
|
||||||
self._debug_draw_baseline(
|
self._debug_draw_baseline(
|
||||||
canvas, line_matrix.inverse().transform(line_box), 0
|
canvas, line_matrix.inverse().transform(line_box), 0
|
||||||
@@ -362,10 +368,15 @@ class HocrTransform:
|
|||||||
self._debug_draw_word_bbox(canvas, box)
|
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 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:
|
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.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)
|
# Get coordinates of the next word (if there is one)
|
||||||
hocr_next_box = (
|
hocr_next_box = (
|
||||||
@@ -385,11 +396,16 @@ class HocrTransform:
|
|||||||
elif text_direction == TextDirection.RTL:
|
elif text_direction == TextDirection.RTL:
|
||||||
space_box = Rectangle(next_box.urx, box.lly, box.llx, next_box.ury)
|
space_box = Rectangle(next_box.urx, box.lly, box.llx, next_box.ury)
|
||||||
self._debug_draw_space_bbox(canvas, space_box)
|
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)
|
space_width = self._font.text_width(' ', fontsize)
|
||||||
if space_width > 0:
|
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.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):
|
def _debug_draw_paragraph_boxes(self, canvas: Canvas, color=CYAN):
|
||||||
"""Draw boxes around paragraphs in the document."""
|
"""Draw boxes around paragraphs in the document."""
|
||||||
|
|||||||
@@ -155,6 +155,13 @@ def extract_image_jbig2(
|
|||||||
with imgname.open('wb') as f:
|
with imgname.open('wb') as f:
|
||||||
ext = pim.extract_to(stream=f)
|
ext = pim.extract_to(stream=f)
|
||||||
imgname.rename(imgname.with_suffix(ext))
|
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:
|
except UnsupportedImageTypeError:
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -854,6 +854,12 @@ class PageInfo:
|
|||||||
width_pt = mediabox[2] - mediabox[0]
|
width_pt = mediabox[2] - mediabox[0]
|
||||||
height_pt = mediabox[3] - mediabox[1]
|
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
|
check_this_page = pageno in check_pages
|
||||||
|
|
||||||
if check_this_page and detailed_analysis:
|
if check_this_page and detailed_analysis:
|
||||||
@@ -970,6 +976,21 @@ class PageInfo:
|
|||||||
else:
|
else:
|
||||||
raise ValueError("rotation must be a cardinal angle")
|
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
|
@property
|
||||||
def images(self) -> list[ImageInfo]:
|
def images(self) -> list[ImageInfo]:
|
||||||
"""Return images."""
|
"""Return images."""
|
||||||
|
|||||||
BIN
Binary file not shown.
+24
@@ -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.
|
||||||
BIN
Binary file not shown.
+15
@@ -0,0 +1,15 @@
|
|||||||
|
Tarnose
|
||||||
|
|
||||||
|
Bokale oa
|
||||||
|
|
||||||
|
Lehuntze
|
||||||
|
|
||||||
|
Mugerre
|
||||||
|
|
||||||
|
Milafranga Komunikabideak
|
||||||
|
|
||||||
|
BAIONA zeiteninsiie —
|
||||||
|
|
||||||
|
7 Trenbideak -----
|
||||||
|
|
||||||
|
t\ Basusarri — spmeans:20141004 ae: . _ ~
|
||||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
|||||||
|
Covfefe is a perfectly cromulent word.
|
||||||
BIN
Binary file not shown.
+24
@@ -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.
|
||||||
BIN
Binary file not shown.
+12
@@ -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.
|
||||||
BIN
Binary file not shown.
+22
@@ -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]
|
||||||
Vendored
+12
@@ -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_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_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.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"]}
|
||||||
|
|||||||
BIN
Binary file not shown.
+118
@@ -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. It’s 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 Sequencer’s
|
||||||
|
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. That’s 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
|
||||||
BIN
Binary file not shown.
Vendored
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -176,9 +176,10 @@ def test_multiple_pngs(resources, outdir):
|
|||||||
)
|
)
|
||||||
mock.assert_called()
|
mock.assert_called()
|
||||||
|
|
||||||
with pikepdf.open(outdir / 'in.pdf') as inpdf, pikepdf.open(
|
with (
|
||||||
outdir / 'out.pdf'
|
pikepdf.open(outdir / 'in.pdf') as inpdf,
|
||||||
) as outpdf:
|
pikepdf.open(outdir / 'out.pdf') as outpdf,
|
||||||
|
):
|
||||||
for n in range(len(inpdf.pages)):
|
for n in range(len(inpdf.pages)):
|
||||||
inim = next(iter(inpdf.pages[n].images.values()))
|
inim = next(iter(inpdf.pages[n].images.values()))
|
||||||
outim = next(iter(outpdf.pages[n].images.values()))
|
outim = next(iter(outpdf.pages[n].images.values()))
|
||||||
|
|||||||
+53
-29
@@ -4,10 +4,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import operator
|
import operator
|
||||||
import warnings
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from math import cos, pi, sin
|
from math import cos, pi, sin
|
||||||
from os import fspath
|
from os import fspath
|
||||||
|
from subprocess import run
|
||||||
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
import pikepdf
|
import pikepdf
|
||||||
@@ -22,10 +22,6 @@ from ocrmypdf.pdfinfo import PdfInfo
|
|||||||
|
|
||||||
from .conftest import check_ocrmypdf, run_ocrmypdf_api
|
from .conftest import check_ocrmypdf, run_ocrmypdf_api
|
||||||
|
|
||||||
warnings.filterwarnings(
|
|
||||||
"ignore", category=DeprecationWarning, module="reportlab.lib.rl_safe_eval"
|
|
||||||
)
|
|
||||||
|
|
||||||
# pylintx: disable=unused-variable
|
# pylintx: disable=unused-variable
|
||||||
|
|
||||||
RENDERERS = ['hocr', 'sandwich']
|
RENDERERS = ['hocr', 'sandwich']
|
||||||
@@ -215,34 +211,35 @@ def test_rotate_deskew_ocr_timeout(resources, outdir):
|
|||||||
assert cmp > 0.95
|
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.slow
|
||||||
@pytest.mark.parametrize('page_angle', (0, 90, 180, 270))
|
@pytest.mark.parametrize('page_angle', (0, 90, 180, 270))
|
||||||
@pytest.mark.parametrize('image_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 test_rotate_page_level(image_angle, page_angle, resources, outdir, caplog):
|
||||||
def make_rotate_test(prefix, image_angle, page_angle):
|
reference = make_rotate_test(resources / 'typewriter.png', outdir, 'ref', 0, 0)
|
||||||
memimg = BytesIO()
|
test = make_rotate_test(resources, outdir, 'test', image_angle, page_angle)
|
||||||
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)
|
|
||||||
out = test.with_suffix('.out.pdf')
|
out = test.with_suffix('.out.pdf')
|
||||||
|
|
||||||
exitcode = run_ocrmypdf_api(
|
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
|
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):
|
def test_rasterize_rotates(resources, tmp_path):
|
||||||
pm = get_plugin_manager([])
|
pm = get_plugin_manager([])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user