Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3438afaffe | ||
|
|
681fa039cc | ||
|
|
69e80f1545 | ||
|
|
983835cce4 | ||
|
|
6c23b137e2 | ||
|
|
d656b2b3f2 | ||
|
|
031b800aac | ||
|
|
05eb85ee77 | ||
|
|
4da5214ca9 | ||
|
|
1ee829dd59 | ||
|
|
99db5d91ae | ||
|
|
3a4490ee36 | ||
|
|
a492e3b472 | ||
|
|
c3719d3b72 | ||
|
|
ad48fc6415 | ||
|
|
7f8018ffde | ||
|
|
80651fe12c | ||
|
|
a58209e895 | ||
|
|
775b958c55 | ||
|
|
cdcdd16865 | ||
|
|
b332d76782 | ||
|
|
3660007fc8 | ||
|
|
b55d7e57af | ||
|
|
6e99e7b346 | ||
|
|
4d26867dee | ||
|
|
78e8bf9cbf | ||
|
|
de61530d4d | ||
|
|
c149f860b5 | ||
|
|
68c852acec | ||
|
|
a8565bac6e | ||
|
|
6e8b0c3194 | ||
|
|
ff860e8362 | ||
|
|
cf4b04c5d1 | ||
|
|
078bc2abe9 |
+21
-22
@@ -1,6 +1,6 @@
|
||||
# OCRmyPDF
|
||||
#
|
||||
FROM ubuntu:19.04 as base
|
||||
FROM ubuntu:19.10 as base
|
||||
|
||||
FROM base as builder
|
||||
|
||||
@@ -10,36 +10,36 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf automake libtool \
|
||||
libleptonica-dev \
|
||||
zlib1g-dev \
|
||||
ocrmypdf \
|
||||
pngquant \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
tesseract-ocr \
|
||||
unpaper \
|
||||
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 \
|
||||
&& cd .. \
|
||||
&& rm -rf jbig2
|
||||
|
||||
RUN python3 -m venv /appenv
|
||||
|
||||
COPY . /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN . /appenv/bin/activate; \
|
||||
pip install --upgrade pip \
|
||||
&& pip install .
|
||||
RUN pip3 install --no-cache-dir \
|
||||
-r requirements/main.txt \
|
||||
-r requirements/webservice.txt \
|
||||
-r requirements/test.txt \
|
||||
.
|
||||
|
||||
FROM base
|
||||
|
||||
@@ -53,7 +53,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
zlib1g \
|
||||
pngquant \
|
||||
python3 \
|
||||
python3-venv \
|
||||
qpdf \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-chi-sim \
|
||||
@@ -62,10 +61,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tesseract-ocr-fra \
|
||||
tesseract-ocr-por \
|
||||
tesseract-ocr-spa \
|
||||
unpaper \
|
||||
wget
|
||||
unpaper
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /usr/local/lib/ /usr/local/lib/
|
||||
COPY --from=builder /usr/local/bin/ /usr/local/bin/
|
||||
|
||||
# Copy
|
||||
COPY --from=builder /app/misc/webservice.py /app/
|
||||
|
||||
# Copy minimal project files to get the test suite.
|
||||
@@ -74,7 +76,4 @@ COPY --from=builder /app/requirements /app/requirements
|
||||
COPY --from=builder /app/tests /app/tests
|
||||
COPY --from=builder /app/src /app/src
|
||||
|
||||
COPY --from=builder /appenv /appenv
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
|
||||
ENTRYPOINT ["/appenv/bin/ocrmypdf"]
|
||||
ENTRYPOINT ["/usr/local/bin/ocrmypdf"]
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
FROM alpine:3.9 as base
|
||||
|
||||
FROM base as builder
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
# Normally:
|
||||
# echo '@testing http://nl.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories
|
||||
|
||||
RUN \
|
||||
echo -e '@testing http://nl.alpinelinux.org/alpine/edge/testing\n@community http://nl.alpinelinux.org/alpine/edge/community'\
|
||||
>> /etc/apk/repositories \
|
||||
# Add runtime dependencies
|
||||
&& apk add --update \
|
||||
python3-dev \
|
||||
py3-setuptools \
|
||||
jbig2enc@community \
|
||||
ghostscript \
|
||||
qpdf@community \
|
||||
qpdf-dev@community \
|
||||
tesseract-ocr \
|
||||
unpaper \
|
||||
pngquant \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
zlib-dev \
|
||||
libffi-dev \
|
||||
leptonica-dev \
|
||||
binutils \
|
||||
&& pip3 install --upgrade pip \
|
||||
# Install pybind11 for pikepdf
|
||||
&& pip3 install pybind11 \
|
||||
# Install flask for the webservice
|
||||
&& pip3 install flask \
|
||||
# Add build dependencies
|
||||
&& apk add --virtual build-dependencies \
|
||||
build-base \
|
||||
git
|
||||
|
||||
COPY . /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip3 install .
|
||||
|
||||
FROM base
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
# Normally:
|
||||
# echo '@testing http://nl.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories
|
||||
|
||||
RUN \
|
||||
echo -e '@testing http://nl.alpinelinux.org/alpine/edge/testing\n@community http://nl.alpinelinux.org/alpine/edge/community'\
|
||||
>> /etc/apk/repositories \
|
||||
# Add runtime dependencies
|
||||
&& apk add --update \
|
||||
python3 \
|
||||
jbig2enc@community \
|
||||
ghostscript \
|
||||
qpdf@community \
|
||||
qpdf-dev@community \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-data-deu \
|
||||
tesseract-ocr-data-chi_sim \
|
||||
unpaper \
|
||||
pngquant \
|
||||
libxml2 \
|
||||
libxslt \
|
||||
zlib \
|
||||
libffi \
|
||||
leptonica-dev \
|
||||
binutils \
|
||||
&& mkdir /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy build artifacts (python site-packages)
|
||||
COPY --from=builder /usr/lib/python3.6/site-packages /usr/lib/python3.6/site-packages
|
||||
COPY --from=builder /usr/bin/ocrmypdf /usr/bin/dumppdf.py /usr/bin/latin2ascii.py /usr/bin/pdf2txt.py /usr/bin/img2pdf /usr/bin/chardetect /usr/bin/
|
||||
|
||||
# Copy
|
||||
COPY --from=builder /app/misc/webservice.py /app/
|
||||
|
||||
# Copy minimal project files to get the test suite.
|
||||
COPY --from=builder /app/setup.cfg /app/setup.py /app/README.md /app/
|
||||
COPY --from=builder /app/requirements /app/requirements
|
||||
COPY --from=builder /app/tests /app/tests
|
||||
COPY --from=builder /app/src /app/src
|
||||
|
||||
ENTRYPOINT ["/usr/bin/ocrmypdf"]
|
||||
@@ -7,6 +7,7 @@
|
||||
*.pyc
|
||||
*.sublime-*
|
||||
*.DS_Store
|
||||
.mypy_cache/
|
||||
|
||||
# Package building
|
||||
.eggs/
|
||||
|
||||
+14
-3
@@ -1,6 +1,17 @@
|
||||
repos:
|
||||
- repo: https://github.com/ambv/black
|
||||
- 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
|
||||
- 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
|
||||
- id: name-tests-test
|
||||
args: ["--django"]
|
||||
|
||||
+44
-28
@@ -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
|
||||
@@ -62,8 +64,6 @@ matrix:
|
||||
mkdir -p bin packages
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade wheel
|
||||
wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O packages/unpaper_6.1-1.deb
|
||||
sudo dpkg -i packages/unpaper_6.1-1.deb
|
||||
- os: linux
|
||||
dist: xenial
|
||||
sudo: required
|
||||
@@ -82,14 +82,35 @@ 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
|
||||
- tesseract-ocr
|
||||
- tesseract-ocr-deu
|
||||
- tesseract-ocr-eng
|
||||
- tesseract-ocr-fra
|
||||
- unpaper
|
||||
- os: osx
|
||||
osx_image: xcode9.2
|
||||
language: generic
|
||||
addons:
|
||||
homebrew:
|
||||
@@ -115,29 +136,24 @@ before_cache:
|
||||
install:
|
||||
- mkdir -p bin
|
||||
- export PATH=$PWD/bin:$PATH
|
||||
- pip3 install pycparser # py3.7 workaround for https://github.com/eliben/pycparser/issues/251
|
||||
- pip3 install -r requirements/main.txt
|
||||
- pip3 install --no-deps .
|
||||
- pip3 install -r requirements/test.txt
|
||||
- pip3 install -r requirements/main.txt -r requirements/test.txt .
|
||||
|
||||
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
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
# requirements
|
||||
recursive-include requirements *
|
||||
|
||||
# git
|
||||
include .git_archival.txt
|
||||
|
||||
# docker
|
||||
include .dockerignore
|
||||
recursive-include .docker *
|
||||
|
||||
# tests
|
||||
include .coveragerc
|
||||
recursive-include tests *.bin
|
||||
recursive-include tests *.jpg
|
||||
recursive-include tests *.jsonl
|
||||
recursive-include tests *.png
|
||||
recursive-include tests *.pdf
|
||||
recursive-include tests *.py
|
||||
recursive-include tests *.rst
|
||||
recursive-include tests *.txt
|
||||
recursive-exclude tests/resources/private *
|
||||
|
||||
# documentation
|
||||
include LICENSE
|
||||
include *.rst
|
||||
recursive-exclude .github *
|
||||
recursive-include docs *.py
|
||||
recursive-include docs *.rst
|
||||
recursive-include docs *.svg
|
||||
recursive-exclude docs/_build *
|
||||
|
||||
|
||||
# support files
|
||||
recursive-include src/ocrmypdf/data *
|
||||
include *.py
|
||||
exclude tasks.py
|
||||
recursive-exclude .travis *
|
||||
exclude .travis*
|
||||
|
||||
|
||||
# code
|
||||
exclude src/ocrmypdf/lib/_leptonica.py
|
||||
exclude scratch.py
|
||||
@@ -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]
|
||||
[![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"
|
||||
|
||||
@@ -10,6 +12,8 @@
|
||||
|
||||
[docs]: https://readthedocs.org/projects/ocrmypdf/badge/?version=latest "RTD"
|
||||
|
||||
[pyversions]: https://img.shields.io/pypi/pyversions/ocrmypdf "Supported Python versions"
|
||||
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched or copy-pasted.
|
||||
|
||||
```bash
|
||||
@@ -46,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
|
||||
@@ -61,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
|
||||
|
||||
@@ -120,7 +124,7 @@ If you detect an issue, please:
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Runs on CPython 3.5, 3.6 and 3.7. 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.
|
||||
|
||||
Press & Media
|
||||
-------------
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
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
|
||||
$env:path = "C:\Program Files\Tesseract-OCR;C:\Program Files\gs\gs9.50\bin;" + $env:path
|
||||
pip install --upgrade pip wheel
|
||||
pip install -r requirements/main.txt -r requirements/test.txt .
|
||||
tesseract --version
|
||||
displayName: "Install Python packages"
|
||||
- pwsh: |
|
||||
refreshenv
|
||||
$env:path = "C:\Program Files\Tesseract-OCR;C:\Program Files\gs\gs9.50\bin;" + $env:path
|
||||
$env:pathext += ';.py'
|
||||
# -n auto helps Windows
|
||||
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()
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: "$(System.DefaultWorkingDirectory)/**/coverage.xml"
|
||||
- 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()
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: "$(System.DefaultWorkingDirectory)/**/coverage.xml"
|
||||
- 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()
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: "$(System.DefaultWorkingDirectory)/**/coverage.xml"
|
||||
- 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 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
|
||||
|
||||
|
||||
+17
-9
@@ -322,15 +322,23 @@ working files on a per page basis have the page number as a prefix
|
||||
(starting with page 1), an infix indicates the processing stage, and a
|
||||
suffix indicates the file type. Some important files include:
|
||||
|
||||
- ``.page.png`` - what the input page looks like
|
||||
- ``.image`` - the image we will show the user if we are in a mode that
|
||||
changes the final appearance; may be in one of several image formats
|
||||
- ``.text.pdf`` - the OCR file; this will load as a blank page but
|
||||
should have visible text if checked with a tool like pdftotext or
|
||||
pdfminder.six
|
||||
- ``.ocr.png`` - the file that is sent to Tesseract for OCR; depending
|
||||
- ``_rasterize.png`` - what the input page looks like
|
||||
- ``_ocr.png`` - the file that is sent to Tesseract for OCR; depending
|
||||
on arguments this may differ from the presentation image
|
||||
- ``layers.rendered.pdf`` - the composite PDF, before metadata repair
|
||||
and optimization
|
||||
- ``_pp_deskew.png`` - the image, after deskewing
|
||||
- ``_pp_clean.png`` - the image, after cleaning with unpaper
|
||||
- ``_ocr_tess.pdf`` - the OCR file; appears as a blank page with invisible
|
||||
text embedded
|
||||
- ``_ocr_tess.txt`` - the OCR text (not necessarily all text on the page,
|
||||
if the page is mixed format)
|
||||
- ``fix_docinfo.pdf`` - a temporary file created to fix the PDF DocumentInfo
|
||||
data structure
|
||||
- ``graft_layers.pdf`` - the rendered PDF with OCR layers grafted on
|
||||
- ``pdfa.pdf`` - ``graft_layers.pdf`` after conversion to PDF/A
|
||||
- ``pdfa.ps`` - a PostScript file used by Ghostscript for PDF/A conversion
|
||||
- ``optimize.pdf`` - the PDF generated before optimization
|
||||
- ``optimize.out.pdf`` - the PDF generated by optimization
|
||||
- ``origin`` - the input file
|
||||
- ``origin.pdf`` - the input file or the input image converted to PDF
|
||||
- ``images/*`` - images extracted during the optimization process; here
|
||||
the prefix indicates a PDF object ID not a page number
|
||||
|
||||
+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
|
||||
----------
|
||||
|
||||
+18
-9
@@ -47,7 +47,7 @@ where the PDFs are stored):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
find . -printf '%p' -name '*.pdf' -exec docker run --rm -v <host dir>:<container dir> jbarlow83/ocrmypdf-alpine '<container dir>/{}' '<container dir>/{}' \;
|
||||
find . -printf '%p' -name '*.pdf' -exec docker run --rm -v <host dir>:<container dir> jbarlow83/ocrmypdf '<container dir>/{}' '<container dir>/{}' \;
|
||||
|
||||
This only runs one ``ocrmypdf`` process at a time. This variation uses
|
||||
``find`` to create a directory list and ``parallel`` to parallelize runs
|
||||
@@ -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)
|
||||
|
||||
@@ -244,6 +250,9 @@ Caveats
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
- `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``.
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
=======================
|
||||
Contributing guidelines
|
||||
=======================
|
||||
|
||||
Contributions are welcome!
|
||||
|
||||
Big changes
|
||||
===========
|
||||
|
||||
Please open a new issue to discuss or propose a major change. Not only is it fun
|
||||
to discuss big ideas, but we might save each other's time too. Perhaps some of the
|
||||
work you're contemplating is already half-done in a development branch.
|
||||
|
||||
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``.
|
||||
|
||||
Tests
|
||||
=====
|
||||
|
||||
New features should come with tests that confirm their correctness.
|
||||
|
||||
New Python dependencies
|
||||
=======================
|
||||
|
||||
If you are proposing a change that will require a new Python dependency, we
|
||||
prefer dependencies that are already packaged by Debian or Red Hat. This makes
|
||||
life much easier for our downstream package maintainers.
|
||||
|
||||
Python dependencies must also be GPLv3 compatible.
|
||||
|
||||
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.
|
||||
@@ -216,6 +216,15 @@ processing or PDF/A conversion.
|
||||
|
||||
ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf
|
||||
|
||||
Optimize images without performing OCR
|
||||
--------------------------------------
|
||||
|
||||
You can also optimize all images without performing any OCR:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --tesseract-timeout=0 --optimize 3 --skip-text input.pdf output.pdf
|
||||
|
||||
Redo existing OCR
|
||||
=================
|
||||
|
||||
|
||||
+24
-29
@@ -22,21 +22,20 @@ Installing the Docker image
|
||||
If you have `Docker <https://docs.docker.com/>`__ installed on your
|
||||
system, you can install a Docker image of the latest release.
|
||||
|
||||
The recommended OCRmyPDF Docker image is currently named
|
||||
``ocrmypdf-alpine``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker pull jbarlow83/ocrmypdf-alpine
|
||||
|
||||
Follow the Docker installation instructions for your platform. If you
|
||||
can run this command successfully, your system is ready to download and
|
||||
If you can run this command successfully, your system is ready to download and
|
||||
execute the image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run hello-world
|
||||
|
||||
The recommended OCRmyPDF Docker image is currently named ``ocrmypdf``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker pull jbarlow83/ocrmypdf
|
||||
|
||||
|
||||
OCRmyPDF will use all available CPU cores. By default, the VirtualBox
|
||||
machine instance on Windows and macOS has only a single CPU core
|
||||
enabled. Use the VirtualBox Manager to determine the name of your Docker
|
||||
@@ -51,6 +50,9 @@ CPUs:
|
||||
docker-machine start "yourVM"
|
||||
eval $(docker-machine env "yourVM")
|
||||
|
||||
See the Docker documentation for
|
||||
`adjusting memory and CPU on other platforms <https://docs.docker.com/config/containers/resource_constraints/>`__.
|
||||
|
||||
Using the Docker image on the command line
|
||||
==========================================
|
||||
|
||||
@@ -63,7 +65,7 @@ To start a Docker container (instance of the image):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker tag jbarlow83/ocrmypdf-alpine ocrmypdf
|
||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||
docker run --rm -i ocrmypdf (... all other arguments here...)
|
||||
|
||||
For convenience, create a shell alias to hide the Docker command. It is
|
||||
@@ -103,12 +105,12 @@ on the public one:
|
||||
|
||||
.. code-block:: dockerfile
|
||||
|
||||
FROM jbarlow83/ocrmypdf-alpine
|
||||
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
|
||||
========================
|
||||
@@ -117,17 +119,16 @@ The OCRmyPDF test suite is installed with image. To run it:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --entrypoint python3 jbarlow83/ocrmypdf-alpine setup.py test
|
||||
docker run --entrypoint python3 jbarlow83/ocrmypdf -m pytest
|
||||
|
||||
Accessing the shell
|
||||
===================
|
||||
|
||||
``bash`` is not installed in the image. To use the busybox shell in the
|
||||
Docker image:
|
||||
To use the bash shell in the Docker image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run -it --entrypoint busybox jbarlow83/ocrmypdf-alpine sh
|
||||
docker run -it --entrypoint bash jbarlow83/ocrmypdf
|
||||
|
||||
Using the OCRmyPDF web service wrapper
|
||||
======================================
|
||||
@@ -137,7 +138,12 @@ service. The webservice may be launched as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --entrypoint python3 -p 5000:5000 jbarlow83/ocrmypdf-alpine webservice.py
|
||||
docker run --entrypoint python3 -p 5000:5000 jbarlow83/ocrmypdf webservice.py
|
||||
|
||||
This will configure the machine to listen on port 5000. On Linux machines
|
||||
this is port 5000 of localhost. On macOS or Windows machines running
|
||||
Docker, this is port 5000 of the virtual machine that runs your Docker
|
||||
images. You can find its IP address using the command ``docker-machine ip``.
|
||||
|
||||
Unlike command line usage this program will open a socket and wait for
|
||||
connections.
|
||||
@@ -162,14 +168,3 @@ also licensed in this way.
|
||||
|
||||
In addition to the above, please read our
|
||||
:ref:`general remarks on using OCRmyPDF as a service <ocr-service>`.
|
||||
|
||||
Ubuntu-based Docker image
|
||||
=========================
|
||||
|
||||
A Ubuntu-based OCRmyPDF image is also available. The main advantage this
|
||||
image offers is that it supports manylinux Python wheels (which are not
|
||||
supported on Alpine Linux). This may be useful for plugins.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker pull jbarlow83/ocrmypdf
|
||||
|
||||
+10
-2
@@ -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
|
||||
|
||||
@@ -22,11 +25,16 @@ PDF is the best format for storing and exchanging scanned documents. Unfortunat
|
||||
cookbook
|
||||
docker
|
||||
advanced
|
||||
api
|
||||
batch
|
||||
security
|
||||
errors
|
||||
|
||||
.. toctree::
|
||||
:caption: Developers
|
||||
:maxdepth: 2
|
||||
|
||||
api
|
||||
contributing
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
+55
-24
@@ -134,8 +134,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 +145,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`.
|
||||
|
||||
@@ -272,15 +277,11 @@ Now we need to install ``pip`` and let it install ocrmypdf:
|
||||
curl https://bootstrap.pypa.io/ez_setup.py -o - | python3.6 && python3.6 -m easy_install pip
|
||||
pip3.6 install ocrmypdf
|
||||
|
||||
These installation instructions omit the optional dependency
|
||||
``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04.
|
||||
The author could not find a backport of ``unpaper``, and created a .deb
|
||||
package to do the job of installing unpaper 6.1 (for x86 64-bit only):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O unpaper_6.1-1.deb
|
||||
sudo dpkg -i unpaper_6.1-1.deb
|
||||
The optional dependency ``unpaper`` is only available at 0.4.2 in Ubuntu 14.04,
|
||||
and no backports are available. Previously the author maintained a backported
|
||||
.deb package for unpaper 6.1, but since Ubuntu 14.04 is now end of life, this is
|
||||
not supported. As such, ``unpaper`` is not available on Ubuntu 14.04 or must by
|
||||
compiled by hand.
|
||||
|
||||
To add JBIG2 encoding, see :ref:`jbig2`.
|
||||
|
||||
@@ -434,8 +435,47 @@ 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 recommended)
|
||||
* 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.
|
||||
|
||||
Modify your ``PATH`` environment variable so that Tesseract and Ghostscript, and
|
||||
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>`.
|
||||
@@ -454,15 +494,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
|
||||
^^^^^^
|
||||
|
||||
@@ -517,7 +548,7 @@ manager. ``pip`` cannot provide them.
|
||||
|
||||
As of ocrmypdf 7.2.1, the following versions are recommended:
|
||||
|
||||
- Python 3.7
|
||||
- Python 3.7 or 3.8
|
||||
- Ghostscript 9.23 or newer
|
||||
- qpdf 8.2.1
|
||||
- Tesseract 4.0.0 or newer
|
||||
|
||||
@@ -208,7 +208,7 @@ consider one of these similar open source programs:
|
||||
Web front-ends
|
||||
==============
|
||||
|
||||
The Docker image ``ocrmypdf-alpine`` provides a web service front-end
|
||||
The Docker image ``ocrmypdf`` provides a web service front-end
|
||||
that allows files to submitted over HTTP and the results "downloaded".
|
||||
This is an HTTP server intended to simplify web services deployments; it
|
||||
is not intended to be deployed on the public internet and no real
|
||||
|
||||
@@ -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,61 @@ Note that it is licensed under GPLv3, so scripts that
|
||||
``import ocrmypdf`` and are released publicly should probably also be
|
||||
licensed under GPLv3.
|
||||
|
||||
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
|
||||
======
|
||||
|
||||
- The Alpine Docker image (jbarlow83/ocrmypdf-alpine) has been dropped due to
|
||||
the difficulties of supporting Alpine Linux.
|
||||
- The primary Docker image (jbarlow83/ocrmypdf) has been improved to take on
|
||||
the extra features that used to be exclusive to the Alpine image.
|
||||
- No changes to application code.
|
||||
- pdfminer.six version 20191020 is now supported.
|
||||
|
||||
v9.0.4
|
||||
======
|
||||
|
||||
- Fixed compatibility with Python 3.8 (but requires source install for the moment).
|
||||
- Fixed Tesseract settings for ``--user-words`` and ``--user-patterns``.
|
||||
- Changed to pikepdf 1.6.5 (for Python 3.8).
|
||||
- Changed to Pillow 6.2.0 (to mitigate a security vulnerability in earlier Pillow).
|
||||
- A debug message now mentions when English is automatically selected if the locale
|
||||
is not English.
|
||||
|
||||
v9.0.3
|
||||
======
|
||||
|
||||
|
||||
+2
-1
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
py36 = true
|
||||
target-version = ["py36", "py37", "py38"]
|
||||
skip-string-normalization = true
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
@@ -28,5 +28,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
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
# requirements.txt can be used to replicate the developer's build environment
|
||||
# setup.py lists a separate set of requirements that are looser to simplify
|
||||
# installation
|
||||
chardet == 3.0.4
|
||||
cffi == 1.12.2
|
||||
cffi == 1.13.2
|
||||
img2pdf == 0.3.3
|
||||
pdfminer.six == 20181108
|
||||
pikepdf == 1.6.1
|
||||
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
|
||||
pycparser == 2.19
|
||||
python-xmp-toolkit == 2.0.1
|
||||
reportlab == 3.5.13
|
||||
tqdm == 4.32.1
|
||||
pdfminer.six == 20191110
|
||||
pikepdf == 1.8.1
|
||||
Pillow >= 6.2.0
|
||||
reportlab == 3.5.32
|
||||
tqdm == 4.37.0
|
||||
|
||||
@@ -2,7 +2,7 @@ pytest >= 5.0.0
|
||||
pytest-helpers-namespace >= 2019.1.8
|
||||
pytest-xdist >= 1.29.0 # For DumpError fix
|
||||
pytest-cov >= 2.6.1
|
||||
python-xmp-toolkit # requires apt-get install libexempi3
|
||||
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 @@
|
||||
Flask >= 1, < 2
|
||||
@@ -68,6 +68,7 @@ setup(
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
@@ -75,6 +76,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",
|
||||
@@ -95,11 +97,9 @@ 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',
|
||||
'pikepdf >= 1.6.0, < 2',
|
||||
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
|
||||
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
|
||||
# block 5.1.0, broken wheels
|
||||
'pdfminer.six >= 20181108, <= 20191110',
|
||||
'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
|
||||
|
||||
@@ -37,18 +37,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, PdfInfo, Encoding
|
||||
|
||||
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 +85,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:
|
||||
@@ -132,7 +132,7 @@ 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)
|
||||
@@ -557,7 +557,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 +701,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,
|
||||
|
||||
+28
-12
@@ -26,6 +26,7 @@ from collections import namedtuple
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from tqdm import tqdm
|
||||
import PIL
|
||||
|
||||
from ._graft import OcrGrafter
|
||||
from ._jobcontext import PDFContext, cleanup_working_files, make_logger
|
||||
@@ -176,7 +177,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 +189,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):
|
||||
@@ -225,15 +232,15 @@ def exec_concurrent(context):
|
||||
if max_workers > 1:
|
||||
context.log.info("Start processing %d pages concurrent", max_workers)
|
||||
|
||||
# Tesseract 4.0 is multithreaded, and we also run multiple workers. We want to
|
||||
# avoid the situation where we end up trying to run NxN jobs on N CPU cores,
|
||||
# as that gives poor performance. Performance testing shows we're better off
|
||||
# Tesseract 4.x can be multithreaded, and we also run multiple workers. We want
|
||||
# to manage how many threads it uses to avoid creating total threads than cores.
|
||||
# Performance testing shows we're better off
|
||||
# parallelizing ocrmypdf and forcing Tesseract to be single threaded, which we
|
||||
# get by setting the envvar OMP_THREAD_LIMIT to 1. But if the page count of the
|
||||
# input file is small, then we allow Tesseract to use threads, subject to the
|
||||
# constraint: (ocrmypdf workers) * (tesseract threads) <= max_workers and limiting
|
||||
# Tesseract to 4 threads.
|
||||
tess_threads = min(4, context.options.jobs // max_workers)
|
||||
# constraint: (ocrmypdf workers) * (tesseract threads) <= max_workers.
|
||||
# As of Tesseract 4.1, 3 threads is the most effective on a 4 core/8 thread system.
|
||||
tess_threads = min(3, context.options.jobs // max_workers)
|
||||
if context.options.tesseract_env is None:
|
||||
context.options.tesseract_env = os.environ.copy()
|
||||
context.options.tesseract_env.setdefault('OMP_THREAD_LIMIT', str(tess_threads))
|
||||
@@ -261,7 +268,9 @@ def exec_concurrent(context):
|
||||
unit_scale=0.5,
|
||||
disable=not context.options.progress_bar,
|
||||
) as pbar, Pool(
|
||||
processes=max_workers, initializer=initializer, initargs=(log_queue,)
|
||||
processes=max_workers,
|
||||
initializer=initializer,
|
||||
initargs=(log_queue, PIL.Image.MAX_IMAGE_PIXELS),
|
||||
) as pool:
|
||||
results = pool.imap_unordered(exec_page_sync, context.get_page_contexts())
|
||||
while True:
|
||||
@@ -301,7 +310,14 @@ def exec_concurrent(context):
|
||||
class NeverRaise(Exception):
|
||||
"""An exception that is never raised"""
|
||||
|
||||
pass
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
def samefile(f1, f2):
|
||||
if os.name == 'nt':
|
||||
return f1 == f2
|
||||
else:
|
||||
return os.path.samefile(f1, f2)
|
||||
|
||||
|
||||
def run_pipeline(options, api=False):
|
||||
@@ -339,7 +355,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'):
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
@@ -41,12 +42,13 @@ 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
|
||||
|
||||
HOCR_OK_LANGS = frozenset(['eng', 'deu', 'spa', 'ita', 'por'])
|
||||
DEFAULT_LANGUAGE = 'eng' # Enforce English hegemony
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,7 +60,10 @@ verify_python3_env()
|
||||
|
||||
def check_options_languages(options):
|
||||
if not options.language:
|
||||
options.language = ['eng'] # Enforce English hegemony
|
||||
options.language = [DEFAULT_LANGUAGE]
|
||||
system_lang = locale.getlocale()[0]
|
||||
if system_lang and not system_lang.startswith('en'):
|
||||
log.debug("No language specified; assuming --language %s", DEFAULT_LANGUAGE)
|
||||
|
||||
# Support v2.x "eng+deu" language syntax
|
||||
if '+' in options.language[0]:
|
||||
@@ -369,7 +374,7 @@ def create_input_file(options, work_folder):
|
||||
else:
|
||||
try:
|
||||
target = os.path.join(work_folder, 'origin')
|
||||
re_symlink(options.input_file, target)
|
||||
safe_symlink(options.input_file, target)
|
||||
return target
|
||||
except FileNotFoundError:
|
||||
raise InputFileError(f"File not found - {options.input_file}")
|
||||
@@ -413,6 +418,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:
|
||||
@@ -427,7 +446,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
|
||||
)
|
||||
|
||||
+58
-46
@@ -18,8 +18,10 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
@@ -33,7 +35,7 @@ class TqdmConsole:
|
||||
|
||||
def __init__(self, file):
|
||||
self.file = file
|
||||
self.py36 = sys.version_info >= (3, 6)
|
||||
self.py36 = sys.version_info[0:2] == (3, 6)
|
||||
|
||||
def write(self, msg):
|
||||
# When no progress bar is active, tqdm.write() routes to print()
|
||||
@@ -122,13 +124,23 @@ def create_options(*, input_file, output_file, **kwargs):
|
||||
for arg, val in kwargs.items():
|
||||
if val is None:
|
||||
continue
|
||||
if arg == 'tesseract_env':
|
||||
|
||||
# These arguments with special handling for which we bypass
|
||||
# argparse
|
||||
if arg in {'tesseract_env', 'progress_bar'}:
|
||||
deferred.append((arg, val))
|
||||
continue
|
||||
|
||||
cmd_style_arg = arg.replace('_', '-')
|
||||
cmdline.append(f"--{cmd_style_arg}")
|
||||
|
||||
# Booleans are special: add only if True, omit for False
|
||||
if isinstance(val, bool):
|
||||
if val:
|
||||
cmdline.append(f"--{cmd_style_arg}")
|
||||
continue
|
||||
|
||||
# We have a parameter
|
||||
cmdline.append(f"--{cmd_style_arg}")
|
||||
if isinstance(val, (int, float)):
|
||||
cmdline.append(str(val))
|
||||
elif isinstance(val, str):
|
||||
@@ -148,58 +160,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.
|
||||
|
||||
|
||||
@@ -21,14 +21,39 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
from collections.abc import Mapping
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, run
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, run as subprocess_run
|
||||
|
||||
from ..exceptions import ExitCode, MissingDependencyError
|
||||
|
||||
log = logging.Logger(__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):
|
||||
if not env:
|
||||
env = os.environ
|
||||
program = _get_program(args, env)
|
||||
if os.name == 'nt' and program.lower().endswith('.py'):
|
||||
args = [sys.executable, program] + args[1:]
|
||||
else:
|
||||
args = [program] + args[1:]
|
||||
log.debug(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
|
||||
return subprocess_run(args, env=env, **kwargs)
|
||||
|
||||
|
||||
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env=None):
|
||||
"Get the version of the specified program"
|
||||
args_prog = [program, version_arg]
|
||||
@@ -111,23 +136,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 re
|
||||
import os
|
||||
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 subprocess import PIPE, CalledProcessError
|
||||
from shutil import which
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from ..exceptions import SubprocessOutputError
|
||||
from . import get_version
|
||||
from ..exceptions import SubprocessOutputError, MissingDependencyError
|
||||
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,58 @@ 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:
|
||||
with suppress(OSError):
|
||||
Path(output_file).unlink() # no unfinished files
|
||||
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 +227,7 @@ def generate_pdfa(
|
||||
output_file,
|
||||
compression,
|
||||
log,
|
||||
threads=1,
|
||||
threads=None, # deprecated parameter
|
||||
pdf_version='1.5',
|
||||
pdfa_part='2',
|
||||
):
|
||||
@@ -216,6 +250,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 +287,58 @@ 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
|
||||
log.debug(args_gs)
|
||||
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
|
||||
with suppress(OSError):
|
||||
Path(output_file).unlink()
|
||||
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 p.stdout.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....
|
||||
@@ -288,11 +347,4 @@ def generate_pdfa(
|
||||
"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')
|
||||
log.debug(stderr)
|
||||
|
||||
@@ -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,22 +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 os
|
||||
import shutil
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from contextlib import suppress
|
||||
from functools import lru_cache
|
||||
import logging
|
||||
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'))
|
||||
|
||||
@@ -52,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)
|
||||
|
||||
@@ -121,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)
|
||||
|
||||
|
||||
@@ -131,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
|
||||
|
||||
@@ -179,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:
|
||||
@@ -200,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):
|
||||
@@ -271,6 +275,9 @@ 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)
|
||||
@@ -314,7 +321,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
|
||||
|
||||
@@ -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
|
||||
@@ -29,7 +29,7 @@ from tempfile import TemporaryDirectory
|
||||
from PIL import Image
|
||||
|
||||
from ..exceptions import MissingDependencyError, SubprocessOutputError
|
||||
from . import get_version
|
||||
from . import get_version, run as external_run
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
@@ -76,7 +76,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+43
-17
@@ -34,12 +34,31 @@ from os import fspath
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
from .lib._leptonica import ffi
|
||||
from .exceptions import MissingDependencyError
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
lept = ffi.dlopen(find_library('lept'))
|
||||
if os.name == 'nt':
|
||||
libname = 'liblept-5'
|
||||
else:
|
||||
libname = 'lept'
|
||||
_libpath = find_library(libname)
|
||||
if not _libpath and os.name == 'nt':
|
||||
raise MissingDependencyError(
|
||||
"""
|
||||
---------------------------------------------------------------------
|
||||
This error normally occurs when ocrmypdf can't find a file named
|
||||
liblept-5.dll (Leptonica). Please ensure Tesseract-OCR is installed
|
||||
and its location is added to the system PATH environment variable.
|
||||
|
||||
For details see:
|
||||
https://ocrmypdf.readthedocs.io/en/latest/installation.html
|
||||
---------------------------------------------------------------------
|
||||
"""
|
||||
)
|
||||
lept = ffi.dlopen(_libpath)
|
||||
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
|
||||
|
||||
|
||||
@@ -292,9 +311,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 +323,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 +531,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
|
||||
@@ -111,6 +111,11 @@ def extract_image_generic(*, pike, root, log, image, xref, options):
|
||||
if pim.bits_per_component == 1:
|
||||
return None
|
||||
|
||||
try:
|
||||
pim.indexed # pikepdf 1.6.3 can't handle [/Indexed [/Array...]]
|
||||
except NotImplementedError:
|
||||
return None
|
||||
|
||||
if filtdp[0] == Name.DCTDecode and options.optimize >= 2:
|
||||
# This is a simple heuristic derived from some training data, that has
|
||||
# about a 70% chance of guessing whether the JPEG is high quality,
|
||||
@@ -487,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:
|
||||
@@ -533,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):
|
||||
@@ -544,11 +549,11 @@ def main(infile, outfile, level, jobs=1):
|
||||
"""Emulate ocrmypdf's options"""
|
||||
|
||||
def __init__(
|
||||
self, input_file, jobs, optimize, jpeg_quality, png_quality, jb2lossy
|
||||
self, input_file, jobs, optimize_, jpeg_quality, png_quality, jb2lossy
|
||||
):
|
||||
self.input_file = input_file
|
||||
self.jobs = jobs
|
||||
self.optimize = optimize
|
||||
self.optimize = optimize_
|
||||
self.jpeg_quality = jpeg_quality
|
||||
self.png_quality = png_quality
|
||||
self.jbig2_page_group_size = 0
|
||||
@@ -559,7 +564,7 @@ def main(infile, outfile, level, jobs=1):
|
||||
options = OptimizeOptions(
|
||||
input_file=infile,
|
||||
jobs=jobs,
|
||||
optimize=int(level),
|
||||
optimize_=int(level),
|
||||
jpeg_quality=0, # Use default
|
||||
png_quality=0,
|
||||
jb2lossy=False,
|
||||
|
||||
+2
-16
@@ -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
|
||||
|
||||
@@ -54,18 +52,7 @@ pdfa_def_template = u"""%!
|
||||
def
|
||||
|
||||
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
|
||||
[{icc_PDFA}
|
||||
<<
|
||||
/N currentpagedevice /ProcessColorModel known {
|
||||
currentpagedevice /ProcessColorModel get dup /DeviceGray eq
|
||||
{pop 1} {
|
||||
/DeviceRGB eq
|
||||
{3}{4} ifelse
|
||||
} ifelse
|
||||
} {
|
||||
(ERROR, unable to determine ProcessColorModel) == flush
|
||||
} ifelse
|
||||
>> /PUT pdfmark
|
||||
[{icc_PDFA} << /N 3 >> /PUT pdfmark
|
||||
[{icc_PDFA} ICCProfile /PUT pdfmark
|
||||
|
||||
% Define the output intent dictionary :
|
||||
@@ -95,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
|
||||
@@ -114,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
|
||||
|
||||
@@ -20,6 +20,7 @@ from math import copysign
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pdfminer
|
||||
import pdfminer.encodingdb
|
||||
import pdfminer.pdfdevice
|
||||
import pdfminer.pdfinterp
|
||||
@@ -36,51 +37,54 @@ from ..exceptions import EncryptedPdfError
|
||||
STRIP_NAME = re.compile(r'[0-9]+')
|
||||
|
||||
#
|
||||
# Unconditional pdfminer patches
|
||||
# pdfminer 20181108 patches
|
||||
#
|
||||
|
||||
if pdfminer.__version__ == '20181108':
|
||||
|
||||
def name2unicode(name):
|
||||
"""Fix pdfminer's name2unicode function
|
||||
def name2unicode(name):
|
||||
"""Fix pdfminer's name2unicode function
|
||||
|
||||
Font cids that are mapped to names of the form /g123 seem to be, by convention
|
||||
characters with no corresponding Unicode entry. These can be subsetted fonts
|
||||
or symbolic fonts. There seems to be no way to map /g123 fonts to Unicode,
|
||||
barring a ToUnicode data structure.
|
||||
"""
|
||||
if name in glyphname2unicode:
|
||||
return glyphname2unicode[name]
|
||||
if name.startswith('g') or name.startswith('a'):
|
||||
raise KeyError(name)
|
||||
if name.startswith('uni'):
|
||||
try:
|
||||
return chr(int(name[3:], 16))
|
||||
except ValueError: # Not hexadecimal
|
||||
Font cids that are mapped to names of the form /g123 seem to be, by convention
|
||||
characters with no corresponding Unicode entry. These can be subsetted fonts
|
||||
or symbolic fonts. There seems to be no way to map /g123 fonts to Unicode,
|
||||
barring a ToUnicode data structure.
|
||||
"""
|
||||
if name in glyphname2unicode:
|
||||
return glyphname2unicode[name]
|
||||
if name.startswith('g') or name.startswith('a'):
|
||||
raise KeyError(name)
|
||||
m = STRIP_NAME.search(name)
|
||||
if not m:
|
||||
raise KeyError(name)
|
||||
return chr(int(m.group(0)))
|
||||
if name.startswith('uni'):
|
||||
try:
|
||||
return chr(int(name[3:], 16))
|
||||
except ValueError: # Not hexadecimal
|
||||
raise KeyError(name)
|
||||
m = STRIP_NAME.search(name)
|
||||
if not m:
|
||||
raise KeyError(name)
|
||||
return chr(int(m.group(0)))
|
||||
|
||||
pdfminer.encodingdb.name2unicode = name2unicode
|
||||
|
||||
pdfminer.encodingdb.name2unicode = name2unicode
|
||||
original_PDFFont_init = PDFFont.__init__
|
||||
|
||||
original_PDFFont_init = PDFFont.__init__
|
||||
def PDFFont__init__(self, descriptor, widths, default_width=None):
|
||||
original_PDFFont_init(self, descriptor, widths, default_width)
|
||||
# PDF spec says descent should be negative
|
||||
# A font with a positive descent implies it floats entirely above the
|
||||
# baseline, i.e. it's not really a baseline anymore. I have fonts that
|
||||
# claim a positive descent, but treating descent as positive always seems
|
||||
# to misposition text.
|
||||
if self.descent > 0:
|
||||
self.descent = -self.descent
|
||||
|
||||
PDFFont.__init__ = PDFFont__init__
|
||||
|
||||
def PDFFont__init__(self, descriptor, widths, default_width=None):
|
||||
original_PDFFont_init(self, descriptor, widths, default_width)
|
||||
# PDF spec says descent should be negative
|
||||
# A font with a positive descent implies it floats entirely above the
|
||||
# baseline, i.e. it's not really a baseline anymore. I have fonts that
|
||||
# claim a positive descent, but treating descent as positive always seems
|
||||
# to misposition text.
|
||||
if self.descent > 0:
|
||||
self.descent = -self.descent
|
||||
#
|
||||
# end of pdfminer 20181108 patches
|
||||
#
|
||||
|
||||
|
||||
PDFFont.__init__ = PDFFont__init__
|
||||
|
||||
original_PDFSimpleFont_init = PDFSimpleFont.__init__
|
||||
|
||||
|
||||
@@ -97,6 +101,7 @@ def PDFSimpleFont__init__(self, descriptor, widths, spec):
|
||||
|
||||
|
||||
PDFSimpleFont.__init__ = PDFSimpleFont__init__
|
||||
|
||||
#
|
||||
# pdfminer patches when creator is PScript5.dll
|
||||
#
|
||||
@@ -207,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"]}
|
||||
|
||||
+54
-31
@@ -15,10 +15,10 @@
|
||||
# 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
|
||||
@@ -81,6 +81,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,39 +115,27 @@ 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')
|
||||
def spoof_tesseract_noop(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, tesseract='tesseract_noop.py')
|
||||
@@ -183,7 +188,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,6 +198,24 @@ 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 and let caller deal with results"
|
||||
|
||||
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)
|
||||
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"
|
||||
|
||||
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
|
||||
@@ -30,10 +30,7 @@ from subprocess import check_call
|
||||
PDF/A creation."""
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
from gs import real_ghostscript
|
||||
|
||||
|
||||
elision_warning = """GPL Ghostscript 9.20: Setting Overprint Mode to 1
|
||||
@@ -41,7 +38,6 @@ 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__))
|
||||
|
||||
@@ -27,15 +27,10 @@ import sys
|
||||
"""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
|
||||
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__))
|
||||
|
||||
@@ -27,14 +27,10 @@ import sys
|
||||
"""Replicate Ghostscript raster failure while allowing rendering"""
|
||||
|
||||
|
||||
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__))
|
||||
|
||||
@@ -26,14 +26,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__))
|
||||
|
||||
@@ -59,8 +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
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# © 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 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
|
||||
@@ -19,6 +19,11 @@ from subprocess import run, PIPE
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
pytest.helpers.running_in_docker(), # pylint: disable=no-member
|
||||
reason="docker can't complete",
|
||||
)
|
||||
|
||||
|
||||
def test_fish():
|
||||
try:
|
||||
|
||||
@@ -18,12 +18,47 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
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(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'
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
def linn(resources):
|
||||
@@ -79,3 +114,32 @@ def test_rasterize_rotated(linn, outdir, caplog):
|
||||
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])
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
+2
-5
@@ -16,14 +16,13 @@
|
||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import ocrmypdf
|
||||
import pikepdf
|
||||
|
||||
os_environ = pytest.helpers.os_environ
|
||||
|
||||
|
||||
def test_no_glyphless_graft(resources, outdir):
|
||||
pdf = pikepdf.open(resources / 'francais.pdf')
|
||||
@@ -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,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 pytest
|
||||
from PIL import Image
|
||||
import img2pdf
|
||||
import pikepdf
|
||||
|
||||
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
|
||||
+7
-13
@@ -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")
|
||||
|
||||
+80
-292
@@ -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,6 +38,7 @@ 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
|
||||
|
||||
|
||||
@@ -55,128 +55,10 @@ 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):
|
||||
@@ -217,7 +99,7 @@ def test_skip_ocr(spoof_tesseract_cache, resources, outpdf):
|
||||
assert pdfinfo[0].has_text
|
||||
|
||||
|
||||
def test_redo_ocr(spoof_tesseract_cache, resources, outpdf):
|
||||
def test_redo_ocr(resources, outpdf):
|
||||
in_ = resources / 'graph_ocred.pdf'
|
||||
before = PdfInfo(in_, detailed_page_analysis=True)
|
||||
out = 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,25 @@ 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):
|
||||
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 +300,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 +312,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 +350,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 +377,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 +394,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,27 +417,16 @@ language_model_penalty_non_freq_dict_word 0
|
||||
)
|
||||
|
||||
check_ocrmypdf(
|
||||
resources / 'ccitt.pdf', outdir / 'out.pdf', '--tesseract-config', cfg_file
|
||||
resources / '3small.pdf',
|
||||
outdir / 'out.pdf',
|
||||
'--tesseract-config',
|
||||
cfg_file,
|
||||
'--pages',
|
||||
'1',
|
||||
)
|
||||
|
||||
|
||||
@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',
|
||||
outdir / 'out.pdf',
|
||||
'--pdf-renderer',
|
||||
renderer,
|
||||
'--tesseract-config',
|
||||
cfg_file,
|
||||
)
|
||||
assert "Can't open" in err, "No error message about missing config file"
|
||||
assert p.returncode == ExitCode.ok, err
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_tesseract_config_invalid(renderer, resources, outdir):
|
||||
cfg_file = outdir / 'test.cfg'
|
||||
@@ -648,7 +445,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
|
||||
|
||||
|
||||
@@ -684,7 +484,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)
|
||||
|
||||
@@ -697,12 +497,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):
|
||||
@@ -716,22 +518,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",
|
||||
@@ -860,7 +646,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',
|
||||
@@ -868,10 +654,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
|
||||
@@ -887,7 +673,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
|
||||
|
||||
@@ -924,17 +710,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):
|
||||
@@ -945,6 +732,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')
|
||||
|
||||
@@ -21,6 +21,7 @@ from datetime import timezone
|
||||
import logging
|
||||
import mmap
|
||||
from os import fspath
|
||||
import os
|
||||
from pathlib import Path
|
||||
from shutil import copyfile, move
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -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'
|
||||
|
||||
+13
-13
@@ -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)
|
||||
+8
-17
@@ -18,7 +18,7 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from subprocess import DEVNULL, PIPE, run, Popen
|
||||
from subprocess import DEVNULL, PIPE, run, Popen, CalledProcessError
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -29,6 +29,7 @@ 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
|
||||
|
||||
|
||||
@@ -76,6 +77,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 +105,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,22 +116,10 @@ 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):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'trivial.pdf', os.devnull, '--force-ocr', env=spoof_tesseract_noop
|
||||
|
||||
@@ -37,30 +37,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
|
||||
|
||||
@@ -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,20 +15,22 @@
|
||||
# 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
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import ocrmypdf._validation as vd
|
||||
from ocrmypdf.api import create_options
|
||||
from ocrmypdf.exceptions import MissingDependencyError, BadArgsError
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
|
||||
|
||||
def make_opts(input_file='a.pdf', output_file='b.pdf', language='eng', **kwargs):
|
||||
return create_options(
|
||||
input_file=input_file, output_file=output_file, language=language, **kwargs
|
||||
)
|
||||
if language is not None:
|
||||
kwargs['language'] = language
|
||||
return create_options(input_file=input_file, output_file=output_file, **kwargs)
|
||||
|
||||
|
||||
def test_hocr_notlatin_warning(caplog):
|
||||
@@ -114,8 +116,63 @@ 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():
|
||||
opts = make_opts(keep_temporary_files=True)
|
||||
assert opts.keep_temporary_files == True
|
||||
opts = make_opts(keep_temporary_files=False)
|
||||
assert opts.keep_temporary_files == False
|
||||
|
||||
|
||||
@pytest.mark.parametrize('progress_bar', [True, False])
|
||||
def test_no_progress_bar(progress_bar, resources):
|
||||
opts = make_opts(progress_bar=progress_bar, input_file=(resources / 'trivial.pdf'))
|
||||
with patch('ocrmypdf.pdfinfo.info.tqdm', autospec=True) as tqdmpatch:
|
||||
vd.check_options(opts)
|
||||
pdfinfo = PdfInfo(opts.input_file, progbar=opts.progress_bar)
|
||||
assert pdfinfo is not None
|
||||
assert tqdmpatch.called
|
||||
_args, kwargs = tqdmpatch.call_args
|
||||
assert kwargs['disable'] != progress_bar
|
||||
|
||||
|
||||
def test_language_warning(caplog):
|
||||
opts = make_opts(language=None)
|
||||
caplog.set_level(logging.DEBUG)
|
||||
with patch(
|
||||
'ocrmypdf._validation.locale.getlocale', return_value=('en_US', 'UTF-8')
|
||||
):
|
||||
vd.check_options_languages(opts)
|
||||
assert opts.language == ['eng']
|
||||
assert '' in caplog.text
|
||||
|
||||
opts = make_opts(language=None)
|
||||
with patch(
|
||||
'ocrmypdf._validation.locale.getlocale', return_value=('fr_FR', 'UTF-8')
|
||||
):
|
||||
vd.check_options_languages(opts)
|
||||
assert opts.language == ['eng']
|
||||
assert 'assuming --language' in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user