Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61a2674317 | ||
|
|
9ad8cbf1f6 | ||
|
|
123fde174d | ||
|
|
fd991a2380 | ||
|
|
6f5d77d930 | ||
|
|
5169ac633b | ||
|
|
5b6ab1e003 | ||
|
|
8f984bf958 | ||
|
|
9c5f0d0ec6 | ||
|
|
32041c43e1 | ||
|
|
599028bebb | ||
|
|
6faa8f7221 | ||
|
|
a4dc5e365f | ||
|
|
e2a563cc76 | ||
|
|
1037d73efb | ||
|
|
aeb7b142a9 | ||
|
|
422ea9777e | ||
|
|
2f1c743227 | ||
|
|
96ee21aee9 | ||
|
|
4b759af6ff | ||
|
|
25d2b0cda4 | ||
|
|
16dd8b54a8 | ||
|
|
c4dc5269d2 | ||
|
|
c36e9950ae | ||
|
|
0c0d53b10f | ||
|
|
63de7e1677 | ||
|
|
b0e92760a2 | ||
|
|
054c0773a3 | ||
|
|
89aa78b724 | ||
|
|
708113a514 | ||
|
|
95ef5410c2 | ||
|
|
868b3b4abd | ||
|
|
045bdff95a | ||
|
|
d12b27ac1d | ||
|
|
e4e00de79f | ||
|
|
a53a3937c2 | ||
|
|
343424b4d2 | ||
|
|
c5edff2c2f | ||
|
|
8c5f8b8ddd | ||
|
|
39da931a56 | ||
|
|
9fe354359b | ||
|
|
facc4750bc | ||
|
|
437c235738 | ||
|
|
9559b0b186 | ||
|
|
91456e19a4 | ||
|
|
a2d89f67c4 | ||
|
|
f34130d193 | ||
|
|
c5571388e2 | ||
|
|
9af59c0d6d | ||
|
|
55ae838cb7 | ||
|
|
c434b97f55 | ||
|
|
607eee198d | ||
|
|
5e2a7f8a56 | ||
|
|
fd9550acda | ||
|
|
b354511ac9 | ||
|
|
7be293f628 | ||
|
|
65855dc14c | ||
|
|
cac4a8b9b6 | ||
|
|
17d97b354a | ||
|
|
1c1b60fa9f | ||
|
|
6b745d892f | ||
|
|
fbf271a3ec | ||
|
|
8077718804 | ||
|
|
66bda3420a | ||
|
|
f6510e2b15 | ||
|
|
51abd79136 | ||
|
|
5607429d9a | ||
|
|
b8b7ecfe7f | ||
|
|
d4abe88452 | ||
|
|
cb3cfaa055 | ||
|
|
9db01c7ff5 | ||
|
|
d0301813cc | ||
|
|
cff37bf681 | ||
|
|
66d04dd6e3 | ||
|
|
06a1f987d4 | ||
|
|
e51e21c6b6 | ||
|
|
c5fa72bd4e | ||
|
|
bf99587aa1 | ||
|
|
d249aef57d | ||
|
|
43ab7c88d7 | ||
|
|
ca9669742d | ||
|
|
8a1dddc3ee | ||
|
|
0cd424ffcb | ||
|
|
fde550f9a7 | ||
|
|
a3726e4ce3 | ||
|
|
4ab0a8ff35 | ||
|
|
37f6f72df3 | ||
|
|
3f92867ae6 | ||
|
|
e63503d64b | ||
|
|
17d20309c7 | ||
|
|
fe7c69ce95 | ||
|
|
9baccee8c5 | ||
|
|
d5bb9929f3 | ||
|
|
72d3ee3a87 | ||
|
|
17c419dfcb | ||
|
|
84cc49b14b | ||
|
|
b7f63bc93d | ||
|
|
ad9a3b5302 | ||
|
|
4e4bcaf243 | ||
|
|
11afe3507f | ||
|
|
7691ba8535 | ||
|
|
b787a369ee | ||
|
|
9fb8b267af | ||
|
|
0a08d6ce1f | ||
|
|
f517efe819 | ||
|
|
5f5421f23d | ||
|
|
703b6db95c | ||
|
|
000040d497 | ||
|
|
5bd6665b49 | ||
|
|
1c303afe21 | ||
|
|
11a5c80917 | ||
|
|
9b2ab92913 | ||
|
|
0c4b69ec5a | ||
|
|
45bea1c0e0 | ||
|
|
db914d4cd1 | ||
|
|
df4a8faecd | ||
|
|
1273e7aeda | ||
|
|
e13a673b1a | ||
|
|
979b0bcaed |
+3
-3
@@ -1,5 +1,3 @@
|
||||
# Coverage isn't really compatible with subprocesses so results are unreliable
|
||||
|
||||
[paths]
|
||||
source =
|
||||
src
|
||||
@@ -8,9 +6,11 @@ source =
|
||||
[run]
|
||||
branch = true
|
||||
parallel = true
|
||||
concurrency =
|
||||
thread
|
||||
multiprocessing
|
||||
source =
|
||||
src/ocrmypdf
|
||||
tests
|
||||
omit =
|
||||
tests/spoof/*
|
||||
|
||||
|
||||
+11
-4
@@ -10,16 +10,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf automake libtool \
|
||||
libleptonica-dev \
|
||||
zlib1g-dev \
|
||||
python3-setuptools \
|
||||
python3-pip \
|
||||
wget \
|
||||
python3 \
|
||||
python3-distutils \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git
|
||||
|
||||
# Get the latest pip (Ubuntu version doesn't support manylinux2010)
|
||||
RUN \
|
||||
curl https://bootstrap.pypa.io/get-pip.py | python3
|
||||
|
||||
# Compile and install jbig2
|
||||
# Needs libleptonica-dev, zlib1g-dev
|
||||
RUN \
|
||||
mkdir jbig2 \
|
||||
&& wget -q https://github.com/agl/jbig2enc/archive/0.29.tar.gz -O - | \
|
||||
&& curl -L https://github.com/agl/jbig2enc/archive/0.29.tar.gz | \
|
||||
tar xz -C jbig2 --strip-components=1 \
|
||||
&& cd jbig2 \
|
||||
&& ./autogen.sh && ./configure && make && make install \
|
||||
@@ -34,6 +39,7 @@ RUN pip3 install --no-cache-dir \
|
||||
-r requirements/main.txt \
|
||||
-r requirements/webservice.txt \
|
||||
-r requirements/test.txt \
|
||||
-r requirements/watcher.txt \
|
||||
.
|
||||
|
||||
FROM base
|
||||
@@ -64,6 +70,7 @@ COPY --from=builder /usr/local/lib/ /usr/local/lib/
|
||||
COPY --from=builder /usr/local/bin/ /usr/local/bin/
|
||||
|
||||
COPY --from=builder /app/misc/webservice.py /app/
|
||||
COPY --from=builder /app/misc/watcher.py /app/
|
||||
|
||||
# Copy minimal project files to get the test suite.
|
||||
COPY --from=builder /app/setup.cfg /app/setup.py /app/README.md /app/
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*.pyc
|
||||
*.sublime-*
|
||||
*.DS_Store
|
||||
.mypy_cache/
|
||||
|
||||
# Package building
|
||||
.eggs/
|
||||
|
||||
+20
-3
@@ -1,6 +1,23 @@
|
||||
repos:
|
||||
- repo: https://github.com/ambv/black
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
- repo: https://github.com/asottile/seed-isort-config
|
||||
rev: v1.9.3
|
||||
hooks:
|
||||
- id: seed-isort-config
|
||||
- repo: https://github.com/pre-commit/mirrors-isort
|
||||
rev: v4.3.21 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/psf/black
|
||||
rev: stable
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.7
|
||||
- id: black
|
||||
language_version: python3.7
|
||||
exclude: ^src/ocrmypdf/lib/_leptonica.py
|
||||
|
||||
+43
-45
@@ -1,3 +1,7 @@
|
||||
branches:
|
||||
except:
|
||||
- azure
|
||||
|
||||
cache:
|
||||
pip: true
|
||||
directories:
|
||||
@@ -22,7 +26,6 @@ matrix:
|
||||
packages:
|
||||
- ghostscript
|
||||
- libffi-dev
|
||||
- qpdf
|
||||
- tesseract-ocr
|
||||
- tesseract-ocr-deu
|
||||
- tesseract-ocr-eng
|
||||
@@ -50,7 +53,6 @@ matrix:
|
||||
- libavformat56
|
||||
- libavutil54
|
||||
- libffi-dev
|
||||
- qpdf
|
||||
- tesseract-ocr
|
||||
- tesseract-ocr-deu
|
||||
- tesseract-ocr-eng
|
||||
@@ -80,36 +82,34 @@ matrix:
|
||||
- libffi-dev
|
||||
- pngquant
|
||||
- poppler-utils
|
||||
- qpdf
|
||||
- tesseract-ocr
|
||||
- tesseract-ocr-deu
|
||||
- tesseract-ocr-eng
|
||||
- tesseract-ocr-fra
|
||||
- unpaper
|
||||
# - os: linux
|
||||
# dist: bionic
|
||||
# sudo: required
|
||||
# language: python
|
||||
# python: "3.8"
|
||||
# env:
|
||||
# - DIST=bionic
|
||||
# addons:
|
||||
# apt:
|
||||
# update: true
|
||||
# sources:
|
||||
# - sourceline: "ppa:alex-p/tesseract-ocr"
|
||||
# packages:
|
||||
# - ghostscript
|
||||
# - libexempi3
|
||||
# - libffi-dev
|
||||
# - pngquant
|
||||
# - poppler-utils
|
||||
# - qpdf
|
||||
# - tesseract-ocr
|
||||
# - tesseract-ocr-deu
|
||||
# - tesseract-ocr-eng
|
||||
# - tesseract-ocr-fra
|
||||
# - unpaper
|
||||
- os: linux
|
||||
dist: bionic
|
||||
sudo: required
|
||||
language: python
|
||||
python: "3.8"
|
||||
env:
|
||||
- DIST=bionic
|
||||
addons:
|
||||
apt:
|
||||
update: true
|
||||
sources:
|
||||
- sourceline: "ppa:alex-p/tesseract-ocr"
|
||||
packages:
|
||||
- ghostscript
|
||||
- libexempi3
|
||||
- libffi-dev
|
||||
- pngquant
|
||||
- poppler-utils
|
||||
- tesseract-ocr
|
||||
- tesseract-ocr-deu
|
||||
- tesseract-ocr-eng
|
||||
- tesseract-ocr-fra
|
||||
- unpaper
|
||||
- os: osx
|
||||
language: generic
|
||||
addons:
|
||||
@@ -140,22 +140,20 @@ install:
|
||||
|
||||
script:
|
||||
- tesseract --version
|
||||
- qpdf --version
|
||||
- pytest -n auto
|
||||
|
||||
deploy:
|
||||
# release for main pypi
|
||||
# 3.7 is considered the build leader and does the deploy, otherwise there is
|
||||
# a race and all versions will try to deploy
|
||||
# OTOH if we ever need separate binary wheels then each version needs its
|
||||
# own deploy
|
||||
- provider: pypi
|
||||
user: ocrmypdf-travis
|
||||
password:
|
||||
secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
|
||||
distributions: "sdist bdist_wheel"
|
||||
on:
|
||||
branch: master
|
||||
tags: true
|
||||
condition: $TRAVIS_PYTHON_VERSION == "3.7" && $TRAVIS_OS_NAME == "linux"
|
||||
skip_upload_docs: true
|
||||
# deploy:
|
||||
# # release for main pypi
|
||||
# # 3.7 is considered the build leader and does the deploy, otherwise there is
|
||||
# # a race and all versions will try to deploy
|
||||
# # OTOH if we ever need separate binary wheels then each version needs its
|
||||
# # own deploy
|
||||
# - provider: pypi
|
||||
# user: ocrmypdf-travis
|
||||
# password:
|
||||
# secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
|
||||
# distributions: "sdist bdist_wheel"
|
||||
# on:
|
||||
# branch: master
|
||||
# tags: true
|
||||
# condition: $TRAVIS_PYTHON_VERSION == "3.7" && $TRAVIS_OS_NAME == "linux"
|
||||
# skip_upload_docs: true
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<img src="docs/images/logo.svg" width="240" alt="OCRmyPDF">
|
||||
|
||||
[![Travis build status][travis]](https://travis-ci.org/jbarlow83/OCRmyPDF) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs] ![Python versions][pyversions]
|
||||
[![Build Status][azure]](https://dev.azure.com/jim0585/ocrmypdf/_build/latest?definitionId=2&branchName=master) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs] ![Python versions][pyversions]
|
||||
|
||||
[azure]: https://dev.azure.com/jim0585/ocrmypdf/_apis/build/status/jbarlow83.OCRmyPDF?branchName=master
|
||||
|
||||
[travis]: https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master "Travis build status"
|
||||
|
||||
@@ -48,7 +50,7 @@ For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/
|
||||
Motivation
|
||||
----------
|
||||
|
||||
I searched the web for a free command line tool to OCR PDF files on Linux/UNIX: I found many, but none of them were really satisfying.
|
||||
I searched the web for a free command line tool to OCR PDF files: I found many, but none of them were really satisfying.
|
||||
|
||||
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
|
||||
- Or they did not handle accents and multilingual characters
|
||||
@@ -63,7 +65,7 @@ I searched the web for a free command line tool to OCR PDF files on Linux/UNIX:
|
||||
Installation
|
||||
------------
|
||||
|
||||
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
|
||||
Linux, Windows, macOS and FreeBSD are supported. Docker images are also available.
|
||||
|
||||
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
|
||||
|
||||
@@ -77,7 +79,7 @@ and users of Fedora 29 or later may simply
|
||||
dnf install ocrmypdf
|
||||
```
|
||||
|
||||
and macOS users with Homebrew may simply
|
||||
and Homebrew users (macOS, Linux, Windows Subsystem for Linux) may simply
|
||||
|
||||
```bash
|
||||
brew install ocrmypdf
|
||||
@@ -111,18 +113,12 @@ ocrmypdf --help
|
||||
|
||||
Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/en/latest/index.html).
|
||||
|
||||
If you detect an issue, please:
|
||||
|
||||
- Check whether your issue is already known
|
||||
- If no problem report exists on github, please create one here: <https://github.com/jbarlow83/OCRmyPDF/issues>
|
||||
- Describe your problem thoroughly
|
||||
- Append the console output of the script when running the debug mode (`-v 1` option)
|
||||
- If possible provide your input PDF file as well as the content of the temporary folder (using a file sharing service like Dropbox)
|
||||
Please report issues on our [GitHub issues](https://github.com/jbarlow83/OCRmyPDF/issues) page, and follow the issue template for quick response.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
In addition to the required Python version (3.6+), OCRmyPDF requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
|
||||
In addition to the required Python version (3.6+), OCRmyPDF requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings. OCRmyPDF works on pretty much everything: Linux, macOS, Windows and FreeBSD.
|
||||
|
||||
Press & Media
|
||||
-------------
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
trigger:
|
||||
tags:
|
||||
include:
|
||||
- v*
|
||||
branches:
|
||||
include:
|
||||
- "*"
|
||||
exclude:
|
||||
- "travis"
|
||||
|
||||
stages:
|
||||
- stage: "Test"
|
||||
jobs:
|
||||
- job: Windows
|
||||
pool:
|
||||
vmImage: "vs2017-win2016"
|
||||
strategy:
|
||||
matrix:
|
||||
Python36:
|
||||
python.version: "3.6"
|
||||
Python37:
|
||||
python.version: "3.7"
|
||||
Python38:
|
||||
python.version: "3.8"
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: "$(python.version)"
|
||||
- pwsh: |
|
||||
choco install --yes --no-progress --pre tesseract
|
||||
choco install --yes --no-progress python3
|
||||
choco install --yes --no-progress ghostscript
|
||||
choco install --yes --no-progress pngquant
|
||||
displayName: "Install system packages"
|
||||
- pwsh: |
|
||||
refreshenv
|
||||
python -m pip install --upgrade pip wheel
|
||||
python -m pip install -r requirements/main.txt -r requirements/test.txt .
|
||||
displayName: "Install Python packages"
|
||||
- pwsh: |
|
||||
refreshenv
|
||||
$env:pathext += ';.py'
|
||||
# -n auto helps Windows
|
||||
python -m pytest -n auto --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
|
||||
displayName: "Test"
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFiles: "test.xml"
|
||||
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
|
||||
condition: succeededOrFailed()
|
||||
- job: "Ubuntu_1804"
|
||||
pool:
|
||||
vmImage: "ubuntu-18.04"
|
||||
strategy:
|
||||
matrix:
|
||||
Python36:
|
||||
python.version: "3.6"
|
||||
Python37:
|
||||
python.version: "3.7"
|
||||
Python38:
|
||||
python.version: "3.8"
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: "$(python.version)"
|
||||
- bash: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
python3-software-properties \
|
||||
curl \
|
||||
ghostscript \
|
||||
img2pdf \
|
||||
libexempi3 \
|
||||
libffi-dev \
|
||||
liblept5 \
|
||||
libsm6 libxext6 libxrender-dev \
|
||||
pngquant \
|
||||
poppler-utils \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu \
|
||||
tesseract-ocr-eng \
|
||||
unpaper \
|
||||
zlib1g
|
||||
displayName: "Install system packages"
|
||||
- bash: |
|
||||
curl https://bootstrap.pypa.io/get-pip.py | python3
|
||||
pip3 install -r requirements/main.txt -r requirements/test.txt .
|
||||
displayName: "Install Python packages"
|
||||
- bash: |
|
||||
tesseract --version
|
||||
displayName: "Record versions"
|
||||
- bash: |
|
||||
# -n auto is slower on Linux and breaks on Python 3.8
|
||||
pytest -n0 --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
|
||||
displayName: "Test"
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFiles: "test.xml"
|
||||
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
|
||||
condition: succeededOrFailed()
|
||||
- job: "Ubuntu_1604"
|
||||
pool:
|
||||
vmImage: "ubuntu-16.04"
|
||||
strategy:
|
||||
matrix:
|
||||
Python36:
|
||||
python.version: "3.6"
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: "$(python.version)"
|
||||
- bash: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
software-properties-common
|
||||
sudo add-apt-repository -y ppa:alex-p/tesseract-ocr
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
ghostscript \
|
||||
img2pdf \
|
||||
libexempi3 \
|
||||
libffi-dev \
|
||||
liblept5 \
|
||||
libsm6 libxext6 libxrender-dev \
|
||||
pngquant \
|
||||
poppler-utils \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu \
|
||||
tesseract-ocr-eng \
|
||||
unpaper \
|
||||
zlib1g
|
||||
displayName: "Install system packages"
|
||||
- bash: |
|
||||
curl https://bootstrap.pypa.io/get-pip.py | python3
|
||||
pip3 install -r requirements/main.txt -r requirements/test.txt .
|
||||
displayName: "Install Python packages"
|
||||
- bash: |
|
||||
tesseract --version
|
||||
displayName: "Record versions"
|
||||
- bash: |
|
||||
# -n auto is slower on Linux and breaks on Python 3.8
|
||||
pytest -n0 --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
|
||||
displayName: "Test"
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFiles: "test.xml"
|
||||
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
|
||||
condition: succeededOrFailed()
|
||||
- job: "macOS_Mojave"
|
||||
pool:
|
||||
vmImage: "macos-10.14"
|
||||
strategy:
|
||||
matrix:
|
||||
Python37:
|
||||
python.version: "3.7"
|
||||
Python38:
|
||||
python.version: "3.8"
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: "$(python.version)"
|
||||
- bash: |
|
||||
brew update
|
||||
brew unlink python@2
|
||||
brew install \
|
||||
exempi \
|
||||
ghostscript \
|
||||
jbig2enc \
|
||||
leptonica \
|
||||
openjpeg \
|
||||
pngquant \
|
||||
tesseract \
|
||||
unpaper
|
||||
displayName: "Install system packages"
|
||||
- bash: |
|
||||
pip3 install --upgrade pip
|
||||
pip3 install -r requirements/main.txt -r requirements/test.txt .
|
||||
displayName: "Install Python packages"
|
||||
- bash: |
|
||||
tesseract --version
|
||||
displayName: "Record versions"
|
||||
- bash: pytest -nauto --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
|
||||
displayName: "Test"
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFiles: "test.xml"
|
||||
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
|
||||
condition: succeededOrFailed()
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: "$(System.DefaultWorkingDirectory)/**/coverage.xml"
|
||||
|
||||
- stage: "Artifacts"
|
||||
jobs:
|
||||
- job: "sdist_wheel"
|
||||
pool:
|
||||
vmImage: "ubuntu-18.04"
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: "3.7"
|
||||
- bash: |
|
||||
python -m pip install --upgrade pip wheel
|
||||
python setup.py sdist bdist_wheel
|
||||
- publish: dist
|
||||
artifact: sdist_wheel
|
||||
|
||||
- stage: "Deploy"
|
||||
jobs:
|
||||
- deployment: "PyPI"
|
||||
pool:
|
||||
vmImage: "ubuntu-18.04"
|
||||
environment: "deploy"
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- download: current
|
||||
artifact: sdist_wheel
|
||||
- script: |
|
||||
mkdir -p dist
|
||||
mv $(Pipeline.Workspace)/sdist_wheel/* dist
|
||||
displayName: "Move dist files"
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: "3.8"
|
||||
architecture: x64
|
||||
- script: |
|
||||
pip install --upgrade twine
|
||||
displayName: "Generate artifacts"
|
||||
- script: |
|
||||
cat <<FILE >.pypirc
|
||||
[distutils]
|
||||
index-servers =
|
||||
pypi
|
||||
|
||||
[pypi]
|
||||
username: __token__
|
||||
password: $(TOKEN_PYPI)
|
||||
|
||||
FILE
|
||||
displayName: "Generate PyPI auth file"
|
||||
- script: |
|
||||
python -m twine upload --config-file .pypirc dist/*
|
||||
displayName: "Upload to PyPI"
|
||||
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))
|
||||
- script: |
|
||||
curl -X POST -d "token=$(TOKEN_RTD)" https://readthedocs.org/api/v2/webhook/pikepdf/39557/
|
||||
displayName: "Trigger ReadTheDocs"
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/tags/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')))
|
||||
Vendored
+2
-2
@@ -79,7 +79,7 @@ Copyright: held by the contributors to the Wikipedia article "Optical character
|
||||
(epson.pdf generated from Wikipedia article as of 2016-09-14)
|
||||
License: CC-BY-SA-3.0
|
||||
|
||||
Files: tests/resources/typewriter.png tests/resources/2400dpi.pdf
|
||||
Files: tests/resources/typewriter.png tests/resources/2400dpi.pdf tests/resources/3small.pdf
|
||||
Copyright: (C) 2005 Ellywa
|
||||
License: GFDL-1.2+ or CC-BY-SA-1.0 or CC-BY-SA-2.0 or CC-BY-SA-2.5 or CC-BY-SA-3.0
|
||||
|
||||
@@ -87,7 +87,7 @@ Files: tests/resources/overlay.pdf
|
||||
Copyright: (C) 2017 Max Anderson
|
||||
License: Expat
|
||||
|
||||
Files: tests/resources/baiona*.png
|
||||
Files: tests/resources/baiona*.png tests/resources/3small.pdf
|
||||
Copyright: (C) 2014 Euskaldunaa
|
||||
License: CC-BY-SA-4.0
|
||||
|
||||
|
||||
+2
-1
@@ -71,7 +71,8 @@ Progress monitoring
|
||||
OCRmyPDF uses the ``tqdm`` package to implement its progress bars.
|
||||
:func:`ocrmypdf.configure_logging` will set up logging output to
|
||||
``sys.stderr`` in a way that is compatible with the display of the
|
||||
progress bar.
|
||||
progress bar. Use ``ocrmypdf.ocr(...progress_bar=False)`` to disable
|
||||
the progress bar.
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
+68
-11
@@ -57,6 +57,12 @@ of ``ocrmypdf``, again updating files in place.
|
||||
|
||||
find . -name '*.pdf' | parallel --tag -j 2 ocrmypdf '{}' '{}'
|
||||
|
||||
In a Windows batch file, use
|
||||
|
||||
.. code-block:: bat
|
||||
|
||||
for /r %%f in (*.pdf) do ocrmypdf %%f %%f
|
||||
|
||||
Sample script
|
||||
-------------
|
||||
|
||||
@@ -67,13 +73,15 @@ processing.
|
||||
|
||||
#!/usr/bin/env python3
|
||||
# Walk through directory tree, replacing all files with OCR'd version
|
||||
# Contributed by DeliciousPickle@github
|
||||
# Original version by DeliciousPickle@github; modified
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
print(script_dir + '/ocr-tree.py: Start')
|
||||
|
||||
@@ -91,6 +99,8 @@ processing.
|
||||
level=logging.INFO, format='%(asctime)s %(message)s',
|
||||
filename=log_file, filemode='w')
|
||||
|
||||
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
|
||||
|
||||
for dir_name, subdirs, file_list in os.walk(start_dir):
|
||||
logging.info('\n')
|
||||
logging.info(dir_name + '\n')
|
||||
@@ -100,14 +110,10 @@ processing.
|
||||
if file_ext == '.pdf':
|
||||
full_path = dir_name + '/' + filename
|
||||
print(full_path)
|
||||
cmd = ["ocrmypdf", "--deskew", filename, filename]
|
||||
logging.info(cmd)
|
||||
proc = subprocess.run(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
result = proc.stdout
|
||||
if proc.returncode == 6:
|
||||
result = ocrmypdf.ocr(filename, filename, deskew=True)
|
||||
if result == ocrmypdf.ExitCode.already_done_ocr:
|
||||
print("Skipped document because it already contained text")
|
||||
elif proc.returncode == 0:
|
||||
elif result == ocrmypdf.ExitCode.ok:
|
||||
print("OCR complete")
|
||||
logging.info(result)
|
||||
|
||||
@@ -192,6 +198,54 @@ and all inquiries are appreciated.
|
||||
Hot (watched) folders
|
||||
=====================
|
||||
|
||||
Watched folders with Docker
|
||||
---------------------------
|
||||
|
||||
The OCRmyPDF Docker image includes a watcher service. This service can
|
||||
be launched as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run \
|
||||
-v <path to files to convert>:/input \
|
||||
-v <path to store results>:/output \
|
||||
-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
||||
-it --entrypoint python3 \
|
||||
jbarlow83/ocrmypdf \
|
||||
watcher.py
|
||||
|
||||
This service will watch for a file that matches ``/input/\*.pdf`` and will
|
||||
convert it to a OCRed PDF in ``/output/``. The parameters to this image are:
|
||||
|
||||
.. csv-table:: watcher.py parameters for Docker
|
||||
:header: "Parameter", "Description"
|
||||
:widths: 50, 50
|
||||
|
||||
"``-v <path to files to convert>:/input``", "Files placed in this location will be OCRed"
|
||||
"``-v <path to store results>:/output``", "This is where OCRed files will be stored"
|
||||
"``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "This will place files in the output in {output}/{year}/{month}/{filename}"
|
||||
|
||||
This service relies on polling to check for changes to the filesystem. It
|
||||
may not be suitable for some environments, such as filesystems shared on a
|
||||
slow network.
|
||||
|
||||
Watched folders with watcher.py
|
||||
-------------------------------
|
||||
|
||||
The watcher service may also be run natively.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install -r reqs/watcher.txt
|
||||
|
||||
env OCR_INPUT_DIRECTORY=/mnt/input-pdfs \
|
||||
OCR_OUTPUT_DIRECTORY=/mnt/output-pdfs \
|
||||
OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
||||
python3 watcher.py
|
||||
|
||||
Watched folders with CLI
|
||||
------------------------
|
||||
|
||||
To set up a "hot folder" that will trigger OCR for every file inserted,
|
||||
use a program like Python
|
||||
`watchdog <https://pypi.python.org/pypi/watchdog>`__ (supports all major
|
||||
@@ -219,12 +273,12 @@ told to run ``ocrmypdf`` on any .pdf added to the current directory
|
||||
--command='ocrmypdf "${watch_src_path}" "out/${watch_src_path}" ' \
|
||||
. # don't forget the final dot
|
||||
|
||||
For more complex behavior you can write a Python script around to use
|
||||
the watchdog API.
|
||||
|
||||
On file servers, you could configure watchmedo as a system service so it
|
||||
will run all the time.
|
||||
|
||||
For more complex behavior you can write a Python script around to use
|
||||
the watchdog API. You can refer to the watcher.py script as an example.
|
||||
|
||||
Caveats
|
||||
-------
|
||||
|
||||
@@ -244,6 +298,9 @@ Caveats
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
- On Linux, `systemd user services <https://wiki.archlinux.org/index.php/Systemd/User>`__
|
||||
can be configured to automatically perform OCR on a collection of files.
|
||||
|
||||
- `Watchman <https://facebook.github.io/watchman/>`__ is a more
|
||||
powerful alternative to ``watchmedo``.
|
||||
|
||||
|
||||
+20
-1
@@ -15,7 +15,10 @@ Code style
|
||||
==========
|
||||
|
||||
We use PEP8, ``black`` for code formatting and ``isort`` for import sorting. The
|
||||
settings for programs are in ``pyproject.toml`` and ``setup.cfg``.
|
||||
settings for these programs are in ``pyproject.toml`` and ``setup.cfg``. Pull
|
||||
requests should follow the style guide. One difference we use from "black" style
|
||||
is that strings shown to the user are always in double quotes (``"``) and strings
|
||||
for internal uses are in single quotes (``'``).
|
||||
|
||||
Tests
|
||||
=====
|
||||
@@ -36,3 +39,19 @@ New non-Python dependencies
|
||||
|
||||
OCRmyPDF uses several external programs (Tesseract, Ghostscript and others) for
|
||||
its functionality. In general we prefer to avoid adding new external programs.
|
||||
|
||||
Style guide: Is it OCRmyPDF or ocrmypdf?
|
||||
========================================
|
||||
|
||||
The program/project is OCRmyPDF and the name of the executable or library is ocrmypdf.
|
||||
|
||||
Known ports/packagers
|
||||
=====================
|
||||
|
||||
OCRmyPDF has been ported to many platforms already. If you are interesting in
|
||||
porting to a new platform, check with
|
||||
`Repology <https://repology.org/projects/?search=ocrmypdf>`__ to see the status
|
||||
of that platform.
|
||||
|
||||
Packager maintainers, please ensure that the command line completion scripts in
|
||||
``misc/`` are installed.
|
||||
|
||||
@@ -89,6 +89,18 @@ This produces a file named "output.pdf" and a companion text file named
|
||||
|
||||
ocrmypdf --sidecar output.txt input.pdf output.pdf
|
||||
|
||||
.. note::
|
||||
|
||||
The sidecar file contains the **OCR text** found by OCRmyPDF. If the document
|
||||
contains pages that already have text, that text will not appear in the
|
||||
sidecar. If the option ``--pages`` is used, only those pages on which OCR
|
||||
was performed will be included in the sidecar. If certain pages were skipped
|
||||
because of options like ``--skip-big`` or ``--tesseract-timeout``, those pages
|
||||
will not be in the sidecar.
|
||||
|
||||
To extract all text from a PDF, whether generated from OCR or otherwise,
|
||||
use a program like Poppler's ``pdftotext`` or ``pdfgrep``.
|
||||
|
||||
OCR images, not PDFs
|
||||
--------------------
|
||||
|
||||
|
||||
+2
-2
@@ -108,9 +108,9 @@ on the public one:
|
||||
FROM jbarlow83/ocrmypdf
|
||||
|
||||
# Add French
|
||||
RUN apk add tesseract-ocr-data-fra
|
||||
RUN apt install tesseract-ocr-fra
|
||||
|
||||
You can also copy training data to ``/usr/share/tessdata``.
|
||||
You can also copy training data to ``/usr/share/tesseract-ocr/<tesseract version>/tessdata``.
|
||||
|
||||
Executing the test suite
|
||||
========================
|
||||
|
||||
+4
-1
@@ -4,7 +4,9 @@ OCRmyPDF documentation
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
|
||||
be searched.
|
||||
|
||||
PDF is the best format for storing and exchanging scanned documents. Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs.
|
||||
PDF is the best format for storing and exchanging scanned documents.
|
||||
Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply
|
||||
image processing and OCR to existing PDFs.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
@@ -12,6 +14,7 @@ PDF is the best format for storing and exchanging scanned documents. Unfortunat
|
||||
introduction
|
||||
release_notes
|
||||
installation
|
||||
optimizer
|
||||
languages
|
||||
jbig2
|
||||
|
||||
|
||||
+82
-23
@@ -8,11 +8,20 @@ Installing OCRmyPDF
|
||||
|latest|
|
||||
|
||||
The easiest way to install OCRmyPDF is to follow the steps for your operating
|
||||
system/platform, although sometimes this version may be out of date.
|
||||
system/platform, although sometimes this version may be out of date. This
|
||||
installation guide provides information allowing you to compare the current
|
||||
version to the one provided by your platform.
|
||||
|
||||
If you want to use the latest version of OCRmyPDF, your best bet is to install
|
||||
the most recent version your platform provides, and then upgrade that version by
|
||||
installing the Python binary wheels.
|
||||
If you want to use the latest version of OCRmyPDF and all of its optional
|
||||
dependencies, the easiest way to get that is install the Homebrew package. Homebrew
|
||||
is best known as a macOS package manger, but also works for
|
||||
`Linux and Windows Subsystem for Linux <https://docs.brew.sh/Homebrew-on-Linux>`__.
|
||||
After Homebrew is installed, simply run ``brew install ocrmypdf``.
|
||||
|
||||
You can also use the more detailed procedures here to manually install OCRmyPDF
|
||||
from source or with the ``pip`` package manager for binary wheels. The reason
|
||||
for these varied steps is that OCRmyPDF requires third-party executables that are
|
||||
not part of Python.
|
||||
|
||||
.. contents:: Platform-specific steps
|
||||
:depth: 2
|
||||
@@ -55,8 +64,8 @@ Debian and Ubuntu 18.04 or newer
|
||||
| |ubu-1804| |ubu-1810| |ubu-1904| |ubu-1910| |
|
||||
+-----------------------------------------------+
|
||||
|
||||
Users of Debian 9 ("stretch") or later or Ubuntu 18.04 or later may
|
||||
simply
|
||||
Users of Debian 9 ("stretch") or later or Ubuntu 18.04 or later, including users
|
||||
of Windows Subsystem for Linux, may simply
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -134,8 +143,7 @@ first install the system version to get most of the dependencies:
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install \
|
||||
ocrmypdf \
|
||||
python3-pip
|
||||
ocrmypdf
|
||||
|
||||
There are a few system dependency changes since ocrmypdf 6.1.2. Let's
|
||||
get these, too.
|
||||
@@ -146,13 +154,19 @@ get these, too.
|
||||
libxml2 \
|
||||
pngquant
|
||||
|
||||
We will need a newer version of ``pip`` then was available for Ubuntu 18.04:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
wget https://bootstrap.pypa.io/get-pip.py && python3 get-pip.py
|
||||
|
||||
Then install the most recent ocrmypdf for the local user and set the
|
||||
user's ``PATH`` to check for the user's Python packages.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export PATH=$HOME/.local/bin:$PATH
|
||||
pip3 install --user ocrmypdf
|
||||
python3 -m pip install --user ocrmypdf
|
||||
|
||||
To add JBIG2 encoding, see :ref:`jbig2`.
|
||||
|
||||
@@ -298,6 +312,19 @@ the following command.
|
||||
If you have any difficulties with installation, check the repository
|
||||
package page.
|
||||
|
||||
Alpine Linux
|
||||
------------
|
||||
|
||||
.. image:: https://repology.org/badge/version-for-repo/alpine_edge/ocrmypdf.svg
|
||||
:alt: Alpine Linux
|
||||
:target: https://repology.org/metapackage/ocrmypdf
|
||||
|
||||
To install OCRmyPDF for Alpine Linux:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
apk add ocrmypdf
|
||||
|
||||
Other Linux packages
|
||||
--------------------
|
||||
|
||||
@@ -375,7 +402,7 @@ packs. If you need other languages you can optionally install them all:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install tesseract --with-all-languages # Option 2: for all language packs
|
||||
brew install tesseract-lang # Option 2: for all language packs
|
||||
|
||||
Update the homebrew pip:
|
||||
|
||||
@@ -422,16 +449,57 @@ Installing the Docker image
|
||||
===========================
|
||||
|
||||
For some users, installing the Docker image will be easier than
|
||||
installing all of OCRmyPDF's dependencies. For Windows, it is the only
|
||||
option.
|
||||
installing all of OCRmyPDF's dependencies.
|
||||
|
||||
See `OCRmyPDF Docker Image <docker>`__ for more information.
|
||||
|
||||
Installing on Windows
|
||||
=====================
|
||||
|
||||
Direct installation on Windows is not currently possible, but it works well in
|
||||
Windows Subsystem for Linux:
|
||||
.. warning::
|
||||
|
||||
Native Windows support is new. Consider it "beta" software. Some
|
||||
functionality is missing or may be more difficult to enable. If you need a
|
||||
production-ready solution, use Windows Subsystem for Linux or a Docker
|
||||
image.
|
||||
|
||||
.. note::
|
||||
|
||||
Administrator privileges will be required for some of these steps.
|
||||
|
||||
You must install the following for Windows:
|
||||
|
||||
* Python 3.7 (64-bit)
|
||||
* Tesseract 4.0 or later
|
||||
* Ghostscript 9.50 or later
|
||||
|
||||
You can install these with the Chocolatey package manager:
|
||||
|
||||
* ``choco install python3``
|
||||
* ``choco install --pre tesseract``
|
||||
* ``choco install ghostscript``
|
||||
|
||||
Also consider adding:
|
||||
|
||||
* ``choco install pngquant``
|
||||
|
||||
Windows 10 64-bit and 64-bit versions of applications are recommended. Earlier
|
||||
versions of Windows and 32-bit versions of these programs are not tested, and not
|
||||
supported at this time.
|
||||
|
||||
OCRmyPDF will check for Tesseract-OCR and Ghostscript in your Program Files folder.
|
||||
If they are in some other location, you may need to modify the ``PATH``
|
||||
environment variable so Tesseract, Ghostscript, and other any optional executables can
|
||||
be found. You can enter it in the command line or
|
||||
`follow these directions <https://www.computerhope.com/issues/ch000549.htm#dospath>`_
|
||||
to make the change persistent and system-wide.
|
||||
|
||||
You may then use pip to install ocrmypdf:
|
||||
|
||||
* ``pip install ocrmypdf``
|
||||
|
||||
Installing on Windows Subsystem for Linux
|
||||
=========================================
|
||||
|
||||
#. Install Ubuntu 18.04 for Windows Subsystem for Linux, if not already installed.
|
||||
#. Follow the procedure to install :ref:`OCRmyPDF on Ubuntu 18.04 <ubuntu-lts-latest>`.
|
||||
@@ -450,15 +518,6 @@ Then confirm that the expected version from PyPI (|latest|) is installed:
|
||||
You can then run OCRmyPDF in the Windows command prompt or Powershell, prefixing
|
||||
``wsl``, and call it from Windows programs or batch files.
|
||||
|
||||
Why no native Windows?
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
It would probably not be too difficult to port on Windows. The main
|
||||
reason this has been avoided is the difficulty of packaging and
|
||||
installing the various non-Python dependencies: Tesseract, QPDF,
|
||||
Ghostscript, Leptonica. Pull requests to add or improve Windows support
|
||||
would be quite welcome.
|
||||
|
||||
Docker
|
||||
^^^^^^
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
OCRmyPDF is a Python 3 package that adds OCR layers to PDFs.
|
||||
OCRmyPDF is a Python 3 application and library that adds OCR layers to PDFs.
|
||||
|
||||
About OCR
|
||||
=========
|
||||
|
||||
@@ -57,3 +57,8 @@ Docker users
|
||||
Users of the OCRmyPDF Docker image should install language packs into a
|
||||
derived Docker image as
|
||||
:ref:`described in that section <docker-lang-packs>`.
|
||||
|
||||
Windows users
|
||||
=============
|
||||
|
||||
The Tesseract installer provided by Chocolatey already includes 100 languages.
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
================
|
||||
PDF optimization
|
||||
================
|
||||
|
||||
OCRmyPDF includes an image-oriented PDF optimizer. By default, the optimizer
|
||||
runs with safe settings with the goal of improving compression at no loss of
|
||||
quality. At higher optimization levels, lossy optimizations may be applied and
|
||||
tuned. Optimization occurs after OCR, and only if OCR succeeded. It does not
|
||||
perform other possible optimizations such as deduplicating resources,
|
||||
consolidating fonts, simplifying vector drawings, or anything of that nature.
|
||||
|
||||
Optimization ranges from ``-O0`` through ``-O3``, where ``0`` disables
|
||||
optimization and ``3`` implements all options. ``1``, the default, performs only
|
||||
safe and lossless optimizations. (This is similar to GCC's optimization
|
||||
parameter.) The exact type of optimizations performed will vary over time.
|
||||
|
||||
PDF optimization requires third-party, optional tools for certain optimizations.
|
||||
If these are not installed or cannot be found by OCRmyPDF, optimization will not
|
||||
be as good.
|
||||
|
||||
Optimizations that always occurs
|
||||
================================
|
||||
|
||||
OCRmyPDF will automatically replace obsolete or inferior compression schemes
|
||||
such as RLE or LZW with superior schemes such as Deflate and converting
|
||||
monochrome images to CCITT G4. Since this is harmless it always occurs and there
|
||||
is no way to disable it. Other non-image compressed objects are compressed as
|
||||
well.
|
||||
|
||||
Fast web view
|
||||
=============
|
||||
|
||||
OCRmyPDF automatically optimizes PDFs for "fast web view" in Adobe Acrobat's
|
||||
parlance, or equivalently, linearizes PDFs so that the resources they reference
|
||||
are presented in the order a viewer needs them for sequential display. This
|
||||
reduces the latency of viewing a PDF both online and from local storage. This
|
||||
actually slightly increases the file size.
|
||||
|
||||
To disable this optimization and all others, use ``ocrmypdf --optimize 0 ...``
|
||||
or the shorthand ``-O0``.
|
||||
|
||||
Lossless optimizations
|
||||
======================
|
||||
|
||||
At optimization level ``-O1`` (the default), OCRmyPDF will also attempt lossless
|
||||
image optimization.
|
||||
|
||||
If a JBIG2 encoder is available, then monochrome images will be converted to
|
||||
JBIG2, with the potential for huge savings on large black and white images,
|
||||
since JBIG2 is far more efficient than any other monochrome (bi-level)
|
||||
compression. (All known US patents related to JBIG2 have probably expired, but
|
||||
it remains the responsibility of the user to supply a JBIG2 encoder such as
|
||||
`jbig2enc <https://github.com/agl/jbig2enc>`__. OCRmyPDF does not implement
|
||||
JBIG2 encoding on its own.)
|
||||
|
||||
OCRmyPDF currently does not attempt to recompress losslessly compressed objects
|
||||
more aggressively.
|
||||
|
||||
Lossy optimizations
|
||||
===================
|
||||
|
||||
At optimization level ``-O2`` and ``-O3``, OCRmyPDF will some attempt lossy
|
||||
image optimization.
|
||||
|
||||
If ``pngquant`` is installed, OCRmyPDF will use it to perform quantize paletted
|
||||
images to reduce their size.
|
||||
|
||||
The quality of JPEGs may be lowered, on the assumption that a lower quality
|
||||
image may be suitable for storage after OCR.
|
||||
|
||||
It is not possible to optimize all image types. Uncommon image types may be
|
||||
skipped by the optimizer.
|
||||
|
||||
OCRmyPDF provides :ref:`lossy mode JBIG2 <jbig2-lossy>` as an advanced feature
|
||||
that additional requires the argument ``--jbig2-lossy``.
|
||||
@@ -13,6 +13,66 @@ Note that it is licensed under GPLv3, so scripts that
|
||||
``import ocrmypdf`` and are released publicly should probably also be
|
||||
licensed under GPLv3.
|
||||
|
||||
v9.4.0
|
||||
======
|
||||
|
||||
- Updated recommended dependency versions.
|
||||
- Improvements to test coverage and changes to facilitate better measurement of
|
||||
test coverage, such as when tests run in subprocesses.
|
||||
- Improvements to error messages when Leptonica is not installed correctly.
|
||||
- Fixed use of pytest "session scope" that may have caused some intermittent
|
||||
CI failures.
|
||||
- When the argument ``--keep-temporary-files`` or verbosity is set to ``-v1``,
|
||||
a debug log file is generated in the working temporary folder.
|
||||
|
||||
v9.3.0
|
||||
======
|
||||
|
||||
- Improved native Windows support: we now check in the obvious places in
|
||||
the "Program Files" folders installations of Tesseract and Ghostscript,
|
||||
rather than relying on the user to edit ``PATH`` to specify their location.
|
||||
The ``PATH`` environment variable can still be used to differentiate when
|
||||
multiple installations are present or the programs are installed to non-
|
||||
standard locations.
|
||||
- Fixed an exception on parsing Ghostscript error messages.
|
||||
- Added an improved example demonstrating how to set up a watched folder
|
||||
for automated OCR processing (thanks to @ianalexander for the contribution).
|
||||
|
||||
v9.2.0
|
||||
======
|
||||
|
||||
- Native Windows is now supported.
|
||||
- Continuous integration moved to Azure Pipelines.
|
||||
- Improved test coverage and speed of tests.
|
||||
- Fixed an issue where a page that was originally a JPEG would be saved as a
|
||||
PNG, increasing file size. This occurred only when a preprocessing option
|
||||
was selected along with ``--output-type=pdf`` and all images on the original
|
||||
page were JPEGs. Regression since v7.0.0.
|
||||
- OCRmyPDF no longer depends on the QPDF executable ``qpdf`` or ``libqpdf``.
|
||||
It uses pikepdf (which in turn depends on ``libqpdf``). Package maintainers
|
||||
should adjust dependencies so that OCRmyPDF no longer calls for libqpdf on
|
||||
its own. For users of Python binary wheels, this change means a separate
|
||||
installation of QPDF is no longer necessary. This change is mainly to
|
||||
simplify installation on Windows.
|
||||
- Fixed a rare case where log messages from Tesseract would be discarded.
|
||||
- Fixed incorrect function signature for pixFindPageForeground, causing
|
||||
exceptions on certain platforms/Leptonica versions.
|
||||
|
||||
v9.1.1
|
||||
======
|
||||
|
||||
- Expand the range of pdfminer.six versions that are supported.
|
||||
- Fixed Docker build when using pikepdf 1.7.0.
|
||||
- Fixed documentation to recommend using pip from get-pip.py.
|
||||
|
||||
v9.1.0
|
||||
======
|
||||
|
||||
- Improved diagnostics when file size increases at output. Now warns if JBIG2
|
||||
or pngquant were not available.
|
||||
- pikepdf 1.7.0 is now required, to pick up changes that remove the need for
|
||||
a source install on Linux systems running Python 3.8.
|
||||
|
||||
v9.0.5
|
||||
======
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ _ocrmypdf()
|
||||
COMPREPLY=( $( compgen -W '{1..13}' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
--sidecar|--title|--author|--subject|--keywords|--unpaper-args|--pages)
|
||||
--sidecar|--title|--author|--subject|--keywords|--unpaper-args|--pages|--fast-web-view)
|
||||
# argument required but no completions available
|
||||
return
|
||||
;;
|
||||
@@ -76,7 +76,8 @@ _ocrmypdf()
|
||||
--max-image-mpixels --tesseract-config --tesseract-pagesegmode
|
||||
--help --tesseract-oem --pdf-renderer --tesseract-timeout
|
||||
--rotate-pages-threshold --pdfa-image-compression --user-words
|
||||
--user-patterns --keep-temporary-files --output-type' \
|
||||
--user-patterns --keep-temporary-files --output-type
|
||||
--no-progress-bar --pages --fast-web-view' \
|
||||
-- "$cur" ) )
|
||||
return
|
||||
else
|
||||
|
||||
@@ -59,6 +59,8 @@ function __fish_ocrmypdf_verbose
|
||||
end
|
||||
complete -c ocrmypdf -x -s v -l verbose -a '(__fish_ocrmypdf_verbose)' -d "set verbosity level"
|
||||
|
||||
complete -c ocrmypdf -x -l no-progress-bar -d "disable the progress bar"
|
||||
|
||||
function __fish_ocrmypdf_pdfa_compression
|
||||
echo -e "auto\t"(_ "let Ghostscript decide how to compress images")
|
||||
echo -e "jpeg\t"(_ "convert color and grayscale images to JPEG")
|
||||
@@ -111,5 +113,6 @@ complete -c ocrmypdf -x -l rotate-pages-threshold -d "page rotation confidence"
|
||||
|
||||
complete -c ocrmypdf -r -l user-words -d "specify location of user words file"
|
||||
complete -c ocrmypdf -r -l user-patterns -d "specify location of user patterns file"
|
||||
complete -c ocrmypdf -x -l fast-web-view -d "if file size if above this amount in MB, linearize PDF"
|
||||
|
||||
complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf)"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Copyright (C) 2019 Ian Alexander: https://github.com/ianalexander
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
|
||||
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
|
||||
OUTPUT_DIRECTORY_YEAR_MONTH = bool(os.getenv('OCR_OUTPUT_DIRECTORY_YEAR_MONTH', False))
|
||||
PATTERNS = ['*.pdf']
|
||||
|
||||
|
||||
def execute_ocrmypdf(file_path):
|
||||
filename = Path(file_path).name
|
||||
if OUTPUT_DIRECTORY_YEAR_MONTH:
|
||||
today = datetime.today()
|
||||
output_directory_year_month = Path(
|
||||
f'{OUTPUT_DIRECTORY}/{today.year}/{today.month}'
|
||||
)
|
||||
if not output_directory_year_month.exists():
|
||||
output_directory_year_month.mkdir(parents=True, exist_ok=True)
|
||||
output_path = Path(output_directory_year_month) / filename
|
||||
else:
|
||||
output_path = Path(OUTPUT_DIRECTORY) / filename
|
||||
print(f'New file: {file_path}.\nAttempting to OCRmyPDF to: {output_path}')
|
||||
ocrmypdf.ocr(file_path, output_path)
|
||||
|
||||
|
||||
class HandleObserverEvent(PatternMatchingEventHandler):
|
||||
def on_any_event(self, event):
|
||||
if event.event_type in ['created', 'modified']:
|
||||
execute_ocrmypdf(event.src_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(
|
||||
f"Starting OCRmyPDF watcher with config:\n"
|
||||
f"Input Directory: {INPUT_DIRECTORY}\n"
|
||||
f"Output Directory: {OUTPUT_DIRECTORY}\n"
|
||||
f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}"
|
||||
)
|
||||
handler = HandleObserverEvent(patterns=PATTERNS)
|
||||
observer = Observer()
|
||||
observer.schedule(handler, INPUT_DIRECTORY, recursive=True)
|
||||
observer.start()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
observer.stop()
|
||||
observer.join()
|
||||
+3
-1
@@ -10,7 +10,8 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ["py36", "py37", "py38"]
|
||||
target-version = ["py36",
|
||||
"py37", "py38"]
|
||||
skip-string-normalization = true
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
@@ -28,5 +29,6 @@ exclude = '''
|
||||
| docs
|
||||
| misc
|
||||
| \.egg-info
|
||||
| src/ocrmypdf/lib/_leptonica.py
|
||||
)/
|
||||
'''
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
check-manifest >= 0.35
|
||||
twine >= 1.8.1
|
||||
coverage >= 4.5
|
||||
GitPython == 2.1.3
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# installation
|
||||
cffi == 1.13.2
|
||||
img2pdf == 0.3.3
|
||||
pdfminer.six == 20191020
|
||||
pikepdf == 1.6.5
|
||||
Pillow >= 6.2.0
|
||||
pdfminer.six == 20200104
|
||||
pikepdf == 1.8.2
|
||||
Pillow == 7.0.0
|
||||
reportlab == 3.5.32
|
||||
tqdm == 4.37.0
|
||||
tqdm == 4.41.1
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
pytest >= 5.0.0
|
||||
pytest-helpers-namespace >= 2019.1.8
|
||||
pytest-xdist >= 1.29.0 # For DumpError fix
|
||||
pytest-cov >= 2.6.1
|
||||
pytest-xdist >= 1.31.0
|
||||
pytest-cov >= 2.8.0
|
||||
python-xmp-toolkit == 2.0.1 # requires apt-get install libexempi3
|
||||
# or brew install exempi
|
||||
PyPDF2 >= 1.26.0
|
||||
#PyMuPDF == 1.13.4 # optional
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
watchdog >= 0.8.2, < 1.0
|
||||
@@ -22,6 +22,8 @@ include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
known_first_party = ocrmypdf
|
||||
known_third_party = PIL,_cffi_backend,cffi,flask,gs,img2pdf,pdfminer,pikepdf,pkg_resources,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug
|
||||
|
||||
[metadata]
|
||||
license_file = LICENSE
|
||||
|
||||
@@ -21,11 +21,12 @@ from __future__ import print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
if sys.version_info < (3, 6):
|
||||
print("Python 3.6 or newer is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
# pylint: disable=w0613
|
||||
|
||||
@@ -76,6 +77,7 @@ setup(
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Operating System :: Microsoft :: Windows :: Windows 10",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: POSIX :: BSD",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
@@ -96,8 +98,8 @@ setup(
|
||||
'chardet >= 3.0.4, < 4', # unlisted requirement of pdfminer.six 20181108
|
||||
'cffi >= 1.9.1', # must be a setup and install requirement
|
||||
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
|
||||
'pdfminer.six >= 20181108, <= 20191020',
|
||||
'pikepdf >= 1.6.5, < 2',
|
||||
'pdfminer.six >= 20181108, <= 20200104',
|
||||
'pikepdf >= 1.8.1, < 2',
|
||||
'Pillow >= 6.2.0',
|
||||
'reportlab >= 3.3.0', # oldest released version with sane image handling
|
||||
'tqdm >= 4',
|
||||
|
||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
|
||||
import pikepdf
|
||||
|
||||
MAX_REPLACE_PAGES = int(os.environ.get('_OCRMYPDF_MAX_REPLACE_PAGES', 100))
|
||||
MAX_REPLACE_PAGES = 100
|
||||
|
||||
|
||||
def _update_page_resources(*, page, font, font_key, procset):
|
||||
|
||||
@@ -100,14 +100,14 @@ def cleanup_working_files(work_folder, options):
|
||||
|
||||
class LogNameAdapter(logging.LoggerAdapter):
|
||||
def process(self, msg, kwargs):
|
||||
# return '[%s] %s' % (self.extra['filename'], msg), kwargs
|
||||
# return '[%s] %s' % (self.extra['input_filename'], msg), kwargs
|
||||
return '%s' % (msg,), kwargs
|
||||
|
||||
|
||||
class LogNamePageAdapter(logging.LoggerAdapter):
|
||||
def process(self, msg, kwargs):
|
||||
return (
|
||||
#'[%s:%05u] %s' % (self.extra['filename'], self.extra['page'], msg),
|
||||
#'[%s:%05u] %s' % (self.extra['input_filename'], self.extra['page'], msg),
|
||||
'%4u: %s' % (self.extra['page'], msg),
|
||||
kwargs,
|
||||
)
|
||||
@@ -116,9 +116,9 @@ class LogNamePageAdapter(logging.LoggerAdapter):
|
||||
def make_logger(options=None, prefix='ocrmypdf', filename=None, page=None):
|
||||
log = logging.getLogger(prefix)
|
||||
if filename and page:
|
||||
adapter = LogNamePageAdapter(log, dict(filename=filename, page=page))
|
||||
adapter = LogNamePageAdapter(log, dict(input_filename=filename, page=page))
|
||||
elif filename:
|
||||
adapter = LogNameAdapter(log, dict(filename=filename))
|
||||
adapter = LogNameAdapter(log, dict(input_filename=filename))
|
||||
else:
|
||||
adapter = log
|
||||
return adapter
|
||||
|
||||
+14
-12
@@ -19,6 +19,7 @@ import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from shutil import copyfileobj
|
||||
|
||||
import img2pdf
|
||||
@@ -37,18 +38,18 @@ from .exceptions import (
|
||||
UnsupportedImageFormatError,
|
||||
)
|
||||
from .exec import ghostscript, tesseract
|
||||
from .helpers import re_symlink
|
||||
from .helpers import safe_symlink
|
||||
from .hocrtransform import HocrTransform
|
||||
from .optimize import optimize
|
||||
from .pdfa import generate_pdfa_ps
|
||||
from .pdfinfo import Colorspace, PdfInfo
|
||||
from .pdfinfo import Colorspace, Encoding, PdfInfo
|
||||
|
||||
VECTOR_PAGE_DPI = 400
|
||||
|
||||
|
||||
def triage_image_file(input_file, output_file, options, log):
|
||||
log.info("Input file is not a PDF, checking if it is an image...")
|
||||
try:
|
||||
log.info("Input file is not a PDF, checking if it is an image...")
|
||||
im = Image.open(input_file)
|
||||
except EnvironmentError as e:
|
||||
# Recover the original filename
|
||||
@@ -85,9 +86,9 @@ def triage_image_file(input_file, output_file, options, log):
|
||||
|
||||
if 'iccprofile' not in im.info:
|
||||
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':
|
||||
log.info('Input CMYK image has no ICC profile, not usable')
|
||||
log.error("Input CMYK image has no ICC profile, not usable")
|
||||
raise UnsupportedImageFormatError()
|
||||
|
||||
try:
|
||||
@@ -123,7 +124,7 @@ def _pdf_guess_version(input_file, search_window=1024):
|
||||
return ''
|
||||
|
||||
|
||||
def triage(input_file, output_file, options, log):
|
||||
def triage(original_filename, input_file, output_file, options, log):
|
||||
try:
|
||||
if _pdf_guess_version(input_file):
|
||||
if options.image_dpi:
|
||||
@@ -132,11 +133,12 @@ def triage(input_file, output_file, options, log):
|
||||
"input file is a PDF, not an image."
|
||||
)
|
||||
# Origin file is a pdf create a symlink with pdf extension
|
||||
re_symlink(input_file, output_file)
|
||||
safe_symlink(input_file, output_file)
|
||||
return output_file
|
||||
except EnvironmentError as e:
|
||||
log.error(e)
|
||||
raise InputFileError() from e
|
||||
log.debug(f"Temporary file was at: {input_file}")
|
||||
msg = str(e).replace(input_file, original_filename)
|
||||
raise InputFileError(msg) from e
|
||||
|
||||
triage_image_file(input_file, output_file, options, log)
|
||||
return output_file
|
||||
@@ -181,7 +183,7 @@ def validate_pdfinfo_options(context):
|
||||
)
|
||||
raise InputFileError()
|
||||
else:
|
||||
log.warn(
|
||||
log.warning(
|
||||
"This PDF has a fillable form. "
|
||||
"Chances are it is a pure digital "
|
||||
"document that does not need OCR."
|
||||
@@ -557,7 +559,7 @@ def ocr_tesseract_hocr(input_file, page_context):
|
||||
|
||||
def should_visible_page_image_use_jpg(pageinfo):
|
||||
# If all images were JPEGs originally, produce a JPEG as output
|
||||
return pageinfo.images and all(im.enc == 'jpeg' for im in pageinfo.images)
|
||||
return pageinfo.images and all(im.enc == Encoding.jpeg for im in pageinfo.images)
|
||||
|
||||
|
||||
def create_visible_page_jpg(image, page_context):
|
||||
@@ -701,7 +703,7 @@ def convert_to_pdfa(input_pdf, input_ps_stub, context):
|
||||
if modified:
|
||||
pdf_file.save(fix_docinfo_file)
|
||||
else:
|
||||
os.symlink(input_pdf, fix_docinfo_file)
|
||||
safe_symlink(input_pdf, fix_docinfo_file)
|
||||
|
||||
ghostscript.generate_pdfa(
|
||||
pdf_version=input_pdfinfo.min_version,
|
||||
|
||||
+76
-24
@@ -23,8 +23,10 @@ import signal
|
||||
import sys
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
|
||||
import PIL
|
||||
from tqdm import tqdm
|
||||
|
||||
from ._graft import OcrGrafter
|
||||
@@ -176,7 +178,7 @@ def post_process(pdf_file, context):
|
||||
return optimize_pdf(pdf_out, context)
|
||||
|
||||
|
||||
def worker_init(queue):
|
||||
def worker_init(queue, max_pixels):
|
||||
"""Initialize a process pool worker"""
|
||||
|
||||
# Ignore SIGINT (our parent process will kill us gracefully)
|
||||
@@ -188,9 +190,15 @@ def worker_init(queue):
|
||||
root.handlers = []
|
||||
root.addHandler(h)
|
||||
|
||||
# In Windows, child process will not inherit our change to this value in
|
||||
# the parent process, so ensure workers get it set
|
||||
PIL.Image.MAX_IMAGE_PIXELS = max_pixels
|
||||
|
||||
def worker_thread_init(_queue):
|
||||
pass
|
||||
|
||||
def worker_thread_init(_queue, max_pixels):
|
||||
# This is probably not needed since threads should all see the same memory,
|
||||
# but done for consistency.
|
||||
PIL.Image.MAX_IMAGE_PIXELS = max_pixels
|
||||
|
||||
|
||||
def log_listener(queue):
|
||||
@@ -260,26 +268,43 @@ def exec_concurrent(context):
|
||||
unit='page',
|
||||
unit_scale=0.5,
|
||||
disable=not context.options.progress_bar,
|
||||
) as pbar, Pool(
|
||||
processes=max_workers, initializer=initializer, initargs=(log_queue,)
|
||||
) as pool:
|
||||
results = pool.imap_unordered(exec_page_sync, context.get_page_contexts())
|
||||
while True:
|
||||
try:
|
||||
page_result = results.next()
|
||||
sidecars[page_result.pageno] = page_result.text
|
||||
pbar.update()
|
||||
ocrgraft.graft_page(page_result)
|
||||
pbar.update()
|
||||
except StopIteration:
|
||||
break
|
||||
except (Exception, KeyboardInterrupt):
|
||||
) as pbar:
|
||||
pool = Pool(
|
||||
processes=max_workers,
|
||||
initializer=initializer,
|
||||
initargs=(log_queue, PIL.Image.MAX_IMAGE_PIXELS),
|
||||
)
|
||||
try:
|
||||
results = pool.imap_unordered(exec_page_sync, context.get_page_contexts())
|
||||
while True:
|
||||
try:
|
||||
page_result = results.next()
|
||||
sidecars[page_result.pageno] = page_result.text
|
||||
pbar.update()
|
||||
ocrgraft.graft_page(page_result)
|
||||
pbar.update()
|
||||
except StopIteration:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
# Terminate pool so we exit instantly
|
||||
pool.terminate()
|
||||
# Don't try listener.join() here, will deadlock
|
||||
raise
|
||||
except Exception:
|
||||
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
|
||||
# Unless inside pytest, exit immediately because no one wants
|
||||
# to wait for child processes to finalize results that will be
|
||||
# thrown away. Inside pytest, we want child processes to exit
|
||||
# cleanly so that they output an error messages or coverage data
|
||||
# we need from them.
|
||||
pool.terminate()
|
||||
log_queue.put_nowait(None) # Terminate log listener
|
||||
# Don't try listener.join() here, will deadlock
|
||||
raise
|
||||
raise
|
||||
finally:
|
||||
# Terminate log listener
|
||||
log_queue.put_nowait(None)
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
log_queue.put_nowait(None)
|
||||
listener.join()
|
||||
|
||||
# Output sidecar text
|
||||
@@ -304,6 +329,24 @@ class NeverRaise(Exception):
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
def samefile(f1, f2):
|
||||
if os.name == 'nt':
|
||||
return f1 == f2
|
||||
else:
|
||||
return os.path.samefile(f1, f2)
|
||||
|
||||
|
||||
def configure_debug_logging(log_filename, prefix=''):
|
||||
log_file_handler = logging.FileHandler(log_filename, delay=True)
|
||||
log_file_handler.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter(
|
||||
'[%(asctime)s] - %(name)s - %(levelname)7s - %(message)s'
|
||||
)
|
||||
log_file_handler.setFormatter(formatter)
|
||||
logging.getLogger(prefix).addHandler(log_file_handler)
|
||||
return log_file_handler
|
||||
|
||||
|
||||
def run_pipeline(options, api=False):
|
||||
log = make_logger(options, __name__)
|
||||
|
||||
@@ -314,13 +357,22 @@ def run_pipeline(options, api=False):
|
||||
options.jobs = available_cpu_count()
|
||||
|
||||
work_folder = mkdtemp(prefix="com.github.ocrmypdf.")
|
||||
if (options.keep_temporary_files or options.verbose >= 1) and not os.environ.get(
|
||||
'PYTEST_CURRENT_TEST', ''
|
||||
):
|
||||
configure_debug_logging(Path(work_folder) / "debug.log")
|
||||
|
||||
try:
|
||||
check_requested_output_file(options)
|
||||
start_input_file = create_input_file(options, work_folder)
|
||||
start_input_file, original_filename = create_input_file(options, work_folder)
|
||||
|
||||
# Triage image or pdf
|
||||
origin_pdf = triage(
|
||||
start_input_file, os.path.join(work_folder, 'origin.pdf'), options, log
|
||||
original_filename,
|
||||
start_input_file,
|
||||
os.path.join(work_folder, 'origin.pdf'),
|
||||
options,
|
||||
log,
|
||||
)
|
||||
|
||||
# Gather pdfinfo and create context
|
||||
@@ -339,7 +391,7 @@ def run_pipeline(options, api=False):
|
||||
|
||||
if options.output_file == '-':
|
||||
log.info("Output sent to stdout")
|
||||
elif os.path.samefile(options.output_file, os.devnull):
|
||||
elif samefile(options.output_file, os.devnull):
|
||||
pass # Say nothing when sending to dev null
|
||||
else:
|
||||
if options.output_type.startswith('pdfa'):
|
||||
|
||||
@@ -42,7 +42,7 @@ from .exec import (
|
||||
tesseract,
|
||||
unpaper,
|
||||
)
|
||||
from .helpers import is_file_writable, is_iterable_notstr, monotonic, re_symlink
|
||||
from .helpers import is_file_writable, is_iterable_notstr, monotonic, safe_symlink
|
||||
|
||||
# -------------
|
||||
# External dependencies
|
||||
@@ -58,6 +58,15 @@ log = logging.getLogger(__name__)
|
||||
verify_python3_env()
|
||||
|
||||
|
||||
def check_platform():
|
||||
if os.name == 'nt' and sys.maxsize <= 2 ** 32: # pragma: no cover
|
||||
# 32-bit interpreter on Windows
|
||||
log.error(
|
||||
"You are running OCRmyPDF in a 32-bit (x86) Python interpreter."
|
||||
"Please use a 64-bit (x86-64) version of Python."
|
||||
)
|
||||
|
||||
|
||||
def check_options_languages(options):
|
||||
if not options.language:
|
||||
options.language = [DEFAULT_LANGUAGE]
|
||||
@@ -292,6 +301,7 @@ def check_options_pillow(options):
|
||||
|
||||
|
||||
def check_options(options):
|
||||
check_platform()
|
||||
check_options_languages(options)
|
||||
check_options_metadata(options)
|
||||
check_options_output(options)
|
||||
@@ -304,7 +314,7 @@ def check_options(options):
|
||||
check_dependency_versions(options)
|
||||
|
||||
|
||||
def check_closed_streams(options):
|
||||
def check_closed_streams(options): # pragma: no cover
|
||||
"""Work around Python issue with multiprocessing forking on closed streams
|
||||
|
||||
https://bugs.python.org/issue28326
|
||||
@@ -370,12 +380,12 @@ def create_input_file(options, work_folder):
|
||||
target = os.path.join(work_folder, 'stdin')
|
||||
with open(target, 'wb') as stream_buffer:
|
||||
copyfileobj(sys.stdin.buffer, stream_buffer)
|
||||
return target
|
||||
return target, "<stdin>"
|
||||
else:
|
||||
try:
|
||||
target = os.path.join(work_folder, 'origin')
|
||||
re_symlink(options.input_file, target)
|
||||
return target
|
||||
safe_symlink(options.input_file, target)
|
||||
return target, os.fspath(options.input_file)
|
||||
except FileNotFoundError:
|
||||
raise InputFileError(f"File not found - {options.input_file}")
|
||||
|
||||
@@ -418,6 +428,20 @@ def report_output_file_size(options, input_file, output_file):
|
||||
f"The argument --{arg.replace('_', '-')} was issued, causing transcoding."
|
||||
)
|
||||
|
||||
if options.optimize == 0:
|
||||
reasons.append("Optimization was disabled.")
|
||||
else:
|
||||
image_optimizers = {
|
||||
'jbig2': jbig2enc.available(),
|
||||
'pngquant': pngquant.available(),
|
||||
}
|
||||
for name, available in image_optimizers.items():
|
||||
if not available:
|
||||
reasons.append(
|
||||
f"The optional dependency '{name}' was not found, so some image "
|
||||
f"optimizations could not be attempted."
|
||||
)
|
||||
|
||||
if reasons:
|
||||
explanation = "Possible reasons for this include:\n" + '\n'.join(reasons) + "\n"
|
||||
else:
|
||||
@@ -432,7 +456,7 @@ def report_output_file_size(options, input_file, output_file):
|
||||
def check_dependency_versions(options):
|
||||
check_external_program(
|
||||
program='tesseract',
|
||||
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'},
|
||||
package={'linux': 'tesseract-ocr'},
|
||||
version_checker=tesseract.version,
|
||||
need_version='4.0.0', # using backport for Travis CI
|
||||
)
|
||||
|
||||
+62
-48
@@ -18,8 +18,11 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from contextlib import suppress
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
@@ -29,7 +32,15 @@ from .cli import parser
|
||||
|
||||
|
||||
class TqdmConsole:
|
||||
"""Wrapper to log messages in a way that is compatible with tqdm progress bar"""
|
||||
"""Wrapper to log messages in a way that is compatible with tqdm progress bar
|
||||
|
||||
This routes log messages through tqdm so that it can print them above the
|
||||
progress bar, and then refresh the progress bar, rather than overwriting
|
||||
it which looks messy.
|
||||
|
||||
For some reason Python 3.6 prints extra empty messages from time to time,
|
||||
so we suppress those.
|
||||
"""
|
||||
|
||||
def __init__(self, file):
|
||||
self.file = file
|
||||
@@ -44,7 +55,7 @@ class TqdmConsole:
|
||||
tqdm.write(msg.rstrip(), end='\n', file=self.file)
|
||||
|
||||
def flush(self):
|
||||
if hasattr(self.file, "flush"):
|
||||
with suppress(AttributeError):
|
||||
self.file.flush()
|
||||
|
||||
|
||||
@@ -78,11 +89,14 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
|
||||
overwrite the progress bar
|
||||
manage_root_logger (bool): Configure the process's root logger, to ensure
|
||||
all log output is sent through
|
||||
|
||||
Returns:
|
||||
The toplevel logger for ocrmypdf (or the root logger, if we are managing it).
|
||||
"""
|
||||
|
||||
prefix = '' if manage_root_logger else 'ocrmypdf'
|
||||
log = logging.getLogger(prefix)
|
||||
log.setLevel(logging.INFO)
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
if progress_bar_friendly:
|
||||
console = logging.StreamHandler(stream=TqdmConsole(sys.stderr))
|
||||
@@ -97,8 +111,6 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
|
||||
console.setLevel(logging.INFO)
|
||||
|
||||
formatter = logging.Formatter('%(levelname)7s - %(message)s')
|
||||
if verbosity >= 1:
|
||||
log.setLevel(logging.DEBUG)
|
||||
if verbosity >= 2:
|
||||
formatter = logging.Formatter('%(name)s - %(levelname)7s - %(message)s')
|
||||
|
||||
@@ -114,6 +126,8 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
|
||||
if manage_root_logger:
|
||||
logging.captureWarnings(True)
|
||||
|
||||
return log
|
||||
|
||||
|
||||
def create_options(*, input_file, output_file, **kwargs):
|
||||
cmdline = []
|
||||
@@ -158,58 +172,58 @@ def create_options(*, input_file, output_file, **kwargs):
|
||||
|
||||
# If we are running a Tesseract spoof, ensure it knows what the input file is
|
||||
if os.environ.get('PYTEST_CURRENT_TEST') and options.tesseract_env:
|
||||
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = input_file
|
||||
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def ocr( # pylint: disable=unused-argument
|
||||
input_file,
|
||||
output_file,
|
||||
input_file: os.PathLike,
|
||||
output_file: os.PathLike,
|
||||
*,
|
||||
language=None,
|
||||
image_dpi=None,
|
||||
language: List[str] = None,
|
||||
image_dpi: int = None,
|
||||
output_type=None,
|
||||
sidecar=None,
|
||||
jobs=None,
|
||||
use_threads=None,
|
||||
title=None,
|
||||
author=None,
|
||||
subject=None,
|
||||
keywords=None,
|
||||
rotate_pages=None,
|
||||
remove_background=None,
|
||||
deskew=None,
|
||||
clean=None,
|
||||
clean_final=None,
|
||||
unpaper_args=None,
|
||||
oversample=None,
|
||||
remove_vectors=None,
|
||||
threshold=None,
|
||||
force_ocr=None,
|
||||
skip_text=None,
|
||||
redo_ocr=None,
|
||||
skip_big=None,
|
||||
optimize=None,
|
||||
jpg_quality=None,
|
||||
png_quality=None,
|
||||
jbig2_lossy=None,
|
||||
jbig2_page_group_size=None,
|
||||
pages=None,
|
||||
max_image_mpixels=None,
|
||||
tesseract_config=None,
|
||||
tesseract_pagesegmode=None,
|
||||
tesseract_oem=None,
|
||||
sidecar: os.PathLike = None,
|
||||
jobs: int = None,
|
||||
use_threads: bool = None,
|
||||
title: str = None,
|
||||
author: str = None,
|
||||
subject: str = None,
|
||||
keywords: str = None,
|
||||
rotate_pages: bool = None,
|
||||
remove_background: bool = None,
|
||||
deskew: bool = None,
|
||||
clean: bool = None,
|
||||
clean_final: bool = None,
|
||||
unpaper_args: str = None,
|
||||
oversample: int = None,
|
||||
remove_vectors: bool = None,
|
||||
threshold: bool = None,
|
||||
force_ocr: bool = None,
|
||||
skip_text: bool = None,
|
||||
redo_ocr: bool = None,
|
||||
skip_big: float = None,
|
||||
optimize: int = None,
|
||||
jpg_quality: int = None,
|
||||
png_quality: int = None,
|
||||
jbig2_lossy: bool = None,
|
||||
jbig2_page_group_size: int = None,
|
||||
pages: str = None,
|
||||
max_image_mpixels: float = None,
|
||||
tesseract_config: List[str] = None,
|
||||
tesseract_pagesegmode: int = None,
|
||||
tesseract_oem: int = None,
|
||||
pdf_renderer=None,
|
||||
tesseract_timeout=None,
|
||||
rotate_pages_threshold=None,
|
||||
tesseract_timeout: float = None,
|
||||
rotate_pages_threshold: float = None,
|
||||
pdfa_image_compression=None,
|
||||
user_words=None,
|
||||
user_patterns=None,
|
||||
fast_web_view=None,
|
||||
keep_temporary_files=None,
|
||||
progress_bar=None,
|
||||
tesseract_env=None,
|
||||
user_words: os.PathLike = None,
|
||||
user_patterns: os.PathLike = None,
|
||||
fast_web_view: float = None,
|
||||
keep_temporary_files: bool = None,
|
||||
progress_bar: bool = None,
|
||||
tesseract_env: Dict[str, str] = None,
|
||||
):
|
||||
"""Run OCRmyPDF on one PDF or image.
|
||||
|
||||
|
||||
@@ -20,17 +20,82 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, run
|
||||
from functools import lru_cache
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError
|
||||
from subprocess import run as subprocess_run
|
||||
|
||||
from ..exceptions import ExitCode, MissingDependencyError
|
||||
|
||||
log = logging.Logger(__name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_program(args, env=None):
|
||||
program = args[0]
|
||||
test_path = env.get('_OCRMYPDF_TEST_PATH', '')
|
||||
if test_path:
|
||||
program = shutil.which(program, path=test_path)
|
||||
return program
|
||||
|
||||
|
||||
def run(args, *, env=None, **kwargs):
|
||||
"""Wrapper around subprocess.run()
|
||||
|
||||
The main purpose of this wrapper is to allow us to substitute the main program
|
||||
for a spoof in the test suite. The hidden variable _OCRMYPDF_TEST_PATH replaces
|
||||
the main PATH as a location to check for programs to run.
|
||||
|
||||
Secondly we have to account for behavioral differences in Windows in particular.
|
||||
Creating symbolic links in Windows requires administrator privileges and
|
||||
may not work if for some reason we're using a FAT file system or the temporary
|
||||
folder is on a different drive from the working folder. The test suite
|
||||
works around this by creating shim Python scripts that perform the same function
|
||||
as a symbolic link, but those shims require support on this side, to ensure
|
||||
we call them with Python.
|
||||
|
||||
"""
|
||||
if not env:
|
||||
env = os.environ
|
||||
|
||||
# Search in spoof path if necessary
|
||||
program = _get_program(args, env)
|
||||
|
||||
# If we are running a .py on Windows, ensure we call it with this Python
|
||||
# (to support test suite shims)
|
||||
if os.name == 'nt' and program.lower().endswith('.py'):
|
||||
args = [sys.executable, program] + args[1:]
|
||||
else:
|
||||
args = [program] + args[1:]
|
||||
|
||||
if os.name == 'nt':
|
||||
paths = os.pathsep.join(os.get_exec_path(env))
|
||||
if not shutil.which(args[0], path=paths):
|
||||
shimmed_path = shim_paths_with_program_files(env)
|
||||
new_args0 = shutil.which(args[0], path=shimmed_path)
|
||||
if new_args0:
|
||||
args[0] = new_args0
|
||||
|
||||
process_log = log.getChild(os.path.basename(program))
|
||||
process_log.debug("Running: %s", args)
|
||||
if sys.version_info < (3, 7) and os.name == 'nt':
|
||||
# Can't use close_fds=True on Windows with Python 3.6 or older
|
||||
# https://bugs.python.org/issue19575, etc.
|
||||
kwargs['close_fds'] = False
|
||||
proc = subprocess_run(args, env=env, **kwargs)
|
||||
if process_log.isEnabledFor(logging.DEBUG):
|
||||
try:
|
||||
stderr = proc.stderr.decode('utf-8', 'replace')
|
||||
except AttributeError:
|
||||
stderr = proc.stderr
|
||||
if stderr:
|
||||
process_log.debug("stderr = %s", stderr)
|
||||
return proc
|
||||
|
||||
|
||||
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env=None):
|
||||
"Get the version of the specified program"
|
||||
"""Get the version of the specified program"""
|
||||
args_prog = [program, version_arg]
|
||||
try:
|
||||
proc = run(
|
||||
@@ -66,6 +131,32 @@ def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env
|
||||
return version
|
||||
|
||||
|
||||
def shim_paths_with_program_files(env=None):
|
||||
if not env:
|
||||
env = os.environ
|
||||
program_files = env.get('PROGRAMFILES', '')
|
||||
if not program_files:
|
||||
return env.get('PATH', '')
|
||||
paths = []
|
||||
try:
|
||||
for dirname in os.listdir(program_files):
|
||||
if dirname.lower() == 'tesseract-ocr':
|
||||
paths.append(os.path.join(program_files, dirname))
|
||||
elif dirname.lower() == 'gs':
|
||||
try:
|
||||
latest_gs = max(
|
||||
os.listdir(os.path.join(program_files, dirname)),
|
||||
key=lambda d: float(d[2:]),
|
||||
)
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
continue
|
||||
paths.append(os.path.join(program_files, dirname, latest_gs, 'bin'))
|
||||
except EnvironmentError:
|
||||
pass
|
||||
paths.extend(path for path in os.get_exec_path(env) if path not in set(paths))
|
||||
return os.pathsep.join(paths)
|
||||
|
||||
|
||||
missing_program = '''
|
||||
The program '{program}' could not be executed or was not found on your
|
||||
system PATH.
|
||||
@@ -111,23 +202,33 @@ On RPM-based systems (Red Hat, Fedora), search for instructions on
|
||||
installing the RPM for {program}.
|
||||
'''
|
||||
|
||||
windows_install_advice = '''
|
||||
If not already installed, install the Chocolatey package manager. Then use
|
||||
a command prompt to install the missing package:
|
||||
choco install {package}
|
||||
'''
|
||||
|
||||
|
||||
def _get_platform():
|
||||
if sys.platform.startswith('freebsd'):
|
||||
return 'freebsd'
|
||||
elif sys.platform.startswith('linux'):
|
||||
return 'linux'
|
||||
elif sys.platform.startswith('win'):
|
||||
return 'windows'
|
||||
return sys.platform
|
||||
|
||||
|
||||
def _error_trailer(program, package, **kwargs):
|
||||
if isinstance(package, Mapping):
|
||||
package = package[_get_platform()]
|
||||
package = package.get(_get_platform(), program)
|
||||
|
||||
if _get_platform() == 'darwin':
|
||||
log.info(osx_install_advice.format(**locals()))
|
||||
elif _get_platform() == 'linux':
|
||||
log.info(linux_install_advice.format(**locals()))
|
||||
elif _get_platform() == 'windows':
|
||||
log.info(windows_install_advice.format(**locals()))
|
||||
|
||||
|
||||
def _error_missing_program(program, package, required_for, recommended):
|
||||
|
||||
@@ -15,25 +15,51 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Interface to Ghostscript executable"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
from contextlib import suppress
|
||||
from functools import lru_cache
|
||||
from io import BytesIO
|
||||
from os import fspath
|
||||
from shutil import copy
|
||||
from subprocess import PIPE, STDOUT, run
|
||||
from tempfile import NamedTemporaryFile
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from subprocess import PIPE, CalledProcessError
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from ..exceptions import SubprocessOutputError
|
||||
from . import get_version
|
||||
from ..exceptions import MissingDependencyError, SubprocessOutputError
|
||||
from . import get_version, run
|
||||
|
||||
gslog = logging.getLogger()
|
||||
|
||||
GS = 'gs'
|
||||
if os.name == 'nt':
|
||||
GS = which('gswin64c')
|
||||
if not GS:
|
||||
GS = which('gswin32c')
|
||||
if not GS:
|
||||
raise MissingDependencyError(
|
||||
"""
|
||||
---------------------------------------------------------------------
|
||||
This error normally occurs when ocrmypdf can't Ghostscript. Please
|
||||
ensure Ghostscript is installed and its location is added to the
|
||||
system PATH environment variable.
|
||||
|
||||
For details see:
|
||||
https://ocrmypdf.readthedocs.io/en/latest/installation.html
|
||||
---------------------------------------------------------------------
|
||||
"""
|
||||
)
|
||||
GS = Path(GS).stem
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def version():
|
||||
return get_version('gs')
|
||||
return get_version(GS)
|
||||
|
||||
|
||||
def jpeg_passthrough_available():
|
||||
@@ -78,9 +104,12 @@ def extract_text(input_file, pageno=1):
|
||||
else:
|
||||
pages = []
|
||||
|
||||
# Note due to bug https://bugs.ghostscript.com/show_bug.cgi?id=701971
|
||||
# Ghostscript <= 9.50 will truncate output unless we write to stdout, so
|
||||
# don't write to a file.
|
||||
args_gs = (
|
||||
[
|
||||
'gs',
|
||||
GS,
|
||||
'-dQUIET',
|
||||
'-dSAFER',
|
||||
'-dBATCH',
|
||||
@@ -89,14 +118,15 @@ def extract_text(input_file, pageno=1):
|
||||
'-dTextFormat=0',
|
||||
]
|
||||
+ pages
|
||||
+ ['-o', '-', fspath(input_file)]
|
||||
+ ['-o', '-', fspath(input_file), "-sstdout=%stderr"]
|
||||
)
|
||||
|
||||
p = run(args_gs, stdout=PIPE, stderr=PIPE)
|
||||
if p.returncode != 0:
|
||||
try:
|
||||
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
|
||||
except CalledProcessError as e:
|
||||
raise SubprocessOutputError(
|
||||
'Ghostscript text extraction failed\n%s\n%s\n%s'
|
||||
% (input_file, p.stdout.decode(), p.stderr.decode())
|
||||
'Ghostscript text extraction failed\n%s\n%s'
|
||||
% (input_file, e.stderr.decode(errors='replace'))
|
||||
)
|
||||
|
||||
return p.stdout
|
||||
@@ -138,54 +168,56 @@ def rasterize_pdf(
|
||||
if not log:
|
||||
log = gslog
|
||||
|
||||
with NamedTemporaryFile(delete=True) as tmp:
|
||||
args_gs = (
|
||||
[
|
||||
'gs',
|
||||
'-dQUIET',
|
||||
'-dSAFER',
|
||||
'-dBATCH',
|
||||
'-dNOPAUSE',
|
||||
f'-sDEVICE={raster_device}',
|
||||
f'-dFirstPage={pageno}',
|
||||
f'-dLastPage={pageno}',
|
||||
f'-r{res[0]:f}x{res[1]:f}',
|
||||
]
|
||||
+ (['-dFILTERVECTOR'] if filter_vector else [])
|
||||
+ [
|
||||
'-o',
|
||||
tmp.name,
|
||||
'-dAutoRotatePages=/None', # Probably has no effect on raster
|
||||
'-f',
|
||||
fspath(input_file),
|
||||
]
|
||||
)
|
||||
args_gs = (
|
||||
[
|
||||
GS,
|
||||
'-dQUIET',
|
||||
'-dSAFER',
|
||||
'-dBATCH',
|
||||
'-dNOPAUSE',
|
||||
f'-sDEVICE={raster_device}',
|
||||
f'-dFirstPage={pageno}',
|
||||
f'-dLastPage={pageno}',
|
||||
f'-r{res[0]:f}x{res[1]:f}',
|
||||
]
|
||||
+ (['-dFILTERVECTOR'] if filter_vector else [])
|
||||
+ [
|
||||
'-o',
|
||||
'-',
|
||||
'-sstdout=%stderr',
|
||||
'-dAutoRotatePages=/None', # Probably has no effect on raster
|
||||
'-f',
|
||||
fspath(input_file),
|
||||
]
|
||||
)
|
||||
|
||||
log.debug(args_gs)
|
||||
p = run(args_gs, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
|
||||
if _gs_error_reported(p.stdout):
|
||||
log.error(p.stdout)
|
||||
elif p.stdout:
|
||||
log.debug(p.stdout)
|
||||
log.debug(args_gs)
|
||||
try:
|
||||
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')
|
||||
else:
|
||||
stderr = p.stderr.decode(errors='replace')
|
||||
if _gs_error_reported(stderr):
|
||||
log.error(stderr)
|
||||
elif stderr:
|
||||
log.debug(stderr)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise SubprocessOutputError('Ghostscript rasterizing failed')
|
||||
|
||||
tmp.seek(0)
|
||||
with Image.open(tmp) as im:
|
||||
if rotation is not None:
|
||||
log.debug("Rotating output by %i", rotation)
|
||||
# rotation is a clockwise angle and Image.ROTATE_* is
|
||||
# counterclockwise so this cancels out the rotation
|
||||
if rotation == 90:
|
||||
im = im.transpose(Image.ROTATE_90)
|
||||
elif rotation == 180:
|
||||
im = im.transpose(Image.ROTATE_180)
|
||||
elif rotation == 270:
|
||||
im = im.transpose(Image.ROTATE_270)
|
||||
if rotation % 180 == 90:
|
||||
page_dpi = page_dpi[1], page_dpi[0]
|
||||
im.save(fspath(output_file), dpi=page_dpi)
|
||||
with Image.open(BytesIO(p.stdout)) as im:
|
||||
if rotation is not None:
|
||||
log.debug("Rotating output by %i", rotation)
|
||||
# rotation is a clockwise angle and Image.ROTATE_* is
|
||||
# counterclockwise so this cancels out the rotation
|
||||
if rotation == 90:
|
||||
im = im.transpose(Image.ROTATE_90)
|
||||
elif rotation == 180:
|
||||
im = im.transpose(Image.ROTATE_180)
|
||||
elif rotation == 270:
|
||||
im = im.transpose(Image.ROTATE_270)
|
||||
if rotation % 180 == 90:
|
||||
page_dpi = page_dpi[1], page_dpi[0]
|
||||
im.save(fspath(output_file), dpi=page_dpi)
|
||||
|
||||
|
||||
def generate_pdfa(
|
||||
@@ -193,7 +225,7 @@ def generate_pdfa(
|
||||
output_file,
|
||||
compression,
|
||||
log,
|
||||
threads=1,
|
||||
threads=None, # deprecated parameter
|
||||
pdf_version='1.5',
|
||||
pdfa_part='2',
|
||||
):
|
||||
@@ -216,6 +248,10 @@ def generate_pdfa(
|
||||
"""
|
||||
if not log:
|
||||
log = gslog
|
||||
if threads is not None:
|
||||
warnings.warn(
|
||||
"use of deprecated parameter 'threads'", category=DeprecationWarning
|
||||
)
|
||||
|
||||
compression_args = []
|
||||
if compression == 'jpeg':
|
||||
@@ -249,37 +285,55 @@ def generate_pdfa(
|
||||
# https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
||||
compression_args.append('-dPassThroughJPEGImages=false')
|
||||
|
||||
with NamedTemporaryFile(delete=True) as gs_pdf:
|
||||
# nb no need to specify ProcessColorModel when ColorConversionStrategy
|
||||
# is set; see:
|
||||
# https://bugs.ghostscript.com/show_bug.cgi?id=699392
|
||||
args_gs = (
|
||||
[
|
||||
"gs",
|
||||
"-dQUIET",
|
||||
"-dBATCH",
|
||||
"-dNOPAUSE",
|
||||
"-dSAFER",
|
||||
"-dCompatibilityLevel=" + str(pdf_version),
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dAutoRotatePages=/None",
|
||||
"-sColorConversionStrategy=" + strategy,
|
||||
]
|
||||
+ compression_args
|
||||
+ [
|
||||
"-dJPEGQ=95",
|
||||
"-dPDFA=" + pdfa_part,
|
||||
"-dPDFACompatibilityPolicy=1",
|
||||
"-sOutputFile=" + gs_pdf.name,
|
||||
]
|
||||
)
|
||||
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
|
||||
log.debug(args_gs)
|
||||
p = run(args_gs, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
|
||||
|
||||
if _gs_error_reported(p.stdout):
|
||||
log.error(p.stdout)
|
||||
elif 'overprint mode not set' in p.stdout:
|
||||
# nb no need to specify ProcessColorModel when ColorConversionStrategy
|
||||
# is set; see:
|
||||
# https://bugs.ghostscript.com/show_bug.cgi?id=699392
|
||||
args_gs = (
|
||||
[
|
||||
GS,
|
||||
"-dQUIET",
|
||||
"-dBATCH",
|
||||
"-dNOPAUSE",
|
||||
"-dSAFER",
|
||||
"-dCompatibilityLevel=" + str(pdf_version),
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dAutoRotatePages=/None",
|
||||
"-sColorConversionStrategy=" + strategy,
|
||||
]
|
||||
+ compression_args
|
||||
+ [
|
||||
"-dJPEGQ=95",
|
||||
"-dPDFA=" + pdfa_part,
|
||||
"-dPDFACompatibilityPolicy=1",
|
||||
"-o",
|
||||
"-",
|
||||
"-sstdout=%stderr",
|
||||
]
|
||||
)
|
||||
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
|
||||
try:
|
||||
with Path(output_file).open('wb') as output:
|
||||
p = run(args_gs, stdout=output, stderr=PIPE, check=True)
|
||||
except CalledProcessError as e:
|
||||
# Ghostscript does not change return code when it fails to create
|
||||
# PDF/A - check PDF/A status elsewhere
|
||||
log.error(e.stderr.decode(errors='replace'))
|
||||
raise SubprocessOutputError('Ghostscript PDF/A rendering failed')
|
||||
else:
|
||||
stderr = p.stderr.decode('utf-8', errors='replace')
|
||||
if _gs_error_reported(stderr):
|
||||
last_part = None
|
||||
repcount = 0
|
||||
for part in stderr.split('****'):
|
||||
if part != last_part:
|
||||
if repcount > 1:
|
||||
log.error(f"(previous error message repeated {repcount} times)")
|
||||
repcount = 0
|
||||
log.error(part)
|
||||
else:
|
||||
repcount += 1
|
||||
last_part = part
|
||||
elif 'overprint mode not set' in stderr:
|
||||
# Unless someone is going to print PDF/A documents on a
|
||||
# magical sRGB printer I can't see the removal of overprinting
|
||||
# being a problem....
|
||||
@@ -287,12 +341,3 @@ def generate_pdfa(
|
||||
"Ghostscript had to remove PDF 'overprinting' from the "
|
||||
"input file to complete PDF/A conversion. "
|
||||
)
|
||||
else:
|
||||
log.debug(p.stdout)
|
||||
|
||||
if p.returncode == 0:
|
||||
# Ghostscript does not change return code when it fails to create
|
||||
# PDF/A - check PDF/A status elsewhere
|
||||
copy(gs_pdf.name, fspath(output_file))
|
||||
else:
|
||||
raise SubprocessOutputError('Ghostscript PDF/A rendering failed')
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Interface to jbig2 executable"""
|
||||
|
||||
from functools import lru_cache
|
||||
from subprocess import PIPE, run
|
||||
from subprocess import PIPE
|
||||
|
||||
from ..exceptions import MissingDependencyError
|
||||
from . import get_version
|
||||
from . import get_version, run
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Interface to pngquant executable"""
|
||||
|
||||
from functools import lru_cache
|
||||
from subprocess import run
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
+35
-23
@@ -15,35 +15,47 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from functools import lru_cache
|
||||
from os import fspath
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, run
|
||||
"""Interface to qpdf executable"""
|
||||
|
||||
from . import get_version
|
||||
from io import StringIO
|
||||
|
||||
import pikepdf
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def version():
|
||||
return get_version('qpdf', regex=r'qpdf version (.+)')
|
||||
return pikepdf.__libqpdf_version__
|
||||
|
||||
|
||||
def check(input_file, log=None):
|
||||
args_qpdf = ['qpdf', '--check', fspath(input_file)]
|
||||
|
||||
if log is None:
|
||||
import logging as log
|
||||
|
||||
pdf = None
|
||||
try:
|
||||
run(args_qpdf, stderr=STDOUT, stdout=PIPE, universal_newlines=True, check=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 2:
|
||||
log.error("%s: not a valid PDF, and could not repair it.", input_file)
|
||||
log.error("Details:")
|
||||
log.error(e.output)
|
||||
elif e.returncode == 3:
|
||||
log.info("qpdf --check returned warnings:")
|
||||
log.info(e.output)
|
||||
else:
|
||||
log.warning(e.output)
|
||||
pdf = pikepdf.open(input_file)
|
||||
except pikepdf.PdfError as e:
|
||||
if log:
|
||||
log.error(e)
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
messages = pdf.check()
|
||||
for msg in messages:
|
||||
if 'error' in msg.lower():
|
||||
log.error(msg)
|
||||
else:
|
||||
log.warning(msg)
|
||||
|
||||
sio = StringIO()
|
||||
linearize = None
|
||||
try:
|
||||
pdf.check_linearization(sio)
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
linearize = sio.getvalue()
|
||||
if linearize:
|
||||
log.warning(linearize)
|
||||
|
||||
if not messages and not linearize:
|
||||
return True
|
||||
return False
|
||||
finally:
|
||||
if pdf:
|
||||
pdf.close()
|
||||
|
||||
@@ -15,20 +15,23 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Interface to Tesseract executable"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from collections import namedtuple
|
||||
from contextlib import suppress
|
||||
from os import fspath
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired, run
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired
|
||||
|
||||
from ..exceptions import (
|
||||
MissingDependencyError,
|
||||
SubprocessOutputError,
|
||||
TesseractConfigError,
|
||||
)
|
||||
from ..helpers import page_number
|
||||
from . import get_version
|
||||
from ..helpers import page_number, safe_symlink
|
||||
from . import get_version, run
|
||||
|
||||
OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence'))
|
||||
|
||||
@@ -50,6 +53,12 @@ HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
"""
|
||||
|
||||
|
||||
class TesseractLoggerAdapter(logging.LoggerAdapter):
|
||||
def process(self, msg, kwargs):
|
||||
kwargs['extra'] = self.extra
|
||||
return '[tesseract] %s' % (msg), kwargs
|
||||
|
||||
|
||||
def version(tesseract_env=None):
|
||||
return get_version('tesseract', regex=r'tesseract\s(.+)', env=tesseract_env)
|
||||
|
||||
@@ -119,9 +128,10 @@ def languages(tesseract_env=None):
|
||||
except CalledProcessError as e:
|
||||
raise MissingDependencyError(lang_error(e.output)) from e
|
||||
|
||||
for line in output.splitlines():
|
||||
if line.startswith('Error'):
|
||||
raise MissingDependencyError(lang_error(output))
|
||||
header, *rest = output.splitlines()
|
||||
if not header.startswith('List of available languages'):
|
||||
raise MissingDependencyError(lang_error(output))
|
||||
return set(lang.strip() for lang in rest)
|
||||
|
||||
|
||||
@@ -129,7 +139,7 @@ def tess_base_args(langs, engine_mode):
|
||||
args = ['tesseract']
|
||||
if langs:
|
||||
args.extend(['-l', '+'.join(langs)])
|
||||
if engine_mode is not None and v4():
|
||||
if engine_mode is not None:
|
||||
args.extend(['--oem', str(engine_mode)])
|
||||
return args
|
||||
|
||||
@@ -177,19 +187,15 @@ def get_orientation(input_file, engine_mode, timeout: float, log, tesseract_env=
|
||||
return oc
|
||||
|
||||
|
||||
def tesseract_log_output(log, stdout, input_file):
|
||||
prefix = "[tesseract] "
|
||||
def tesseract_log_output(mainlog, stdout, input_file):
|
||||
log = TesseractLoggerAdapter(
|
||||
mainlog, extra=mainlog.extra if hasattr(mainlog, 'extra') else None
|
||||
)
|
||||
|
||||
try:
|
||||
text = stdout.decode()
|
||||
except UnicodeDecodeError:
|
||||
log.error(
|
||||
prefix
|
||||
+ "command line output was not utf-8. "
|
||||
+ "This usually means Tesseract's language packs do not match "
|
||||
"the installed version of Tesseract."
|
||||
)
|
||||
text = stdout.decode('utf-8', 'backslashreplace')
|
||||
text = stdout.decode('utf-8', 'ignore')
|
||||
|
||||
lines = text.splitlines()
|
||||
for line in lines:
|
||||
@@ -198,25 +204,25 @@ def tesseract_log_output(log, stdout, input_file):
|
||||
elif line.startswith("Warning in pixReadMem"):
|
||||
continue
|
||||
elif 'diacritics' in line:
|
||||
log.warning(prefix + "lots of diacritics - possibly poor OCR")
|
||||
log.warning("lots of diacritics - possibly poor OCR")
|
||||
elif line.startswith('OSD: Weak margin'):
|
||||
log.warning(prefix + "unsure about page orientation")
|
||||
log.warning("unsure about page orientation")
|
||||
elif 'Error in pixScanForForeground' in line:
|
||||
pass # Appears to be spurious/problem with nonwhite borders
|
||||
elif 'Error in boxClipToRectangle' in line:
|
||||
pass # Always appears with pixScanForForeground message
|
||||
elif 'parameter not found: ' in line.lower():
|
||||
log.error(prefix + line.strip())
|
||||
log.error(line.strip())
|
||||
problem = line.split('found: ')[1]
|
||||
raise TesseractConfigError(problem)
|
||||
elif 'error' in line.lower() or 'exception' in line.lower():
|
||||
log.error(prefix + line.strip())
|
||||
log.error(line.strip())
|
||||
elif 'warning' in line.lower():
|
||||
log.warning(prefix + line.strip())
|
||||
log.warning(line.strip())
|
||||
elif 'read_params_file' in line.lower():
|
||||
log.error(prefix + line.strip())
|
||||
log.error(line.strip())
|
||||
else:
|
||||
log.info(prefix + line.strip())
|
||||
log.info(line.strip())
|
||||
|
||||
|
||||
def page_timedout(log, input_file, timeout):
|
||||
@@ -254,8 +260,8 @@ def generate_hocr(
|
||||
log,
|
||||
):
|
||||
|
||||
output_hocr = next(o for o in output_files if o.endswith('.hocr'))
|
||||
output_sidecar = next(o for o in output_files if o.endswith('.txt'))
|
||||
output_hocr = next(o for o in output_files if fspath(o).endswith('.hocr'))
|
||||
output_sidecar = next(o for o in output_files if fspath(o).endswith('.txt'))
|
||||
prefix = os.path.splitext(output_hocr)[0]
|
||||
|
||||
args_tesseract = tess_base_args(language, engine_mode)
|
||||
@@ -269,14 +275,10 @@ def generate_hocr(
|
||||
if user_patterns:
|
||||
args_tesseract.extend(['--user-patterns', user_patterns])
|
||||
|
||||
if user_words or user_patterns:
|
||||
args_tesseract.extend(['-c', 'lstm_use_matrix=1'])
|
||||
|
||||
# Reminder: test suite tesseract spoofers will break after any changes
|
||||
# to the number of order parameters here
|
||||
args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig)
|
||||
try:
|
||||
log.debug(args_tesseract)
|
||||
p = run(
|
||||
args_tesseract,
|
||||
stdout=PIPE,
|
||||
@@ -315,7 +317,7 @@ def use_skip_page(text_only, skip_pdf, output_pdf, output_text):
|
||||
# Substitute a "skipped page"
|
||||
with suppress(FileNotFoundError):
|
||||
os.remove(output_pdf) # In case it was partially created
|
||||
os.symlink(skip_pdf, output_pdf)
|
||||
safe_symlink(skip_pdf, output_pdf)
|
||||
return
|
||||
|
||||
# Or normally, just write a 0 byte file to the output to indicate a skip
|
||||
@@ -375,7 +377,6 @@ def generate_pdf(
|
||||
|
||||
args_tesseract.extend([input_image, prefix, 'pdf', 'txt'] + tessconfig)
|
||||
try:
|
||||
log.debug(args_tesseract)
|
||||
p = run(
|
||||
args_tesseract,
|
||||
stdout=PIPE,
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
# unpaper documentation:
|
||||
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
|
||||
|
||||
"""Interface to unpaper executable"""
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError
|
||||
from tempfile import TemporaryDirectory
|
||||
@@ -30,6 +30,7 @@ from PIL import Image
|
||||
|
||||
from ..exceptions import MissingDependencyError, SubprocessOutputError
|
||||
from . import get_version
|
||||
from . import run as external_run
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
@@ -76,7 +77,7 @@ def run(input_file, output_file, dpi, log, mode_args):
|
||||
# their unpaper arguments (whether intentionally or otherwise)
|
||||
args_unpaper.extend([input_pnm, output_pnm])
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
proc = external_run(
|
||||
args_unpaper,
|
||||
check=True,
|
||||
close_fds=True,
|
||||
|
||||
+38
-24
@@ -18,6 +18,7 @@
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import shutil
|
||||
import warnings
|
||||
from collections.abc import Iterable
|
||||
from contextlib import suppress
|
||||
@@ -27,14 +28,14 @@ from pathlib import Path
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def re_symlink(input_file, soft_link_name, *args, **kwargs):
|
||||
def safe_symlink(input_file, soft_link_name, *args, **kwargs):
|
||||
"""
|
||||
Helper function: relinks soft symbolic link if necessary
|
||||
"""
|
||||
if len(args) == 1 and isinstance(args[0], logging.Logger):
|
||||
log.warning("Deprecated: re_symlink(,log)")
|
||||
log.warning("Deprecated: safe_symlink(,log)")
|
||||
if 'log' in kwargs:
|
||||
log.warning('Deprecated: re_symlink(...log=)')
|
||||
log.warning('Deprecated: safe_symlink(...log=)')
|
||||
|
||||
input_file = os.fspath(input_file)
|
||||
soft_link_name = os.fspath(soft_link_name)
|
||||
@@ -60,6 +61,11 @@ def re_symlink(input_file, soft_link_name, *args, **kwargs):
|
||||
if not os.path.exists(input_file):
|
||||
raise FileNotFoundError(f"trying to create a broken symlink to {input_file}")
|
||||
|
||||
if os.name == 'nt':
|
||||
# Don't actually use symlinks on Windows due to permission issues
|
||||
shutil.copyfile(input_file, soft_link_name)
|
||||
return
|
||||
|
||||
log.debug("os.symlink(%s, %s)", input_file, soft_link_name)
|
||||
|
||||
# Create symbolic link using absolute path
|
||||
@@ -98,28 +104,36 @@ def is_file_writable(test_file):
|
||||
can replace it atomically. Before doing the OCR work, make sure
|
||||
the location is writable.
|
||||
"""
|
||||
p = Path(test_file)
|
||||
|
||||
if p.is_symlink():
|
||||
p = p.resolve(strict=False)
|
||||
|
||||
# p.is_file() throws an exception in some cases
|
||||
if p.exists() and p.is_file():
|
||||
return os.access(
|
||||
os.fspath(p),
|
||||
os.W_OK,
|
||||
effective_ids=(os.access in os.supports_effective_ids),
|
||||
)
|
||||
else:
|
||||
try:
|
||||
fp = p.open('wb')
|
||||
except OSError:
|
||||
return False
|
||||
try:
|
||||
if not isinstance(test_file, Path):
|
||||
p = Path(test_file)
|
||||
else:
|
||||
fp.close()
|
||||
with suppress(OSError):
|
||||
p.unlink()
|
||||
return True
|
||||
p = test_file
|
||||
|
||||
if p.is_symlink():
|
||||
p = p.resolve(strict=False)
|
||||
|
||||
# p.is_file() throws an exception in some cases
|
||||
if p.exists() and p.is_file():
|
||||
return os.access(
|
||||
os.fspath(p),
|
||||
os.W_OK,
|
||||
effective_ids=(os.access in os.supports_effective_ids),
|
||||
)
|
||||
else:
|
||||
try:
|
||||
fp = p.open('wb')
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
fp.close()
|
||||
with suppress(OSError):
|
||||
p.unlink()
|
||||
return True
|
||||
except (EnvironmentError, RuntimeError) as e:
|
||||
log.debug(e)
|
||||
log.error(str(e))
|
||||
return False
|
||||
|
||||
|
||||
def deprecated(func):
|
||||
|
||||
@@ -88,7 +88,7 @@ class HocrTransform:
|
||||
if self.width is None or self.height is None:
|
||||
raise HocrTransformError("hocr file is missing page dimensions")
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self): # pragma: no cover
|
||||
"""
|
||||
Return the textual content of the HTML body
|
||||
"""
|
||||
@@ -190,7 +190,7 @@ class HocrTransform:
|
||||
pt = self.pt_from_pixel(pxl_coords)
|
||||
|
||||
# draw the bbox border
|
||||
if showBoundingboxes:
|
||||
if showBoundingboxes: # pragma: no cover
|
||||
pdf.rect(
|
||||
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=1
|
||||
)
|
||||
@@ -231,7 +231,7 @@ class HocrTransform:
|
||||
pdf.save()
|
||||
|
||||
@classmethod
|
||||
def polyval(cls, poly, x):
|
||||
def polyval(cls, poly, x): # pragma: no cover
|
||||
return x * poly[0] + poly[1]
|
||||
|
||||
def _do_line(
|
||||
@@ -269,7 +269,7 @@ class HocrTransform:
|
||||
# of the line box
|
||||
baseline_y2 = self.height - (line_box.y2 + intercept)
|
||||
|
||||
if showBoundingboxes:
|
||||
if showBoundingboxes: # pragma: no cover
|
||||
# draw the baseline in magenta, dashed
|
||||
pdf.setDash()
|
||||
pdf.setStrokeColorRGB(0.95, 0.65, 0.95)
|
||||
@@ -318,7 +318,7 @@ class HocrTransform:
|
||||
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
|
||||
|
||||
# draw the bbox border
|
||||
if showBoundingboxes:
|
||||
if showBoundingboxes: # pragma: no cover
|
||||
pdf.rect(
|
||||
box.x1, self.height - line_box.y2, box_width, line_height, fill=0
|
||||
)
|
||||
|
||||
+57
-18
@@ -33,14 +33,46 @@ from io import BytesIO
|
||||
from os import fspath
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
from .exceptions import MissingDependencyError
|
||||
from .exec import shim_paths_with_program_files
|
||||
from .lib._leptonica import ffi
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
lept = ffi.dlopen(find_library('lept'))
|
||||
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
|
||||
if os.name == 'nt':
|
||||
libname = 'liblept-5'
|
||||
os.environ['PATH'] = shim_paths_with_program_files()
|
||||
else:
|
||||
libname = 'lept'
|
||||
_libpath = find_library(libname)
|
||||
if not _libpath:
|
||||
raise MissingDependencyError(
|
||||
"""
|
||||
---------------------------------------------------------------------
|
||||
This error normally occurs when ocrmypdf can't find the Leptonica
|
||||
library, which is usually installed with Tesseract OCR. It could be that
|
||||
Tesseract is not installed properly, we can't find the installation
|
||||
on your system PATH environment variable.
|
||||
|
||||
The library we are looking for is usually called:
|
||||
liblept-5.dll (Windows)
|
||||
liblept*.dylib (macOS)
|
||||
liblept*.so (Linux/BSD)
|
||||
|
||||
Please review our installation procedures to find a solution:
|
||||
https://ocrmypdf.readthedocs.io/en/latest/installation.html
|
||||
---------------------------------------------------------------------
|
||||
"""
|
||||
)
|
||||
try:
|
||||
lept = ffi.dlopen(_libpath)
|
||||
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
|
||||
except ffi.error as e:
|
||||
raise MissingDependencyError(
|
||||
f"Leptonica library found at {_libpath}, but we could not access it"
|
||||
) from e
|
||||
|
||||
|
||||
class _LeptonicaErrorTrap:
|
||||
@@ -292,9 +324,11 @@ class Pix(LeptonicaObject):
|
||||
Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If
|
||||
loading fails then the object will wrap a C null pointer.
|
||||
"""
|
||||
filename = fspath(path)
|
||||
with _LeptonicaErrorTrap():
|
||||
return cls(lept.pixRead(os.fsencode(filename)))
|
||||
with open(path, 'rb') as py_file:
|
||||
data = py_file.read()
|
||||
buffer = ffi.from_buffer(data)
|
||||
with _LeptonicaErrorTrap():
|
||||
return cls(lept.pixReadMem(buffer, len(buffer)))
|
||||
|
||||
def write_implied_format(self, path, jpeg_quality=0, jpeg_progressive=0):
|
||||
"""Write pix to the filename, with the extension indicating format.
|
||||
@@ -302,11 +336,19 @@ class Pix(LeptonicaObject):
|
||||
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
|
||||
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
|
||||
"""
|
||||
filename = fspath(path)
|
||||
with _LeptonicaErrorTrap():
|
||||
lept.pixWriteImpliedFormat(
|
||||
os.fsencode(filename), self._cdata, jpeg_quality, jpeg_progressive
|
||||
)
|
||||
lept_format = lept.getImpliedFileFormat(os.fsencode(path))
|
||||
with open(path, 'wb') as py_file:
|
||||
data = ffi.new('l_uint8 **pdata')
|
||||
size = ffi.new('size_t *psize')
|
||||
with _LeptonicaErrorTrap():
|
||||
if lept_format == lept.L_JPEG_ENCODE:
|
||||
lept.pixWriteMemJpeg(
|
||||
data, size, self._cdata, jpeg_quality, jpeg_progressive
|
||||
)
|
||||
else:
|
||||
lept.pixWriteMem(data, size, self._cdata, lept_format)
|
||||
buffer = ffi.buffer(data[0], size[0])
|
||||
py_file.write(buffer)
|
||||
|
||||
@classmethod
|
||||
def frompil(self, pillow_image):
|
||||
@@ -502,17 +544,14 @@ class Pix(LeptonicaObject):
|
||||
display=0,
|
||||
pdfdir=ffi.NULL,
|
||||
):
|
||||
if get_leptonica_version() < 'leptonica-1.76':
|
||||
# Leptonica 1.76 changed the API for pixFindPageForeground; we don't
|
||||
# support the old version
|
||||
raise LeptonicaError("Not available in this version of Leptonica")
|
||||
with _LeptonicaErrorTrap():
|
||||
cropbox = Box(
|
||||
lept.pixFindPageForeground(
|
||||
self._cdata,
|
||||
threshold,
|
||||
mindist,
|
||||
erasedist,
|
||||
pagenum,
|
||||
showmorph,
|
||||
display,
|
||||
pdfdir,
|
||||
self._cdata, threshold, mindist, erasedist, showmorph, ffi.NULL
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,6 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from cffi import FFI
|
||||
|
||||
ffibuilder = FFI()
|
||||
@@ -74,6 +76,17 @@ struct Pixa
|
||||
};
|
||||
typedef struct Pixa PIXA;
|
||||
|
||||
/*! Array of compressed pix */
|
||||
struct PixaComp
|
||||
{
|
||||
l_int32 n; /*!< number of PixComp in ptr array */
|
||||
l_int32 nalloc; /*!< number of PixComp ptrs allocated */
|
||||
l_int32 offset; /*!< indexing offset into ptr array */
|
||||
struct PixComp **pixc; /*!< the array of ptrs to PixComp */
|
||||
struct Boxa *boxa; /*!< array of boxes */
|
||||
};
|
||||
typedef struct PixaComp PIXAC;
|
||||
|
||||
struct Box
|
||||
{
|
||||
l_int32 x;
|
||||
@@ -210,9 +223,15 @@ ffibuilder.cdef(
|
||||
"""
|
||||
PIX * pixRead ( const char *filename );
|
||||
PIX * pixReadMem ( const l_uint8 *data, size_t size );
|
||||
PIX * pixReadStream ( FILE *fp, l_int32 hint );
|
||||
PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
|
||||
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
|
||||
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
|
||||
l_int32 getImpliedFileFormat ( const char *filename );
|
||||
l_ok pixWriteStream ( FILE *fp, PIX *pix, l_int32 format );
|
||||
l_ok pixWriteStreamJpeg ( FILE *fp, PIX *pixs, l_int32 quality, l_int32 progressive );
|
||||
l_ok pixWriteMem ( l_uint8 **pdata, size_t *psize, PIX *pix, l_int32 format );
|
||||
l_ok pixWriteMemJpeg ( l_uint8 **pdata, size_t *psize, PIX *pix, l_int32 quality, l_int32 progressive );
|
||||
l_int32
|
||||
pixWriteMemPng(l_uint8 **pdata,
|
||||
size_t *psize,
|
||||
@@ -294,14 +313,12 @@ pixCleanBackgroundToWhite(PIX *pixs,
|
||||
l_int32 whiteval);
|
||||
|
||||
BOX *
|
||||
pixFindPageForeground(PIX *pixs,
|
||||
l_int32 threshold,
|
||||
l_int32 mindist,
|
||||
l_int32 erasedist,
|
||||
l_int32 pagenum,
|
||||
l_int32 showmorph,
|
||||
l_int32 display,
|
||||
const char *pdfdir);
|
||||
pixFindPageForeground ( PIX *pixs,
|
||||
l_int32 threshold,
|
||||
l_int32 mindist,
|
||||
l_int32 erasedist,
|
||||
l_int32 showmorph,
|
||||
PIXAC *pixac );
|
||||
|
||||
PIX *
|
||||
pixClipRectangle(PIX *pixs,
|
||||
@@ -414,7 +431,10 @@ pixExtractBarcodes(PIX *pixs,
|
||||
l_int32 debugflag);
|
||||
|
||||
BOXA *
|
||||
pixLocateBarcodes ( PIX *pixs, l_int32 thresh, PIX **ppixb, PIX **ppixm );
|
||||
pixLocateBarcodes ( PIX *pixs,
|
||||
l_int32 thresh,
|
||||
PIX **ppixb,
|
||||
PIX **ppixm );
|
||||
|
||||
SARRAY *
|
||||
pixReadBarcodes(PIXA *pixa,
|
||||
@@ -491,3 +511,8 @@ ffibuilder.set_source("ocrmypdf.lib._leptonica", None)
|
||||
|
||||
if __name__ == '__main__':
|
||||
ffibuilder.compile(verbose=True)
|
||||
if Path('ocrmypdf/lib/_leptonica.py').exists() and Path('src/ocrmypdf').exists():
|
||||
output = Path('ocrmypdf/lib/_leptonica.py')
|
||||
output.rename('src/ocrmypdf/lib/_leptonica.py')
|
||||
Path('ocrmypdf/lib').rmdir()
|
||||
Path('ocrmypdf').rmdir()
|
||||
|
||||
@@ -31,7 +31,7 @@ from . import leptonica
|
||||
from ._jobcontext import PDFContext
|
||||
from .exceptions import OutputFileAccessError
|
||||
from .exec import jbig2enc, pngquant
|
||||
from .helpers import re_symlink
|
||||
from .helpers import safe_symlink
|
||||
|
||||
DEFAULT_JPEG_QUALITY = 75
|
||||
DEFAULT_PNG_QUALITY = 70
|
||||
@@ -492,7 +492,7 @@ def optimize(input_file, output_file, context, save_settings):
|
||||
log = context.log
|
||||
options = context.options
|
||||
if options.optimize == 0:
|
||||
re_symlink(input_file, output_file)
|
||||
safe_symlink(input_file, output_file)
|
||||
return
|
||||
|
||||
if options.jpeg_quality == 0:
|
||||
@@ -538,7 +538,7 @@ def optimize(input_file, output_file, context, save_settings):
|
||||
pike.remove_unreferenced_resources()
|
||||
pike.save(output_file, **save_settings)
|
||||
else:
|
||||
re_symlink(target_file, output_file)
|
||||
safe_symlink(target_file, output_file)
|
||||
|
||||
|
||||
def main(infile, outfile, level, jobs=1):
|
||||
|
||||
@@ -32,8 +32,6 @@ Ghostscript's handling of pdfmark.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import os
|
||||
from binascii import hexlify
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
|
||||
@@ -84,8 +82,6 @@ def generate_pdfa_ps(target_filename, icc='sRGB'):
|
||||
|
||||
:param target_filename: filename to save
|
||||
:param icc: ICC identifier such as 'sRGB'
|
||||
|
||||
:returns: a string containing the entire pdfmark
|
||||
"""
|
||||
if icc == 'sRGB':
|
||||
icc_profile = SRGB_ICC_PROFILE
|
||||
@@ -103,6 +99,7 @@ def generate_pdfa_ps(target_filename, icc='sRGB'):
|
||||
# We should have encoded everything to pure ASCII by this point, and
|
||||
# to be safe, only allow ASCII in PostScript
|
||||
Path(target_filename).write_text(ps, encoding='ascii')
|
||||
return target_filename
|
||||
|
||||
|
||||
def file_claims_pdfa(filename):
|
||||
|
||||
@@ -96,6 +96,7 @@ def extract_text_xml(infile, pdf, pageno=None, log=gslog):
|
||||
page_count_difference = len(pdf.pages) - len(page_xml)
|
||||
if page_count_difference != 0:
|
||||
log.error("The number of pages in the input file is inconsistent.")
|
||||
log.error(f"Expected {len(pdf.pages)}, txtwrite says {len(page_xml)}")
|
||||
if page_count_difference > 0:
|
||||
page_xml.extend([None] * page_count_difference)
|
||||
return page_xml
|
||||
|
||||
@@ -212,6 +212,7 @@ class TextPositionTracker(PDFLayoutAnalyzer):
|
||||
super().__init__(rsrcmgr, pageno, laparams)
|
||||
self.textstate = None
|
||||
self.result = None
|
||||
self.cur_item = None # not defined in pdfminer code as it should be
|
||||
|
||||
def begin_page(self, page, ctm):
|
||||
super().begin_page(page, ctm)
|
||||
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
Tesseract Open Source OCR Engine v4.1.0 with Leptonica
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
Tarnose
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bokale oa
|
||||
|
||||
|
||||
|
||||
Lehuntze
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Mugerre
|
||||
|
||||
|
||||
|
||||
|
||||
Milafranga Komunikabideak
|
||||
|
||||
BAIONA i zeettnansise —
|
||||
|
||||
1 Trenbideak -- ~~~
|
||||
|
||||
t\ Basusarri — spmsans20141004 se: . a ~
|
||||
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
Tesseract Open Source OCR Engine v4.1.0 with Leptonica
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Covfefe is a perfectly cromulent word.
|
||||
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
Tesseract Open Source OCR Engine v4.1.0 with Leptonica
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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 Kruicdnagel.
|
||||
Alle uien, kerrie en gout 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
|
||||
gaar Zijn is de soep klaar.
|
||||
|
||||
Vendored
+3
@@ -66,3 +66,6 @@
|
||||
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.2 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.6.0-x86_64-i386-64bit", "python": "3.7.3", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/cmyk.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.2 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.6.0-x86_64-i386-64bit", "python": "3.7.3", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.3 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.6.0-x86_64-i386-64bit", "python": "3.7.4", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/graph_ocred.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
{"tesseract_version": "tesseract 4.1.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.1.0 : zlib 1.2.11 : libwebp 1.0.3 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.5", "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": "tesseract 4.1.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.1.0 : zlib 1.2.11 : libwebp 1.0.3 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.5", "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": "tesseract 4.1.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.1.0 : zlib 1.2.11 : libwebp 1.0.3 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.5", "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"]}
|
||||
|
||||
+74
-41
@@ -15,24 +15,19 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ast
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, run
|
||||
from ocrmypdf import api, cli
|
||||
|
||||
import pytest
|
||||
|
||||
from ocrmypdf import api, cli
|
||||
|
||||
pytest_plugins = ['helpers_namespace']
|
||||
|
||||
try:
|
||||
from pytest_cov.embed import cleanup_on_sigterm
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
cleanup_on_sigterm()
|
||||
|
||||
# pylint: disable=E1101
|
||||
# pytest.helpers is dynamic so it confuses pylint
|
||||
@@ -81,6 +76,23 @@ PROJECT_ROOT = os.path.dirname(TESTS_ROOT)
|
||||
OCRMYPDF = [sys.executable, '-m', 'ocrmypdf']
|
||||
|
||||
|
||||
WINDOWS_SHIM_TEMPLATE = """
|
||||
# This is a shim for Windows that has the same effect as a symlink to the target .py
|
||||
# file
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
args = [sys.executable, {spoofer}, *sys.argv[1:]]
|
||||
p = subprocess.run(args, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
sys.stdout.buffer.write(p.stdout)
|
||||
sys.stderr.buffer.write(p.stderr)
|
||||
sys.exit(p.returncode)
|
||||
"""
|
||||
|
||||
assert ast.parse(WINDOWS_SHIM_TEMPLATE.format(spoofer=repr(r"C:\\Temp\\file.py")))
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
def spoof(tmp_path_factory, **kwargs):
|
||||
"""Modify PATH to override subprocess executables
|
||||
@@ -98,45 +110,33 @@ def spoof(tmp_path_factory, **kwargs):
|
||||
|
||||
for replace_program, with_spoof in kwargs.items():
|
||||
spoofer = Path(SPOOF_PATH) / with_spoof
|
||||
spoofer.chmod(0o755)
|
||||
(tmpdir / replace_program).symlink_to(spoofer)
|
||||
|
||||
env['_OCRMYPDF_SAVE_PATH'] = env['PATH']
|
||||
env['PATH'] = str(tmpdir) + ":" + env['PATH']
|
||||
if os.name != 'nt':
|
||||
spoofer.chmod(0o755)
|
||||
(tmpdir / replace_program).symlink_to(spoofer)
|
||||
else:
|
||||
py_file = WINDOWS_SHIM_TEMPLATE.format(
|
||||
spoofer=repr(os.fspath(spoofer.absolute()))
|
||||
)
|
||||
if replace_program == 'gs':
|
||||
programs = ['gswin64c', 'gswin32c']
|
||||
else:
|
||||
programs = [replace_program]
|
||||
for prog in programs:
|
||||
(tmpdir / f'{prog}.py').write_text(py_file, encoding='utf-8')
|
||||
|
||||
env['_OCRMYPDF_TEST_PATH'] = str(tmpdir) + os.pathsep + env['PATH']
|
||||
if os.name == 'nt':
|
||||
if '.py' not in env['PATHEXT'].lower():
|
||||
raise EnvironmentError("PATHEXT is not configured to support .py")
|
||||
return env
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
@contextmanager
|
||||
def os_environ(new_env):
|
||||
old_env = os.environ.copy()
|
||||
if new_env is None:
|
||||
new_env = {}
|
||||
|
||||
for k, v in new_env.items():
|
||||
if k != 'PYTEST_CURRENT_TEST':
|
||||
os.environ[k] = v
|
||||
yield
|
||||
new_keys = set(os.environ.copy()) - set(old_env)
|
||||
for k in new_keys:
|
||||
if k != 'PYTEST_CURRENT_TEST':
|
||||
del os.environ[k]
|
||||
for k in old_env:
|
||||
if k != 'PYTEST_CURRENT_TEST':
|
||||
os.environ[k] = old_env[k]
|
||||
|
||||
for k, v in os.environ.copy().items():
|
||||
if k != 'PYTEST_CURRENT_TEST':
|
||||
assert v == old_env[k]
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_noop(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, tesseract='tesseract_noop.py')
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_cache(tmp_path_factory):
|
||||
if running_in_docker():
|
||||
return os.environ.copy()
|
||||
@@ -183,7 +183,7 @@ def check_ocrmypdf(input_file, output_file, *args, env=None):
|
||||
api.check_options(options)
|
||||
if env:
|
||||
options.tesseract_env = env
|
||||
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = input_file
|
||||
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
|
||||
result = api.run_pipeline(options, api=True)
|
||||
|
||||
assert result == 0
|
||||
@@ -193,18 +193,51 @@ def check_ocrmypdf(input_file, output_file, *args, env=None):
|
||||
return output_file
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
def run_ocrmypdf_api(input_file, output_file, *args, env=None):
|
||||
"""Run ocrmypdf via API and let caller deal with results
|
||||
|
||||
Does not currently have a way to manipulate the PATH except for Tesseract.
|
||||
"""
|
||||
|
||||
options = cli.parser.parse_args(
|
||||
[str(input_file), str(output_file)]
|
||||
+ [str(arg) for arg in args if arg is not None]
|
||||
)
|
||||
api.check_options(options)
|
||||
if env:
|
||||
options.tesseract_env = env.copy()
|
||||
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
|
||||
first_path = env.get('_OCRMYPDF_TEST_PATH', '').split(os.pathsep)[0]
|
||||
if 'spoof' in first_path:
|
||||
assert 'gs' not in first_path, "use run_ocrmypdf() for gs"
|
||||
assert 'tesseract' in first_path
|
||||
if options.tesseract_env:
|
||||
assert all(isinstance(v, (str, bytes)) for v in options.tesseract_env.values())
|
||||
|
||||
return api.run_pipeline(options, api=False)
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
def run_ocrmypdf(input_file, output_file, *args, env=None, universal_newlines=True):
|
||||
"Run ocrmypdf and let caller deal with results"
|
||||
|
||||
if env is None:
|
||||
env = os.environ
|
||||
env = os.environ.copy()
|
||||
|
||||
p_args = (
|
||||
OCRMYPDF
|
||||
+ [str(arg) for arg in args if arg is not None]
|
||||
+ [str(input_file), str(output_file)]
|
||||
)
|
||||
|
||||
# Tell subprocess where to find coverage.py configuration
|
||||
# This has no effect except when coverage is running
|
||||
# Details: https://coverage.readthedocs.io/en/coverage-5.0/subprocess.html
|
||||
coverage_rc = Path(__file__).parent.parent / '.coveragerc'
|
||||
assert coverage_rc.exists()
|
||||
env['COVERAGE_PROCESS_START'] = os.fspath(coverage_rc)
|
||||
|
||||
p = run(
|
||||
p_args, stdout=PIPE, stderr=PIPE, universal_newlines=universal_newlines, env=env
|
||||
)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
@@ -0,0 +1,40 @@
|
||||
# © 2019 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
||||
# copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included
|
||||
# in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"""Find Ghostscript executable"""
|
||||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
if os.name != 'nt':
|
||||
gs = shutil.which('gs')
|
||||
gs_args = [gs] + argv[1:]
|
||||
os.execv(gs_args[0], gs_args)
|
||||
else:
|
||||
gs = shutil.which('gswin64c')
|
||||
if not gs:
|
||||
gs = shutil.which('gswin32c')
|
||||
os.execv(gs, argv[1:])
|
||||
|
||||
return # Not reachable
|
||||
@@ -25,23 +25,17 @@ import os
|
||||
import sys
|
||||
from subprocess import check_call
|
||||
|
||||
from gs import real_ghostscript
|
||||
|
||||
"""Replicate one type of Ghostscript feature elision warning during
|
||||
PDF/A creation."""
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
|
||||
|
||||
elision_warning = """GPL Ghostscript 9.20: Setting Overprint Mode to 1
|
||||
not permitted in PDF/A-2, overprint mode not set"""
|
||||
|
||||
|
||||
def main():
|
||||
os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH']
|
||||
if '--version' in sys.argv:
|
||||
print('9.20')
|
||||
print('SPOOFED: ' + os.path.basename(__file__))
|
||||
|
||||
@@ -23,19 +23,14 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from gs import real_ghostscript
|
||||
|
||||
|
||||
"""Replicate Ghostscript PDF/A conversion failure by suppressing some
|
||||
arguments"""
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
|
||||
|
||||
def main():
|
||||
os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH']
|
||||
if '--version' in sys.argv:
|
||||
print('9.20')
|
||||
print('SPOOFED: ' + os.path.basename(__file__))
|
||||
|
||||
@@ -24,29 +24,24 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from gs import real_ghostscript
|
||||
|
||||
"""Replicate Ghostscript raster failure while allowing rendering"""
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
|
||||
|
||||
def main():
|
||||
os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH']
|
||||
if '--version' in sys.argv:
|
||||
print('9.20')
|
||||
print('SPOOFED: ' + os.path.basename(__file__))
|
||||
sys.exit(0)
|
||||
|
||||
# For any rendering calls (device == pdfwrite) call real ghostscript
|
||||
if '-sDEVICE=pdfwrite' in sys.argv:
|
||||
# For non-image rastering calls, use real ghostscript
|
||||
if '-sDEVICE=pdfwrite' in sys.argv or '-sDEVICE=txtwrite' in sys.argv:
|
||||
real_ghostscript(sys.argv)
|
||||
return
|
||||
|
||||
# Fail
|
||||
print("ERROR: Ghost story archive not found")
|
||||
print("ERROR: Ghost story archive not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@@ -25,15 +25,10 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
from gs import real_ghostscript
|
||||
|
||||
|
||||
def main():
|
||||
os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH']
|
||||
if '--version' in sys.argv:
|
||||
print('9.20')
|
||||
print('SPOOFED: ' + os.path.basename(__file__))
|
||||
@@ -45,7 +40,7 @@ def main():
|
||||
return
|
||||
|
||||
# Fail
|
||||
print("ERROR: Casper is not a friendly ghost")
|
||||
print("ERROR: Casper is not a friendly ghost", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
"""Tesseract bad utf8 spoof
|
||||
|
||||
In 'hocr' mode or 'pdf' mode, return error code 1 and some non-Unicode
|
||||
|
||||
@@ -59,9 +59,6 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
if '_OCRMYPDF_SAVE_PATH' in os.environ:
|
||||
os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH']
|
||||
|
||||
__version__ = subprocess.check_output(
|
||||
['tesseract', '--version'], stderr=subprocess.STDOUT
|
||||
).decode()
|
||||
|
||||
@@ -32,9 +32,10 @@ In orientation check mode, report the orientation is upright.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import img2pdf
|
||||
import PyPDF2 as pypdf
|
||||
import pikepdf
|
||||
from PIL import Image
|
||||
|
||||
VERSION_STRING = '''tesseract 4.0.0
|
||||
@@ -99,12 +100,10 @@ def main():
|
||||
pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1]
|
||||
ptsize = pagesize[0] * 72, pagesize[1] * 72
|
||||
|
||||
pdf_out = pypdf.PdfFileWriter()
|
||||
pdf_out.addBlankPage(ptsize[0], ptsize[1])
|
||||
with open(output + '.pdf', 'wb') as f:
|
||||
pdf_out.write(f)
|
||||
with open(output + '.txt', 'w') as f:
|
||||
f.write('')
|
||||
pdf_out = pikepdf.new()
|
||||
pdf_out.add_blank_page(page_size=ptsize)
|
||||
pdf_out.save(Path(output).with_suffix('.pdf'), static_id=True)
|
||||
Path(output).with_suffix('.txt').write_text('')
|
||||
else:
|
||||
inputf = sys.argv[-4]
|
||||
output = sys.argv[-3]
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# © 2019 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This file is part of OCRmyPDF.
|
||||
#
|
||||
# OCRmyPDF is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# OCRmyPDF is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acroform(resources):
|
||||
return resources / 'acroform.pdf'
|
||||
|
||||
|
||||
def test_acroform_and_redo(acroform, caplog, no_outpdf):
|
||||
with pytest.raises(ocrmypdf.exceptions.InputFileError):
|
||||
check_ocrmypdf(acroform, no_outpdf, '--redo-ocr')
|
||||
assert '--redo-ocr is not currently possible' in caplog.text
|
||||
|
||||
|
||||
def test_acroform_message(acroform, caplog, spoof_tesseract_noop, outpdf):
|
||||
caplog.set_level(logging.INFO)
|
||||
check_ocrmypdf(acroform, outpdf, env=spoof_tesseract_noop)
|
||||
assert 'fillable form' in caplog.text
|
||||
assert '--force-ocr' in caplog.text
|
||||
@@ -0,0 +1,61 @@
|
||||
# © 2019 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This file is part of OCRmyPDF.
|
||||
#
|
||||
# OCRmyPDF is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# OCRmyPDF is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
from io import StringIO
|
||||
|
||||
import pytest
|
||||
from tqdm import tqdm
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
|
||||
def test_raw_console():
|
||||
bio = StringIO()
|
||||
tqconsole = ocrmypdf.api.TqdmConsole(file=bio)
|
||||
tqconsole.write("Test")
|
||||
tqconsole.flush()
|
||||
assert "Test" in bio.getvalue()
|
||||
|
||||
|
||||
def test_tqdm_console():
|
||||
log = logging.getLogger()
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
formatter = logging.Formatter('%(message)s')
|
||||
|
||||
bio = StringIO()
|
||||
console = logging.StreamHandler(ocrmypdf.api.TqdmConsole(file=bio))
|
||||
console.setFormatter(formatter)
|
||||
|
||||
log.addHandler(console)
|
||||
|
||||
def before_pbar(message):
|
||||
# Ensure that log messages appear before the progress bar, even when
|
||||
# printed after the progress bar updates.
|
||||
v = bio.getvalue()
|
||||
pbar_start_marker = '|#'
|
||||
return v.index(message) < v.index(pbar_start_marker)
|
||||
|
||||
with tqdm(total=2, file=bio, disable=False) as pbar:
|
||||
pbar.update()
|
||||
msg = "1/2 above progress bar"
|
||||
log.info(msg)
|
||||
assert before_pbar(msg)
|
||||
|
||||
log.info("done")
|
||||
assert not before_pbar("done")
|
||||
@@ -15,7 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from subprocess import run, PIPE
|
||||
from subprocess import PIPE, run
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
+77
-14
@@ -22,22 +22,56 @@ import pikepdf
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
from ocrmypdf.exec.ghostscript import rasterize_pdf
|
||||
|
||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||
run_ocrmypdf = pytest.helpers.run_ocrmypdf
|
||||
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def linn(resources):
|
||||
path = resources / 'linn.pdf'
|
||||
def spoof_no_tess_gs_render_fail(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_render_failure.py'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_gs_raster_fail(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_raster_failure.py'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_no_pdfa(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_pdfa_warning(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_feature_elision.py'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def francais(resources):
|
||||
path = resources / 'francais.pdf'
|
||||
return path, pikepdf.open(path)
|
||||
|
||||
|
||||
def test_rasterize_size(linn, outdir, caplog):
|
||||
path, pdf = linn
|
||||
def test_rasterize_size(francais, outdir, caplog):
|
||||
path, pdf = francais
|
||||
page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3])
|
||||
assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0
|
||||
page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72))
|
||||
target_size = Decimal('200.0'), Decimal('150.0')
|
||||
target_dpi = 42.0, 4242.0
|
||||
target_size = Decimal('50.0'), Decimal('30.0')
|
||||
forced_dpi = 42.0, 4242.0
|
||||
|
||||
log = logging.getLogger()
|
||||
rasterize_pdf(
|
||||
@@ -47,21 +81,21 @@ def test_rasterize_size(linn, outdir, caplog):
|
||||
target_size[1] / page_size[1],
|
||||
raster_device='pngmono',
|
||||
log=log,
|
||||
page_dpi=target_dpi,
|
||||
page_dpi=forced_dpi,
|
||||
)
|
||||
|
||||
with Image.open(outdir / 'out.png') as im:
|
||||
assert im.size == target_size
|
||||
assert im.info['dpi'] == target_dpi
|
||||
assert im.info['dpi'] == forced_dpi
|
||||
|
||||
|
||||
def test_rasterize_rotated(linn, outdir, caplog):
|
||||
path, pdf = linn
|
||||
def test_rasterize_rotated(francais, outdir, caplog):
|
||||
path, pdf = francais
|
||||
page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3])
|
||||
assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0
|
||||
page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72))
|
||||
target_size = Decimal('200.0'), Decimal('150.0')
|
||||
target_dpi = 42.0, 4242.0
|
||||
target_size = Decimal('50.0'), Decimal('30.0')
|
||||
forced_dpi = 42.0, 4242.0
|
||||
|
||||
log = logging.getLogger()
|
||||
caplog.set_level(logging.DEBUG)
|
||||
@@ -72,10 +106,39 @@ def test_rasterize_rotated(linn, outdir, caplog):
|
||||
target_size[1] / page_size[1],
|
||||
raster_device='pngmono',
|
||||
log=log,
|
||||
page_dpi=target_dpi,
|
||||
page_dpi=forced_dpi,
|
||||
rotation=90,
|
||||
)
|
||||
|
||||
with Image.open(outdir / 'out.png') as im:
|
||||
assert im.size == (target_size[1], target_size[0])
|
||||
assert im.info['dpi'] == (target_dpi[1], target_dpi[0])
|
||||
assert im.info['dpi'] == (forced_dpi[1], forced_dpi[0])
|
||||
|
||||
|
||||
def test_gs_render_failure(spoof_no_tess_gs_render_fail, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'blank.pdf', outpdf, env=spoof_no_tess_gs_render_fail
|
||||
)
|
||||
assert 'Casper is not a friendly ghost' in err
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
|
||||
|
||||
def test_gs_raster_failure(spoof_no_tess_gs_raster_fail, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'francais.pdf', outpdf, env=spoof_no_tess_gs_raster_fail
|
||||
)
|
||||
assert 'Ghost story archive not found' in err
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
|
||||
|
||||
def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'francais.pdf', outpdf, env=spoof_no_tess_no_pdfa
|
||||
)
|
||||
assert (
|
||||
p.returncode == ExitCode.pdfa_conversion_failed
|
||||
), "Unexpected return when PDF/A fails"
|
||||
|
||||
|
||||
def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'francais.pdf', outpdf, env=spoof_no_tess_pdfa_warning)
|
||||
|
||||
+3
-6
@@ -16,13 +16,12 @@
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pikepdf
|
||||
import pytest
|
||||
|
||||
import ocrmypdf
|
||||
import pikepdf
|
||||
|
||||
os_environ = pytest.helpers.os_environ
|
||||
|
||||
|
||||
def test_no_glyphless_graft(resources, outdir):
|
||||
@@ -33,9 +32,7 @@ def test_no_glyphless_graft(resources, outdir):
|
||||
pdf.pages.extend(pdf_cmyk.pages)
|
||||
pdf.save(outdir / 'test.pdf')
|
||||
|
||||
env = os.environ.copy()
|
||||
env['_OCRMYPDF_MAX_REPLACE_PAGES'] = '2'
|
||||
with os_environ(env):
|
||||
with patch('ocrmypdf._graft.MAX_REPLACE_PAGES', 2):
|
||||
ocrmypdf.ocr(
|
||||
outdir / 'test.pdf', outdir / 'out.pdf', deskew=True, tesseract_timeout=0
|
||||
)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# © 2019 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This file is part of OCRmyPDF.
|
||||
#
|
||||
# OCRmyPDF is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# OCRmyPDF is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import multiprocessing
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import ocrmypdf.helpers as helpers
|
||||
|
||||
|
||||
class TestSafeSymlink:
|
||||
def test_safe_symlink_link_self(self, tmp_path, caplog):
|
||||
helpers.safe_symlink(tmp_path / 'self', tmp_path / 'self')
|
||||
assert caplog.record_tuples[0][1] == logging.WARNING
|
||||
|
||||
def test_safe_symlink_overwrite(self, tmp_path):
|
||||
(tmp_path / 'regular_file').touch()
|
||||
with pytest.raises(FileExistsError):
|
||||
helpers.safe_symlink(tmp_path / 'input', tmp_path / 'regular_file')
|
||||
|
||||
def test_safe_symlink_relink(self, tmp_path):
|
||||
(tmp_path / 'regular_file_a').touch()
|
||||
(tmp_path / 'regular_file_b').write_bytes(b'ABC')
|
||||
(tmp_path / 'link').symlink_to(tmp_path / 'regular_file_a')
|
||||
helpers.safe_symlink(tmp_path / 'regular_file_b', tmp_path / 'link')
|
||||
assert (tmp_path / 'link').samefile(tmp_path / 'regular_file_b') or (
|
||||
tmp_path / 'link'
|
||||
).read_bytes() == b'ABC'
|
||||
|
||||
|
||||
def test_no_cpu_count(monkeypatch):
|
||||
def cpu_count_raises():
|
||||
raise NotImplementedError()
|
||||
|
||||
monkeypatch.setattr(multiprocessing, 'cpu_count', cpu_count_raises)
|
||||
with pytest.warns(expected_warning=UserWarning):
|
||||
assert helpers.available_cpu_count() == 1
|
||||
|
||||
|
||||
def test_deprecated():
|
||||
@helpers.deprecated
|
||||
def old_function():
|
||||
return 42
|
||||
|
||||
with pytest.deprecated_call():
|
||||
assert old_function() == 42
|
||||
|
||||
|
||||
class TestFileIsWritable:
|
||||
@pytest.fixture
|
||||
def non_existent(self, tmp_path):
|
||||
return tmp_path / 'nofile'
|
||||
|
||||
@pytest.fixture
|
||||
def basic_file(self, tmp_path):
|
||||
basic = tmp_path / 'basic'
|
||||
basic.touch()
|
||||
return basic
|
||||
|
||||
def test_plain(self, non_existent):
|
||||
assert helpers.is_file_writable(non_existent)
|
||||
|
||||
def test_symlink_loop(self, tmp_path):
|
||||
loop = tmp_path / 'loop'
|
||||
loop.symlink_to(loop)
|
||||
assert not helpers.is_file_writable(loop)
|
||||
|
||||
def test_chmod(self, basic_file):
|
||||
assert helpers.is_file_writable(basic_file)
|
||||
basic_file.chmod(0o400)
|
||||
assert not helpers.is_file_writable(basic_file)
|
||||
basic_file.chmod(0o000)
|
||||
assert not helpers.is_file_writable(basic_file)
|
||||
|
||||
def test_permission_error(self, basic_file):
|
||||
pathmock = MagicMock(spec_set=basic_file)
|
||||
pathmock.is_symlink.return_value = False
|
||||
pathmock.exists.return_value = True
|
||||
pathmock.is_file.side_effect = PermissionError
|
||||
assert not helpers.is_file_writable(pathmock)
|
||||
@@ -0,0 +1,92 @@
|
||||
# © 2019 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This file is part of OCRmyPDF.
|
||||
#
|
||||
# OCRmyPDF is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# OCRmyPDF is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import img2pdf
|
||||
import pikepdf
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def baiona(resources):
|
||||
return Image.open(resources / 'baiona_gray.png')
|
||||
|
||||
|
||||
def test_image_to_pdf(spoof_tesseract_noop, resources, outpdf):
|
||||
check_ocrmypdf(
|
||||
resources / 'crom.png', outpdf, '--image-dpi', '200', env=spoof_tesseract_noop
|
||||
)
|
||||
|
||||
|
||||
def test_no_dpi_info(caplog, baiona, outdir, no_outpdf):
|
||||
im = baiona
|
||||
assert 'dpi' not in im.info
|
||||
input_image = outdir / 'baiona_no_dpi.png'
|
||||
im.save(input_image)
|
||||
|
||||
rc = run_ocrmypdf_api(input_image, no_outpdf)
|
||||
assert rc == ocrmypdf.ExitCode.input_file
|
||||
assert "--image-dpi" in caplog.text
|
||||
|
||||
|
||||
def test_dpi_not_credible(caplog, baiona, outdir, no_outpdf):
|
||||
im = baiona
|
||||
assert 'dpi' not in im.info
|
||||
input_image = outdir / 'baiona_no_dpi.png'
|
||||
im.save(input_image, dpi=(30, 30))
|
||||
|
||||
rc = run_ocrmypdf_api(input_image, no_outpdf)
|
||||
assert rc == ocrmypdf.ExitCode.input_file
|
||||
assert "not credible" in caplog.text
|
||||
|
||||
|
||||
def test_cmyk_no_icc(caplog, resources, no_outpdf):
|
||||
rc = run_ocrmypdf_api(resources / 'baiona_cmyk.jpg', no_outpdf)
|
||||
assert rc == ocrmypdf.ExitCode.input_file
|
||||
assert "no ICC profile" in caplog.text
|
||||
|
||||
|
||||
def test_img2pdf_fails(resources, no_outpdf):
|
||||
with patch(
|
||||
'ocrmypdf._pipeline.img2pdf.convert', side_effect=img2pdf.ImageOpenError()
|
||||
):
|
||||
rc = run_ocrmypdf_api(
|
||||
resources / 'baiona_gray.png', no_outpdf, '--image-dpi', '200'
|
||||
)
|
||||
assert rc == ocrmypdf.ExitCode.input_file
|
||||
|
||||
|
||||
def test_jpeg_in_jpeg_out(resources, outpdf, spoof_tesseract_noop):
|
||||
check_ocrmypdf(
|
||||
resources / 'congress.jpg',
|
||||
outpdf,
|
||||
'--image-dpi',
|
||||
'100',
|
||||
'--output-type',
|
||||
'pdf', # specifically check pdf because Ghostscript may convert to JPEG
|
||||
'--remove-background',
|
||||
env=spoof_tesseract_noop,
|
||||
)
|
||||
with pikepdf.open(outpdf) as pdf:
|
||||
assert next(pdf.pages[0].images.values()).Filter == pikepdf.Name.DCTDecode
|
||||
+8
-14
@@ -16,8 +16,8 @@
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from os import fspath
|
||||
import os
|
||||
from os import fspath
|
||||
from pickle import dumps, loads
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -63,6 +63,10 @@ def test_pix_otsu(crom_pix):
|
||||
assert im1bpp.mode == '1'
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
lept.get_leptonica_version() < 'leptonica-1.76',
|
||||
reason="needs new leptonica for API change",
|
||||
)
|
||||
def test_crop(resources):
|
||||
pix = lept.Pix.open(resources / 'linn.png')
|
||||
foreground = pix.crop_to_foreground()
|
||||
@@ -90,16 +94,6 @@ def test_leptonica_compile(tmp_path):
|
||||
ffibuilder.compile(tmpdir=fspath(tmp_path), target=fspath(tmp_path / 'lepttest.*'))
|
||||
|
||||
|
||||
def test_with_stderr(capsys):
|
||||
# pytest redirects stderr too; we must disable this for the test to be valid
|
||||
with capsys.disabled():
|
||||
with pytest.raises(FileNotFoundError):
|
||||
lept.Pix.open("does_not_exist1")
|
||||
|
||||
|
||||
def test_without_stderr(capsys):
|
||||
# pytest redirects stderr too; we must disable this for the test to be valid
|
||||
with capsys.disabled():
|
||||
with patch('sys.stderr', new=None):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
lept.Pix.open("does_not_exist2")
|
||||
def test_file_not_found():
|
||||
with pytest.raises(FileNotFoundError):
|
||||
lept.Pix.open("does_not_exist1")
|
||||
|
||||
+88
-291
@@ -15,22 +15,21 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from math import isclose
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, run
|
||||
from unittest.mock import patch
|
||||
|
||||
import pikepdf
|
||||
import PIL
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
import ocrmypdf
|
||||
import pikepdf
|
||||
from ocrmypdf.exceptions import ExitCode, MissingDependencyError
|
||||
from ocrmypdf.exec import ghostscript, qpdf, tesseract
|
||||
from ocrmypdf.leptonica import Pix
|
||||
from ocrmypdf.pdfa import file_claims_pdfa
|
||||
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
|
||||
|
||||
@@ -39,144 +38,27 @@ from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
|
||||
|
||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||
run_ocrmypdf = pytest.helpers.run_ocrmypdf
|
||||
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
RENDERERS = ['hocr', 'sandwich']
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_crash(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, tesseract='tesseract_crash.py')
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_big_image_error(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, tesseract='tesseract_big_image_error.py')
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def spoof_no_tess_no_pdfa(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def spoof_no_tess_pdfa_warning(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_feature_elision.py'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def spoof_no_tess_gs_render_fail(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_render_failure.py'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def spoof_no_tess_gs_raster_fail(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_raster_failure.py'
|
||||
)
|
||||
|
||||
|
||||
def test_quick(spoof_tesseract_cache, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'ccitt.pdf', outpdf, env=spoof_tesseract_cache)
|
||||
|
||||
|
||||
def test_deskew(spoof_tesseract_noop, resources, outdir):
|
||||
# Run with deskew
|
||||
deskewed_pdf = check_ocrmypdf(
|
||||
resources / 'skew.pdf', outdir / 'skew.pdf', '-d', env=spoof_tesseract_noop
|
||||
)
|
||||
|
||||
# Now render as an image again and use Leptonica to find the skew angle
|
||||
# to confirm that it was deskewed
|
||||
log = logging.getLogger()
|
||||
|
||||
deskewed_png = outdir / 'deskewed.png'
|
||||
|
||||
ghostscript.rasterize_pdf(
|
||||
deskewed_pdf,
|
||||
deskewed_png,
|
||||
xres=150,
|
||||
yres=150,
|
||||
raster_device='pngmono',
|
||||
log=log,
|
||||
pageno=1,
|
||||
)
|
||||
|
||||
pix = Pix.open(deskewed_png)
|
||||
skew_angle, _skew_confidence = pix.find_skew()
|
||||
|
||||
print(skew_angle)
|
||||
assert -0.5 < skew_angle < 0.5, "Deskewing failed"
|
||||
|
||||
|
||||
def test_remove_background(spoof_tesseract_noop, resources, outdir):
|
||||
# Ensure the input image does not contain pure white/black
|
||||
with Image.open(resources / 'congress.jpg') as im:
|
||||
assert im.getextrema() != ((0, 255), (0, 255), (0, 255))
|
||||
|
||||
output_pdf = check_ocrmypdf(
|
||||
resources / 'congress.jpg',
|
||||
outdir / 'test_remove_bg.pdf',
|
||||
'--remove-background',
|
||||
'--image-dpi',
|
||||
'150',
|
||||
env=spoof_tesseract_noop,
|
||||
)
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
output_png = outdir / 'remove_bg.png'
|
||||
|
||||
ghostscript.rasterize_pdf(
|
||||
output_pdf,
|
||||
output_png,
|
||||
xres=100,
|
||||
yres=100,
|
||||
raster_device='png16m',
|
||||
log=log,
|
||||
pageno=1,
|
||||
)
|
||||
|
||||
# The output image should contain pure white and black
|
||||
with Image.open(output_png) as im:
|
||||
assert im.getextrema() == ((0, 255), (0, 255), (0, 255))
|
||||
|
||||
|
||||
# This will run 5 * 2 * 2 = 20 test cases
|
||||
@pytest.mark.parametrize(
|
||||
"pdf", ['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf']
|
||||
)
|
||||
@pytest.mark.parametrize("renderer", ['sandwich', 'hocr'])
|
||||
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
|
||||
def test_exotic_image(
|
||||
spoof_tesseract_cache, pdf, renderer, output_type, resources, outdir
|
||||
):
|
||||
outfile = outdir / f'test_{pdf}_{renderer}.pdf'
|
||||
check_ocrmypdf(
|
||||
resources / pdf,
|
||||
outfile,
|
||||
'-dc' if pytest.helpers.have_unpaper() else '-d',
|
||||
'-v',
|
||||
'1',
|
||||
'--output-type',
|
||||
output_type,
|
||||
'--sidecar',
|
||||
'--skip-text',
|
||||
'--pdf-renderer',
|
||||
renderer,
|
||||
env=spoof_tesseract_cache,
|
||||
)
|
||||
|
||||
assert outfile.with_suffix('.pdf.txt').exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_oversample(spoof_tesseract_cache, renderer, resources, outpdf):
|
||||
oversampled_pdf = check_ocrmypdf(
|
||||
@@ -197,8 +79,8 @@ def test_oversample(spoof_tesseract_cache, renderer, resources, outpdf):
|
||||
|
||||
|
||||
def test_repeat_ocr(resources, no_outpdf):
|
||||
p, _, _ = run_ocrmypdf(resources / 'graph_ocred.pdf', no_outpdf)
|
||||
assert p.returncode != 0
|
||||
result = run_ocrmypdf_api(resources / 'graph_ocred.pdf', no_outpdf)
|
||||
assert result == ExitCode.already_done_ocr
|
||||
|
||||
|
||||
def test_force_ocr(spoof_tesseract_cache, resources, outpdf):
|
||||
@@ -300,34 +182,34 @@ def test_maximum_options(
|
||||
)
|
||||
|
||||
|
||||
def test_tesseract_missing_tessdata(resources, no_outpdf):
|
||||
def test_tesseract_missing_tessdata(resources, no_outpdf, tmpdir):
|
||||
env = os.environ.copy()
|
||||
env['TESSDATA_PREFIX'] = '/tmp'
|
||||
env['TESSDATA_PREFIX'] = os.fspath(tmpdir)
|
||||
|
||||
p, _, err = run_ocrmypdf(
|
||||
resources / 'graph_ocred.pdf', no_outpdf, '-v', '1', '--skip-text', env=env
|
||||
returncode = run_ocrmypdf_api(
|
||||
resources / 'graph.pdf', no_outpdf, '-v', '1', '--skip-text', env=env
|
||||
)
|
||||
assert p.returncode == ExitCode.missing_dependency, err
|
||||
assert returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
def test_invalid_input_pdf(resources, no_outpdf):
|
||||
p, out, err = run_ocrmypdf(resources / 'invalid.pdf', no_outpdf)
|
||||
assert p.returncode == ExitCode.input_file, err
|
||||
result = run_ocrmypdf_api(resources / 'invalid.pdf', no_outpdf)
|
||||
assert result == ExitCode.input_file
|
||||
|
||||
|
||||
def test_blank_input_pdf(resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(resources / 'blank.pdf', outpdf)
|
||||
assert p.returncode == ExitCode.ok
|
||||
result = run_ocrmypdf_api(resources / 'blank.pdf', outpdf)
|
||||
assert result == ExitCode.ok
|
||||
|
||||
|
||||
def test_force_ocr_on_pdf_with_no_images(spoof_tesseract_crash, resources, no_outpdf):
|
||||
# As a correctness test, make sure that --force-ocr on a PDF with no
|
||||
# content still triggers tesseract. If tesseract crashes, then it was
|
||||
# called.
|
||||
p, _, err = run_ocrmypdf(
|
||||
p, _, _ = run_ocrmypdf(
|
||||
resources / 'blank.pdf', no_outpdf, '--force-ocr', env=spoof_tesseract_crash
|
||||
)
|
||||
assert p.returncode == ExitCode.child_process_error, err
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
assert not os.path.exists(no_outpdf)
|
||||
|
||||
|
||||
@@ -340,27 +222,29 @@ def test_german(spoof_tesseract_cache, resources, outdir):
|
||||
# properly. It is fine that we are testing -l deu on a French file because
|
||||
# we are exercising the functionality not going for accuracy.
|
||||
sidecar = outdir / 'francais.txt'
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'francais.pdf',
|
||||
outdir / 'francais.pdf',
|
||||
'-l',
|
||||
'deu', # more commonly installed
|
||||
'--sidecar',
|
||||
sidecar,
|
||||
env=spoof_tesseract_cache,
|
||||
)
|
||||
if 'deu' not in tesseract.languages():
|
||||
pytest.xfail(reason="tesseract-deu language pack not installed")
|
||||
assert p.returncode == ExitCode.ok, "Requires tesseract deu language pack"
|
||||
try:
|
||||
check_ocrmypdf(
|
||||
resources / 'francais.pdf',
|
||||
outdir / 'francais.pdf',
|
||||
'-l',
|
||||
'deu', # more commonly installed
|
||||
'--sidecar',
|
||||
sidecar,
|
||||
env=spoof_tesseract_cache,
|
||||
)
|
||||
except MissingDependencyError:
|
||||
if 'deu' not in tesseract.languages():
|
||||
pytest.xfail(reason="tesseract-deu language pack not installed")
|
||||
raise
|
||||
|
||||
|
||||
def test_klingon(resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(resources / 'francais.pdf', outpdf, '-l', 'klz')
|
||||
p, _, _ = run_ocrmypdf(resources / 'francais.pdf', outpdf, '-l', 'klz')
|
||||
assert p.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
def test_missing_docinfo(spoof_tesseract_noop, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
result = run_ocrmypdf_api(
|
||||
resources / 'missing_docinfo.pdf',
|
||||
outpdf,
|
||||
'-l',
|
||||
@@ -368,7 +252,7 @@ def test_missing_docinfo(spoof_tesseract_noop, resources, outpdf):
|
||||
'--skip-text',
|
||||
env=spoof_tesseract_noop,
|
||||
)
|
||||
assert p.returncode == ExitCode.ok, err
|
||||
assert result == ExitCode.ok
|
||||
|
||||
|
||||
def test_uppercase_extension(spoof_tesseract_noop, resources, outdir):
|
||||
@@ -379,24 +263,35 @@ def test_uppercase_extension(spoof_tesseract_noop, resources, outdir):
|
||||
)
|
||||
|
||||
|
||||
def test_input_file_not_found(no_outpdf):
|
||||
def test_input_file_not_found(caplog, no_outpdf):
|
||||
input_file = "does not exist.pdf"
|
||||
p, out, err = run_ocrmypdf(input_file, no_outpdf)
|
||||
assert p.returncode == ExitCode.input_file
|
||||
assert input_file in out or input_file in err
|
||||
result = run_ocrmypdf_api(input_file, no_outpdf)
|
||||
assert result == ExitCode.input_file
|
||||
assert input_file in caplog.text
|
||||
|
||||
|
||||
def test_input_file_not_a_pdf(no_outpdf):
|
||||
@pytest.mark.skipif(os.name == 'nt', reason="chmod")
|
||||
def test_input_file_not_readable(caplog, resources, outdir, no_outpdf):
|
||||
input_file = outdir / 'trivial.pdf'
|
||||
shutil.copy(resources / 'trivial.pdf', input_file)
|
||||
input_file.chmod(0o000)
|
||||
result = run_ocrmypdf_api(input_file, no_outpdf)
|
||||
assert result == ExitCode.input_file
|
||||
assert str(input_file) in caplog.text
|
||||
|
||||
|
||||
def test_input_file_not_a_pdf(caplog, no_outpdf):
|
||||
input_file = __file__ # Try to OCR this file
|
||||
p, out, err = run_ocrmypdf(input_file, no_outpdf)
|
||||
assert p.returncode == ExitCode.input_file
|
||||
assert input_file in out or input_file in err
|
||||
result = run_ocrmypdf_api(input_file, no_outpdf)
|
||||
assert result == ExitCode.input_file
|
||||
if os.name != 'nt': # name will be mangled with \\'s on nt
|
||||
assert input_file in caplog.text
|
||||
|
||||
|
||||
def test_encrypted(resources, no_outpdf):
|
||||
p, out, err = run_ocrmypdf(resources / 'skew-encrypted.pdf', no_outpdf)
|
||||
assert p.returncode == ExitCode.encrypted_pdf
|
||||
assert out.find('encrypted')
|
||||
def test_encrypted(resources, caplog, no_outpdf):
|
||||
result = run_ocrmypdf_api(resources / 'skew-encrypted.pdf', no_outpdf)
|
||||
assert result == ExitCode.encrypted_pdf
|
||||
assert 'encryption must be removed' in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
@@ -415,8 +310,8 @@ def test_pagesegmode(renderer, spoof_tesseract_cache, resources, outpdf):
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_tesseract_crash(renderer, spoof_tesseract_crash, resources, no_outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
def test_tesseract_crash(renderer, spoof_tesseract_crash, resources, no_outpdf, caplog):
|
||||
p, _, err = run_ocrmypdf(
|
||||
resources / 'ccitt.pdf',
|
||||
no_outpdf,
|
||||
'-v',
|
||||
@@ -427,7 +322,7 @@ def test_tesseract_crash(renderer, spoof_tesseract_crash, resources, no_outpdf):
|
||||
)
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
assert not os.path.exists(no_outpdf)
|
||||
assert "ERROR" in err
|
||||
assert "SubprocessOutputError" in err
|
||||
|
||||
|
||||
def test_tesseract_crash_autorotate(spoof_tesseract_crash, resources, no_outpdf):
|
||||
@@ -465,70 +360,6 @@ def test_algo4(resources, spoof_tesseract_noop, outpdf):
|
||||
assert p.returncode == ExitCode.encrypted_pdf
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_non_square_resolution(renderer, spoof_tesseract_cache, resources, outpdf):
|
||||
# Confirm input image is non-square resolution
|
||||
in_pageinfo = PdfInfo(resources / 'aspect.pdf')
|
||||
assert in_pageinfo[0].xres != in_pageinfo[0].yres
|
||||
|
||||
check_ocrmypdf(
|
||||
resources / 'aspect.pdf',
|
||||
outpdf,
|
||||
'--pdf-renderer',
|
||||
renderer,
|
||||
env=spoof_tesseract_cache,
|
||||
)
|
||||
|
||||
out_pageinfo = PdfInfo(outpdf)
|
||||
|
||||
# Confirm resolution was kept the same
|
||||
assert in_pageinfo[0].xres == out_pageinfo[0].xres
|
||||
assert in_pageinfo[0].yres == out_pageinfo[0].yres
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_convert_to_square_resolution(
|
||||
renderer, spoof_tesseract_cache, resources, outpdf
|
||||
):
|
||||
# Confirm input image is non-square resolution
|
||||
in_pageinfo = PdfInfo(resources / 'aspect.pdf')
|
||||
assert in_pageinfo[0].xres != in_pageinfo[0].yres
|
||||
|
||||
# --force-ocr requires means forced conversion to square resolution
|
||||
check_ocrmypdf(
|
||||
resources / 'aspect.pdf',
|
||||
outpdf,
|
||||
'--force-ocr',
|
||||
'--pdf-renderer',
|
||||
renderer,
|
||||
env=spoof_tesseract_cache,
|
||||
)
|
||||
|
||||
out_pageinfo = PdfInfo(outpdf)
|
||||
|
||||
in_p0, out_p0 = in_pageinfo[0], out_pageinfo[0]
|
||||
|
||||
# Resolution show now be equal
|
||||
assert out_p0.xres == out_p0.yres
|
||||
|
||||
# Page size should match input page size
|
||||
assert isclose(in_p0.width_inches, out_p0.width_inches)
|
||||
assert isclose(in_p0.height_inches, out_p0.height_inches)
|
||||
|
||||
# Because we rasterized the page to produce a new image, it should occupy
|
||||
# the entire page
|
||||
out_im_w = out_p0.images[0].width / out_p0.images[0].xres
|
||||
out_im_h = out_p0.images[0].height / out_p0.images[0].yres
|
||||
assert isclose(out_p0.width_inches, out_im_w)
|
||||
assert isclose(out_p0.height_inches, out_im_h)
|
||||
|
||||
|
||||
def test_image_to_pdf(spoof_tesseract_noop, resources, outpdf):
|
||||
check_ocrmypdf(
|
||||
resources / 'crom.png', outpdf, '--image-dpi', '200', env=spoof_tesseract_noop
|
||||
)
|
||||
|
||||
|
||||
def test_jbig2_passthrough(spoof_tesseract_cache, resources, outpdf):
|
||||
out = check_ocrmypdf(
|
||||
resources / 'jbig2.pdf',
|
||||
@@ -556,19 +387,6 @@ def test_linearized_pdf_and_indirect_object(spoof_tesseract_noop, resources, out
|
||||
check_ocrmypdf(resources / 'epson.pdf', outpdf, env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_no_pdfa
|
||||
)
|
||||
assert (
|
||||
p.returncode == ExitCode.pdfa_conversion_failed
|
||||
), "Unexpected return when PDF/A fails"
|
||||
|
||||
|
||||
def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_pdfa_warning)
|
||||
|
||||
|
||||
def test_very_high_dpi(spoof_tesseract_cache, resources, outpdf):
|
||||
"Checks for a Decimal quantize error with high DPI, etc"
|
||||
check_ocrmypdf(resources / '2400dpi.pdf', outpdf, env=spoof_tesseract_cache)
|
||||
@@ -586,7 +404,7 @@ def test_overlay(spoof_tesseract_noop, resources, outpdf):
|
||||
|
||||
|
||||
def test_destination_not_writable(spoof_tesseract_noop, resources, outdir):
|
||||
if os.getuid() == 0 or os.geteuid() == 0:
|
||||
if os.name != 'nt' and (os.getuid() == 0 or os.geteuid() == 0):
|
||||
pytest.xfail(reason="root can write to anything")
|
||||
protected_file = outdir / 'protected.pdf'
|
||||
protected_file.touch()
|
||||
@@ -609,25 +427,13 @@ language_model_penalty_non_freq_dict_word 0
|
||||
)
|
||||
|
||||
check_ocrmypdf(
|
||||
resources / 'ccitt.pdf', outdir / 'out.pdf', '--tesseract-config', cfg_file
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.slow # This test sometimes times out in CI
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_tesseract_config_notfound(renderer, resources, outdir):
|
||||
cfg_file = outdir / 'nofile.cfg'
|
||||
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'ccitt.pdf',
|
||||
resources / '3small.pdf',
|
||||
outdir / 'out.pdf',
|
||||
'--pdf-renderer',
|
||||
renderer,
|
||||
'--tesseract-config',
|
||||
cfg_file,
|
||||
'--pages',
|
||||
'1',
|
||||
)
|
||||
assert "Can't open" in err, "No error message about missing config file"
|
||||
assert p.returncode == ExitCode.ok, err
|
||||
|
||||
|
||||
@pytest.mark.slow # This test sometimes times out in CI
|
||||
@@ -649,7 +455,10 @@ THIS FILE IS INVALID
|
||||
'--tesseract-config',
|
||||
cfg_file,
|
||||
)
|
||||
assert "parameter not found" in err.lower(), "No error message"
|
||||
assert (
|
||||
"parameter not found" in err.lower()
|
||||
or "error occurred while parsing" in err.lower()
|
||||
), "No error message"
|
||||
assert p.returncode == ExitCode.invalid_config
|
||||
|
||||
|
||||
@@ -685,7 +494,7 @@ def test_pagesize_consistency(renderer, resources, outpdf):
|
||||
|
||||
first_page_dimensions = pytest.helpers.first_page_dimensions
|
||||
|
||||
infile = resources / 'linn.pdf'
|
||||
infile = resources / '3small.pdf'
|
||||
|
||||
before_dims = first_page_dimensions(infile)
|
||||
|
||||
@@ -698,12 +507,14 @@ def test_pagesize_consistency(renderer, resources, outpdf):
|
||||
'--deskew',
|
||||
'--remove-background',
|
||||
'--clean-final' if pytest.helpers.have_unpaper() else None,
|
||||
'--pages',
|
||||
'1',
|
||||
)
|
||||
|
||||
after_dims = first_page_dimensions(outpdf)
|
||||
|
||||
assert isclose(before_dims[0], after_dims[0])
|
||||
assert isclose(before_dims[1], after_dims[1])
|
||||
assert isclose(before_dims[0], after_dims[0], rel_tol=1e-4)
|
||||
assert isclose(before_dims[1], after_dims[1], rel_tol=1e-4)
|
||||
|
||||
|
||||
def test_skip_big_with_no_images(spoof_tesseract_noop, resources, outpdf):
|
||||
@@ -717,22 +528,6 @@ def test_skip_big_with_no_images(spoof_tesseract_noop, resources, outpdf):
|
||||
)
|
||||
|
||||
|
||||
def test_gs_render_failure(spoof_no_tess_gs_render_fail, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'blank.pdf', outpdf, env=spoof_no_tess_gs_render_fail
|
||||
)
|
||||
print(err)
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
|
||||
|
||||
def test_gs_raster_failure(spoof_no_tess_gs_raster_fail, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_gs_raster_fail
|
||||
)
|
||||
print(err)
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
'8.0.0' <= qpdf.version() <= '8.0.1',
|
||||
reason="qpdf regression on pages with no contents",
|
||||
@@ -861,7 +656,7 @@ def test_compression_changed(
|
||||
def test_sidecar_pagecount(spoof_tesseract_cache, resources, outpdf):
|
||||
sidecar = outpdf.with_suffix('.txt')
|
||||
check_ocrmypdf(
|
||||
resources / 'multipage.pdf',
|
||||
resources / '3small.pdf',
|
||||
outpdf,
|
||||
'--skip-text',
|
||||
'--sidecar',
|
||||
@@ -869,10 +664,10 @@ def test_sidecar_pagecount(spoof_tesseract_cache, resources, outpdf):
|
||||
env=spoof_tesseract_cache,
|
||||
)
|
||||
|
||||
pdfinfo = PdfInfo(resources / 'multipage.pdf')
|
||||
pdfinfo = PdfInfo(resources / '3small.pdf')
|
||||
num_pages = len(pdfinfo)
|
||||
|
||||
with open(sidecar, 'r') as f:
|
||||
with open(sidecar, 'r', encoding='utf-8') as f:
|
||||
ocr_text = f.read()
|
||||
|
||||
# There should a formfeed between each pair of pages, so the count of
|
||||
@@ -888,7 +683,7 @@ def test_sidecar_nonempty(spoof_tesseract_cache, resources, outpdf):
|
||||
resources / 'ccitt.pdf', outpdf, '--sidecar', sidecar, env=spoof_tesseract_cache
|
||||
)
|
||||
|
||||
with open(sidecar, 'r') as f:
|
||||
with open(sidecar, 'r', encoding='utf-8') as f:
|
||||
ocr_text = f.read()
|
||||
assert 'the' in ocr_text
|
||||
|
||||
@@ -925,17 +720,18 @@ def test_decompression_bomb(resources, outpdf):
|
||||
|
||||
|
||||
def test_text_curves(spoof_tesseract_noop, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'vector.pdf', outpdf, env=spoof_tesseract_noop)
|
||||
with patch('ocrmypdf._pipeline.VECTOR_PAGE_DPI', 100):
|
||||
check_ocrmypdf(resources / 'vector.pdf', outpdf, env=spoof_tesseract_noop)
|
||||
|
||||
info = PdfInfo(outpdf)
|
||||
assert len(info.pages[0].images) == 0, "added images to the vector PDF"
|
||||
info = PdfInfo(outpdf)
|
||||
assert len(info.pages[0].images) == 0, "added images to the vector PDF"
|
||||
|
||||
check_ocrmypdf(
|
||||
resources / 'vector.pdf', outpdf, '--force-ocr', env=spoof_tesseract_noop
|
||||
)
|
||||
check_ocrmypdf(
|
||||
resources / 'vector.pdf', outpdf, '--force-ocr', env=spoof_tesseract_noop
|
||||
)
|
||||
|
||||
info = PdfInfo(outpdf)
|
||||
assert len(info.pages[0].images) != 0, "force did not rasterize"
|
||||
info = PdfInfo(outpdf)
|
||||
assert len(info.pages[0].images) != 0, "force did not rasterize"
|
||||
|
||||
|
||||
def test_output_is_dir(spoof_tesseract_noop, resources, outdir):
|
||||
@@ -946,6 +742,7 @@ def test_output_is_dir(spoof_tesseract_noop, resources, outdir):
|
||||
assert 'is not a writable file' in err
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == 'nt', reason="symlink needs admin permissions")
|
||||
def test_output_is_symlink(spoof_tesseract_noop, resources, outdir):
|
||||
sym = Path(outdir / 'this_is_a_symlink')
|
||||
sym.symlink_to(outdir / 'out.pdf')
|
||||
|
||||
@@ -17,21 +17,22 @@
|
||||
|
||||
|
||||
import datetime
|
||||
from datetime import timezone
|
||||
import logging
|
||||
import mmap
|
||||
import os
|
||||
from datetime import timezone
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from shutil import copyfile, move
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import pikepdf
|
||||
import pytest
|
||||
from pikepdf.models.metadata import decode_pdf_date
|
||||
|
||||
from ocrmypdf._jobcontext import PDFContext
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
from ocrmypdf.pdfa import SRGB_ICC_PROFILE, file_claims_pdfa, generate_pdfa_ps
|
||||
from pikepdf.models.metadata import decode_pdf_date
|
||||
|
||||
try:
|
||||
import fitz
|
||||
@@ -330,10 +331,8 @@ def test_prevent_gs_invalid_xml(resources, outdir):
|
||||
str(outdir / 'layers.rendered.pdf'), str(outdir / 'pdfa.ps'), context
|
||||
)
|
||||
|
||||
with open(outdir / 'pdfa.pdf', 'rb') as f:
|
||||
with mmap.mmap(
|
||||
f.fileno(), 0, flags=mmap.MAP_PRIVATE, prot=mmap.PROT_READ
|
||||
) as mm:
|
||||
with open(outdir / 'pdfa.pdf', 'r+b') as f:
|
||||
with mmap.mmap(f.fileno(), 0) as mm:
|
||||
# Since the XML may be invalid, we scan instead of actually feeding it
|
||||
# to a parser.
|
||||
XMP_MAGIC = b'W5M0MpCehiHzreSzNTczkc9d'
|
||||
|
||||
@@ -19,10 +19,10 @@ import logging
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
|
||||
import pikepdf
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
import pikepdf
|
||||
from ocrmypdf import optimize as opt
|
||||
from ocrmypdf.exec import jbig2enc, pngquant
|
||||
from ocrmypdf.exec.ghostscript import rasterize_pdf
|
||||
|
||||
@@ -19,8 +19,8 @@ import pytest
|
||||
|
||||
import ocrmypdf
|
||||
from ocrmypdf._validation import _pages_from_ranges
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
from ocrmypdf.exceptions import BadArgsError
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
+14
-14
@@ -20,11 +20,11 @@ from math import isclose
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import img2pdf
|
||||
import pikepdf
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
|
||||
import pikepdf
|
||||
from ocrmypdf import pdfinfo
|
||||
from ocrmypdf.pdfinfo import Colorspace, Encoding
|
||||
|
||||
@@ -91,21 +91,21 @@ def test_single_page_image(outdir):
|
||||
def test_single_page_inline_image(outdir):
|
||||
filename = outdir / 'image-mono-inline.pdf'
|
||||
pdf = Canvas(str(filename), pagesize=(8 * 72, 6 * 72))
|
||||
with NamedTemporaryFile() as im_tmp:
|
||||
im = Image.new('1', (8, 8), 0)
|
||||
for n in range(8):
|
||||
im.putpixel((n, n), 1)
|
||||
im.save(im_tmp.name, format='PNG')
|
||||
# Draw image in a 72x72 pt or 1"x1" area
|
||||
pdf.drawInlineImage(im_tmp.name, 0, 0, width=72, height=72)
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
|
||||
pdf = pdfinfo.PdfInfo(filename)
|
||||
print(pdf)
|
||||
pdfimage = pdf[0].images[0]
|
||||
im = Image.new('1', (8, 8), 0)
|
||||
for n in range(8):
|
||||
im.putpixel((n, n), 1)
|
||||
|
||||
# Draw image in a 72x72 pt or 1"x1" area
|
||||
pdf.drawInlineImage(im, 0, 0, width=72, height=72)
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
|
||||
info = pdfinfo.PdfInfo(filename)
|
||||
print(info)
|
||||
pdfimage = info[0].images[0]
|
||||
assert isclose(pdfimage.xres, 8)
|
||||
assert pdfimage.color == Colorspace.rgb # reportlab produces color image
|
||||
assert pdfimage.color == Colorspace.gray
|
||||
assert pdfimage.width == 8
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# © 2019 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This file is part of OCRmyPDF.
|
||||
#
|
||||
# OCRmyPDF is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# OCRmyPDF is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
from math import isclose
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf.exec import ghostscript
|
||||
from ocrmypdf.leptonica import Pix
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
|
||||
# pytest.helpers is dynamic
|
||||
# pylint: disable=no-member,redefined-outer-name
|
||||
|
||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||
run_ocrmypdf = pytest.helpers.run_ocrmypdf
|
||||
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
RENDERERS = ['hocr', 'sandwich']
|
||||
|
||||
|
||||
def test_deskew(spoof_tesseract_noop, resources, outdir):
|
||||
# Run with deskew
|
||||
deskewed_pdf = check_ocrmypdf(
|
||||
resources / 'skew.pdf', outdir / 'skew.pdf', '-d', env=spoof_tesseract_noop
|
||||
)
|
||||
|
||||
# Now render as an image again and use Leptonica to find the skew angle
|
||||
# to confirm that it was deskewed
|
||||
log = logging.getLogger()
|
||||
|
||||
deskewed_png = outdir / 'deskewed.png'
|
||||
|
||||
ghostscript.rasterize_pdf(
|
||||
deskewed_pdf,
|
||||
deskewed_png,
|
||||
xres=150,
|
||||
yres=150,
|
||||
raster_device='pngmono',
|
||||
log=log,
|
||||
pageno=1,
|
||||
)
|
||||
|
||||
pix = Pix.open(deskewed_png)
|
||||
skew_angle, _skew_confidence = pix.find_skew()
|
||||
|
||||
print(skew_angle)
|
||||
assert -0.5 < skew_angle < 0.5, "Deskewing failed"
|
||||
|
||||
|
||||
def test_remove_background(spoof_tesseract_noop, resources, outdir):
|
||||
# Ensure the input image does not contain pure white/black
|
||||
with Image.open(resources / 'congress.jpg') as im:
|
||||
assert im.getextrema() != ((0, 255), (0, 255), (0, 255))
|
||||
|
||||
output_pdf = check_ocrmypdf(
|
||||
resources / 'congress.jpg',
|
||||
outdir / 'test_remove_bg.pdf',
|
||||
'--remove-background',
|
||||
'--image-dpi',
|
||||
'150',
|
||||
env=spoof_tesseract_noop,
|
||||
)
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
output_png = outdir / 'remove_bg.png'
|
||||
|
||||
ghostscript.rasterize_pdf(
|
||||
output_pdf,
|
||||
output_png,
|
||||
xres=100,
|
||||
yres=100,
|
||||
raster_device='png16m',
|
||||
log=log,
|
||||
pageno=1,
|
||||
)
|
||||
|
||||
# The output image should contain pure white and black
|
||||
with Image.open(output_png) as im:
|
||||
assert im.getextrema() == ((0, 255), (0, 255), (0, 255))
|
||||
|
||||
|
||||
# This will run 5 * 2 * 2 = 20 test cases
|
||||
@pytest.mark.parametrize(
|
||||
"pdf", ['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf']
|
||||
)
|
||||
@pytest.mark.parametrize("renderer", ['sandwich', 'hocr'])
|
||||
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
|
||||
def test_exotic_image(
|
||||
spoof_tesseract_cache, pdf, renderer, output_type, resources, outdir
|
||||
):
|
||||
outfile = outdir / f'test_{pdf}_{renderer}.pdf'
|
||||
check_ocrmypdf(
|
||||
resources / pdf,
|
||||
outfile,
|
||||
'-dc' if pytest.helpers.have_unpaper() else '-d',
|
||||
'-v',
|
||||
'1',
|
||||
'--output-type',
|
||||
output_type,
|
||||
'--sidecar',
|
||||
'--skip-text',
|
||||
'--pdf-renderer',
|
||||
renderer,
|
||||
env=spoof_tesseract_cache,
|
||||
)
|
||||
|
||||
assert outfile.with_suffix('.pdf.txt').exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_non_square_resolution(renderer, spoof_tesseract_cache, resources, outpdf):
|
||||
# Confirm input image is non-square resolution
|
||||
in_pageinfo = PdfInfo(resources / 'aspect.pdf')
|
||||
assert in_pageinfo[0].xres != in_pageinfo[0].yres
|
||||
|
||||
check_ocrmypdf(
|
||||
resources / 'aspect.pdf',
|
||||
outpdf,
|
||||
'--pdf-renderer',
|
||||
renderer,
|
||||
env=spoof_tesseract_cache,
|
||||
)
|
||||
|
||||
out_pageinfo = PdfInfo(outpdf)
|
||||
|
||||
# Confirm resolution was kept the same
|
||||
assert in_pageinfo[0].xres == out_pageinfo[0].xres
|
||||
assert in_pageinfo[0].yres == out_pageinfo[0].yres
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_convert_to_square_resolution(
|
||||
renderer, spoof_tesseract_cache, resources, outpdf
|
||||
):
|
||||
# Confirm input image is non-square resolution
|
||||
in_pageinfo = PdfInfo(resources / 'aspect.pdf')
|
||||
assert in_pageinfo[0].xres != in_pageinfo[0].yres
|
||||
|
||||
# --force-ocr requires means forced conversion to square resolution
|
||||
check_ocrmypdf(
|
||||
resources / 'aspect.pdf',
|
||||
outpdf,
|
||||
'--force-ocr',
|
||||
'--pdf-renderer',
|
||||
renderer,
|
||||
env=spoof_tesseract_cache,
|
||||
)
|
||||
|
||||
out_pageinfo = PdfInfo(outpdf)
|
||||
|
||||
in_p0, out_p0 = in_pageinfo[0], out_pageinfo[0]
|
||||
|
||||
# Resolution show now be equal
|
||||
assert out_p0.xres == out_p0.yres
|
||||
|
||||
# Page size should match input page size
|
||||
assert isclose(in_p0.width_inches, out_p0.width_inches)
|
||||
assert isclose(in_p0.height_inches, out_p0.height_inches)
|
||||
|
||||
# Because we rasterized the page to produce a new image, it should occupy
|
||||
# the entire page
|
||||
out_im_w = out_p0.images[0].width / out_p0.images[0].xres
|
||||
out_im_h = out_p0.images[0].height / out_p0.images[0].yres
|
||||
assert isclose(out_p0.width_inches, out_im_w)
|
||||
assert isclose(out_p0.height_inches, out_im_h)
|
||||
@@ -21,10 +21,10 @@ from os import fspath
|
||||
from unittest.mock import Mock
|
||||
|
||||
import img2pdf
|
||||
import pikepdf
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
import pikepdf
|
||||
from ocrmypdf import leptonica
|
||||
from ocrmypdf.exec import ghostscript, tesseract
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
|
||||
+15
-18
@@ -18,7 +18,7 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from subprocess import DEVNULL, PIPE, run, Popen
|
||||
from subprocess import DEVNULL, PIPE, CalledProcessError, Popen, run
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -29,10 +29,11 @@ from ocrmypdf.exec import qpdf
|
||||
# pylint: disable=no-member,redefined-outer-name
|
||||
|
||||
run_ocrmypdf = pytest.helpers.run_ocrmypdf
|
||||
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_tess_bad_utf8(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, tesseract='tesseract_badutf8.py')
|
||||
|
||||
@@ -55,6 +56,9 @@ def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||
|
||||
|
||||
def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||
if 'COV_CORE_DATAFILE' in spoof_tesseract_noop:
|
||||
pytest.skip(msg="Coverage uses stdout")
|
||||
|
||||
input_file = str(resources / 'francais.pdf')
|
||||
output_file = str(outpdf)
|
||||
|
||||
@@ -76,6 +80,7 @@ def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info[0:3] >= (3, 6, 4), reason="issue fixed in Python 3.6.4"
|
||||
)
|
||||
@pytest.mark.skipif(os.name == 'nt', reason="POSIX problem")
|
||||
def test_closed_streams(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||
input_file = str(resources / 'francais.pdf')
|
||||
output_file = str(outpdf)
|
||||
@@ -103,6 +108,7 @@ def test_closed_streams(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||
@pytest.mark.skipif(
|
||||
Path('/etc/alpine-release').exists(), reason="invalid test on alpine"
|
||||
)
|
||||
@pytest.mark.skipif(os.name == 'nt', reason="invalid test on Windows")
|
||||
def test_bad_locale():
|
||||
env = os.environ.copy()
|
||||
env['LC_ALL'] = 'C'
|
||||
@@ -113,23 +119,14 @@ def test_bad_locale():
|
||||
assert 'configured to use ASCII as encoding' in err, "should whine"
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', ['hocr', 'sandwich'])
|
||||
def test_bad_utf8(spoof_tess_bad_utf8, renderer, resources, no_outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'ccitt.pdf',
|
||||
no_outpdf,
|
||||
'--pdf-renderer',
|
||||
renderer,
|
||||
env=spoof_tess_bad_utf8,
|
||||
)
|
||||
|
||||
assert out == '', "stdout not clean"
|
||||
assert p.returncode != 0
|
||||
assert 'not utf-8' in err, "should whine about utf-8"
|
||||
assert '\\x96' in err, 'should repeat backslash encoded output'
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
os.name == 'nt' and sys.version_info < (3, 8),
|
||||
reason="Windows does not like this; not sure how to fix",
|
||||
)
|
||||
def test_dev_null(spoof_tesseract_noop, resources):
|
||||
if 'COV_CORE_DATAFILE' in spoof_tesseract_noop:
|
||||
pytest.skip(msg="Coverage uses stdout")
|
||||
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'trivial.pdf', os.devnull, '--force-ocr', env=spoof_tesseract_noop
|
||||
)
|
||||
|
||||
+94
-24
@@ -15,7 +15,9 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from contextlib import contextmanager
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
@@ -37,30 +39,6 @@ def test_tesseract_v4():
|
||||
assert tesseract.v4()
|
||||
|
||||
|
||||
def test_pagesize_consistency_tess4(resources, outpdf):
|
||||
from math import isclose
|
||||
|
||||
infile = resources / 'linn.pdf'
|
||||
|
||||
before_dims = pytest.helpers.first_page_dimensions(infile)
|
||||
|
||||
check_ocrmypdf(
|
||||
infile,
|
||||
outpdf,
|
||||
'--pdf-renderer',
|
||||
'sandwich',
|
||||
'--clean' if pytest.helpers.have_unpaper() else None,
|
||||
'--deskew',
|
||||
'--remove-background',
|
||||
'--clean-final' if pytest.helpers.have_unpaper() else None,
|
||||
)
|
||||
|
||||
after_dims = pytest.helpers.first_page_dimensions(outpdf)
|
||||
|
||||
assert isclose(before_dims[0], after_dims[0])
|
||||
assert isclose(before_dims[1], after_dims[1])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('basename', ['graph_ocred.pdf', 'cardinal.pdf'])
|
||||
def test_skip_pages_does_not_replicate(resources, basename, outdir):
|
||||
infile = resources / basename
|
||||
@@ -105,3 +83,95 @@ def test_no_languages(tmp_path):
|
||||
|
||||
with pytest.raises(MissingDependencyError):
|
||||
tesseract.languages(tesseract_env=env)
|
||||
|
||||
|
||||
def test_image_too_large_hocr(monkeypatch, resources, outdir):
|
||||
log = logging.getLogger('test_image_too_large_hocr')
|
||||
|
||||
def dummy_run(args, *, env=None, **kwargs):
|
||||
raise subprocess.CalledProcessError(1, 'tesseract', output=b'Image too large')
|
||||
|
||||
monkeypatch.setattr(tesseract, 'run', dummy_run)
|
||||
tesseract.generate_hocr(
|
||||
input_file=resources / 'crom.png',
|
||||
output_files=[outdir / 'out.hocr', outdir / 'out.txt'],
|
||||
language=['eng'],
|
||||
engine_mode=None,
|
||||
tessconfig=[],
|
||||
timeout=180.0,
|
||||
pagesegmode=None,
|
||||
log=log,
|
||||
user_words=None,
|
||||
user_patterns=None,
|
||||
tesseract_env=None,
|
||||
)
|
||||
assert "name='ocr-capabilities'" in Path(outdir / 'out.hocr').read_text()
|
||||
|
||||
|
||||
def test_image_too_large_pdf(monkeypatch, resources, outdir):
|
||||
log = logging.getLogger('test_image_too_large_pdf')
|
||||
|
||||
def dummy_run(args, *, env=None, **kwargs):
|
||||
raise subprocess.CalledProcessError(1, 'tesseract', output=b'Image too large')
|
||||
|
||||
monkeypatch.setattr(tesseract, 'run', dummy_run)
|
||||
tesseract.generate_pdf(
|
||||
input_image=resources / 'crom.png',
|
||||
skip_pdf=resources / 'blank.pdf',
|
||||
output_pdf=outdir / 'pdf.pdf',
|
||||
output_text=outdir / 'txt.txt',
|
||||
language=['eng'],
|
||||
engine_mode=None,
|
||||
text_only=False,
|
||||
tessconfig=[],
|
||||
timeout=180.0,
|
||||
pagesegmode=None,
|
||||
log=log,
|
||||
user_words=None,
|
||||
user_patterns=None,
|
||||
tesseract_env=None,
|
||||
)
|
||||
assert Path(outdir / 'txt.txt').read_text() == '[skipped page]'
|
||||
if os.name != 'nt': # different semantics
|
||||
assert Path(outdir / 'pdf.pdf').samefile(resources / 'blank.pdf')
|
||||
|
||||
|
||||
def test_timeout(caplog):
|
||||
log = logging.getLogger('test_timeout')
|
||||
tesseract.page_timedout(log, '123456.png', 5)
|
||||
assert "123456" in caplog.text
|
||||
assert "took too long" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'in_, logged',
|
||||
[
|
||||
(b'Tesseract Open Source', ''),
|
||||
(b'lots of diacritics blah blah', 'diacritics'),
|
||||
(b'Warning in pixReadMem', ''),
|
||||
(b'OSD: Weak margin', 'unsure about page orientation'),
|
||||
(b'Error in pixScanForForeground', ''),
|
||||
(b'Error in boxClipToRectangle', ''),
|
||||
(b'an unexpected error', 'an unexpected error'),
|
||||
(b'a dire warning', 'a dire warning'),
|
||||
(b'read_params_file something', 'read_params_file'),
|
||||
(b'an innocent message', 'innocent'),
|
||||
(b'\x7f\x7f\x80innocent unicode failure', 'innocent'),
|
||||
],
|
||||
)
|
||||
def test_tesseract_log_output(caplog, in_, logged):
|
||||
log = logging.getLogger('tesseract_log_output')
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
tesseract.tesseract_log_output(log, in_, 'dummy')
|
||||
if logged == '':
|
||||
assert caplog.text == ''
|
||||
else:
|
||||
assert logged in caplog.text
|
||||
|
||||
|
||||
def test_tesseract_log_output_raises(caplog):
|
||||
log = logging.getLogger('tesseract_log_output')
|
||||
with pytest.raises(tesseract.TesseractConfigError):
|
||||
tesseract.tesseract_log_output(log, b'parameter not found: moo', 'dummy')
|
||||
assert 'not found' in caplog.text
|
||||
|
||||
@@ -20,8 +20,8 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from ocrmypdf.cli import parser
|
||||
from ocrmypdf._validation import check_options
|
||||
from ocrmypdf.cli import parser
|
||||
from ocrmypdf.exceptions import ExitCode, MissingDependencyError
|
||||
from ocrmypdf.exec import unpaper
|
||||
|
||||
@@ -43,7 +43,7 @@ def have_unpaper():
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@pytest.fixture
|
||||
def spoof_unpaper_oldversion(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, unpaper="unpaper_oldversion.py")
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from ocrmypdf.pdfinfo import PdfInfo
|
||||
|
||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||
run_ocrmypdf = pytest.helpers.run_ocrmypdf
|
||||
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
@@ -32,9 +33,10 @@ def poster(resources):
|
||||
return resources / 'poster.pdf'
|
||||
|
||||
|
||||
def test_userunit_ghostscript_fails(poster, no_outpdf):
|
||||
p, out, err = run_ocrmypdf(poster, no_outpdf, '--output-type=pdfa')
|
||||
assert p.returncode == ExitCode.input_file
|
||||
def test_userunit_ghostscript_fails(poster, no_outpdf, caplog):
|
||||
result = run_ocrmypdf_api(poster, no_outpdf, '--output-type=pdfa')
|
||||
assert result == ExitCode.input_file
|
||||
assert 'not supported by Ghostscript' in caplog.text
|
||||
|
||||
|
||||
def test_userunit_qpdf_passes(spoof_tesseract_cache, poster, outpdf):
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
@@ -24,7 +23,7 @@ import pytest
|
||||
|
||||
import ocrmypdf._validation as vd
|
||||
from ocrmypdf.api import create_options
|
||||
from ocrmypdf.exceptions import MissingDependencyError, BadArgsError
|
||||
from ocrmypdf.exceptions import BadArgsError, MissingDependencyError
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
|
||||
|
||||
@@ -117,11 +116,28 @@ def test_report_file_size(tmp_path, caplog):
|
||||
opts = make_opts()
|
||||
vd.report_output_file_size(opts, in_, out)
|
||||
assert caplog.text == ''
|
||||
caplog.clear()
|
||||
|
||||
os.truncate(in_, 25001)
|
||||
os.truncate(out, 50000)
|
||||
with patch('ocrmypdf._validation.jbig2enc.available', return_value=True), patch(
|
||||
'ocrmypdf._validation.pngquant.available', return_value=True
|
||||
):
|
||||
vd.report_output_file_size(opts, in_, out)
|
||||
assert 'No reason' in caplog.text
|
||||
caplog.clear()
|
||||
|
||||
with patch('ocrmypdf._validation.jbig2enc.available', return_value=False), patch(
|
||||
'ocrmypdf._validation.pngquant.available', return_value=True
|
||||
):
|
||||
vd.report_output_file_size(opts, in_, out)
|
||||
assert 'optional dependency' in caplog.text
|
||||
caplog.clear()
|
||||
|
||||
opts = make_opts(in_, out, optimize=0)
|
||||
vd.report_output_file_size(opts, in_, out)
|
||||
assert 'No reason' in caplog.text
|
||||
assert 'disabled' in caplog.text
|
||||
caplog.clear()
|
||||
|
||||
|
||||
def test_false_action_store_true():
|
||||
|
||||
Reference in New Issue
Block a user