Compare commits

...
29 Commits
Author SHA1 Message Date
James R. Barlow 02d85ff070 Tell uv not to sync dev harder 2024-12-08 23:20:18 -08:00
James R. Barlow 179c36151b Tell uv not to sync dev 2024-12-08 17:27:31 -08:00
James R. Barlow 3c4b099cb1 Don't try to install dev dependencies in build 2024-12-08 14:03:04 -08:00
James R. Barlow 15df9c370c Update notes 2024-12-08 12:20:40 -08:00
James R. Barlow 86d92ef490 Disable logging of markup
Fixes #1444
2024-12-08 12:17:19 -08:00
Elliott Sales de AndradeandGitHub 8f44b29ca3 Update intersphinx mapping to current format (#1443)
This will break somewhere in Sphinx 8.
2024-12-05 23:54:21 -08:00
joskezelenskyandGitHub 5a08a6cfeb Update cookbook.rst (#1440) 2024-12-05 00:49:13 -08:00
James R. Barlow cc058be4b2 v16.7.0 release notes 2024-12-02 11:45:01 -08:00
James R. Barlow 7565d20c0a Fix Docker build and restore ubuntu 24.04 as base Docker image
Ubuntu version changed on suspicion of Ghostscript 10, but that issue is resolved.
2024-12-02 11:36:26 -08:00
James R. Barlow 9a075039b5 Remove empty test file 2024-12-02 11:23:35 -08:00
James R. Barlow 5a1c043331 Check in uv.lock and update uv version 2024-11-27 17:14:07 -08:00
James R. Barlow fe89be5dc0 Fix test broken in commit 85d6fb8c 2024-11-27 15:44:12 -08:00
James R. Barlow d70296b97a Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-11-27 00:10:32 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7d7658018d Bump codecov/codecov-action from 4 to 5 (#1433)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [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/v4...v5)

---
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-11-27 00:04:27 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8fb8e9f72c Bump astral-sh/setup-uv from 3 to 4 (#1436)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 3 to 4.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v4)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  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-11-27 00:04:04 -08:00
James R. Barlow 85d6fb8ce9 Don't use stdout for Ghostscript
Apparently, Ghostscript simply uses temporary files when asked to write to stdout. We're already using temporary files internally, so this eliminates some redundant copies.
2024-11-21 17:02:03 -08:00
James R. Barlow 828e741c24 README add more features 2024-11-17 13:43:47 -08:00
James R. Barlow 36837f8353 Remove conda from installables list (conda's ocrmypdf is old/unmaintained) 2024-11-17 13:38:17 -08:00
James R. Barlow 12fd4f70f1 docs: fix typo 2024-11-17 13:37:31 -08:00
James R. Barlow 250615561d v16.6.2 release notes 2024-11-16 19:05:20 -08:00
James R. Barlow a659f83d67 Remove invalid hyperlink annotations to satisfy Ghostscript 10.x during PDF/A conversion
Closes #1425
2024-11-16 19:02:10 -08:00
James R. Barlow 08f95c0b13 v16.6.1 release notes 2024-11-10 02:09:10 -08:00
James R. Barlow dbd3c93757 Fix issue with unpickling HOCRResult
Fixes [Bug]: HOCRResult.from_json() not unpickling correctly #1427
2024-11-10 02:05:57 -08:00
James R. Barlow 5d128a91d2 Adjust tesseract-ocr5 package and revert to Ubuntu 22.04
22.04 has older Ghostscript which has fewer regressions.
2024-11-08 15:20:40 -08:00
James R. Barlow a1b8113d56 Add bisect script 2024-11-08 11:09:13 -08:00
James R. Barlow f052e910c9 docs: Improve batch command 2024-11-07 00:09:55 -08:00
James R. Barlow 116e2692d0 Also use stable Tesseract 5 for Docker 2024-11-01 15:47:25 -07:00
James R. Barlow b2669c7d71 Use stable tesseract ppa for build 2024-11-01 15:08:53 -07:00
James R. Barlow c8c53d38a3 Remove .git from Docker images nad fix link to alternate services 2024-11-01 10:32:29 -07:00
22 changed files with 2220 additions and 87 deletions
+8 -4
View File
@@ -40,18 +40,18 @@ COPY . /app
WORKDIR /app
RUN curl -LsSf https://astral.sh/uv/0.4.27/install.sh | sh
# Copy uv from ghcr
COPY --from=ghcr.io/astral-sh/uv:0.5.5 /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
# Instead of restarting the shell, use uv directly from its installed location.
RUN /root/.cargo/bin/uv sync --extra test --extra webservice --extra watcher
RUN uv sync --extra test --extra webservice --extra watcher --no-dev
FROM base
RUN apt-get update && apt-get install -y software-properties-common
RUN add-apt-repository -y ppa:alex-p/tesseract-ocr-devel
RUN add-apt-repository -y ppa:alex-p/tesseract-ocr5
RUN apt-get update && apt-get install -y --no-install-recommends \
ghostscript \
@@ -75,6 +75,10 @@ COPY --from=builder /usr/local/bin/ /usr/local/bin/
COPY --from=builder --chown=app:app /app /app
RUN rm -rf /app/.git && \
ln -s /app/misc/webservice.py /app/webservice.py && \
ln -s /app/misc/watcher.py /app/watcher.py
ENV PATH="/app/.venv/bin:${PATH}"
ENTRYPOINT ["/app/.venv/bin/ocrmypdf"]
+6 -2
View File
@@ -29,12 +29,12 @@ COPY . /app
WORKDIR /app
RUN curl -LsSf https://astral.sh/uv/0.4.27/install.sh | sh
COPY --from=ghcr.io/astral-sh/uv:0.5.5 /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
# Instead of restarting the shell, use uv directly from its installed location.
RUN /root/.cargo/bin/uv sync --extra test --extra webservice --extra watcher
RUN uv sync --extra test --extra webservice --extra watcher --no-dev
FROM base
@@ -59,6 +59,10 @@ WORKDIR /app
COPY --from=builder --chown=app:app /app /app
RUN rm -rf /app/.git && \
ln -s /app/misc/webservice.py /app/webservice.py && \
ln -s /app/misc/watcher.py /app/watcher.py
ENV PATH="/app/.venv/bin:${PATH}"
ENTRYPOINT ["/app/.venv/bin/ocrmypdf"]
+21 -22
View File
@@ -40,9 +40,9 @@ jobs:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v4
with:
version: "0.4.27"
version: "0.5.x"
- name: "Set up Python"
uses: actions/setup-python@v5
@@ -50,9 +50,9 @@ jobs:
python-version: ${{ matrix.python }}
- name: Install Tesseract from PPA
if: matrix.tesseract_ppa
if: matrix.tesseract_ppa == 'ppa'
run: |
sudo add-apt-repository -y ppa:alex-p/tesseract-ocr-devel
sudo add-apt-repository -y ppa:alex-p/tesseract-ocr5.3
- name: Install common packages
run: |
@@ -84,7 +84,7 @@ jobs:
- name: Install Python packages
run: |
uv sync --extra test
uv sync --extra test --no-dev
- name: Report versions
run: |
@@ -92,21 +92,20 @@ jobs:
gs --version
pngquant --version
unpaper --version
uv run img2pdf --version
uv run --no-dev img2pdf --version
- name: Test
run: |
uv run pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
uv run --no-dev pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
env_vars: OS,PYTHON
test_macos:
name: Test macOS
runs-on: ${{ matrix.os }}
@@ -137,9 +136,9 @@ jobs:
tesseract
- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v4
with:
version: "0.4.27"
version: "0.5.x"
- name: "Set up Python"
uses: actions/setup-python@v5
@@ -148,21 +147,21 @@ jobs:
- name: Install Python packages
run: |
uv sync --extra test
uv sync --extra test --no-dev
- name: Report versions
run: |
tesseract --version
gs --version
pngquant --version
uv run img2pdf --version
uv run --no-dev img2pdf --version
- name: Test
run: |
uv run pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
uv run --no-dev pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -187,9 +186,9 @@ jobs:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v4
with:
version: "0.4.27"
version: "0.5.x"
- name: "Set up Python"
uses: actions/setup-python@v5
@@ -203,14 +202,14 @@ jobs:
- name: Install Python packages
run: |
uv sync --extra test
uv sync --extra test --no-dev
- name: Test
run: |
uv run pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
uv run --no-dev pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -226,9 +225,9 @@ jobs:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v4
with:
version: "0.4.27"
version: "0.5.x"
- name: Make wheels and sdist
run: |
+25 -2
View File
@@ -74,7 +74,6 @@ Linux, Windows, macOS and FreeBSD are supported. Docker images are also availabl
| macOS (nix) | ``nix-env -i ocrmypdf`` |
| LinuxBrew | ``brew install ocrmypdf`` |
| FreeBSD | ``pkg install py-ocrmypdf`` |
| Conda | ``conda install ocrmypdf`` |
| Ubuntu Snap | ``snap install ocrmypdf`` |
For everyone else, [see our documentation](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for installation steps.
@@ -113,9 +112,33 @@ Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/e
Please report issues on our [GitHub issues](https://github.com/ocrmypdf/OCRmyPDF/issues) page, and follow the issue template for quick response.
## Feature demo
```bash
# Add an OCR layer and convert to PDF/A
ocrmypdf input.pdf output.pdf
# Convert an image to single page PDF
ocrmypdf input.jpg output.pdf
# Add OCR to a file in place (only modifies file on success)
ocrmypdf myfile.pdf myfile.pdf
# OCR with non-English languages (look up your language's ISO 639-3 code)
ocrmypdf -l fra LeParisien.pdf LeParisien.pdf
# OCR multilingual documents
ocrmypdf -l eng+fra Bilingual-English-French.pdf Bilingual-English-French.pdf
# Deskew (straighten crooked pages)
ocrmypdf --deskew input.pdf output.pdf
```
For more features, see the [documentation](https://ocrmypdf.readthedocs.io/en/latest/index.html).
## Requirements
In addition to the required Python version (3.8+), OCRmyPDF requires external program installations of Ghostscript and Tesseract OCR. OCRmyPDF is pure Python, and runs on pretty much everything: Linux, macOS, Windows and FreeBSD.
In addition to the required Python version, OCRmyPDF requires external program installations of Ghostscript and Tesseract OCR. OCRmyPDF is pure Python, and runs on pretty much everything: Linux, macOS, Windows and FreeBSD.
## Press & Media
+3 -3
View File
@@ -235,7 +235,7 @@ The directive ``--tesseract-pagesegmode Nmode`` forwards the desired page segmen
mode to Tesseract OCR. The default is 3.
Page segmentation can improve OCR results when you know that a PDF ought to be
analyzed a particular way, such as PDFs whose pages contain only a single line of
analyzed a particular way, such as PDFs whose pages contain only a single line of
text. For the vast majority of users, changing the page segmentation mode will only
make things worse.
@@ -274,7 +274,7 @@ As of June 2024, the Tesseract page segmentation modes are:
| | Tesseract-specific. |
+-----+----------------------------------------------------------------------------------+
Modes 0, 1, 2, and 12 (all of those that enable orientation and script detection)
Modes 0, 1, 2, and 12 (all of those that enable orientation and script detection)
are not compatible with OCRmyPDF, which performs OSD in a separate step from OCR.
Their use may interfere with ``--rotate-pages`` and other features.
@@ -445,7 +445,7 @@ Debugging the intermediate files
OCRmyPDF normally saves its intermediate results to a temporary folder
and deletes this folder when it exits, whether it succeeded or failed.
If the ``--keep-temporary-files`` (``-k```) argument is issued on the
If the ``--keep-temporary-files`` (``-k``) argument is issued on the
command line, OCRmyPDF will keep the temporary folder and print the location,
whether it succeeded or failed. An example message is:
+1 -1
View File
@@ -44,7 +44,7 @@ place, and printing each filename in between runs:
.. code-block:: bash
find . -printf '%p\n' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
find . -name '*.pdf' -printf '%p\n' -exec ocrmypdf '{}' '{}' \;
This only runs one ``ocrmypdf`` process at a time. This variation uses
``find`` to create a directory list and ``parallel`` to parallelize runs
+1 -1
View File
@@ -42,7 +42,7 @@ extensions = [
]
# Extension settings
intersphinx_mapping = {'https://docs.python.org/': None}
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
napoleon_use_rtype = False
issues_github_path = "ocrmypdf/OCRmyPDF"
+1 -1
View File
@@ -399,7 +399,7 @@ Some users may consider enabling lossy JBIG2. See: :ref:`jbig2-lossy`.
Digitally signed PDFs
=====================
OCRmyPDF cannot preserve digital signatures in PDFs and also add to OCR to them.
OCRmyPDF cannot preserve digital signatures in PDFs and also add OCR to them.
By default, it will refuse to modify a signed PDF regardless of other settings. You can
override this behavior with ``--invalidate-digital-signatures``; as the name suggests,
any digital signatures will be invalidated.
+3 -3
View File
@@ -69,9 +69,9 @@ OCRmyPDF will use all available CPU cores. See the Docker documentation for
if you are using Docker on macOS or Windows, where you may need to manually assign
more resources. On Linux, all resources will be available automatically.
The underlying operating system and other details in Docker images are subject
to change at minor releases. If you are modifying the image, you should pin
the version you intend to use.
The underlying operating system and other details in Docker images are considered
implementation details and **subject to change at minor releases**. If you are
modifying the image, you should pin the version you intend to use.
Using the Docker image on the command line
==========================================
-2
View File
@@ -31,8 +31,6 @@ These platforms have one-liner installs:
+-------------------------------+-----------------------------------------+
| FreeBSD | ``pkg install textproc/py-ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| Conda (WSL, macOS, Linux) | ``conda install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| Snap (snapcraft packaging) | ``snap install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
+27
View File
@@ -30,6 +30,33 @@ OCRmyPDF typically supports the three most recent Python versions.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
v16.7.0
=======
- Fixed further issues with Docker build and updated some versions.
- Main Docker image returned to Ubuntu 24.04 since the fix in v16.6.2 resolved
that concern.
- Code that previously sent Ghostscript output to stdout has been changed to
output to temporary files, since Ghostscript was doing that anyway internally.
This is a modest efficiency improvement.
- Fixed an issue with debug log output being parsed as rich markup. :issue:`1444`
v16.6.2
=======
- Remove invalid hyperlink annotations to satisfy Ghostscript 10.x during PDF/A
conversion. :issue:`1425`
v16.6.1
=======
- Fixed some issues with Docker build, such as removing unnecessary content and using
a stable Tesseract version.
- Reverted Docker image to Ubuntu 22.04 to access older/more stable Ghostscript
for now.
- Clarified batch commands in documentation.
- Fixed an issue with JSON serialization and pickling of HOCRResult. :issue:`1427`
v16.6.0
=======
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MIT
"""Helper script for bisecting PDFs to find a page with an issue."""
import sys
import pikepdf
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <input.pdf>")
sys.exit(1)
with pikepdf.open(sys.argv[1]) as pdf:
num_pages = len(pdf.pages)
low = 0
high = num_pages - 1
while low <= high:
mid = (low + high) // 2
with pikepdf.new() as new_pdf:
new_pdf.pages.extend(pdf.pages[low : mid + 1])
new_pdf.save(f"bisect-issue-{low + 1}-{mid + 1}.pdf")
print(f"Is bisect-issue-{low + 1}-{mid + 1}.pdf good or bad?", end=" ")
while True:
response = input().lower()
if response == "good":
low = mid + 1
break
elif response == "bad":
high = mid - 1
break
else:
print("Please respond with 'good' or 'bad'.")
print(f"The issue is on page {low + 1} of the original PDF.")
with pikepdf.new() as new_pdf:
new_pdf.pages.extend(pdf.pages[low])
new_pdf.save(f"bisect-issue-bad-{low + 1}.pdf")
with pikepdf.new() as new_pdf:
new_pdf.pages.extend(pdf.pages[:low])
new_pdf.pages.extend(pdf.pages[low + 1 :])
new_pdf.save(f"bisect-issue-good-{low + 1}.pdf")
+8 -2
View File
@@ -118,8 +118,6 @@ filterwarnings = [
[[tool.mypy.overrides]]
module = [
'pluggy',
'tqdm',
'coloredlogs',
'img2pdf',
'pdfminer.*',
'reportlab.*',
@@ -155,3 +153,11 @@ convention = "google"
[tool.ruff.format]
quote-style = "preserve"
[dependency-groups]
dev = [
"mypy>=1.13.0",
"pymupdf>=1.24.14",
"streamlit-pdf-viewer>=0.0.19",
"streamlit>=1.40.2",
]
+66
View File
@@ -0,0 +1,66 @@
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""OCRmyPDF PDF annotation cleanup."""
from __future__ import annotations
import logging
from pikepdf import Dictionary, Name, NameTree, Pdf
log = logging.getLogger(__name__)
def remove_broken_goto_annotations(pdf: Pdf) -> bool:
"""Remove broken goto annotations from a PDF.
If a PDF contains a GoTo Action that points to a named destination that does not
exist, Ghostscript PDF/A conversion will fail. In any event, a named destination
that is not defined is not useful.
Args:
pdf: Opened PDF file.
Returns:
bool: True if the file was modified, False if not.
"""
modified = False
# Check if there are any named destinations
if Name.Names not in pdf.Root:
return modified
if Name.Dests not in pdf.Root[Name.Names]:
return modified
dests = pdf.Root[Name.Names][Name.Dests]
if not isinstance(dests, Dictionary):
return modified
nametree = NameTree(dests)
# Create a set of all named destinations
names = set(k for k in nametree.keys())
for n, page in enumerate(pdf.pages):
if Name.Annots not in page:
continue
for annot in page[Name.Annots]:
if not isinstance(annot, Dictionary):
continue
if Name.A not in annot or Name.D not in annot[Name.A]:
continue
# We found an annotation that points to a named destination
named_destination = str(annot[Name.A][Name.D])
if named_destination not in names:
# If there is no corresponding named destination, remove the
# annotation. Having no destination set is still valid and just
# makes the link non-functional.
log.warning(
f"Disabling a hyperlink annotation on page {n + 1} to a "
"non-existent named destination "
f"{named_destination}."
)
del annot[Name.A][Name.D]
modified = True
return modified
+17 -14
View File
@@ -125,7 +125,7 @@ def rasterize_pdf(
+ (['-dPDFSTOPONERROR'] if stop_on_error else [])
+ [
'-o',
'-',
fspath(output_file),
'-sstdout=%stderr', # Literal %s, not string interpolation
'-dAutoRotatePages=/None', # Probably has no effect on raster
'-f',
@@ -137,14 +137,15 @@ def rasterize_pdf(
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
except CalledProcessError as e:
log.error(e.stderr.decode(errors='replace'))
raise SubprocessOutputError('Ghostscript rasterizing failed') from e
else:
stderr = p.stderr.decode(errors='replace')
if _gs_error_reported(stderr):
log.error(stderr)
Path(output_file).unlink(missing_ok=True)
raise SubprocessOutputError("Ghostscript rasterizing failed") from e
stderr = p.stderr.decode(errors='replace')
if _gs_error_reported(stderr):
log.error(stderr)
try:
with Image.open(BytesIO(p.stdout)) as im:
with Image.open(output_file) as im:
if rotation is not None:
log.debug("Rotating output by %i", rotation)
# rotation is a clockwise angle and Image.ROTATE_* is
@@ -157,13 +158,19 @@ def rasterize_pdf(
im = im.transpose(Image.Transpose.ROTATE_270)
if rotation % 180 == 90:
page_dpi = page_dpi.flip_axis()
im.save(fspath(output_file), dpi=page_dpi)
im.save(output_file, dpi=page_dpi)
except UnidentifiedImageError:
log.error(
f"Ghostscript (using {raster_device} at {raster_dpi} dpi) produced "
"an invalid page image file."
)
raise
except OSError as e:
log.error(
f"Ghostscript (using {raster_device} at {raster_dpi} dpi) produced "
"an invalid page image file."
)
raise UnidentifiedImageError() from e
class GhostscriptFollower:
@@ -271,19 +278,15 @@ def generate_pdfa(
f"-dPDFA={pdfa_part}",
"-dPDFACompatibilityPolicy=1",
"-o",
"-",
fspath(output_file),
"-sstdout=%stderr", # Literal %s, not string interpolation
]
)
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
try:
with (
Path(output_file).open('wb') as output,
GhostscriptFollower(progressbar_class) as pbar,
):
with GhostscriptFollower(progressbar_class) as pbar:
p = run_polling_stderr(
args_gs,
stdout=output,
stderr=PIPE,
check=True,
text=True,
+1 -1
View File
@@ -26,5 +26,5 @@ class PageNumberFilter(logging.Filter):
class RichLoggingHandler(RichHandler):
def __init__(self, console: Console, **kwargs):
super().__init__(
console=console, show_level=False, show_time=False, markup=True, **kwargs
console=console, show_level=False, show_time=False, markup=False, **kwargs
)
+1
View File
@@ -15,6 +15,7 @@ from pikepdf import Dictionary, Name, Pdf
from pikepdf import __version__ as PIKEPDF_VERSION
from pikepdf.models.metadata import PdfMetadata, encode_pdf_date
from ocrmypdf._annots import remove_broken_goto_annotations
from ocrmypdf._defaults import PROGRAM_NAME
from ocrmypdf._jobcontext import PdfContext
from ocrmypdf._version import __version__ as OCRMYPF_VERSION
+29 -27
View File
@@ -20,7 +20,9 @@ from pathlib import Path
from typing import NamedTuple, cast
import PIL
from pikepdf import Pdf
from ocrmypdf._annots import remove_broken_goto_annotations
from ocrmypdf._concurrent import Executor, setup_executor
from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._logging import PageNumberFilter
@@ -104,6 +106,23 @@ class PageResult(NamedTuple):
"""Orientation correction in degrees."""
class HOCRResultEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Path):
return {'Path': str(obj)}
return super().default(obj)
class HOCRResultDecoder(json.JSONDecoder):
def __init__(self, *args, **kwargs):
super().__init__(object_hook=self.dict_to_object, *args, **kwargs)
def dict_to_object(self, d):
if 'Path' in d:
return Path(d['Path'])
return d
@dataclass
class HOCRResult:
"""Result when hOCR is finished processing."""
@@ -123,38 +142,14 @@ class HOCRResult:
orientation_correction: int = 0
"""Orientation correction in degrees."""
def __getstate__(self):
"""Return state values to be pickled."""
return {
k: (
('Path://' + str(v))
if k in ('pdf_page_from_image', 'hocr', 'textpdf') and v is not None
else v
)
for k, v in self.__dict__.items()
}
def __setstate__(self, state):
"""Restore state from the unpickled state values."""
self.__dict__.update(
{
k: (
Path(v.removeprefix('Path://'))
if k in ('pdf_page_from_image', 'hocr', 'textpdf') and v is not None
else v
)
for k, v in state.items()
}
)
@classmethod
def from_json(cls, json_str: str) -> HOCRResult:
"""Create an instance from a dict."""
return cls(**json.loads(json_str))
return cls(**json.loads(json_str, cls=HOCRResultDecoder))
def to_json(self) -> str:
"""Serialize to a JSON string."""
return json.dumps(self.__getstate__())
return json.dumps(self.__dict__, cls=HOCRResultEncoder)
def configure_debug_logging(
@@ -445,7 +440,14 @@ def postprocess(
pdf_file: Path, context: PdfContext, executor: Executor
) -> tuple[Path, Sequence[str]]:
"""Postprocess the PDF file."""
pdf_out = pdf_file
# pdf_out = pdf_file
with Pdf.open(pdf_file) as pdf:
fix_annots = context.get_path('fix_annots.pdf')
if remove_broken_goto_annotations(pdf):
pdf.save(fix_annots)
pdf_out = fix_annots
else:
pdf_out = pdf_file
if context.options.output_type.startswith('pdfa'):
ps_stub_out = generate_postscript_stub(context)
pdf_out = convert_to_pdfa(pdf_out, ps_stub_out, context)
+31
View File
@@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
import pytest
from pikepdf import Array, Dictionary, Name, NameTree, Pdf
from ocrmypdf._annots import remove_broken_goto_annotations
def test_remove_broken_goto_annotations(resources):
with Pdf.open(resources / 'link.pdf') as pdf:
assert not remove_broken_goto_annotations(pdf), "File should not be modified"
# Construct Dests nametree
nt = NameTree.new(pdf)
names = pdf.Root[Name.Names] = pdf.make_indirect(Dictionary())
names[Name.Dests] = nt.obj
# Create a broken named destination
nt['Invalid'] = pdf.make_indirect(Dictionary())
# Create a valid named destination
nt['Valid'] = Array([pdf.pages[0].obj, Name.XYZ, 0, 0, 0])
pdf.pages[0].Annots[0].A.D = 'Missing'
pdf.pages[1].Annots[0].A.D = 'Valid'
assert remove_broken_goto_annotations(pdf), "File should be modified"
assert Name.D not in pdf.pages[0].Annots[0].A
assert Name.D in pdf.pages[1].Annots[0].A
+29 -1
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import pickle
from io import BytesIO
from pathlib import Path
@@ -10,6 +11,7 @@ import pytest
from pdfminer.high_level import extract_text
import ocrmypdf
import ocrmypdf._pipelines
import ocrmypdf.api
@@ -35,7 +37,7 @@ def test_sidecar_stringio(resources: Path, outdir: Path, outpdf: Path):
resources / 'ccitt.pdf',
outpdf,
plugins=['tests/plugins/tesseract_cache.py'],
sidecar=s
sidecar=s,
)
s.seek(0)
assert b'the' in s.getvalue()
@@ -75,3 +77,29 @@ def test_hocr_to_pdf_api(resources: Path, outdir: Path, outpdf: Path):
text = extract_text(outpdf)
assert 'hocr' in text and 'the' not in text
def test_hocr_result_json():
result = ocrmypdf._pipelines._common.HOCRResult(
pageno=1,
pdf_page_from_image=Path('a'),
hocr=Path('b'),
textpdf=Path('c'),
orientation_correction=180,
)
assert (
result.to_json()
== '{"pageno": 1, "pdf_page_from_image": {"Path": "a"}, "hocr": {"Path": "b"}, '
'"textpdf": {"Path": "c"}, "orientation_correction": 180}'
)
assert ocrmypdf._pipelines._common.HOCRResult.from_json(result.to_json()) == result
def test_hocr_result_pickle():
result = ocrmypdf._pipelines._common.HOCRResult(
pageno=1,
pdf_page_from_image=Path('a'),
hocr=Path('b'),
textpdf=Path('c'),
orientation_correction=180,
)
assert result == pickle.loads(pickle.dumps(result))
+1 -1
View File
@@ -139,7 +139,7 @@ def test_ghostscript_mandatory_color_conversion(resources, outpdf):
def test_rasterize_pdf_errors(resources, no_outpdf, caplog):
with patch('ocrmypdf._exec.ghostscript.run') as mock:
# ghostscript can produce
# ghostscript can produce empty files with return code 0
mock.return_value = subprocess.CompletedProcess(
['fakegs'], returncode=0, stdout=b'', stderr=b'error this is an error'
)
Generated
+1899
View File
File diff suppressed because it is too large Load Diff