Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c663943523 | ||
|
|
07df94b195 | ||
|
|
7f7a688c76 |
+4
-18
@@ -17,32 +17,16 @@ FROM base as builder
|
|||||||
|
|
||||||
# Note we need leptonica here to build jbig2
|
# Note we need leptonica here to build jbig2
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential autoconf automake libtool \
|
|
||||||
libleptonica-dev \
|
|
||||||
zlib1g-dev \
|
|
||||||
python3-dev \
|
python3-dev \
|
||||||
python3-distutils \
|
python3-distutils \
|
||||||
libffi-dev \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
git \
|
git \
|
||||||
libcairo2-dev \
|
libcairo2-dev \
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|
||||||
# Get the latest pip (Ubuntu version doesn't support manylinux2010)
|
# Get the latest pip
|
||||||
RUN \
|
RUN curl https://bootstrap.pypa.io/get-pip.py | python3
|
||||||
curl https://bootstrap.pypa.io/get-pip.py | python3
|
|
||||||
|
|
||||||
# Compile and install jbig2
|
|
||||||
# Needs libleptonica-dev, zlib1g-dev
|
|
||||||
RUN \
|
|
||||||
mkdir jbig2 \
|
|
||||||
&& curl -L https://github.com/agl/jbig2enc/archive/ea6a40a.tar.gz | \
|
|
||||||
tar xz -C jbig2 --strip-components=1 \
|
|
||||||
&& cd jbig2 \
|
|
||||||
&& ./autogen.sh && ./configure && make && make install \
|
|
||||||
&& cd .. \
|
|
||||||
&& rm -rf jbig2
|
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
@@ -56,11 +40,13 @@ FROM base
|
|||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
software-properties-common gpg-agent
|
software-properties-common gpg-agent
|
||||||
RUN add-apt-repository -y ppa:alex-p/tesseract-ocr-devel
|
RUN add-apt-repository -y ppa:alex-p/tesseract-ocr-devel
|
||||||
|
RUN add-apt-repository -y ppa:alex-p/jbig2enc
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
fonts-droid-fallback \
|
fonts-droid-fallback \
|
||||||
jbig2dec \
|
jbig2dec \
|
||||||
|
jbig2 \
|
||||||
img2pdf \
|
img2pdf \
|
||||||
libsm6 libxext6 libxrender-dev \
|
libsm6 libxext6 libxrender-dev \
|
||||||
pngquant \
|
pngquant \
|
||||||
|
|||||||
@@ -255,32 +255,6 @@ jobs:
|
|||||||
password: ${{ secrets.TOKEN_PYPI }}
|
password: ${{ secrets.TOKEN_PYPI }}
|
||||||
# repository_url: https://test.pypi.org/legacy/
|
# repository_url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
create_release:
|
|
||||||
name: Create GitHub release
|
|
||||||
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
|
|
||||||
permissions:
|
|
||||||
# Required to create a release
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: artifact
|
|
||||||
path: dist
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
id: create-release
|
|
||||||
uses: shogo82148/actions-create-release@v1
|
|
||||||
|
|
||||||
- name: Upload Assets
|
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
|
||||||
asset_path: |
|
|
||||||
./dist/*.whl
|
|
||||||
./dist/*.tar.gz
|
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
name: Build Docker images
|
name: Build Docker images
|
||||||
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
||||||
|
|||||||
@@ -372,17 +372,3 @@ Some users may consider enabling lossy JBIG2. See: :ref:`jbig2-lossy`.
|
|||||||
|
|
||||||
Image processing and PDF/A conversion can also introduce lossy transformations
|
Image processing and PDF/A conversion can also introduce lossy transformations
|
||||||
to your PDF images, even when ``--optimize 1`` is in use.
|
to your PDF images, even when ``--optimize 1`` is in use.
|
||||||
|
|
||||||
|
|
||||||
Digitally signed PDFs
|
|
||||||
=====================
|
|
||||||
|
|
||||||
OCRmyPDF cannot preserve digital signatures in PDFs and also add to 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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
@@ -24,6 +24,14 @@ On macOS, Homebrew packages jbig2enc and OCRmyPDF includes it by
|
|||||||
default. The Docker image for OCRmyPDF also builds its own JBIG2 encoder
|
default. The Docker image for OCRmyPDF also builds its own JBIG2 encoder
|
||||||
from source.
|
from source.
|
||||||
|
|
||||||
|
On Ubuntu, you can install the JBIG2 encoder using the following PPA:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sudo add-apt-repository ppa:alex-p/jbig2enc
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install jbig2enc
|
||||||
|
|
||||||
For all other Linux, you must build a JBIG2 encoder from source:
|
For all other Linux, you must build a JBIG2 encoder from source:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|||||||
@@ -28,19 +28,6 @@ tagged yet.
|
|||||||
|
|
||||||
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
|
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
|
||||||
|
|
||||||
v14.4.0
|
|
||||||
=======
|
|
||||||
|
|
||||||
- Digitally signed PDFs are now detected. If the PDF is signed, OCRmyPDF will
|
|
||||||
refuse to modify it. Previously, only encrypted PDFs were detected, not
|
|
||||||
those that were signed but not encrypted. :issue:`1040`
|
|
||||||
- In addition, `--invalidate-digital-signatures` can be used to override the
|
|
||||||
above behavior and modify the PDF anyway. :issue:`1040`
|
|
||||||
- tqdm progress bars replaced with "rich" progress bars. The rich library is
|
|
||||||
a new dependency. Certain APIs that used tqdm are now deprecated and will
|
|
||||||
be removed in the next major release.
|
|
||||||
- Improved integration with GitHub Releases. Thanks to @stumpylog.
|
|
||||||
|
|
||||||
v14.3.0
|
v14.3.0
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ __ocrmypdf_arguments()
|
|||||||
--force-ocr (OCR documents that already have printable text)
|
--force-ocr (OCR documents that already have printable text)
|
||||||
--skip-text (skip OCR on any pages that already contain text)
|
--skip-text (skip OCR on any pages that already contain text)
|
||||||
--redo-ocr (redo OCR on any pages that seem to have OCR already)
|
--redo-ocr (redo OCR on any pages that seem to have OCR already)
|
||||||
--invalidate-digital-signatures (remove digital signatures from PDF)
|
|
||||||
--skip-big (skip OCR on pages larger than this many MPixels)
|
--skip-big (skip OCR on pages larger than this many MPixels)
|
||||||
--optimize (select optimization level)
|
--optimize (select optimization level)
|
||||||
--jpeg-quality (JPEG quality [0..100])
|
--jpeg-quality (JPEG quality [0..100])
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ complete -c ocrmypdf -l remove-vectors -d "don't send vector objects to OCR"
|
|||||||
complete -c ocrmypdf -s f -l force-ocr -d "OCR documents that already have printable text"
|
complete -c ocrmypdf -s f -l force-ocr -d "OCR documents that already have printable text"
|
||||||
complete -c ocrmypdf -s s -l skip-ocr -d "skip OCR on pages that text, otherwise try OCR"
|
complete -c ocrmypdf -s s -l skip-ocr -d "skip OCR on pages that text, otherwise try OCR"
|
||||||
complete -c ocrmypdf -l redo-ocr -d "redo OCR on any pages that seem to have OCR already"
|
complete -c ocrmypdf -l redo-ocr -d "redo OCR on any pages that seem to have OCR already"
|
||||||
complete -c ocrmypdf -l invalidate-digital-signatures -d "invalidate digital signatures and allow OCR to proceed"
|
|
||||||
|
|
||||||
complete -c ocrmypdf -s k -l keep-temporary-files -d "keep temporary files (debug)"
|
complete -c ocrmypdf -s k -l keep-temporary-files -d "keep temporary files (debug)"
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -17,6 +17,7 @@ license = {text = "MPL-2.0"}
|
|||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Pillow>=8.2.0",
|
"Pillow>=8.2.0",
|
||||||
|
"coloredlogs>=14.0",
|
||||||
"deprecation>=2.1.0",
|
"deprecation>=2.1.0",
|
||||||
"img2pdf>=0.3.0", # pure Python
|
"img2pdf>=0.3.0", # pure Python
|
||||||
"packaging>=20",
|
"packaging>=20",
|
||||||
@@ -24,7 +25,6 @@ dependencies = [
|
|||||||
"pikepdf>=5.0.1",
|
"pikepdf>=5.0.1",
|
||||||
"pluggy>=0.13.0",
|
"pluggy>=0.13.0",
|
||||||
"reportlab>=3.5.66",
|
"reportlab>=3.5.66",
|
||||||
"rich>=13",
|
|
||||||
"tqdm>=4",
|
"tqdm>=4",
|
||||||
"importlib-resources>=5;python_version<'3.9'", # until Python 3.9
|
"importlib-resources>=5;python_version<'3.9'", # until Python 3.9
|
||||||
"typing-extensions>=4;python_version<'3.10'",
|
"typing-extensions>=4;python_version<'3.10'",
|
||||||
|
|||||||
@@ -8,17 +8,6 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|
||||||
from rich.console import Console
|
|
||||||
from rich.logging import RichHandler
|
|
||||||
from rich.progress import (
|
|
||||||
BarColumn,
|
|
||||||
MofNCompleteColumn,
|
|
||||||
Progress,
|
|
||||||
TaskProgressColumn,
|
|
||||||
TextColumn,
|
|
||||||
TimeRemainingColumn,
|
|
||||||
)
|
|
||||||
from rich.table import Column
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
|
||||||
@@ -52,59 +41,3 @@ class TqdmConsole:
|
|||||||
def flush(self):
|
def flush(self):
|
||||||
with suppress(AttributeError):
|
with suppress(AttributeError):
|
||||||
self.file.flush()
|
self.file.flush()
|
||||||
|
|
||||||
|
|
||||||
class RichLoggingHandler(RichHandler):
|
|
||||||
def __init__(self, console: Console, **kwargs):
|
|
||||||
super().__init__(
|
|
||||||
console=console, show_level=False, show_time=False, markup=True, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RichTqdmProgressAdapter:
|
|
||||||
"""Adapt tqdm API to rich progress bar."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
console: Console,
|
|
||||||
desc: str,
|
|
||||||
total: float | None = None,
|
|
||||||
unit: str | None = None,
|
|
||||||
unit_scale: float | None = 1.0,
|
|
||||||
disable: bool = False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
self.progress = Progress(
|
|
||||||
TextColumn(
|
|
||||||
"[progress.description]{task.description}",
|
|
||||||
table_column=Column(min_width=20),
|
|
||||||
),
|
|
||||||
BarColumn(),
|
|
||||||
TaskProgressColumn(),
|
|
||||||
MofNCompleteColumn(),
|
|
||||||
TimeRemainingColumn(),
|
|
||||||
console=console,
|
|
||||||
auto_refresh=True,
|
|
||||||
redirect_stderr=True,
|
|
||||||
redirect_stdout=False,
|
|
||||||
disable=disable,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
self.unit_scale = unit_scale
|
|
||||||
self.progress_bar = self.progress.add_task(
|
|
||||||
desc, total=total * self.unit_scale, unit=unit
|
|
||||||
)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.progress.start()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
|
||||||
self.progress.refresh()
|
|
||||||
self.progress.stop()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def update(self, value=None):
|
|
||||||
advance = self.unit_scale if value is None else value
|
|
||||||
self.progress.update(self.progress_bar, advance=advance)
|
|
||||||
|
|||||||
+14
-17
@@ -27,7 +27,6 @@ from ocrmypdf._jobcontext import PageContext, PdfContext
|
|||||||
from ocrmypdf._version import PROGRAM_NAME
|
from ocrmypdf._version import PROGRAM_NAME
|
||||||
from ocrmypdf._version import __version__ as VERSION
|
from ocrmypdf._version import __version__ as VERSION
|
||||||
from ocrmypdf.exceptions import (
|
from ocrmypdf.exceptions import (
|
||||||
DigitalSignatureError,
|
|
||||||
DpiError,
|
DpiError,
|
||||||
EncryptedPdfError,
|
EncryptedPdfError,
|
||||||
InputFileError,
|
InputFileError,
|
||||||
@@ -81,32 +80,34 @@ def triage_image_file(input_file: Path, output_file: Path, options) -> None:
|
|||||||
if im.info['dpi'] <= (96, 96) and not options.image_dpi:
|
if im.info['dpi'] <= (96, 96) and not options.image_dpi:
|
||||||
log.info("Image size: (%d, %d)", *im.size)
|
log.info("Image size: (%d, %d)", *im.size)
|
||||||
log.info("Image resolution: (%d, %d)", *im.info['dpi'])
|
log.info("Image resolution: (%d, %d)", *im.info['dpi'])
|
||||||
raise DpiError(
|
log.error(
|
||||||
"Input file is an image, but the resolution (DPI) is "
|
"Input file is an image, but the resolution (DPI) is "
|
||||||
"not credible. Estimate the resolution at which the "
|
"not credible. Estimate the resolution at which the "
|
||||||
"image was scanned and specify it using --image-dpi."
|
"image was scanned and specify it using --image-dpi."
|
||||||
)
|
)
|
||||||
|
raise DpiError()
|
||||||
elif not options.image_dpi:
|
elif not options.image_dpi:
|
||||||
log.info("Image size: (%d, %d)", *im.size)
|
log.info("Image size: (%d, %d)", *im.size)
|
||||||
raise DpiError(
|
log.error(
|
||||||
"Input file is an image, but has no resolution (DPI) "
|
"Input file is an image, but has no resolution (DPI) "
|
||||||
"in its metadata. Estimate the resolution at which "
|
"in its metadata. Estimate the resolution at which "
|
||||||
"image was scanned and specify it using --image-dpi."
|
"image was scanned and specify it using --image-dpi."
|
||||||
)
|
)
|
||||||
|
raise DpiError()
|
||||||
|
|
||||||
if im.mode in ('RGBA', 'LA'):
|
if im.mode in ('RGBA', 'LA'):
|
||||||
raise UnsupportedImageFormatError(
|
log.error(
|
||||||
"The input image has an alpha channel. Remove the alpha "
|
"The input image has an alpha channel. Remove the alpha "
|
||||||
"channel first."
|
"channel first."
|
||||||
)
|
)
|
||||||
|
raise UnsupportedImageFormatError()
|
||||||
|
|
||||||
if 'iccprofile' not in im.info:
|
if 'iccprofile' not in im.info:
|
||||||
if im.mode == 'RGB':
|
if im.mode == 'RGB':
|
||||||
log.info("Input image has no ICC profile, assuming sRGB")
|
log.info("Input image has no ICC profile, assuming sRGB")
|
||||||
elif im.mode == 'CMYK':
|
elif im.mode == 'CMYK':
|
||||||
raise UnsupportedImageFormatError(
|
log.error("Input CMYK image has no ICC profile, not usable")
|
||||||
"Input CMYK image has no ICC profile, not usable"
|
raise UnsupportedImageFormatError()
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log.info("Image seems valid. Try converting to PDF...")
|
log.info("Image seems valid. Try converting to PDF...")
|
||||||
@@ -124,6 +125,7 @@ def triage_image_file(input_file: Path, output_file: Path, options) -> None:
|
|||||||
)
|
)
|
||||||
log.info("Successfully converted to PDF, processing...")
|
log.info("Successfully converted to PDF, processing...")
|
||||||
except img2pdf.ImageOpenError as e:
|
except img2pdf.ImageOpenError as e:
|
||||||
|
log.error(e)
|
||||||
raise UnsupportedImageFormatError() from e
|
raise UnsupportedImageFormatError() from e
|
||||||
|
|
||||||
|
|
||||||
@@ -193,21 +195,18 @@ def validate_pdfinfo_options(context: PdfContext) -> None:
|
|||||||
options = context.options
|
options = context.options
|
||||||
|
|
||||||
if pdfinfo.needs_rendering:
|
if pdfinfo.needs_rendering:
|
||||||
raise InputFileError(
|
log.error(
|
||||||
"This PDF contains dynamic XFA forms created by Adobe LiveCycle "
|
"This PDF contains dynamic XFA forms created by Adobe LiveCycle "
|
||||||
"Designer and can only be read by Adobe Acrobat or Adobe Reader."
|
"Designer and can only be read by Adobe Acrobat or Adobe Reader."
|
||||||
)
|
)
|
||||||
if pdfinfo.has_signature:
|
raise InputFileError()
|
||||||
if options.invalidate_digital_signatures:
|
|
||||||
log.warning("All digital signatures will be invalidated")
|
|
||||||
else:
|
|
||||||
raise DigitalSignatureError()
|
|
||||||
if pdfinfo.has_acroform:
|
if pdfinfo.has_acroform:
|
||||||
if options.redo_ocr:
|
if options.redo_ocr:
|
||||||
raise InputFileError(
|
log.error(
|
||||||
"This PDF has a user fillable form. --redo-ocr is not "
|
"This PDF has a user fillable form. --redo-ocr is not "
|
||||||
"currently possible on such files."
|
"currently possible on such files."
|
||||||
)
|
)
|
||||||
|
raise InputFileError()
|
||||||
else:
|
else:
|
||||||
log.warning(
|
log.warning(
|
||||||
"This PDF has a fillable form. "
|
"This PDF has a fillable form. "
|
||||||
@@ -854,9 +853,7 @@ def metadata_fixup(working_file: Path, context: PdfContext) -> Path:
|
|||||||
with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf:
|
with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf:
|
||||||
docinfo = get_docinfo(original, context)
|
docinfo = get_docinfo(original, context)
|
||||||
with pdf.open_metadata() as meta_pdf:
|
with pdf.open_metadata() as meta_pdf:
|
||||||
meta_pdf.load_from_docinfo(
|
meta_pdf.load_from_docinfo(docinfo, delete_missing=False, raise_failure=False)
|
||||||
docinfo, delete_missing=False, raise_failure=False
|
|
||||||
)
|
|
||||||
# If xmp:CreateDate is missing, set it to the modify date to
|
# If xmp:CreateDate is missing, set it to the modify date to
|
||||||
# ensure consistency with Ghostscript.
|
# ensure consistency with Ghostscript.
|
||||||
if 'xmp:CreateDate' not in meta_pdf:
|
if 'xmp:CreateDate' not in meta_pdf:
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ from pathlib import Path
|
|||||||
from typing import AnyStr, BinaryIO, Iterable, Union
|
from typing import AnyStr, BinaryIO, Iterable, Union
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
|
import coloredlogs
|
||||||
|
from humanfriendly.terminal import enable_ansi_support
|
||||||
|
|
||||||
from ocrmypdf._logging import PageNumberFilter, TqdmConsole
|
from ocrmypdf._logging import PageNumberFilter, TqdmConsole
|
||||||
from ocrmypdf._plugin_manager import get_plugin_manager
|
from ocrmypdf._plugin_manager import get_plugin_manager
|
||||||
from ocrmypdf._sync import run_pipeline
|
from ocrmypdf._sync import run_pipeline
|
||||||
@@ -109,7 +112,14 @@ def configure_logging(
|
|||||||
else:
|
else:
|
||||||
fmt = '%(pageno)s%(message)s'
|
fmt = '%(pageno)s%(message)s'
|
||||||
|
|
||||||
|
use_colors = progress_bar_friendly
|
||||||
formatter = None
|
formatter = None
|
||||||
|
if use_colors:
|
||||||
|
use_colors = enable_ansi_support()
|
||||||
|
if use_colors:
|
||||||
|
use_colors = coloredlogs.terminal_supports_colors()
|
||||||
|
if use_colors:
|
||||||
|
formatter = coloredlogs.ColoredFormatter(fmt=fmt)
|
||||||
|
|
||||||
if not formatter:
|
if not formatter:
|
||||||
formatter = logging.Formatter(fmt=fmt)
|
formatter = logging.Formatter(fmt=fmt)
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_compl
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from typing import Callable, Iterable, Type, Union
|
from typing import Callable, Iterable, Type, Union
|
||||||
|
|
||||||
from rich.console import Console as RichConsole
|
from tqdm import tqdm
|
||||||
|
|
||||||
from ocrmypdf import Executor, hookimpl
|
from ocrmypdf import Executor, hookimpl
|
||||||
from ocrmypdf._logging import RichLoggingHandler, RichTqdmProgressAdapter
|
from ocrmypdf._logging import TqdmConsole
|
||||||
from ocrmypdf.exceptions import InputFileError
|
from ocrmypdf.exceptions import InputFileError
|
||||||
from ocrmypdf.helpers import remove_all_log_handlers
|
from ocrmypdf.helpers import remove_all_log_handlers
|
||||||
|
|
||||||
@@ -168,20 +168,13 @@ def get_executor(progressbar_class):
|
|||||||
return StandardExecutor(pbar_class=progressbar_class)
|
return StandardExecutor(pbar_class=progressbar_class)
|
||||||
|
|
||||||
|
|
||||||
RICH_CONSOLE = RichConsole(stderr=True)
|
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def get_progressbar_class():
|
def get_progressbar_class():
|
||||||
"""Return the default progress bar class."""
|
"""Return the default progress bar class."""
|
||||||
|
return tqdm
|
||||||
def partial_RichTqdmProgressAdapter(*args, **kwargs):
|
|
||||||
return RichTqdmProgressAdapter(*args, **kwargs, console=RICH_CONSOLE)
|
|
||||||
|
|
||||||
return partial_RichTqdmProgressAdapter
|
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def get_logging_console():
|
def get_logging_console():
|
||||||
"""Return the default logging console handler."""
|
"""Return the default logging console handler."""
|
||||||
return RichLoggingHandler(console=RICH_CONSOLE)
|
return logging.StreamHandler(stream=TqdmConsole(sys.stderr))
|
||||||
|
|||||||
@@ -359,13 +359,6 @@ Online documentation is located at:
|
|||||||
help="Skip OCR on pages larger than the specified amount of megapixels, "
|
help="Skip OCR on pages larger than the specified amount of megapixels, "
|
||||||
"but include skipped pages in final output",
|
"but include skipped pages in final output",
|
||||||
)
|
)
|
||||||
ocrsettings.add_argument(
|
|
||||||
'--invalidate-digital-signatures',
|
|
||||||
action='store_true',
|
|
||||||
help="Normally, OCRmyPDF will refuse to OCR a PDF that has a digital "
|
|
||||||
"signature. This option allows OCR to proceed, but the digital signature "
|
|
||||||
"will be invalidated.",
|
|
||||||
)
|
|
||||||
|
|
||||||
advanced = parser.add_argument_group(
|
advanced = parser.add_argument_group(
|
||||||
"Advanced", "Advanced options to control OCRmyPDF"
|
"Advanced", "Advanced options to control OCRmyPDF"
|
||||||
|
|||||||
@@ -108,18 +108,6 @@ class EncryptedPdfError(ExitCodeException):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DigitalSignatureError(ExitCodeException):
|
|
||||||
"""PDF has a digital signature."""
|
|
||||||
|
|
||||||
exit_code = ExitCode.input_file
|
|
||||||
message = dedent(
|
|
||||||
"""\
|
|
||||||
Input PDF has a digital signature. OCR would alter the document,
|
|
||||||
invalidating the signature.
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TesseractConfigError(ExitCodeException):
|
class TesseractConfigError(ExitCodeException):
|
||||||
"""Tesseract config can't be parsed."""
|
"""Tesseract config can't be parsed."""
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ worker communicates only with the main process.
|
|||||||
|
|
||||||
This is not without drawbacks. If the tasks are not "even" in size, which cannot
|
This is not without drawbacks. If the tasks are not "even" in size, which cannot
|
||||||
be guaranteed, some workers may end up with too much work while others are idle.
|
be guaranteed, some workers may end up with too much work while others are idle.
|
||||||
It is less efficient than the standard implementation, so not the default.
|
It is less efficient than the standard implementation, so not th edefault.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ from typing import Container, Iterable, Iterator, Mapping, NamedTuple, Sequence,
|
|||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from pikepdf import (
|
from pikepdf import (
|
||||||
Name,
|
|
||||||
Object,
|
Object,
|
||||||
Page,
|
|
||||||
Pdf,
|
Pdf,
|
||||||
PdfImage,
|
PdfImage,
|
||||||
PdfInlineImage,
|
PdfInlineImage,
|
||||||
@@ -493,15 +491,15 @@ def _image_xobjects(container) -> Iterator[tuple[Object, str]]:
|
|||||||
since the object does not know its own name.
|
since the object does not know its own name.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if Name.Resources not in container:
|
if '/Resources' not in container:
|
||||||
return
|
return
|
||||||
resources = container[Name.Resources]
|
resources = container['/Resources']
|
||||||
if Name.XObject not in resources:
|
if '/XObject' not in resources:
|
||||||
return
|
return
|
||||||
for key, candidate in resources[Name.XObject].items():
|
for key, candidate in resources['/XObject'].items():
|
||||||
if candidate is None or Name.Subtype not in candidate:
|
if candidate is None or '/Subtype' not in candidate:
|
||||||
continue
|
continue
|
||||||
if candidate[Name.Subtype] == Name.Image:
|
if candidate['/Subtype'] == '/Image':
|
||||||
pdfimage = candidate
|
pdfimage = candidate
|
||||||
yield (pdfimage, key)
|
yield (pdfimage, key)
|
||||||
|
|
||||||
@@ -537,15 +535,15 @@ def _find_form_xobject_images(pdf: Pdf, container: Object, contentsinfo: Content
|
|||||||
The container may be a page, or a parent Form XObject.
|
The container may be a page, or a parent Form XObject.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if Name.Resources not in container:
|
if '/Resources' not in container:
|
||||||
return
|
return
|
||||||
resources = container[Name.Resources]
|
resources = container['/Resources']
|
||||||
if Name.XObject not in resources:
|
if '/XObject' not in resources:
|
||||||
return
|
return
|
||||||
xobjs = resources[Name.XObject].as_dict()
|
xobjs = resources['/XObject'].as_dict()
|
||||||
for xobj in xobjs:
|
for xobj in xobjs:
|
||||||
candidate = xobjs[xobj]
|
candidate = xobjs[xobj]
|
||||||
if candidate is None or candidate[Name.Subtype] != Name.Form:
|
if candidate is None or candidate['/Subtype'] != '/Form':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
form_xobject = candidate
|
form_xobject = candidate
|
||||||
@@ -583,19 +581,16 @@ def _process_content_streams(
|
|||||||
downsampling.
|
downsampling.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if container.get(Name.Type) == Name.Page and Name.Contents in container:
|
if container.get('/Type') == '/Page' and '/Contents' in container:
|
||||||
initial_shorthand = shorthand or UNIT_SQUARE
|
initial_shorthand = shorthand or UNIT_SQUARE
|
||||||
elif (
|
elif container.get('/Type') == '/XObject' and container['/Subtype'] == '/Form':
|
||||||
container.get(Name.Type) == Name.XObject
|
|
||||||
and container[Name.Subtype] == Name.Form
|
|
||||||
):
|
|
||||||
# Set the CTM to the state it was when the "Do" operator was
|
# Set the CTM to the state it was when the "Do" operator was
|
||||||
# encountered that is drawing this instance of the Form XObject
|
# encountered that is drawing this instance of the Form XObject
|
||||||
ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity()
|
ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity()
|
||||||
|
|
||||||
# A Form XObject may provide its own matrix to map form space into
|
# A Form XObject may provide its own matrix to map form space into
|
||||||
# user space. Get this if one exists
|
# user space. Get this if one exists
|
||||||
form_shorthand = container.get(Name.Matrix, PdfMatrix.identity())
|
form_shorthand = container.get('/Matrix', PdfMatrix.identity())
|
||||||
form_matrix = PdfMatrix(form_shorthand)
|
form_matrix = PdfMatrix(form_shorthand)
|
||||||
|
|
||||||
# Concatenate form matrix with CTM to ensure CTM is correct for
|
# Concatenate form matrix with CTM to ensure CTM is correct for
|
||||||
@@ -776,7 +771,7 @@ class PageInfo:
|
|||||||
check_pages: Container[int],
|
check_pages: Container[int],
|
||||||
detailed_analysis: bool,
|
detailed_analysis: bool,
|
||||||
):
|
):
|
||||||
page: Page = pdf.pages[pageno]
|
page = pdf.pages[pageno]
|
||||||
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
|
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
|
||||||
width_pt = mediabox[2] - mediabox[0]
|
width_pt = mediabox[2] - mediabox[0]
|
||||||
height_pt = mediabox[3] - mediabox[1]
|
height_pt = mediabox[3] - mediabox[1]
|
||||||
@@ -784,7 +779,7 @@ class PageInfo:
|
|||||||
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:
|
||||||
pscript5_mode = str(pdf.docinfo.get(Name.Creator)).startswith('PScript5')
|
pscript5_mode = str(pdf.docinfo.get('/Creator')).startswith('PScript5')
|
||||||
miner = get_page_analysis(infile, pageno, pscript5_mode)
|
miner = get_page_analysis(infile, pageno, pscript5_mode)
|
||||||
self._textboxes = list(simplify_textboxes(miner, get_text_boxes))
|
self._textboxes = list(simplify_textboxes(miner, get_text_boxes))
|
||||||
bboxes = (box.bbox for box in self._textboxes)
|
bboxes = (box.bbox for box in self._textboxes)
|
||||||
@@ -794,13 +789,17 @@ class PageInfo:
|
|||||||
self._textboxes = []
|
self._textboxes = []
|
||||||
self._has_text = None # i.e. "no information"
|
self._has_text = None # i.e. "no information"
|
||||||
|
|
||||||
userunit = page.get(Name.UserUnit, Decimal(1.0))
|
userunit = page.get('/UserUnit', Decimal(1.0))
|
||||||
if not isinstance(userunit, Decimal):
|
if not isinstance(userunit, Decimal):
|
||||||
userunit = Decimal(userunit)
|
userunit = Decimal(userunit)
|
||||||
self._userunit = userunit
|
self._userunit = userunit
|
||||||
self._width_inches = width_pt * userunit / Decimal(72.0)
|
self._width_inches = width_pt * userunit / Decimal(72.0)
|
||||||
self._height_inches = height_pt * userunit / Decimal(72.0)
|
self._height_inches = height_pt * userunit / Decimal(72.0)
|
||||||
self._rotate = int(getattr(page.obj, 'Rotate', 0))
|
|
||||||
|
try:
|
||||||
|
self._rotate = int(page['/Rotate'])
|
||||||
|
except KeyError:
|
||||||
|
self._rotate = 0
|
||||||
|
|
||||||
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
|
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
|
||||||
|
|
||||||
@@ -954,10 +953,6 @@ DEFAULT_EXECUTOR = SerialExecutor()
|
|||||||
class PdfInfo:
|
class PdfInfo:
|
||||||
"""Get summary information about a PDF."""
|
"""Get summary information about a PDF."""
|
||||||
|
|
||||||
_has_acroform: bool = False
|
|
||||||
_has_signature: bool = False
|
|
||||||
_needs_rendering: bool = False
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
infile,
|
infile,
|
||||||
@@ -985,13 +980,13 @@ class PdfInfo:
|
|||||||
check_pages=check_pages,
|
check_pages=check_pages,
|
||||||
detailed_analysis=detailed_analysis,
|
detailed_analysis=detailed_analysis,
|
||||||
)
|
)
|
||||||
self._needs_rendering = pdf.Root.get(Name.NeedsRendering, False)
|
self._needs_rendering = pdf.Root.get('/NeedsRendering', False)
|
||||||
if Name.AcroForm in pdf.Root:
|
self._has_acroform = False
|
||||||
if len(pdf.Root.AcroForm.get(Name.Fields, [])) > 0:
|
if '/AcroForm' in pdf.Root:
|
||||||
|
if len(pdf.Root.AcroForm.get('/Fields', [])) > 0:
|
||||||
self._has_acroform = True
|
self._has_acroform = True
|
||||||
elif Name.XFA in pdf.Root.AcroForm:
|
elif '/XFA' in pdf.Root.AcroForm:
|
||||||
self._has_acroform = True
|
self._has_acroform = True
|
||||||
self._has_signature = bool(pdf.Root.AcroForm.get(Name.SigFlags, 0) & 1)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pages(self) -> Sequence[PageInfo | None]:
|
def pages(self) -> Sequence[PageInfo | None]:
|
||||||
@@ -1011,14 +1006,9 @@ class PdfInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def has_acroform(self) -> bool:
|
def has_acroform(self) -> bool:
|
||||||
"""Return True if the document catalog has an AcroForm."""
|
"""Return True if any page has an AcroForm."""
|
||||||
return self._has_acroform
|
return self._has_acroform
|
||||||
|
|
||||||
@property
|
|
||||||
def has_signature(self) -> bool:
|
|
||||||
"""Return True if the document annotations has a digital signature."""
|
|
||||||
return self._has_signature
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self) -> str | Path:
|
def filename(self) -> str | Path:
|
||||||
"""Return filename of PDF."""
|
"""Return filename of PDF."""
|
||||||
|
|||||||
+3
-25
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pikepdf
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import ocrmypdf
|
import ocrmypdf
|
||||||
@@ -20,11 +19,10 @@ def acroform(resources):
|
|||||||
return resources / 'acroform.pdf'
|
return resources / 'acroform.pdf'
|
||||||
|
|
||||||
|
|
||||||
def test_acroform_and_redo(acroform, no_outpdf):
|
def test_acroform_and_redo(acroform, caplog, no_outpdf):
|
||||||
with pytest.raises(
|
with pytest.raises(ocrmypdf.exceptions.InputFileError):
|
||||||
ocrmypdf.exceptions.InputFileError, match='--redo-ocr is not currently possible'
|
|
||||||
):
|
|
||||||
check_ocrmypdf(acroform, no_outpdf, '--redo-ocr')
|
check_ocrmypdf(acroform, no_outpdf, '--redo-ocr')
|
||||||
|
assert '--redo-ocr is not currently possible' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_acroform_message(acroform, caplog, outpdf):
|
def test_acroform_message(acroform, caplog, outpdf):
|
||||||
@@ -32,23 +30,3 @@ def test_acroform_message(acroform, caplog, outpdf):
|
|||||||
check_ocrmypdf(acroform, outpdf, '--plugin', 'tests/plugins/tesseract_noop.py')
|
check_ocrmypdf(acroform, outpdf, '--plugin', 'tests/plugins/tesseract_noop.py')
|
||||||
assert 'fillable form' in caplog.text
|
assert 'fillable form' in caplog.text
|
||||||
assert '--force-ocr' in caplog.text
|
assert '--force-ocr' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def digitally_signed(acroform, outdir):
|
|
||||||
out = outdir / 'acroform_signed.pdf'
|
|
||||||
with pikepdf.open(acroform) as pdf:
|
|
||||||
pdf.Root.AcroForm.SigFlags = 3
|
|
||||||
pdf.save(out)
|
|
||||||
yield out
|
|
||||||
|
|
||||||
|
|
||||||
def test_digital_signature(digitally_signed, no_outpdf):
|
|
||||||
with pytest.raises(ocrmypdf.exceptions.DigitalSignatureError):
|
|
||||||
check_ocrmypdf(digitally_signed, no_outpdf)
|
|
||||||
|
|
||||||
|
|
||||||
def test_digital_signature_invalidate(digitally_signed, no_outpdf):
|
|
||||||
check_ocrmypdf(
|
|
||||||
digitally_signed, no_outpdf, '--force-ocr', '--invalidate-digital-signatures'
|
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user