Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f3fa46a40 | ||
|
|
b1f79e4d97 | ||
|
|
115d6df94f | ||
|
|
559af9635f | ||
|
|
cb06359c0b | ||
|
|
5e26bb29d9 | ||
|
|
b0e95842b8 | ||
|
|
08e678f21f | ||
|
|
c17817810f | ||
|
|
ff5c38b1f7 | ||
|
|
64314c1b82 | ||
|
|
83230097ae | ||
|
|
8f91acf956 | ||
|
|
d211722a2f | ||
|
|
56e6ed1249 | ||
|
|
21982cf1cb | ||
|
|
edc01408da | ||
|
|
aee33c87ed | ||
|
|
0dae1602c7 | ||
|
|
d926f07ac1 | ||
|
|
96045e98f4 | ||
|
|
01b7205e2c | ||
|
|
c8a4cbcf17 | ||
|
|
16b6442b23 | ||
|
|
183eafa587 | ||
|
|
47a2997538 | ||
|
|
74d98216f1 | ||
|
|
4bdebf573e | ||
|
|
1606b6a383 | ||
|
|
2a61902df5 | ||
|
|
01a1c2b576 | ||
|
|
c4f01de231 | ||
|
|
63a4a761dd | ||
|
|
d077c03686 | ||
|
|
c97ea1f2a9 | ||
|
|
fd27df2abb | ||
|
|
bf04f03c4c | ||
|
|
93e802f473 | ||
|
|
1464b9087a | ||
|
|
e8cc8fc879 | ||
|
|
fae2119b1e | ||
|
|
aa859a4139 | ||
|
|
b9b12e2879 | ||
|
|
cf643c9f43 | ||
|
|
5b1a7880a9 | ||
|
|
474b6b0500 | ||
|
|
6c8c1d8173 | ||
|
|
6a91fa637f | ||
|
|
2846fb4e31 | ||
|
|
a1033cdc64 | ||
|
|
204336e1a5 | ||
|
|
8954e6c3b9 | ||
|
|
fee22b6b0b | ||
|
|
2b82c31b85 | ||
|
|
9a4813089c | ||
|
|
554fcc8b9d | ||
|
|
345256ee99 | ||
|
|
58d1042147 | ||
|
|
7b7e3a3e03 | ||
|
|
1e7fbd4202 | ||
|
|
6e907856f2 | ||
|
|
8bc6019172 | ||
|
|
059f79242e | ||
|
|
89599b4812 | ||
|
|
a9f4047a97 | ||
|
|
23227ae763 | ||
|
|
4a9e9e9db2 | ||
|
|
88ef2718f1 | ||
|
|
e71e8ca3ad | ||
|
|
45e9257d6e | ||
|
|
2954e72652 | ||
|
|
199de96cff | ||
|
|
8ddbe81513 | ||
|
|
a3e26e0498 | ||
|
|
4ad129d8d8 | ||
|
|
dfb9fa0736 | ||
|
|
eb036898e9 | ||
|
|
7c6aa76a2a | ||
|
|
f035cb1088 | ||
|
|
35162166c5 | ||
|
|
107f6abcb1 | ||
|
|
760a939e7d | ||
|
|
72660d0dec | ||
|
|
8444a8f211 | ||
|
|
4a1fec8328 | ||
|
|
42547f6017 | ||
|
|
0ccf564f03 | ||
|
|
65c9a07dde | ||
|
|
4700a19322 | ||
|
|
3d3b3abc1b | ||
|
|
7cd2770a13 | ||
|
|
7b94129d9e | ||
|
|
d1a0065ef8 | ||
|
|
5a817370fd | ||
|
|
ab0a210763 | ||
|
|
9f800736bc | ||
|
|
c9a83afad6 | ||
|
|
5e14274f10 | ||
|
|
167470b4bd | ||
|
|
f06d3c2ec2 | ||
|
|
74c99a8a77 | ||
|
|
0e4d312ee2 | ||
|
|
589f19559d | ||
|
|
f28bc25dc0 | ||
|
|
a0657ad937 | ||
|
|
5b8d88af4c | ||
|
|
fa82b50340 | ||
|
|
005216bc57 |
+5
-2
@@ -11,11 +11,14 @@
|
||||
*/*/*/*/*/*/*.pyc
|
||||
*/*/*/*/*/*/*/*.pyc
|
||||
.cache/
|
||||
.eggs/
|
||||
.github/
|
||||
.git/
|
||||
.ipynb_checkpoints/
|
||||
.ruffus_history.sqlite
|
||||
bin/
|
||||
build/
|
||||
docs/
|
||||
dist/
|
||||
htmlcov/
|
||||
include/
|
||||
@@ -27,5 +30,5 @@ tests/cache/
|
||||
tests/output/
|
||||
tests/resources/private/
|
||||
tmp/
|
||||
venv-3.4/
|
||||
venv-3.5/
|
||||
venv*/
|
||||
wheelhouse/
|
||||
|
||||
@@ -6,5 +6,8 @@
|
||||
*.jar binary
|
||||
*.pdf binary
|
||||
*.PDF binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
|
||||
.git_archival.txt export-subst
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
venv*/
|
||||
pyvenv.cfg
|
||||
tasks.py
|
||||
.bash_history
|
||||
.ruffus_history.sqlite
|
||||
.idea/
|
||||
|
||||
# Package building
|
||||
*.egg-info/
|
||||
@@ -19,6 +22,7 @@ ocrmypdf/version.py
|
||||
docs/_build/
|
||||
docs/_static/
|
||||
docs/_templates/
|
||||
docs/Makefile
|
||||
|
||||
# Code coverage
|
||||
.coverage
|
||||
@@ -27,8 +31,11 @@ htmlcov/
|
||||
# Testing
|
||||
log/
|
||||
/*.pdf
|
||||
/*.qdf
|
||||
*.ipynb
|
||||
.ipynb_checkpoints/
|
||||
tests/cache/
|
||||
tests/output/
|
||||
tests/resources/private/
|
||||
tmp/
|
||||
pdfbox-app*.jar
|
||||
+83
-35
@@ -1,47 +1,95 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
language: python
|
||||
|
||||
cache:
|
||||
pip: true
|
||||
directories:
|
||||
- packages
|
||||
- tests/cache
|
||||
pip: true
|
||||
directories:
|
||||
- packages
|
||||
- tests/cache
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
|
||||
python:
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
env:
|
||||
global:
|
||||
- secure: "oyX5xesoHD7qcDXKIxMyGZPi+H/WxcvfFkaviEmq84K1DDyHk48+9e92IKgrw8/lcTADnEo/AgVKfnhCPflFimk1xTkgaK4sUg1WLI2YjmaHcwl5SlBHa2rN3uGBwy1hyP92qyv/mMc9R59NtRJ8u76lbn6eN9wi7lkFWdE6BTw=" # DOCKERHUB_OCRMYPDF_TOKEN
|
||||
- secure: "WlyII8YLsiUUyLtEA563GvEZmbneDb/T8q/P1uNbyQ2ps1U82tH0nSUV2CspSMxOFtZzPHCrRvnAmuTYKshBj+GNnBb1J9FKQmFwF+4NPeqsFdUkQ1NeeCmfIRShuNC3Otg2GGwj4Zssdg+QnVy43t2L11qizzfY+lY+MVzAYcM=" # DOCKERHUB_OCRMYPDF_TESS4_TOKEN
|
||||
- secure: "hsf6MT+n2x3OiDM2fQyJZdV0/PWYmv81LdVqC6cfnHBE/8N3DloJRqQ7WfO14TxhiK9PEC7MpyCj0lSabUHEO7gSH6Vks6I1asoSkt8S9/bSMlhT4hei+pwVpeGEiU5xHVATNjY+D919VC3IFvc3XmjT74h/2SLhaZ+jhEmDggM=" # HOMEBREW_OCRMYPDF_TOKEN
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
sudo: required
|
||||
language: python
|
||||
python: 3.5
|
||||
- os: linux
|
||||
sudo: required
|
||||
language: python
|
||||
python: 3.6
|
||||
- os: osx
|
||||
osx_image: xcode8
|
||||
language: generic
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.cache/pip/log/debug.log
|
||||
- rm -f $HOME/.cache/pip/log/debug.log
|
||||
|
||||
before_install:
|
||||
# Ubuntu packages
|
||||
- sudo add-apt-repository ppa:vshn/ghostscript -y # for ghostscript 9.16 (trusty has 9.10)
|
||||
- sudo add-apt-repository ppa:heyarje/libav-11 -y # for libav11, which is for unpaper
|
||||
- sudo apt-get update -qq # must go after all add-apt-repo
|
||||
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils libavformat56 libavcodec56 libavutil54 libffi-dev
|
||||
|
||||
# pip
|
||||
- pip install --upgrade pip
|
||||
|
||||
# install unpaper from a previously built .deb since ubuntu's version is too old
|
||||
- mkdir -p packages
|
||||
- "[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb"
|
||||
- sudo dpkg -i packages/unpaper_6.1-1.deb
|
||||
before_install: |
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
|
||||
bash .travis/linux_before_install.sh
|
||||
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
bash .travis/osx_before_install.sh
|
||||
fi
|
||||
|
||||
install:
|
||||
- pip install .
|
||||
- pip install -r requirements.txt -r test_requirements.txt
|
||||
- pip3 install .
|
||||
- pip3 install -r requirements.txt -r test_requirements.txt
|
||||
|
||||
script:
|
||||
# "pip install" installs the ocrmypdf into the system environment.
|
||||
# The working copy of ocrmypdf is not an installed version and its cffi
|
||||
# module has not been compiled. As such, move the local copy out of the
|
||||
# way and ensure that the system copy gets tested.
|
||||
- mv ocrmypdf dont_import_this_ocrmypdf
|
||||
- pytest
|
||||
- mv ocrmypdf dont_import_this_ocrmypdf
|
||||
- pytest -n auto
|
||||
- mv dont_import_this_ocrmypdf ocrmypdf
|
||||
|
||||
os:
|
||||
- linux
|
||||
deploy:
|
||||
# release for main pypi
|
||||
# 3.6 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.6" && $TRAVIS_OS_NAME == "linux"
|
||||
skip_upload_docs: true
|
||||
|
||||
# test pypi
|
||||
- provider: pypi
|
||||
server: https://testpypi.python.org/pypi
|
||||
user: ocrmypdf-travis
|
||||
password:
|
||||
secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
|
||||
distributions: "sdist"
|
||||
on:
|
||||
branch: develop
|
||||
tags: false
|
||||
condition: $TRAVIS_OS_NAME == "osx"
|
||||
skip_upload_docs: true
|
||||
|
||||
# null deploy for osx
|
||||
# we really just want to run after_deploy *after* pypi upload is done, but
|
||||
# after_deploy on runs if a given box deployed
|
||||
- provider: script
|
||||
script: .travis/null_deploy.sh
|
||||
on:
|
||||
branch: master
|
||||
tags: false
|
||||
condition: $TRAVIS_OS_NAME == "osx"
|
||||
|
||||
after_deploy: |
|
||||
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
bash .travis/osx_brew.sh
|
||||
elif [[ "$TRAVIS_PYTHON_VERSION" == "3.6" && "$TRAVIS_OS_NAME" == "linux" ]]; then
|
||||
curl -H "Content-Type: application/json" --data '{"build": true}' -X POST https://registry.hub.docker.com/u/jbarlow83/ocrmypdf/trigger/$DOCKERHUB_OCRMYPDF_TOKEN/
|
||||
curl -H "Content-Type: application/json" --data '{"build": true}' -X POST https://registry.hub.docker.com/u/jbarlow83/ocrmypdf-tess4/trigger/$DOCKERHUB_OCRMYPDF_TESS4_TOKEN/
|
||||
fi
|
||||
@@ -0,0 +1,74 @@
|
||||
from string import Template
|
||||
from subprocess import run, PIPE
|
||||
import re
|
||||
|
||||
recipe_template = Template("""
|
||||
class Ocrmypdf < Formula
|
||||
include Language::Python::Virtualenv
|
||||
|
||||
desc "Adds an OCR text layer to scanned PDF files"
|
||||
homepage "https://github.com/jbarlow83/OCRmyPDF"
|
||||
${ocrmypdf_url}
|
||||
${ocrmypdf_sha256}
|
||||
|
||||
depends_on :python3
|
||||
depends_on "pkg-config" => :build
|
||||
depends_on "libffi"
|
||||
depends_on "tesseract"
|
||||
depends_on "ghostscript"
|
||||
depends_on "unpaper"
|
||||
depends_on "qpdf"
|
||||
|
||||
# For Pillow source install
|
||||
depends_on "openjpeg"
|
||||
depends_on "freetype"
|
||||
depends_on "libpng"
|
||||
|
||||
${resources}
|
||||
|
||||
def install
|
||||
ENV.append ["SETUPTOOLS_SCM_PRETEND_VERSION"], "v${ocrmypdf_version}"
|
||||
ENV.each do |key, value|
|
||||
puts "#{key}:#{value}"
|
||||
end
|
||||
virtualenv_install_with_resources
|
||||
end
|
||||
|
||||
test do
|
||||
# `test do` will create, run in and delete a temporary directory.
|
||||
#
|
||||
# The installed folder is not in the path, so use the entire path to any
|
||||
# executables being tested: `system "#{bin}/program", "do", "something"`.
|
||||
system "#{bin}/ocrmypdf", "--version"
|
||||
end
|
||||
end
|
||||
""")
|
||||
|
||||
def main():
|
||||
p = run(['poet', '--single', 'ocrmypdf'],
|
||||
encoding='utf-8', stdout=PIPE, check=True)
|
||||
|
||||
ocrmypdf_lines = p.stdout.splitlines()
|
||||
ocrmypdf_url = ocrmypdf_lines[1].strip()
|
||||
ocrmypdf_sha256 = ocrmypdf_lines[2].strip()
|
||||
|
||||
ocrmypdf_version = re.search(
|
||||
r'ocrmypdf-(.+)\.tar.*', ocrmypdf_url).group(1)
|
||||
print(f"Autobrewing {ocrmypdf_version}")
|
||||
|
||||
p = run(['poet', '--resources', 'ocrmypdf'],
|
||||
encoding='utf-8', stdout=PIPE, check=True)
|
||||
|
||||
poet_resources = p.stdout
|
||||
|
||||
# Remove the duplicate "ocrmypdf" resource block
|
||||
all_resources = poet_resources.split('resource')
|
||||
kept_resources = [block for block in all_resources if 'ocrmypdf' not in block]
|
||||
resources = 'resource'.join(kept_resources)
|
||||
|
||||
with open('ocrmypdf.rb', 'w') as out:
|
||||
out.write(recipe_template.substitute(**locals()))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
sudo add-apt-repository ppa:vshn/ghostscript -y
|
||||
sudo add-apt-repository ppa:heyarje/libav-11 -y
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y \
|
||||
ghostscript \
|
||||
qpdf \
|
||||
poppler-utils \
|
||||
libavformat56 \
|
||||
libavcodec56 \
|
||||
libavutil54 \
|
||||
libffi-dev
|
||||
|
||||
sudo add-apt-repository ppa:alex-p/tesseract-ocr -y
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get autoremove -y
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
tesseract-ocr-fra \
|
||||
tesseract-ocr-deu
|
||||
|
||||
pip install --upgrade pip
|
||||
mkdir -p packages
|
||||
[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb
|
||||
sudo dpkg -i packages/unpaper_6.1-1.deb
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
brew update
|
||||
|
||||
brew install openjpeg jbig2dec libtiff # image libraries
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2 libffi leptonica
|
||||
brew install unpaper # optional
|
||||
brew install tesseract
|
||||
|
||||
pip3 install --upgrade pip
|
||||
pip3 install wheel
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
pip3 install homebrew-pypi-poet
|
||||
python3 .travis/autobrew.py
|
||||
cat ocrmypdf.rb
|
||||
brew audit ocrmypdf.rb
|
||||
|
||||
# Important: disable debug output so token is hidden
|
||||
set +x
|
||||
git clone https://$HOMEBREW_OCRMYPDF_TOKEN@github.com/jbarlow83/homebrew-ocrmypdf.git
|
||||
set -x
|
||||
|
||||
pushd homebrew-ocrmypdf
|
||||
cp ../ocrmypdf.rb Formula/ocrmypdf.rb
|
||||
git add Formula/ocrmypdf.rb
|
||||
git commit -m "homebrew-ocrmypdf: automatic release $TRAVIS_BUILD_NUMBER $TRAVIS_TAG"
|
||||
git push origin master
|
||||
popd
|
||||
+41
-78
@@ -1,91 +1,54 @@
|
||||
# OCRmyPDF
|
||||
#
|
||||
# VERSION 3.2
|
||||
FROM debian:stretch
|
||||
# VERSION 4.4.2
|
||||
FROM ubuntu:16.10
|
||||
MAINTAINER James R. Barlow <jim@purplerock.ca>
|
||||
|
||||
# Add unprivileged user
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
software-properties-common python-software-properties \
|
||||
python3-wheel \
|
||||
python3-reportlab \
|
||||
python3-venv \
|
||||
ghostscript \
|
||||
qpdf \
|
||||
poppler-utils \
|
||||
unpaper \
|
||||
libffi-dev \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
tesseract-ocr-fra \
|
||||
tesseract-ocr-spa \
|
||||
tesseract-ocr-deu
|
||||
|
||||
RUN python3 -m venv --system-site-packages /appenv
|
||||
|
||||
# This installs the latest binary wheel instead of the code in the current
|
||||
# folder. Installing from source will fail, apparently because cffi needs
|
||||
# build-essentials (gcc) to do a source installation
|
||||
# (i.e. "pip install ."). It's unclear to me why this is the case.
|
||||
RUN . /appenv/bin/activate; \
|
||||
pip install --upgrade pip \
|
||||
&& pip install ocrmypdf
|
||||
|
||||
# Now copy the application in, mainly to get the test suite.
|
||||
# Do this now to make the best use of Docker cache.
|
||||
COPY . /application
|
||||
RUN . /appenv/bin/activate; \
|
||||
pip install -r /application/test_requirements.txt
|
||||
|
||||
# Remove the junk, including the source version of application since it was
|
||||
# already installed
|
||||
RUN rm -rf /tmp/* /var/tmp/* /root/* /application/ocrmypdf \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get autoclean -y
|
||||
|
||||
RUN useradd docker \
|
||||
&& mkdir /home/docker \
|
||||
&& chown docker:docker /home/docker
|
||||
|
||||
# Update system and install our dependencies
|
||||
# If this command takes too Docker hub's automated build will timeout,
|
||||
# so try it in portions
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
locales \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
python3-reportlab \
|
||||
python3-pil \
|
||||
python3-wheel
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
unpaper \
|
||||
qpdf \
|
||||
poppler-utils \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu tesseract-ocr-spa tesseract-ocr-eng tesseract-ocr-fra
|
||||
|
||||
RUN apt-get install -qy --no-install-recommends \
|
||||
libffi-dev \
|
||||
libpython3-dev \
|
||||
gcc
|
||||
|
||||
# Install Ghostscript from Debian sid to work around JPEG 2000 issue in
|
||||
# Debian stretch libgs9 or gs 9.16~dfsg-2.1
|
||||
|
||||
COPY ./share/etc-apt-sources.list /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y ghostscript/sid
|
||||
|
||||
|
||||
# Enforce UTF-8
|
||||
# Borrowed from https://index.docker.io/u/crosbymichael/python/
|
||||
RUN dpkg-reconfigure locales && \
|
||||
locale-gen C.UTF-8 && \
|
||||
/usr/sbin/update-locale LANG=C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
|
||||
# Set up a Python virtualenv and take all of the system packages, so we can
|
||||
# rely on the platform packages rather than importing GCC and compiling them
|
||||
RUN pyvenv /appenv \
|
||||
&& pyvenv --system-site-packages /appenv
|
||||
|
||||
COPY . /application/
|
||||
|
||||
# Replace stock Tesseract 3.04.00 font with improved sharp2.ttf that resolves
|
||||
# issues in many PDF viewers.
|
||||
# Discussion is in https://github.com/tesseract-ocr/tesseract/issues/182
|
||||
COPY ./share/sharp2.ttf /usr/share/tesseract-ocr/tessdata/pdf.ttf
|
||||
RUN chmod 644 /usr/share/tesseract-ocr/tessdata/pdf.ttf
|
||||
|
||||
# Set this here to force a docker version, allowing non-tagged versions to
|
||||
# be built
|
||||
# ENV SETUPTOOLS_SCM_PRETEND_VERSION=v3.3.0
|
||||
|
||||
# Install application and dependencies
|
||||
# In this arrangement Pillow and reportlab will be provided by the system
|
||||
# Even though ocrmypdf is locally present, pull from PyPI because
|
||||
# Dockerhub and setuptools_scm clash
|
||||
RUN . /appenv/bin/activate; \
|
||||
pip install --upgrade pip \
|
||||
&& pip install ocrmypdf \
|
||||
&& pip install --no-cache-dir -r /application/test_requirements.txt
|
||||
|
||||
# Remove the junk
|
||||
RUN apt-get remove -qy gcc
|
||||
RUN apt-get autoremove -y && apt-get clean -y
|
||||
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /root/*
|
||||
|
||||
USER docker
|
||||
WORKDIR /home/docker
|
||||
|
||||
ENV OCRMYPDF_TEST_OUTPUT=/tmp/test-output
|
||||
ENV OCRMYPDF_SHARP_TTF=1
|
||||
|
||||
# Must use array form of ENTRYPOINT
|
||||
# Non-array form does not append other arguments, because that is "intuitive"
|
||||
ENTRYPOINT ["/application/docker-wrapper.sh"]
|
||||
ENTRYPOINT ["/application/docker-wrapper.sh"]
|
||||
|
||||
+5
-2
@@ -1,14 +1,17 @@
|
||||
# OCRmyPDF polyglot
|
||||
#
|
||||
# VERSION 3.2
|
||||
# VERSION 4.4.2
|
||||
FROM jbarlow83/ocrmypdf:latest
|
||||
MAINTAINER James R. Barlow <jim@purplerock.ca>
|
||||
|
||||
# Update system and install our dependencies
|
||||
USER root
|
||||
|
||||
# Update system and install our dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tesseract-ocr-all
|
||||
|
||||
RUN apt-get autoremove -y && apt-get clean -y
|
||||
|
||||
USER docker
|
||||
|
||||
# Must use array form of ENTRYPOINT
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# OCRmyPDF
|
||||
#
|
||||
# VERSION 4.4.2
|
||||
FROM ubuntu:16.10
|
||||
MAINTAINER James R. Barlow <jim@purplerock.ca>
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
software-properties-common python-software-properties \
|
||||
python3-wheel \
|
||||
python3-reportlab \
|
||||
python3-venv \
|
||||
ghostscript \
|
||||
qpdf \
|
||||
poppler-utils \
|
||||
unpaper \
|
||||
libffi-dev
|
||||
|
||||
RUN add-apt-repository ppa:alex-p/tesseract-ocr
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
tesseract-ocr-fra \
|
||||
tesseract-ocr-deu \
|
||||
tesseract-ocr-spa \
|
||||
tesseract-ocr-por \
|
||||
tesseract-ocr-ara \
|
||||
tesseract-ocr-rus \
|
||||
tesseract-ocr-chi-sim
|
||||
|
||||
RUN python3 -m venv --system-site-packages /appenv
|
||||
|
||||
# This installs the latest binary wheel instead of the code in the current
|
||||
# folder. Installing from source will fail, apparently because cffi needs
|
||||
# build-essentials (gcc) to do a source installation
|
||||
# (i.e. "pip install ."). It's unclear to me why this is the case.
|
||||
RUN . /appenv/bin/activate; \
|
||||
pip install --upgrade pip \
|
||||
&& pip install ocrmypdf
|
||||
|
||||
# Now copy the application in, mainly to get the test suite.
|
||||
# Do this now to make the best use of Docker cache.
|
||||
COPY . /application
|
||||
RUN . /appenv/bin/activate; \
|
||||
pip install -r /application/test_requirements.txt
|
||||
|
||||
# Remove the junk, including the source version of application since it was
|
||||
# already installed
|
||||
RUN rm -rf /tmp/* /var/tmp/* /root/* /application/ocrmypdf \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get autoclean -y
|
||||
|
||||
RUN useradd docker \
|
||||
&& mkdir /home/docker \
|
||||
&& chown docker:docker /home/docker
|
||||
|
||||
USER docker
|
||||
WORKDIR /home/docker
|
||||
|
||||
# Must use array form of ENTRYPOINT
|
||||
# Non-array form does not append other arguments, because that is "intuitive"
|
||||
ENTRYPOINT ["/application/docker-wrapper.sh"]
|
||||
@@ -36,7 +36,6 @@ recursive-exclude docs/_build *
|
||||
|
||||
# support files
|
||||
recursive-include ocrmypdf/data *
|
||||
recursive-include share *
|
||||
include *.py
|
||||
exclude tasks.py
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
##############################################################################
|
||||
# Copyright (c) 2013-14: fritz-hh from Github (https://github.com/fritz-hh)
|
||||
##############################################################################
|
||||
|
||||
echo "This script is deprecated. Use 'ocrmypdf' instead."
|
||||
|
||||
python3 -m ocrmypdf.main "$@"
|
||||
+12
-2
@@ -35,7 +35,7 @@ Main features
|
||||
- Supports more than `100 languages <https://github.com/tesseract-ocr/tessdata>`_ recognized by Tesseract
|
||||
- Battle-tested on thousands of PDFs, a test suite and continuous integration
|
||||
|
||||
For details: please consult the `release notes <RELEASE_NOTES.rst>`_.
|
||||
For details: please consult the `documentation <https://ocrmypdf.readthedocs.io/en/latest/>`_.
|
||||
|
||||
Motivation
|
||||
----------
|
||||
@@ -60,7 +60,17 @@ Installation
|
||||
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
|
||||
|
||||
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
|
||||
``apt-get install ocrmypdf``.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
apt-get install ocrmypdf
|
||||
|
||||
and macOS users may simply
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew tap jbarlow83/ocrmypdf
|
||||
brew install ocrmypdf
|
||||
|
||||
For everyone else, `see our documentation <https://ocrmypdf.readthedocs.io/en/latest/installation.html>`_ for installation steps.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
check-manifest >= 0.34
|
||||
check-manifest >= 0.35
|
||||
twine >= 1.8.1
|
||||
coverage >= 4.3.1
|
||||
pytest-xdist >= 1.15.0
|
||||
GitPython == 2.1.0
|
||||
coverage >= 4.4
|
||||
pytest-xdist >= 1.16.0
|
||||
GitPython == 2.1.3
|
||||
|
||||
# Known good versions: 1.11.1
|
||||
# Known broken versions: 1.15.0
|
||||
|
||||
+20
-2
@@ -10,11 +10,29 @@ Consider using the excellent `GNU Parallel <https://www.gnu.org/software/paralle
|
||||
|
||||
Both ``parallel`` and ``ocrmypdf`` will try to use all available processors. To maximize parallelism without overloading your system with processes, consider using ``parallel -j 2`` to limit parallel to running two jobs at once.
|
||||
|
||||
This command will run all ocrmypdf all files named ``*.pdf`` in the current directory and write them to the previous created ``output/`` folder.
|
||||
This command will run all ocrmypdf all files named ``*.pdf`` in the current directory and write them to the previous created ``output/`` folder. It will not search subdirectories.
|
||||
|
||||
The ``--tag`` argument tells parallel to print the filename as a prefix whenever a message is printed, so that one can trace any errors to the file that produced them.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
parallel -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
|
||||
parallel --tag -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
|
||||
|
||||
Directory trees
|
||||
---------------
|
||||
|
||||
This will walk through a directory tree and run OCR on all files in place, printing the output in a way that makes
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
find . --printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
|
||||
|
||||
This only runs one ``ocrmypdf`` process at a time. This variation uses ``find`` to create a directory list and ``parallel`` to parallelize runs of ``ocrmypdf``, again updating files in place.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
find . -name '*.pdf' | parallel --tag -j 2 ocrmypdf '{}' '{}'
|
||||
|
||||
|
||||
Sample script
|
||||
"""""""""""""
|
||||
|
||||
@@ -28,6 +28,13 @@ Add an OCR layer and output a standard PDF
|
||||
|
||||
ocrmypdf --output-type pdf input.pdf output.pdf
|
||||
|
||||
Create a PDF/A with all color and grayscale images converted to JPEG
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --output-type pdfa --pdfa-image-compression jpeg input.pdf output.pdf
|
||||
|
||||
Modify a file in place
|
||||
""""""""""""""""""""""
|
||||
|
||||
|
||||
+7
-4
@@ -11,14 +11,18 @@ be searched.
|
||||
|
||||
PDFs are the best format for scanned documents. Unfortunately, PDFs can be difficult to work with. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs.
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:maxdepth: 1
|
||||
|
||||
introduction
|
||||
release_notes
|
||||
installation
|
||||
languages
|
||||
|
||||
.. toctree::
|
||||
:caption: Usage
|
||||
:maxdepth: 2
|
||||
|
||||
cookbook
|
||||
advanced
|
||||
batch
|
||||
@@ -26,7 +30,6 @@ Contents:
|
||||
errors
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
||||
+149
-106
@@ -1,14 +1,27 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Installing on Debian and Ubuntu
|
||||
-------------------------------
|
||||
OCRmyPDF requires Python 3.5 (or newer) and Tesseract 3.04 (or newer).
|
||||
|
||||
Installing on Debian and Ubuntu 16.10 or newer
|
||||
----------------------------------------------
|
||||
|
||||
Users of Debian 9 ("stretch") or later or Ubuntu 16.10 or later may simply
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
apt-get install ocrmypdf
|
||||
apt-get install ocrmypdf
|
||||
|
||||
Installing on macOS
|
||||
-------------------
|
||||
|
||||
.. code-block:: bash
|
||||
brew tap jbarlow83/ocrmypdf
|
||||
brew install ocrmypdf
|
||||
|
||||
.. warning::
|
||||
|
||||
Users who previously installed OCRmyPDF on macOS using ``pip install ocrmypdf`` should remove the pip version (``pip3 uninstall ocrmypdf``) before switching to the Homebrew version.
|
||||
|
||||
.. _Docker:
|
||||
|
||||
@@ -24,69 +37,88 @@ Follow the Docker installation instructions for your platform. If you can run t
|
||||
successfully, your system is ready to download and execute the image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run hello-world
|
||||
docker run hello-world
|
||||
|
||||
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
|
||||
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 engine host, and then follow these optional steps to enable multiple CPUs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Optional step for Mac OS X users
|
||||
docker-machine stop "yourVM"
|
||||
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
|
||||
docker-machine start "yourVM"
|
||||
eval $(docker-machine env "yourVM")
|
||||
# Optional step for Mac OS X users
|
||||
docker-machine stop "yourVM"
|
||||
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
|
||||
docker-machine start "yourVM"
|
||||
eval $(docker-machine env "yourVM")
|
||||
|
||||
Assuming you have a Docker engine running somewhere, you can run these commands to download
|
||||
the image:
|
||||
Assuming you have a Docker engine running, you can download one of the three available images:
|
||||
|
||||
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
|
||||
| Image name | Download command | Notes |
|
||||
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
|
||||
| ocrmypdf | ``docker pull jbarlow83/ocrmypdf`` | Latest ocrmypdf with Tesseract 3.04. Includes English, French, German, Spanish. |
|
||||
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
|
||||
| ocrmypdf-polyglot | ``docker pull jbarlow83/ocrmypdf-polyglot`` | As above, with all available language packs. |
|
||||
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
|
||||
| ocrmypdf-tess4 | ``docker pull jbarlow83/ocrmypdf-tess4`` | Latest ocrmypdf with Tesseract 4.00.00alpha and English, French, German, |
|
||||
| | | Spanish, Portuguese, Chinese Simplified, Arabic and Russian (the top 8). |
|
||||
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker pull jbarlow83/ocrmypdf
|
||||
docker pull jbarlow83/ocrmypdf-tess4
|
||||
|
||||
Then tag it to give a more convenient name, just ocrmypdf:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||
docker tag jbarlow83/ocrmypdf-tess4 ocrmypdf
|
||||
|
||||
.. _docker-polyglot:
|
||||
|
||||
This image contains language packs for English, French, Spanish and German. The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Alternative step: If you need all language packs
|
||||
docker pull jbarlow83/ocrmypdf-polyglot
|
||||
docker tag jbarlow83/ocrmypdf-polyglot ocrmypdf
|
||||
The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_.
|
||||
|
||||
You can then run ocrmypdf using the command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm ocrmypdf --help
|
||||
docker run --rm ocrmypdf --help
|
||||
|
||||
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, and both the input and output file must be inside the writable volume. This example command uses the current working directory as the writable volume:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
|
||||
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
|
||||
|
||||
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
|
||||
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
|
||||
|
||||
.. note:: The working directory should be a writable local volume or Docker may not have permission to access it.
|
||||
|
||||
Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` image name and all arguments to ``ocrmypdf`` should be listed after.
|
||||
|
||||
For convenience, a shell alias can hide the docker command:
|
||||
|
||||
Installing on macOS (formerly Mac OS X)
|
||||
---------------------------------------
|
||||
.. code-block:: bash
|
||||
|
||||
These instructions probably work on all macOS supported by Homebrew. OCRmyPDF is known to work on Yosemite and El Capitan, and regularly tested on El Capitan.
|
||||
alias ocrmypdf='docker run --rm -v "$(pwd):/home/docker" ocrmypdf'
|
||||
ocrmypdf --version # runs docker version
|
||||
|
||||
Or in the wonderful `fish shell <https://fishshell.com/>`_:
|
||||
|
||||
.. code-block:: fish
|
||||
|
||||
alias ocrmypdf 'docker run --rm -v (pwd):/home/docker ocrmypdf'
|
||||
funcsave ocrmypdf
|
||||
|
||||
|
||||
Manual installation on macOS
|
||||
----------------------------
|
||||
|
||||
These instructions probably work on all macOS supported by Homebrew.
|
||||
|
||||
If it's not already present, `install Homebrew <http://brew.sh/>`_.
|
||||
|
||||
@@ -94,51 +126,51 @@ Update Homebrew:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew update
|
||||
|
||||
brew update
|
||||
|
||||
Install or upgrade the required Homebrew packages, if any are missing:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install libpng openjpeg jbig2dec libtiff # image libraries
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2 libffi leptonica
|
||||
brew install unpaper # optional
|
||||
brew install libpng openjpeg jbig2dec libtiff # image libraries
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2 libffi leptonica
|
||||
brew install unpaper # optional
|
||||
|
||||
Python 3.4, 3.5 and 3.6 are supported.
|
||||
Python 3.5 and 3.6 are supported.
|
||||
|
||||
Install the required Tesseract OCR engine with the language packs you plan to use:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install tesseract # Option 1: for English, French, German, Spanish
|
||||
brew install tesseract # Option 1: for English, French, German, Spanish
|
||||
|
||||
.. _macos-all-languages:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install tesseract --with-all-languages # Option 2: for all language packs
|
||||
|
||||
|
||||
brew install tesseract --with-all-languages # Option 2: for all language packs
|
||||
|
||||
Update the homebrew pip and install Pillow:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade pillow
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade pillow
|
||||
|
||||
You can then install OCRmyPDF from PyPI:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install ocrmypdf
|
||||
pip3 install ocrmypdf
|
||||
|
||||
The command line program should now be available:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
ocrmypdf --help
|
||||
|
||||
|
||||
Installing on Ubuntu 16.04 LTS
|
||||
@@ -148,99 +180,110 @@ No package is currently available for Ubuntu 16.04, but you can install the depe
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install \
|
||||
unpaper \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-pil \
|
||||
python3-pytest \
|
||||
python3-reportlab \
|
||||
python3-ruffus \
|
||||
python3-cffi
|
||||
sudo apt-get update
|
||||
sudo apt-get install \
|
||||
unpaper \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-cffi
|
||||
|
||||
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
|
||||
into your system Python, which could interfere with other programs):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install ocrmypdf
|
||||
sudo pip3 install ocrmypdf
|
||||
|
||||
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
|
||||
follow these steps.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python3 -m venv --system-site-packages venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip3 install ocrmypdf
|
||||
python3 -m venv venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip3 install ocrmypdf
|
||||
|
||||
|
||||
Installing on Ubuntu 14.04 LTS
|
||||
------------------------------
|
||||
|
||||
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation.
|
||||
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation and because OCRmyPDF depends on some packages newer than are available in the main distribution.
|
||||
|
||||
Add new "apt" repositories needed for backports of Ghostscript 9.16 and libav-11, which supports unpaper 6.1. This will replace Ghostscript on your system.
|
||||
Add new "apt" repositories needed for backports of Ghostscript 9.16, libav-11 (for unpaper 6.1) and Tesseract 4.00 (alpha). This will replace Ghostscript and Tesseract 3.x on your system. If you prefer to not modify your system in this matter, consider using a Docker container.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo add-apt-repository ppa:vshn/ghostscript -y
|
||||
sudo add-apt-repository ppa:heyarje/libav-11 -y
|
||||
sudo add-apt-repository ppa:vshn/ghostscript -y
|
||||
sudo add-apt-repository ppa:heyarje/libav-11 -y
|
||||
sudo add-apt-repository ppa:alex-p/tesseract-ocr
|
||||
|
||||
Update apt-get:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get update
|
||||
|
||||
sudo apt-get update
|
||||
|
||||
Install system dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
libffi-dev \
|
||||
libavformat56 libavcodec56 libavutil54 \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-pil \
|
||||
python3-pytest \
|
||||
python3-reportlab
|
||||
sudo apt-get install \
|
||||
software-properties-common python-software-properties \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
libffi-dev \
|
||||
libavformat56 libavcodec56 libavutil54 \
|
||||
ghostscript \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-pil \
|
||||
python3-pytest \
|
||||
python3-reportlab \
|
||||
python3-wheel \
|
||||
python3-venv \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng
|
||||
|
||||
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
|
||||
into your system Python, which could interfere with other programs):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install ocrmypdf
|
||||
|
||||
sudo pip3 install ocrmypdf
|
||||
|
||||
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
|
||||
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
|
||||
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install python3-venv
|
||||
python3 -m venv venv-ocrmypdf --without-pip
|
||||
source venv-ocrmypdf/bin/activate
|
||||
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
|
||||
deactivate
|
||||
python3 -m venv --system-site-packages venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip install ocrmypdf
|
||||
sudo apt-get install python3-venv
|
||||
python3 -m venv venv-ocrmypdf --without-pip
|
||||
source venv-ocrmypdf/bin/activate
|
||||
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
|
||||
deactivate
|
||||
python3 -m venv --system-site-packages venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip 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://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
|
||||
sudo dpkg -i unpaper_6.1-1.deb
|
||||
wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
|
||||
sudo dpkg -i unpaper_6.1-1.deb
|
||||
|
||||
|
||||
Installing on ArchLinux
|
||||
-----------------------
|
||||
|
||||
The author is aware of an `ArchLinux package for ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`_. It seems like the following command might work.
|
||||
|
||||
.. code-block::
|
||||
|
||||
pacman -S ocrmypdf
|
||||
|
||||
|
||||
Installing on Windows
|
||||
@@ -255,45 +298,45 @@ The command line syntax to run ocrmypdf from a command prompt will resemble:
|
||||
|
||||
.. code-block:: bat
|
||||
|
||||
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
|
||||
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
|
||||
|
||||
where /c/Users/sampleuser is a Unix representation of the Windows path C:\\Users\\sampleuser, assuming a user named "sampleuser" is running ocrmypdf on a file in their home directory, and the files "test.pdf" and "output.pdf" are in the sampleuser folder. The Windows user must have read and write permissions.
|
||||
|
||||
|
||||
Installing HEAD revision from sources
|
||||
-------------------------------------
|
||||
|
||||
If you have ``git`` and Python 3.4 or newer installed, you can install from source. When the ``pip`` installer runs,
|
||||
If you have ``git`` and Python 3.5 or newer installed, you can install from source. When the ``pip`` installer runs,
|
||||
it will alert you if dependencies are missing.
|
||||
|
||||
To install the HEAD revision from sources in the current Python 3 environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`_, allowing customization of OCRmyPDF, use the ``-e`` flag:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
On certain Linux distributions such as Ubuntu, you may need to use
|
||||
run the install command as superuser:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
Note that this will alter your system's Python distribution. If you prefer
|
||||
to not install as superuser, you can install the package in a Python virtual environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||
python3 -m venv
|
||||
source venv/bin/activate
|
||||
cd OCRmyPDF
|
||||
pip3 install .
|
||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||
python3 -m venv
|
||||
source venv/bin/activate
|
||||
cd OCRmyPDF
|
||||
pip3 install .
|
||||
|
||||
However, ``ocrmypdf`` will only be accessible on the system PATH after
|
||||
you activate the virtual environment.
|
||||
@@ -301,8 +344,8 @@ you activate the virtual environment.
|
||||
To run the program:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
If not yet installed, the script will notify you about dependencies that
|
||||
need to be installed. The script requires specific versions of the
|
||||
|
||||
+29
-1
@@ -53,6 +53,20 @@ By default, OCRmyPDF will convert the file to a PDF/A. This behavior can be dis
|
||||
Depending on the settings selected, OCRmyPDF may "graft" the OCR layer into the existing PDF, or reconstruct a visually equivalent new PDF.
|
||||
|
||||
|
||||
Why you shouldn't do this manually
|
||||
----------------------------------
|
||||
|
||||
There are two routes to manually applying OCR to an existing PDF, both of which destroy information in the original PDF.
|
||||
|
||||
1. Rasterize each page as an image, OCR the images, and combine the output into a PDF. This preserves the appearance of each page, but resamples all images (possibly losing quality, increasing file size, introducing compression artifacts, etc.)
|
||||
|
||||
2. Extract each image, OCR, and combine the output into a PDF. This loses the context in which images are used in the PDF, meaning that cropping, rotation and scaling of pages may be lost. Some scanned PDFs use multiple images segmented into black and white, grayscale and color regions, with stencil masks to prevent overlap, as this can enhance the appearance of a file while reducing file size. Clearly, reassembling these images will be easy. This also loses and text or vector art on any pages in a PDF with both scanned and pure digital content.
|
||||
|
||||
In the case of a PDF that is nothing other than a container of images (no rotation, scaling, cropping, one image per page), the second approach can be lossless.
|
||||
|
||||
OCRmyPDF uses several strategies depending on input options and the input PDF itself, but generally speaking it rasterizes a page for OCR and then grafts the OCR back onto the original. As such it can handle complex PDFs and still preserve their contents as much as possible.
|
||||
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
@@ -64,6 +78,7 @@ OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences these l
|
||||
* If a document contains languages outside of those given in the ``-l LANG`` arguments, results may be poor.
|
||||
* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns and join text across the columns.
|
||||
* Poor quality scans may produce poor quality OCR. Garbage in, garbage out.
|
||||
* PDFs that use transparent layers are not currently checked in the test suite, so they may not work correctly.
|
||||
|
||||
OCRmyPDF is also limited by the PDF specification:
|
||||
|
||||
@@ -72,5 +87,18 @@ OCRmyPDF is also limited by the PDF specification:
|
||||
Ghostscript also imposes some limitations:
|
||||
|
||||
* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled.
|
||||
* PDFs containing JPEG 2000-encoded content will be converted to JPEG encoding, which may introduce compression artifacts, if Ghostscript PDF/A is enabled.
|
||||
* Ghostscript may transcode grayscale and color images, either lossy to lossless or lossless to lossy, based on an internal algorithm. This behavior can be suppressed by setting ``--pdfa-image-compression`` to ``jpeg`` or ``lossless`` to set all images to one type or the other. Ghostscript has no option to maintain the input image's format.
|
||||
|
||||
OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable.
|
||||
OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable.
|
||||
|
||||
|
||||
Similar programs
|
||||
----------------
|
||||
|
||||
To the author's knowledge, OCRmyPDF is the most feature-rich and thoroughly tested command line OCR PDF conversion tool. If it does not meet your needs, contributions and suggestions are welcome. If not, consider one of these similar open source programs:
|
||||
|
||||
* pdf2pdfocr
|
||||
* pdfsandwich
|
||||
* pypdfocr
|
||||
* pdfbeads
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
@@ -1,19 +1,100 @@
|
||||
RELEASE NOTES
|
||||
Release notes
|
||||
=============
|
||||
|
||||
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
|
||||
|
||||
v4.4.1:
|
||||
=======
|
||||
|
||||
v5.0
|
||||
----
|
||||
|
||||
- Backward incompatible changes
|
||||
|
||||
+ Support for Python 3.4 dropped. Python 3.5 is now required.
|
||||
+ Support for Tesseract 3.02 and 3.03 dropped. Tesseract 3.04 or newer is required. Tesseract 4.00 (alpha) is supported.
|
||||
+ The OCRmyPDF.sh script was removed.
|
||||
|
||||
- Add a new feature, ``--sidecar``, which allows creating "sidecar" text files which contain the OCR results in plain text. These OCR text is more reliable than extracting text from PDFs. Closes #126.
|
||||
- New feature: ``--pdfa-image-compression``, which allows overriding Ghostscript's lossy-or-lossless image encoding heuristic and making all images JPEG encoded or lossless encoded as desired. Fixes #163.
|
||||
- Fixed issue #143, added ``--quiet`` to suppress "INFO" messages
|
||||
- Fixed issue #164, a typo
|
||||
- Removed the command line parameters ``-n`` and ``--just-print`` since they have not worked for some time (reported as Ubuntu bug `#1687308 <https://bugs.launchpad.net/ubuntu/+source/ocrmypdf/+bug/1687308>`_)
|
||||
|
||||
|
||||
v4.5.6
|
||||
------
|
||||
|
||||
- Fixed issue #156, 'NoneType' object has no attribute 'getObject' on pages with no optional /Contents record. This should resolve all issues related to pages with no /Contents record.
|
||||
- Fixed issue #158, ocrmypdf now stops and terminates if Ghostscript fails on an intermediate step, as it is not possible to proceed.
|
||||
- Fixed issue #160, exception thrown on certain invalid arguments instead of error message
|
||||
|
||||
|
||||
v4.5.5
|
||||
------
|
||||
|
||||
- Automated update of macOS homebrew tap
|
||||
- Fixed issue #154, KeyError '/Contents' when searching for text on blank pages that have no /Contents record. Note: incomplete fix for this issue.
|
||||
|
||||
|
||||
v4.5.4
|
||||
------
|
||||
|
||||
- Fix ``--skip-big`` raising an exception if a page contains no images (#152) (thanks to @TomRaz)
|
||||
- Fix an issue where pages with no images might trigger "cannot write mode P as JPEG" (#151)
|
||||
|
||||
|
||||
v4.5.3
|
||||
------
|
||||
|
||||
- Added a workaround for Ghostscript 9.21 and probably earlier versions would fail with the error message "VMerror -25", due to a Ghostscript bug in XMP metadata handling
|
||||
- High Unicode characters (U+10000 and up) are no longer accepted for setting metadata on the command line, as Ghostscript may not handle them correctly.
|
||||
- Fixed an issue where the ``tess4`` renderer would duplicate content onto output pages if tesseract failed or timed out
|
||||
- Fixed ``tess4`` renderer not recognized when lossless reconstruction is possible
|
||||
|
||||
|
||||
v4.5.2
|
||||
------
|
||||
|
||||
- Fix issue #147. ``--pdf-renderer tess4 --clean`` will produce an oversized page containing the original image in the bottom left corner, due to loss DPI information.
|
||||
- Make "using Tesseract 4.0" warning less ominous
|
||||
- Set up machinery for homebrew OCRmyPDF tap
|
||||
|
||||
|
||||
v4.5.1
|
||||
------
|
||||
|
||||
- Fix issue #137, proportions of images with a non-square pixel aspect ratio would be distorted in output for ``--force-ocr`` and some other combinations of flags
|
||||
|
||||
|
||||
v4.5
|
||||
----
|
||||
|
||||
- Exotic PDFs containing "Form XObjects" are now supported (issue #134; PDF reference manual 8.10), and images they contain are taken into account when determining the resolution for rasterizing
|
||||
- The Tesseract 4 Docker image no longer includes all languages, because it took so long to build something would tend to fail
|
||||
- OCRmyPDF now warns about using ``--pdf-renderer tesseract`` with Tesseract 3.04 or lower due to issues with Ghostscript corrupting the OCR text in these cases
|
||||
|
||||
|
||||
v4.4.2
|
||||
------
|
||||
|
||||
- The Docker images (ocrmypdf, ocrmypdf-polyglot, ocrmypdf-tess4) are now based on Ubuntu 16.10 instead of Debian stretch
|
||||
|
||||
+ This makes supporting the Tesseract 4 image easier
|
||||
+ This could be a disruptive change for any Docker users who built customized these images with their own changes, and made those changes in a way that depends on Debian and not Ubuntu
|
||||
|
||||
- OCRmyPDF now prevents running the Tesseract 4 renderer with Tesseract 3.04, which was permitted in v4.4 and v4.4.1 but will not work
|
||||
|
||||
|
||||
v4.4.1
|
||||
------
|
||||
|
||||
- To prevent a `TIFF output error <https://github.com/python-pillow/Pillow/issues/2206>`_ caused by img2pdf >= 0.2.1 and Pillow <= 3.4.2, dependencies have been tightened
|
||||
- The Tesseract 4.00 simultaenous process limit was increased from 1 to 2, since it was observed that 1 lowers performance
|
||||
- The Tesseract 4.00 simultaneous process limit was increased from 1 to 2, since it was observed that 1 lowers performance
|
||||
- Documentation improvements to describe the ``--tesseract-config`` feature
|
||||
- Added test cases and fixed error handling for ``--tesseract-config``
|
||||
- Tweaks to setup.py to deal with issues in the v4.4 release
|
||||
|
||||
v4.4:
|
||||
=====
|
||||
v4.4
|
||||
----
|
||||
|
||||
- Tesseract 4.00 is now supported on an experimental basis.
|
||||
|
||||
@@ -30,33 +111,33 @@ v4.4:
|
||||
+ However, OCRmyPDF's dependency "ruffus" is not re-entrant, so no Python API is available. Scripts should continue to use the command line interface.
|
||||
|
||||
|
||||
v4.3.5:
|
||||
=======
|
||||
v4.3.5
|
||||
------
|
||||
|
||||
- Update documentation to confirm Python 3.6.0 compatibility. No code changes were needed, so many earlier versions are likely supported.
|
||||
|
||||
|
||||
v4.3.4:
|
||||
=======
|
||||
v4.3.4
|
||||
------
|
||||
|
||||
- Fixed "decimal.InvalidOperation: quantize result has too many digits" for high DPI images
|
||||
|
||||
|
||||
v4.3.3:
|
||||
=======
|
||||
v4.3.3
|
||||
------
|
||||
|
||||
- Fixed PDF/A creation with Ghostscript 9.20 properly
|
||||
- Fixed an exception on inline stencil masks with a missing optional parameter
|
||||
|
||||
|
||||
v4.3.2:
|
||||
=======
|
||||
v4.3.2
|
||||
------
|
||||
|
||||
- Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix did not actually work)
|
||||
|
||||
|
||||
v4.3.1:
|
||||
=======
|
||||
v4.3.1
|
||||
------
|
||||
|
||||
- Fixed an issue where pages produced by the "hocr" renderer after a Tesseract timeout would be rotated incorrectly if the input page was rotated with a /Rotate marker
|
||||
- Fixed a file handle leak in LeptonicaErrorTrap that would cause a "too many open files" error for files around hundred pages of pages long when ``--deskew`` or ``--remove-background`` or other Leptonica based image processing features were in use, depending on the system value of ``ulimit -n``
|
||||
@@ -66,8 +147,8 @@ v4.3.1:
|
||||
- Tesseract caching in test cases is now more cautious about false cache hits and reproducing exact output, not that any problems were observed
|
||||
|
||||
|
||||
v4.3:
|
||||
=====
|
||||
v4.3
|
||||
----
|
||||
|
||||
- New feature ``--remove-background`` to detect and erase the background of color and grayscale images
|
||||
- Better documentation
|
||||
@@ -77,21 +158,21 @@ v4.3:
|
||||
+ This does not improve performance since temporary files are still used for buffering
|
||||
+ Some output validation is disabled in this mode
|
||||
|
||||
v4.2.5:
|
||||
=======
|
||||
v4.2.5
|
||||
------
|
||||
|
||||
- Fixed an issue (#100) with PDFs that omit the optional /BitsPerComponent parameter on images
|
||||
- Removed non-free file milk.pdf
|
||||
|
||||
|
||||
v4.2.4:
|
||||
=======
|
||||
v4.2.4
|
||||
------
|
||||
|
||||
- Fixed an error (#90) caused by PDFs that use stencil masks properly
|
||||
- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI)
|
||||
|
||||
v4.2.3:
|
||||
=======
|
||||
v4.2.3
|
||||
------
|
||||
|
||||
- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object
|
||||
- Integrated a few fixes to simplify downstream packaging (Debian)
|
||||
@@ -104,21 +185,21 @@ v4.2.3:
|
||||
- Deprecated the OCRmyPDF.sh shell script
|
||||
|
||||
|
||||
v4.2.2:
|
||||
=======
|
||||
v4.2.2
|
||||
------
|
||||
|
||||
- Improvements to documentation
|
||||
|
||||
|
||||
v4.2.1:
|
||||
=======
|
||||
v4.2.1
|
||||
------
|
||||
|
||||
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
|
||||
- Implemented stdin streaming
|
||||
|
||||
|
||||
v4.2:
|
||||
=====
|
||||
v4.2
|
||||
----
|
||||
|
||||
- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15)
|
||||
|
||||
@@ -150,14 +231,14 @@ v4.2:
|
||||
|
||||
- Ghostscript now runs in "safer" mode where possible
|
||||
|
||||
v4.1.4:
|
||||
=======
|
||||
v4.1.4
|
||||
------
|
||||
|
||||
- Bug fix: monochrome images with an ICC profile attached were incorrectly converted to full color images if lossless reconstruction was not possible due to other settings; consequence was increased file size for these images
|
||||
|
||||
|
||||
v4.1.3:
|
||||
=======
|
||||
v4.1.3
|
||||
------
|
||||
|
||||
- More helpful error message for PDFs with version 4 security handler
|
||||
- Update usage instructions for Windows/Docker users
|
||||
@@ -165,59 +246,59 @@ v4.1.3:
|
||||
- Add a few leptonica wrapper functions (no effect on most users)
|
||||
|
||||
|
||||
v4.1.2:
|
||||
=======
|
||||
v4.1.2
|
||||
------
|
||||
|
||||
- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license
|
||||
- More helpful error message for an error related to certain types of malformed PDFs
|
||||
|
||||
|
||||
v4.1:
|
||||
=====
|
||||
v4.1
|
||||
----
|
||||
|
||||
- ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold``
|
||||
- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time
|
||||
- Fixed problems with "RethrownJobError" errors during error handling that suppressed the useful error messages
|
||||
|
||||
|
||||
v4.0.7:
|
||||
=======
|
||||
v4.0.7
|
||||
------
|
||||
|
||||
- Minor correction to Ghostscript output settings
|
||||
|
||||
|
||||
v4.0.6:
|
||||
=======
|
||||
v4.0.6
|
||||
------
|
||||
|
||||
- Update install instructions
|
||||
- Provide a sRGB profile instead of using Ghostscript's
|
||||
|
||||
|
||||
v4.0.5:
|
||||
=======
|
||||
v4.0.5
|
||||
------
|
||||
|
||||
- Remove some verbose debug messages from v4.0.4
|
||||
- Fixed temporary that wasn't being deleted
|
||||
- DPI is now calculated correctly for cropped images, along with other image transformations
|
||||
- Inline images are now checked during DPI calculation instead of rejecting the image
|
||||
|
||||
v4.0.4:
|
||||
=======
|
||||
v4.0.4
|
||||
------
|
||||
|
||||
Released with verbose debug message turned on. Do not use. Skip to v4.0.5.
|
||||
|
||||
|
||||
v4.0.3:
|
||||
=======
|
||||
v4.0.3
|
||||
------
|
||||
|
||||
New features
|
||||
------------
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Page orientations detected are now reported in a summary comment
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
^^^^^
|
||||
|
||||
- Show stack trace if unexpected errors occur
|
||||
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
|
||||
@@ -225,11 +306,11 @@ Fixes
|
||||
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
|
||||
|
||||
|
||||
v4.0.2:
|
||||
=======
|
||||
v4.0.2
|
||||
------
|
||||
|
||||
Fixes
|
||||
-----
|
||||
^^^^^
|
||||
|
||||
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
|
||||
orientation information
|
||||
@@ -237,20 +318,20 @@ Fixes
|
||||
- Fixed use of chmod on Docker that broke most test cases
|
||||
|
||||
|
||||
v4.0.1:
|
||||
=======
|
||||
v4.0.1
|
||||
------
|
||||
|
||||
Fixes
|
||||
-----
|
||||
^^^^^
|
||||
|
||||
- Fixed a KeyError if tesseract fails to find page orientation information
|
||||
|
||||
|
||||
v4.0:
|
||||
=====
|
||||
v4.0
|
||||
----
|
||||
|
||||
New features
|
||||
------------
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information
|
||||
on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
|
||||
@@ -260,7 +341,7 @@ New features
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
^^^^^
|
||||
|
||||
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly
|
||||
if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
|
||||
@@ -269,7 +350,7 @@ Fixes
|
||||
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- Logging output is now much easier to read
|
||||
- ``--deskew`` is now performed by Leptonica instead of unpaper (#25)
|
||||
@@ -281,21 +362,21 @@ Changes
|
||||
to correct the problem.
|
||||
|
||||
|
||||
v3.2.1:
|
||||
=======
|
||||
v3.2.1
|
||||
------
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- Fixed issue #47 "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2
|
||||
- Tweaked the Dockerfiles
|
||||
|
||||
|
||||
v3.2:
|
||||
=====
|
||||
v3.2
|
||||
----
|
||||
|
||||
New features
|
||||
------------
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Lossless reconstruction: when possible, OCRmyPDF will inject text layers without
|
||||
otherwise manipulating the content and layout of a PDF page. For example, a PDF containing a mix
|
||||
@@ -307,25 +388,25 @@ New features
|
||||
for the polyglots among us. It is much larger.
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- JPEG transcoding quality is now 95 instead of the default 75. Bigger file sizes for less degradation.
|
||||
|
||||
|
||||
|
||||
v3.1.1:
|
||||
=======
|
||||
v3.1.1
|
||||
------
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- Fixed bug that caused incorrect page size and DPI calculations on documents with mixed page sizes
|
||||
|
||||
v3.1:
|
||||
=====
|
||||
v3.1
|
||||
----
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- Default output format is now PDF/A-2b instead of PDF/A-1b
|
||||
- Python 3.5 and macOS El Capitan are now supported platforms - no changes were
|
||||
@@ -339,11 +420,11 @@ Changes
|
||||
Currently it always chooses the 'hocrtransform' renderer but that behavior may change.
|
||||
- Set up Travis CI automatic integration testing
|
||||
|
||||
v3.0:
|
||||
=====
|
||||
v3.0
|
||||
----
|
||||
|
||||
New features
|
||||
------------
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Easier installation with a Docker container or Python's ``pip`` package manager
|
||||
- Eliminated many external dependencies, so it's easier to setup
|
||||
@@ -366,7 +447,7 @@ New features
|
||||
- Multiple images on the same PDF page are now supported
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- New, robust rewrite in Python 3.4+ with ruffus_ pipelines
|
||||
- Now uses Ghostscript 9.14's improved color conversion model to preserve PDF colors
|
||||
@@ -404,7 +485,7 @@ Changes
|
||||
.. _JHOVE: http://jhove.sourceforge.net/
|
||||
|
||||
Release candidates
|
||||
------------------
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- rc9:
|
||||
|
||||
@@ -472,12 +553,12 @@ where ``settings.txt`` contains *one argument per line*, for example:
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
^^^^^
|
||||
|
||||
- Handling of filenames containing spaces: fixed
|
||||
|
||||
Notes and known issues
|
||||
----------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- Some dependencies may work with lower versions than tested, so try
|
||||
overriding dependencies if they are "in the way" to see if they work.
|
||||
@@ -489,8 +570,8 @@ Notes and known issues
|
||||
images almost never contain inline images.
|
||||
|
||||
|
||||
v2.2-stable (2014-09-29):
|
||||
=========================
|
||||
v2.2-stable (2014-09-29)
|
||||
------------------------
|
||||
|
||||
OCRmyPDF versions 1 and 2 were implemented as shell scripts. OCRmyPDF 3.0+ is a fork that gradually replaced all shell scripts with Python while maintaining the existing command line arguments. No one is maintaining old versions.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
PDF Security Issues
|
||||
PDF security issues
|
||||
===================
|
||||
|
||||
OCRmyPDF should only be used on PDFs you trust. It is not designed to protect you against malware.
|
||||
|
||||
@@ -5,11 +5,3 @@ PROGRAM_NAME = 'ocrmypdf'
|
||||
VERSION = pkg_resources.get_distribution('ocrmypdf').version
|
||||
|
||||
|
||||
# These imports are for v4.x backward compatibility for consumers of ocrmypdf
|
||||
# (if any). They are deprecated and will be removed in v5.x.
|
||||
from .exec import ghostscript, qpdf, tesseract, unpaper, get_program
|
||||
from .exceptions import ExitCode
|
||||
from .helpers import page_number, is_iterable_notstr
|
||||
|
||||
|
||||
|
||||
|
||||
+206
-63
@@ -39,13 +39,17 @@ warnings.simplefilter('ignore', pypdf.utils.PdfReadWarning)
|
||||
# -------------
|
||||
# External dependencies
|
||||
|
||||
MINIMUM_TESS_VERSION = '3.02.02'
|
||||
MINIMUM_TESS_VERSION = '3.04'
|
||||
|
||||
|
||||
def complain(message):
|
||||
print(*textwrap.wrap(message), file=sys.stderr)
|
||||
|
||||
|
||||
# Hack to help debugger context find /usr/local/bin
|
||||
if 'IDE_PROJECT_ROOTS' in os.environ:
|
||||
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
|
||||
|
||||
if tesseract.version() < MINIMUM_TESS_VERSION:
|
||||
complain(
|
||||
"Please install tesseract {0} or newer "
|
||||
@@ -53,18 +57,12 @@ if tesseract.version() < MINIMUM_TESS_VERSION:
|
||||
MINIMUM_TESS_VERSION, tesseract.version()))
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
|
||||
# -------------
|
||||
# Parser
|
||||
|
||||
parser = cmdline.get_argparse(
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=PROGRAM_NAME,
|
||||
version=VERSION,
|
||||
fromfile_prefix_chars='@',
|
||||
ignored_args=[
|
||||
'touch_files_only', 'recreate_database', 'checksum_file_name',
|
||||
'key_legend_in_graph', 'draw_graph_horizontally', 'flowchart_format',
|
||||
'forced_tasks', 'target_tasks', 'use_threads', 'jobs', 'log_file'],
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description="""\
|
||||
Generates a searchable PDF or PDF/A from a regular PDF.
|
||||
@@ -116,24 +114,22 @@ Online documentation is located at:
|
||||
""")
|
||||
|
||||
parser.add_argument(
|
||||
'input_file',
|
||||
'input_file', metavar="input_pdf_or_image",
|
||||
help="PDF file containing the images to be OCRed (or '-' to read from "
|
||||
"standard input)")
|
||||
parser.add_argument(
|
||||
'output_file',
|
||||
help="output searchable PDF file (or '-' to write to standard output)")
|
||||
'output_file', metavar="output_pdf",
|
||||
help="Output searchable PDF file (or '-' to write to standard output). "
|
||||
"Existing files will be ovewritten. If same as input file, the "
|
||||
"input file will be updated only if processing is successful.")
|
||||
parser.add_argument(
|
||||
'-l', '--language', action='append',
|
||||
help="Language(s) of the file to be OCRed (see tesseract --list-langs for "
|
||||
"all language packs installed in your system). To specify multiple "
|
||||
"languages, join them with '+' or issue this argument once for each "
|
||||
"language.")
|
||||
parser.add_argument(
|
||||
'-j', '--jobs', metavar='N', type=int,
|
||||
help="Use up to N CPU cores simultaneously (default: use all)")
|
||||
"all language packs installed in your system). Use -l eng+deu for "
|
||||
"multiple languages.")
|
||||
parser.add_argument(
|
||||
'--image-dpi', metavar='DPI', type=int,
|
||||
help="for input image instead of PDF, use this DPI instead of file's")
|
||||
help="For input image instead of PDF, use this DPI instead of file's.")
|
||||
parser.add_argument(
|
||||
'--output-type', choices=['pdfa', 'pdf'], default='pdfa',
|
||||
help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for "
|
||||
@@ -142,47 +138,74 @@ parser.add_argument(
|
||||
"also has problems with full Unicode text. 'pdf' attempts to "
|
||||
"preserve file contents as much as possible.")
|
||||
|
||||
# Use null string '\0' as sentinel to indicate the user supplied no argument,
|
||||
# since that is the only invalid character for filepaths on all platforms
|
||||
# bool('\0') is True in Python
|
||||
parser.add_argument(
|
||||
'--sidecar', nargs='?', const='\0', default=None, metavar='FILE',
|
||||
help="Generate sidecar text files that contain the same text recognized "
|
||||
"by Tesseract. This may be useful for building a OCR text database. "
|
||||
"If FILE is omitted, the sidecar file be named {output_file}.txt "
|
||||
"If FILE is set to '-', the sidecar is written to stdout (a "
|
||||
"convenient way to preview OCR quality). The output file and sidecar "
|
||||
"may not both use stdout at the same time.")
|
||||
|
||||
parser.add_argument(
|
||||
'--version', action='version', version=VERSION,
|
||||
help="Print program version and exit")
|
||||
|
||||
jobcontrol = parser.add_argument_group(
|
||||
"Job control options")
|
||||
jobcontrol.add_argument(
|
||||
'-j', '--jobs', metavar='N', type=int,
|
||||
help="Use up to N CPU cores simultaneously (default: use all).")
|
||||
jobcontrol.add_argument(
|
||||
'-q', '--quiet', action='store_true', help="Suppress INFO messages")
|
||||
jobcontrol.add_argument(
|
||||
'-v', '--verbose', const="+", default=[], nargs='?', action="append",
|
||||
help="Print more verbose messages for each additional verbose level")
|
||||
|
||||
metadata = parser.add_argument_group(
|
||||
"Metadata options",
|
||||
"Set output PDF/A metadata (default: use input document's metadata)")
|
||||
"Set output PDF/A metadata (default: copy input document's metadata)")
|
||||
metadata.add_argument(
|
||||
'--title', type=str,
|
||||
help="set document title (place multiple words in quotes)")
|
||||
help="Set document title (place multiple words in quotes)")
|
||||
metadata.add_argument(
|
||||
'--author', type=str,
|
||||
help="set document author")
|
||||
help="Set document author")
|
||||
metadata.add_argument(
|
||||
'--subject', type=str,
|
||||
help="set document subject description")
|
||||
help="Set document subject description")
|
||||
metadata.add_argument(
|
||||
'--keywords', type=str,
|
||||
help="set document keywords")
|
||||
help="Set document keywords")
|
||||
|
||||
preprocessing = parser.add_argument_group(
|
||||
"Image preprocessing options",
|
||||
"Options to improve the quality of the final PDF and OCR")
|
||||
preprocessing.add_argument(
|
||||
'-r', '--rotate-pages', action='store_true',
|
||||
help="automatically rotate pages based on detected text orientation")
|
||||
help="Automatically rotate pages based on detected text orientation")
|
||||
preprocessing.add_argument(
|
||||
'--remove-background', action='store_true',
|
||||
help="attempt to remove background from gray or color pages, setting it "
|
||||
help="Attempt to remove background from gray or color pages, setting it "
|
||||
"to white ")
|
||||
preprocessing.add_argument(
|
||||
'-d', '--deskew', action='store_true',
|
||||
help="deskew each page before performing OCR")
|
||||
help="Deskew each page before performing OCR")
|
||||
preprocessing.add_argument(
|
||||
'-c', '--clean', action='store_true',
|
||||
help="clean pages from scanning artifacts before performing OCR, and send "
|
||||
help="Clean pages from scanning artifacts before performing OCR, and send "
|
||||
"the cleaned page to OCR, but do not include the cleaned page in "
|
||||
"the output ")
|
||||
"the output")
|
||||
preprocessing.add_argument(
|
||||
'-i', '--clean-final', action='store_true',
|
||||
help="clean page as above, and incorporate the cleaned image in the final "
|
||||
"PDF")
|
||||
help="Clean page as above, and incorporate the cleaned image in the final "
|
||||
"PDF. Might remove desired content.")
|
||||
preprocessing.add_argument(
|
||||
'--oversample', metavar='DPI', type=int, default=0,
|
||||
help="oversample images to at least the specified DPI, to improve OCR "
|
||||
help="Oversample images to at least the specified DPI, to improve OCR "
|
||||
"results slightly")
|
||||
|
||||
ocrsettings = parser.add_argument_group(
|
||||
@@ -190,32 +213,37 @@ ocrsettings = parser.add_argument_group(
|
||||
"Control how OCR is applied")
|
||||
ocrsettings.add_argument(
|
||||
'-f', '--force-ocr', action='store_true',
|
||||
help="rasterize any fonts or vector objects on each page, apply OCR, and "
|
||||
help="Rasterize any fonts or vector objects on each page, apply OCR, and "
|
||||
"save the rastered output (this rewrites the PDF)")
|
||||
ocrsettings.add_argument(
|
||||
'-s', '--skip-text', action='store_true',
|
||||
help="skip OCR on any pages that already contain text, but include the "
|
||||
help="Skip OCR on any pages that already contain text, but include the "
|
||||
"page in final output; useful for PDFs that contain a mix of "
|
||||
"images, text pages, and/or previously OCRed pages")
|
||||
# ocrsettings.add_argument(
|
||||
# '--redo-ocr', action='store_true',
|
||||
# help="removing any existing OCR text, but otherwise preserve mixed PDF "
|
||||
# "pages")
|
||||
|
||||
ocrsettings.add_argument(
|
||||
'--skip-big', type=float, metavar='MPixels',
|
||||
help="skip OCR on pages larger than the specified amount of megapixels, "
|
||||
help="Skip OCR on pages larger than the specified amount of megapixels, "
|
||||
"but include skipped pages in final output")
|
||||
|
||||
advanced = parser.add_argument_group(
|
||||
"Advanced",
|
||||
"Advanced options for power users")
|
||||
"Advanced options to control Tesseract's OCR behavior")
|
||||
advanced.add_argument(
|
||||
'--tesseract-config', action='append', metavar='CFG', default=[],
|
||||
help="additional Tesseract configuration files -- see documentation")
|
||||
help="Additional Tesseract configuration files -- see documentation")
|
||||
advanced.add_argument(
|
||||
'--tesseract-pagesegmode', action='store', type=int, metavar='PSM',
|
||||
choices=range(0, 14),
|
||||
help="set Tesseract page segmentation mode (see tesseract --help)")
|
||||
help="Set Tesseract page segmentation mode (see tesseract --help)")
|
||||
advanced.add_argument(
|
||||
'--tesseract-oem', action='store', type=int, metavar='MODE',
|
||||
choices=range(0, 4),
|
||||
help=("set Tesseract 4.0 OCR engine mode: "
|
||||
help=("Set Tesseract 4.0 OCR engine mode: "
|
||||
"0 - original Tesseract only; "
|
||||
"1 - neural nets LSTM only; "
|
||||
"2 - Tesseract + LSTM; "
|
||||
@@ -223,7 +251,7 @@ advanced.add_argument(
|
||||
)
|
||||
advanced.add_argument(
|
||||
'--pdf-renderer', choices=['auto', 'tesseract', 'hocr', 'tess4'], default='auto',
|
||||
help="choose OCR PDF renderer - the default option is to let OCRmyPDF "
|
||||
help="Choose OCR PDF renderer - the default option is to let OCRmyPDF "
|
||||
"choose. The 'tesseract' PDF renderer is more accurate and does a "
|
||||
"better job and document structure such as recognizing columns. It "
|
||||
"also does a better job on non-Latin languages. However, it does "
|
||||
@@ -233,22 +261,36 @@ advanced.add_argument(
|
||||
"to 'tesseract', requires tesseract 4, and gives superior results.")
|
||||
advanced.add_argument(
|
||||
'--tesseract-timeout', default=180.0, type=float, metavar='SECONDS',
|
||||
help='give up on OCR after the timeout, but copy the preprocessed page '
|
||||
help='Give up on OCR after the timeout, but copy the preprocessed page '
|
||||
'into the final output')
|
||||
advanced.add_argument(
|
||||
'--rotate-pages-threshold', default=14.0, type=float, metavar='CONFIDENCE',
|
||||
help="only rotate pages when confidence is above this value (arbitrary "
|
||||
help="Only rotate pages when confidence is above this value (arbitrary "
|
||||
"units reported by tesseract)")
|
||||
advanced.add_argument(
|
||||
'--pdfa-image-compression', choices=['auto', 'jpeg', 'lossless'],
|
||||
default='auto',
|
||||
help="Specify how to compress images in the output PDF/A. 'auto' lets "
|
||||
"OCRmyPDF decide. 'jpeg' changes all grayscale and color images to "
|
||||
"JPEG compression. 'lossless' uses PNG-style lossless compression "
|
||||
"for all images. Monochrome images are always compressed using a "
|
||||
"lossless codec. Compression settings "
|
||||
"are applied to all pages, including those for which OCR was "
|
||||
"skipped. Not supported for --output-type=pdf ; that setting "
|
||||
"preserves the original compression of all images.")
|
||||
|
||||
debugging = parser.add_argument_group(
|
||||
"Debugging",
|
||||
"Arguments to help with troubleshooting and debugging")
|
||||
debugging.add_argument(
|
||||
'-k', '--keep-temporary-files', action='store_true',
|
||||
help="keep temporary files (helpful for debugging)")
|
||||
help="Keep temporary files (helpful for debugging)")
|
||||
debugging.add_argument(
|
||||
'-g', '--debug-rendering', action='store_true',
|
||||
help="render each page twice with debug information on second page")
|
||||
help="Render each page twice with debug information on second page")
|
||||
debugging.add_argument(
|
||||
'--flowchart', type=str,
|
||||
help="Generate the pipeline execution flowchart")
|
||||
|
||||
|
||||
def check_options_languages(options, _log):
|
||||
@@ -265,20 +307,25 @@ def check_options_languages(options, _log):
|
||||
"data for the following requested languages: \n")
|
||||
for lang in (set(options.language) - tesseract.languages()):
|
||||
msg += lang + '\n'
|
||||
raise argparse.ArgumentError(msg)
|
||||
raise argparse.ArgumentError(None, msg)
|
||||
|
||||
|
||||
def check_options_output(options, log):
|
||||
if options.pdf_renderer == 'auto':
|
||||
options.pdf_renderer = 'hocr'
|
||||
|
||||
if options.pdf_renderer == 'tesseract' and \
|
||||
tesseract.version() < '3.04.01' and \
|
||||
os.environ.get('OCRMYPDF_SHARP_TTF', '') != '1':
|
||||
log.warning(
|
||||
"Your version of tesseract has problems with PDF output."
|
||||
" Some PDF viewers will fail to find searchable text.\n"
|
||||
"--pdf-renderer=tesseract is not recommended.")
|
||||
if options.pdf_renderer in ('tesseract', 'tess4'):
|
||||
if tesseract.version() < '3.05':
|
||||
log.warning(
|
||||
"The setting --pdf-renderer=tesseract is not recommend for "
|
||||
" use with tesseract versions less than 3.05, because it "
|
||||
" produces OCR text that is incompatible with Ghostscript and "
|
||||
" some other software.")
|
||||
elif tesseract.version() == '4.00.00alpha':
|
||||
log.warning(
|
||||
"The setting --pdf-renderer={tesseract,tess4} is not"
|
||||
" recommended for builds of tesseract 4.00.00alpha older than"
|
||||
" February 2017. Make sure you are using a recent build.")
|
||||
|
||||
if options.debug_rendering and options.pdf_renderer == 'tesseract':
|
||||
log.info(
|
||||
@@ -286,13 +333,23 @@ def check_options_output(options, log):
|
||||
"--pdf-renderer=tesseract.")
|
||||
|
||||
lossless_reconstruction = False
|
||||
if options.pdf_renderer == 'hocr':
|
||||
if options.pdf_renderer in ('hocr', 'tess4'):
|
||||
if not any((options.deskew, options.clean_final, options.force_ocr,
|
||||
options.remove_background)):
|
||||
lossless_reconstruction = True
|
||||
options.lossless_reconstruction = lossless_reconstruction
|
||||
|
||||
|
||||
def check_options_sidecar(options, log):
|
||||
if options.sidecar == '\0':
|
||||
if options.output_file == '-':
|
||||
raise argparse.ArgumentError(
|
||||
None,
|
||||
"--sidecar filename must be specified when output file is "
|
||||
"stdout.")
|
||||
options.sidecar = options.output_file + '.txt'
|
||||
|
||||
|
||||
def check_options_preprocessing(options, log):
|
||||
if any((options.clean, options.clean_final)):
|
||||
from .exec import unpaper
|
||||
@@ -316,8 +373,13 @@ def check_options_preprocessing(options, log):
|
||||
def check_options_ocr_behavior(options, log):
|
||||
if options.force_ocr and options.skip_text:
|
||||
raise argparse.ArgumentError(
|
||||
None,
|
||||
"Error: --force-ocr and --skip-text are mutually incompatible.")
|
||||
|
||||
# if options.redo_ocr and (options.skip_text or options.force_ocr):
|
||||
# raise argparse.ArgumentError(
|
||||
# "Error: --redo-ocr and other OCR options are incompatible.")
|
||||
|
||||
if set(options.language) & {'chi_sim', 'chi_tra'} and \
|
||||
(options.pdf_renderer == 'hocr' or options.output_type == 'pdfa'):
|
||||
log.warning(
|
||||
@@ -333,15 +395,44 @@ def check_options_advanced(options, log):
|
||||
if options.tesseract_oem and not tesseract.v4():
|
||||
log.warning(
|
||||
"--tesseract-oem requires Tesseract 4.x -- argument ignored")
|
||||
if options.pdf_renderer == 'tess4' and not tesseract.has_textonly_pdf():
|
||||
raise MissingDependencyError(
|
||||
"--pdf-renderer tess4 requires Tesseract 4.x "
|
||||
"commit 3d9fb3b or later")
|
||||
if options.pdfa_image_compression != 'auto' and \
|
||||
options.output_type != 'pdfa':
|
||||
log.warning(
|
||||
"--pdfa-image-compression argument has no effect when "
|
||||
"--output-type is not 'pdfa'"
|
||||
)
|
||||
|
||||
|
||||
def check_options_metadata(options, log):
|
||||
import unicodedata
|
||||
metadata = [options.title, options.author, options.keywords,
|
||||
options.subject]
|
||||
for s in (m for m in metadata if m):
|
||||
for c in s:
|
||||
if unicodedata.category(c) == 'Co' or ord(c) >= 0x10000:
|
||||
raise ValueError(
|
||||
"One of the metadata strings contains "
|
||||
"an unsupported Unicode character: '{}' (U+{})".format(
|
||||
c, hex(ord(c))[2:].upper()
|
||||
))
|
||||
|
||||
|
||||
def check_options(options, log):
|
||||
try:
|
||||
check_options_languages(options, log)
|
||||
check_options_metadata(options, log)
|
||||
check_options_output(options, log)
|
||||
check_options_sidecar(options, log)
|
||||
check_options_preprocessing(options, log)
|
||||
check_options_ocr_behavior(options, log)
|
||||
check_options_advanced(options, log)
|
||||
except ValueError as e:
|
||||
log.error(e)
|
||||
sys.exit(ExitCode.bad_args)
|
||||
except argparse.ArgumentError as e:
|
||||
log.error(e)
|
||||
sys.exit(ExitCode.bad_args)
|
||||
@@ -350,15 +441,13 @@ def check_options(options, log):
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ----------
|
||||
# Logging
|
||||
|
||||
|
||||
def logging_factory(logger_name, listargs):
|
||||
log_file_name, verbose = listargs
|
||||
def logging_factory(logger_name, logger_args):
|
||||
verbose = logger_args['verbose']
|
||||
quiet = logger_args['quiet']
|
||||
|
||||
root_logger = logging.getLogger(logger_name)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
@@ -368,6 +457,8 @@ def logging_factory(logger_name, listargs):
|
||||
handler.setFormatter(formatter_)
|
||||
if verbose:
|
||||
handler.setLevel(logging.DEBUG)
|
||||
elif quiet:
|
||||
handler.setLevel(logging.WARNING)
|
||||
else:
|
||||
handler.setLevel(logging.INFO)
|
||||
root_logger.addHandler(handler)
|
||||
@@ -393,7 +484,7 @@ def available_cpu_count():
|
||||
|
||||
|
||||
def cleanup_ruffus_error_message(msg):
|
||||
msg = re.sub(r'\s+', r' ', msg, re.MULTILINE)
|
||||
msg = re.sub(r'\s+', r' ', msg)
|
||||
msg = re.sub(r"\((.+?)\)", r'\1', msg)
|
||||
msg = msg.strip()
|
||||
return msg
|
||||
@@ -475,12 +566,62 @@ def traverse_ruffus_exception(e_args, options, log):
|
||||
return traverse_ruffus_exception(exc, options, log)
|
||||
|
||||
|
||||
def check_closed_streams(options):
|
||||
"""Work around Python issue with multiprocessing forking on closed streams
|
||||
|
||||
https://bugs.python.org/issue28326
|
||||
|
||||
Attempting to a fork/exec a new Python process when any of std{in,out,err}
|
||||
are closed or not flushable for some reason may raise an exception.
|
||||
Fix this by opening devnull if the handle seems to be closed. Do this
|
||||
globally to avoid tracking places all places that fork.
|
||||
|
||||
Seems to be specific to multiprocessing.Process not all Python process
|
||||
forkers.
|
||||
|
||||
The error actually occurs when the stream object is not flushable,
|
||||
but replacing an open stream object that is not flushable with
|
||||
/dev/null is a bad idea since it will create a silent failure. Replacing
|
||||
a closed handle with /dev/null seems safe.
|
||||
|
||||
"""
|
||||
|
||||
if sys.stderr is None:
|
||||
sys.stderr = open(os.devnull, 'w')
|
||||
|
||||
if sys.stdin is None:
|
||||
if options.input_file == '-':
|
||||
print("Trying to read from stdin but stdin seems closed",
|
||||
file=sys.stderr)
|
||||
return False
|
||||
sys.stdin = open(os.devnull, 'r')
|
||||
|
||||
if sys.stdout is None:
|
||||
if options.output_file == '-':
|
||||
# Can't replace stdout if the user is piping
|
||||
# If this case can even happen, it must be some kind of weird
|
||||
# stream.
|
||||
print(textwrap.dedent("""\
|
||||
Output was set to stdout '-' but the stream attached to
|
||||
stdout does not support the flush() system call. This
|
||||
will fail."""), file=sys.stderr)
|
||||
return False
|
||||
sys.stdout = open(os.devnull, 'w')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def run_pipeline():
|
||||
options = parser.parse_args()
|
||||
options.verbose_abbreviated_path = 1
|
||||
|
||||
if not check_closed_streams(options):
|
||||
return ExitCode.bad_args
|
||||
|
||||
logger_args = {'verbose': options.verbose, 'quiet': options.quiet}
|
||||
|
||||
_log, _log_mutex = proxy_logger.make_shared_logger_and_proxy(
|
||||
logging_factory, __name__, [None, options.verbose])
|
||||
logging_factory, __name__, logger_args)
|
||||
_log.debug('ocrmypdf ' + VERSION)
|
||||
_log.debug('tesseract ' + tesseract.version())
|
||||
|
||||
@@ -519,9 +660,9 @@ def run_pipeline():
|
||||
file."""))
|
||||
return ExitCode.bad_args
|
||||
elif not is_file_writable(options.output_file):
|
||||
_log.error(textwrap.dedent("""\
|
||||
Cutput file location is not writable."""))
|
||||
return ExitCode.file_access_error
|
||||
_log.error(textwrap.dedent("""\
|
||||
Output file location is not writable."""))
|
||||
return ExitCode.file_access_error
|
||||
|
||||
manager = JobContextManager()
|
||||
manager.register('JobContext', JobContext)
|
||||
@@ -566,7 +707,9 @@ def run_pipeline():
|
||||
_log.error(e)
|
||||
return ExitCode.other_error
|
||||
|
||||
if options.output_file != '-':
|
||||
if options.flowchart:
|
||||
_log.info("Flowchart saved to {}".format(options.flowchart))
|
||||
elif options.output_file != '-':
|
||||
if options.output_type == 'pdfa':
|
||||
pdfa_info = file_claims_pdfa(options.output_file)
|
||||
if pdfa_info['pass']:
|
||||
|
||||
@@ -48,7 +48,7 @@ class InputFileError(ExitCodeException):
|
||||
|
||||
|
||||
class SubprocessOutputError(ExitCodeException):
|
||||
exit_code = ExitCode.other_error
|
||||
exit_code = ExitCode.child_process_error
|
||||
|
||||
|
||||
class EncryptedPdfError(ExitCodeException):
|
||||
|
||||
@@ -2,10 +2,34 @@
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
from subprocess import Popen, PIPE, STDOUT, check_call
|
||||
from subprocess import run, PIPE, STDOUT, CalledProcessError
|
||||
from shutil import copy
|
||||
from functools import lru_cache
|
||||
import re
|
||||
from . import get_program
|
||||
from ..pdfa import SRGB_ICC_PROFILE
|
||||
from ..exceptions import SubprocessOutputError
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def version():
|
||||
args_gs = [
|
||||
get_program('gs'),
|
||||
'--version'
|
||||
]
|
||||
try:
|
||||
version = check_output(
|
||||
args_gs, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError as e:
|
||||
print("Could not find Ghostscript executable on system PATH.",
|
||||
file=sys.stderr)
|
||||
raise MissingDependencyError from e
|
||||
|
||||
return version.strip()
|
||||
|
||||
|
||||
def _gs_error_reported(stream):
|
||||
return re.search(r'error', stream, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
@@ -25,21 +49,42 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
input_file
|
||||
]
|
||||
|
||||
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
stdout, _ = p.communicate()
|
||||
if 'error' in stdout:
|
||||
log.error(stdout) # Ghostscript puts errors in stdout
|
||||
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
if _gs_error_reported(p.stdout):
|
||||
log.error(p.stdout)
|
||||
else:
|
||||
log.debug(stdout)
|
||||
log.debug(p.stdout)
|
||||
|
||||
if p.returncode == 0:
|
||||
copy(tmp.name, output_file)
|
||||
else:
|
||||
log.error('Ghostscript rendering failed')
|
||||
log.error('Ghostscript rasterizing failed')
|
||||
raise SubprocessOutputError()
|
||||
|
||||
|
||||
def generate_pdfa(pdf_pages, output_file, log, threads=1):
|
||||
def generate_pdfa(pdf_pages, output_file, compression, log, threads=1):
|
||||
compression_args = []
|
||||
if compression == 'jpeg':
|
||||
compression_args = [
|
||||
"-dAutoFilterColorImages=false",
|
||||
"-dColorImageFilter=/DCTEncode",
|
||||
"-dAutoFilterGrayImages=false",
|
||||
"-dGrayImageFilter=/DCTEncode",
|
||||
]
|
||||
elif compression == 'lossless':
|
||||
compression_args = [
|
||||
"-dAutoFilterColorImages=false",
|
||||
"-dColorImageFilter=/FlateEncode",
|
||||
"-dAutoFilterGrayImages=false",
|
||||
"-dGrayImageFilter=/FlateEncode",
|
||||
]
|
||||
else:
|
||||
compression_args = [
|
||||
"-dAutoFilterColorImages=true",
|
||||
"-dAutoFilterGrayImages=true",
|
||||
]
|
||||
|
||||
with NamedTemporaryFile(delete=True) as gs_pdf:
|
||||
args_gs = [
|
||||
get_program("gs"),
|
||||
@@ -50,20 +95,20 @@ def generate_pdfa(pdf_pages, output_file, log, threads=1):
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dAutoRotatePages=/None",
|
||||
"-sColorConversionStrategy=/RGB",
|
||||
"-sProcessColorModel=DeviceRGB",
|
||||
"-sProcessColorModel=DeviceRGB"
|
||||
] + compression_args + [
|
||||
"-dJPEGQ=95",
|
||||
"-dPDFA=2",
|
||||
"-dPDFACompatibilityPolicy=1",
|
||||
"-sOutputFile=" + gs_pdf.name,
|
||||
]
|
||||
args_gs.extend(pdf_pages)
|
||||
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
stdout, _ = p.communicate()
|
||||
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
|
||||
if 'error' in stdout:
|
||||
log.error(stdout)
|
||||
elif 'overprint mode not set' in stdout:
|
||||
if _gs_error_reported(p.stdout):
|
||||
log.error(p.stdout)
|
||||
elif 'overprint mode not set' in p.stdout:
|
||||
# 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....
|
||||
@@ -72,11 +117,12 @@ def generate_pdfa(pdf_pages, output_file, log, threads=1):
|
||||
"input file to complete PDF/A conversion. "
|
||||
)
|
||||
else:
|
||||
log.debug(stdout)
|
||||
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, output_file)
|
||||
else:
|
||||
log.error('Ghostscript PDF/A failed')
|
||||
log.error('Ghostscript PDF/A rendering failed')
|
||||
raise SubprocessOutputError()
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from subprocess import CalledProcessError, check_output, STDOUT, check_call
|
||||
from subprocess import CalledProcessError, STDOUT, PIPE, run, check_output
|
||||
from functools import lru_cache
|
||||
import sys
|
||||
import os
|
||||
@@ -19,15 +19,14 @@ def version():
|
||||
'--version'
|
||||
]
|
||||
try:
|
||||
versions = check_output(
|
||||
args_qpdf, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
p = run(args_qpdf, universal_newlines=True, stderr=STDOUT,
|
||||
stdout=PIPE)
|
||||
except CalledProcessError as e:
|
||||
print("Could not find qpdf executable on system PATH.",
|
||||
file=sys.stderr)
|
||||
raise MissingDependencyError() from e
|
||||
|
||||
qpdf_version = re.match(r'qpdf version (.+)', versions).group(1)
|
||||
qpdf_version = re.match(r'qpdf version (.+)', p.stdout).group(1)
|
||||
return qpdf_version
|
||||
|
||||
|
||||
@@ -118,7 +117,7 @@ def split_pages(input_file, work_folder, npages):
|
||||
'--pages', input_file, '{0}'.format(n + 1), '--',
|
||||
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
|
||||
]
|
||||
check_call(args_qpdf)
|
||||
run(args_qpdf, check=True)
|
||||
|
||||
|
||||
def merge(input_files, output_file):
|
||||
@@ -129,5 +128,5 @@ def merge(input_files, output_file):
|
||||
args_qpdf = [
|
||||
get_program('qpdf'), input_files[0], '--pages'
|
||||
] + input_files + ['--', output_file]
|
||||
check_call(args_qpdf)
|
||||
run(args_qpdf, check=True)
|
||||
|
||||
|
||||
+55
-33
@@ -11,6 +11,7 @@ from ..helpers import page_number
|
||||
from . import get_program
|
||||
from collections import namedtuple
|
||||
from textwrap import dedent
|
||||
import PyPDF2 as pypdf
|
||||
|
||||
from subprocess import Popen, PIPE, CalledProcessError, \
|
||||
TimeoutExpired, check_output, STDOUT, DEVNULL
|
||||
@@ -186,6 +187,8 @@ def tesseract_log_output(log, stdout, input_file):
|
||||
log.warning(prefix + "unsure about page orientation")
|
||||
elif 'error' in line.lower() or 'exception' in line.lower():
|
||||
log.error(prefix + line.strip())
|
||||
elif 'warning' in line.lower():
|
||||
log.warning(prefix + line.strip())
|
||||
elif 'read_params_file' in line.lower():
|
||||
log.error(prefix + line.strip())
|
||||
else:
|
||||
@@ -197,7 +200,7 @@ def page_timedout(log, input_file):
|
||||
log.warning(prefix + " took too long to OCR - skipping")
|
||||
|
||||
|
||||
def _generate_null_hocr(output_hocr, image):
|
||||
def _generate_null_hocr(output_hocr, output_sidecar, image):
|
||||
"""Produce a .hocr file that reports no text detected on a page that is
|
||||
the same size as the input image."""
|
||||
from PIL import Image
|
||||
@@ -207,22 +210,29 @@ def _generate_null_hocr(output_hocr, image):
|
||||
|
||||
with open(output_hocr, 'w', encoding="utf-8") as f:
|
||||
f.write(HOCR_TEMPLATE.format(w, h))
|
||||
with open(output_sidecar, 'w', encoding='utf-8') as f:
|
||||
f.write('[skipped page]')
|
||||
|
||||
|
||||
def generate_hocr(input_file, output_hocr, language: list, engine_mode,
|
||||
def generate_hocr(input_file, output_files, language: list, engine_mode,
|
||||
tessconfig: list,
|
||||
timeout: float, pagesegmode: int, log):
|
||||
|
||||
badxml = os.path.splitext(output_hocr)[0] + '.badxml'
|
||||
output_hocr = next(o for o in output_files if o.endswith('.hocr'))
|
||||
output_sidecar = next(o for o in output_files if o.endswith('.txt'))
|
||||
prefix = os.path.splitext(output_hocr)[0]
|
||||
|
||||
args_tesseract = tess_base_args(language, engine_mode)
|
||||
|
||||
if pagesegmode is not None:
|
||||
args_tesseract.extend([psm(), str(pagesegmode)])
|
||||
|
||||
# Reminder: test suite tesseract spoofers will break after any changes
|
||||
# to the number of order parameters here
|
||||
args_tesseract.extend([
|
||||
input_file,
|
||||
badxml,
|
||||
prefix,
|
||||
'txt',
|
||||
'hocr'
|
||||
] + tessconfig)
|
||||
try:
|
||||
@@ -235,49 +245,54 @@ def generate_hocr(input_file, output_hocr, language: list, engine_mode,
|
||||
# Temporary workaround to hocrTransform not being able to function if
|
||||
# it does not have a valid hOCR file.
|
||||
page_timedout(log, input_file)
|
||||
_generate_null_hocr(output_hocr, input_file)
|
||||
_generate_null_hocr(output_hocr, output_sidecar, input_file)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_file)
|
||||
if 'read_params_file: parameter not found' in e.output:
|
||||
raise TesseractConfigError() from e
|
||||
if 'Image too large' in e.output:
|
||||
_generate_null_hocr(output_hocr, input_file)
|
||||
_generate_null_hocr(output_hocr, output_sidecar, input_file)
|
||||
return
|
||||
|
||||
raise e from e
|
||||
else:
|
||||
tesseract_log_output(log, stdout, input_file)
|
||||
|
||||
if os.path.exists(badxml + '.html'):
|
||||
# Tesseract 3.02 appends suffix ".html" on its own (.badxml.html)
|
||||
shutil.move(badxml + '.html', badxml)
|
||||
elif os.path.exists(badxml + '.hocr'):
|
||||
# Tesseract 3.03 appends suffix ".hocr" on its own (.badxml.hocr)
|
||||
shutil.move(badxml + '.hocr', badxml)
|
||||
|
||||
# Tesseract 3.03 inserts source filename into hocr file without
|
||||
# escaping it, creating invalid XML and breaking the parser.
|
||||
# As a workaround, rewrite the hocr file, replacing the filename
|
||||
# with a space. Don't know if Tesseract 3.02 does the same.
|
||||
|
||||
regex_nested_single_quotes = re.compile(
|
||||
r"""title='image "([^"]*)";""")
|
||||
with open(badxml, mode='r', encoding='utf-8') as f_in, \
|
||||
open(output_hocr, mode='w', encoding='utf-8') as f_out:
|
||||
for line in f_in:
|
||||
line = regex_nested_single_quotes.sub(
|
||||
r"""title='image " ";""", line)
|
||||
f_out.write(line)
|
||||
# The sidecar text file will get the suffix .txt; rename it to
|
||||
# whatever caller wants it named
|
||||
if os.path.exists(prefix + '.txt'):
|
||||
shutil.move(prefix + '.txt', output_sidecar)
|
||||
|
||||
|
||||
def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
|
||||
engine_mode, text_only: bool,
|
||||
def use_skip_page(text_only, skip_pdf, output_pdf, output_text):
|
||||
with open(output_text, 'w') as f:
|
||||
f.write('[skipped page]')
|
||||
|
||||
if not text_only:
|
||||
os.symlink(skip_pdf, output_pdf)
|
||||
return
|
||||
|
||||
# For text only we must create a blank page with dimensions identical
|
||||
# to the skip page because this is equivalent to a page with no text
|
||||
|
||||
pdf_in = pypdf.PdfFileReader(skip_pdf)
|
||||
page0 = pdf_in.pages[0]
|
||||
|
||||
with open(output_pdf, 'wb') as out:
|
||||
pdf_out = pypdf.PdfFileWriter()
|
||||
w, h = page0.mediaBox.getWidth(), page0.mediaBox.getHeight()
|
||||
pdf_out.addBlankPage(w, h)
|
||||
pdf_out.write(out)
|
||||
|
||||
|
||||
def generate_pdf(*, input_image, skip_pdf, output_pdf, output_text,
|
||||
language: list, engine_mode, text_only: bool,
|
||||
tessconfig: list, timeout: float, pagesegmode: int, log):
|
||||
'''Use Tesseract to render a PDF.
|
||||
|
||||
input_image -- image to analyze
|
||||
skip_pdf -- if we time out, use this file as output
|
||||
output_pdf -- file to generate
|
||||
output_text -- OCR text file
|
||||
language -- list of languages to consider
|
||||
engine_mode -- engine mode argument for tess v4
|
||||
text_only -- enable tesseract text only mode?
|
||||
@@ -294,10 +309,15 @@ def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
|
||||
if text_only:
|
||||
args_tesseract.extend(['-c', 'textonly_pdf=1'])
|
||||
|
||||
prefix = os.path.splitext(output_pdf)[0] # Tesseract appends suffixes
|
||||
|
||||
# Reminder: test suite tesseract spoofers will break after any changes
|
||||
# to the number of order parameters here
|
||||
args_tesseract.extend([
|
||||
input_image,
|
||||
os.path.splitext(output_pdf)[0], # Tesseract appends suffix
|
||||
'pdf'
|
||||
prefix,
|
||||
'txt',
|
||||
'pdf',
|
||||
] + tessconfig)
|
||||
|
||||
try:
|
||||
@@ -305,16 +325,18 @@ def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
if os.path.exists(prefix + '.txt'):
|
||||
shutil.move(prefix + '.txt', output_text)
|
||||
except TimeoutExpired:
|
||||
page_timedout(log, input_image)
|
||||
shutil.copy(skip_pdf, output_pdf)
|
||||
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_image)
|
||||
if 'read_params_file: parameter not found' in e.output:
|
||||
raise TesseractConfigError() from e
|
||||
|
||||
if 'Image too large' in e.output:
|
||||
shutil.copy(skip_pdf, output_pdf)
|
||||
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
|
||||
return
|
||||
raise e from e
|
||||
else:
|
||||
|
||||
@@ -78,7 +78,8 @@ def run(input_file, output_file, dpi, log, mode_args):
|
||||
raise e from e
|
||||
else:
|
||||
log.debug(stdout)
|
||||
Image.open(output_pnm.name).save(output_file)
|
||||
# unpaper sets dpi to 72
|
||||
Image.open(output_pnm.name).save(output_file, dpi=(dpi, dpi))
|
||||
|
||||
|
||||
def deskew(input_file, output_file, dpi, log):
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015-16 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
# This file is now an alias for __main__
|
||||
# Consider removing in future releases
|
||||
|
||||
from ocrmypdf.__main__ import *
|
||||
+134
-45
@@ -3,19 +3,12 @@
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from decimal import Decimal
|
||||
from math import hypot
|
||||
from math import hypot, isclose
|
||||
import re
|
||||
import sys
|
||||
import PyPDF2 as pypdf
|
||||
from collections import namedtuple
|
||||
|
||||
try:
|
||||
from math import isclose
|
||||
except ImportError:
|
||||
def isclose(a, b, rel_tol=1e-9):
|
||||
"Python 3.4 does not have math.isclose()"
|
||||
diff = abs(b - a)
|
||||
return diff <= abs(rel_tol * b) or diff <= abs(rel_tol * a)
|
||||
|
||||
matrix_mult = pypdf.pdf.utils.matrixMultiply
|
||||
|
||||
@@ -89,13 +82,13 @@ def _is_unit_square(shorthand):
|
||||
pairwise = zip(values, UNIT_SQUARE)
|
||||
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
|
||||
|
||||
RasterSettings = namedtuple('RasterSettings',
|
||||
XobjectSettings = namedtuple('XobjectSettings',
|
||||
['name', 'shorthand', 'stack_depth'])
|
||||
|
||||
InlineSettings = namedtuple('InlineSettings',
|
||||
['settings', 'shorthand', 'stack_depth'])
|
||||
|
||||
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
|
||||
ContentsInfo = namedtuple('ContentsInfo', ['xobject_settings', 'inline_images'])
|
||||
|
||||
|
||||
def _normalize_stack(operations):
|
||||
@@ -114,7 +107,7 @@ def _normalize_stack(operations):
|
||||
yield (operands, command)
|
||||
|
||||
|
||||
def _interpret_contents(contentstream):
|
||||
def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
|
||||
"""Interpret the PDF content stream
|
||||
|
||||
The stack represents the state of the PDF graphics stack. We are only
|
||||
@@ -139,8 +132,8 @@ def _interpret_contents(contentstream):
|
||||
|
||||
operations = contentstream.operations
|
||||
stack = []
|
||||
ctm = _matrix_from_shorthand(UNIT_SQUARE)
|
||||
image_raster_settings = []
|
||||
ctm = _matrix_from_shorthand(initial_shorthand)
|
||||
xobject_settings = []
|
||||
inline_images = []
|
||||
|
||||
for n, op in enumerate(_normalize_stack(operations)):
|
||||
@@ -161,10 +154,10 @@ def _interpret_contents(contentstream):
|
||||
_matrix_from_shorthand(operands), ctm)
|
||||
elif command == b'Do':
|
||||
image_name = operands[0]
|
||||
raster = RasterSettings(
|
||||
settings = XobjectSettings(
|
||||
name=image_name, shorthand=_shorthand_from_matrix(ctm),
|
||||
stack_depth=len(stack))
|
||||
image_raster_settings.append(raster)
|
||||
xobject_settings.append(settings)
|
||||
elif command == b'INLINE IMAGE':
|
||||
settings = operands['settings']
|
||||
inline = InlineSettings(
|
||||
@@ -173,7 +166,7 @@ def _interpret_contents(contentstream):
|
||||
inline_images.append(inline)
|
||||
|
||||
return ContentsInfo(
|
||||
raster_settings=image_raster_settings,
|
||||
xobject_settings=xobject_settings,
|
||||
inline_images=inline_images)
|
||||
|
||||
|
||||
@@ -241,8 +234,8 @@ def _get_dpi(ctm_shorthand, image_size):
|
||||
return (dpi_w, dpi_h)
|
||||
|
||||
|
||||
def _find_page_inline_images(page, pageinfo, contentsinfo):
|
||||
"Find inline images on the page"
|
||||
def _find_inline_images(contentsinfo):
|
||||
"Find inline images in the contentstream"
|
||||
|
||||
for n, inline in enumerate(contentsinfo.inline_images):
|
||||
image = {}
|
||||
@@ -272,20 +265,44 @@ def _find_page_inline_images(page, pageinfo, contentsinfo):
|
||||
yield image
|
||||
|
||||
|
||||
def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||
"Find images stored in XObject resources"
|
||||
def _image_xobjects(container):
|
||||
"""Search for all XObject-based images in the container
|
||||
|
||||
try:
|
||||
page['/Resources']['/XObject']
|
||||
except KeyError:
|
||||
Usually the container is a page, but it could also be a Form XObject
|
||||
that contains images. Filter out the Form XObjects which are dealt with
|
||||
elsewhere.
|
||||
|
||||
Generate a sequence of tuples (image, xobj container), where container,
|
||||
where xobj is the name of the object and image is the object itself,
|
||||
since the object does not know its own name.
|
||||
|
||||
"""
|
||||
|
||||
if '/Resources' not in container:
|
||||
return
|
||||
for xobj in page['/Resources']['/XObject']:
|
||||
# PyPDF2 returns the keys as an iterator
|
||||
pdfimage = page['/Resources']['/XObject'][xobj]
|
||||
if pdfimage['/Subtype'] != '/Image':
|
||||
continue
|
||||
resources = container['/Resources']
|
||||
if '/XObject' not in resources:
|
||||
return
|
||||
for xobj in resources['/XObject']:
|
||||
candidate = resources['/XObject'][xobj]
|
||||
if candidate['/Subtype'] == '/Image':
|
||||
image = candidate
|
||||
yield (image, xobj)
|
||||
|
||||
|
||||
def _find_regular_images(container, contentsinfo):
|
||||
"""Find images stored in the container's /Resources /XObject
|
||||
|
||||
Usually the container is a page, but it could also be a Form XObject
|
||||
that contains images.
|
||||
|
||||
Generates images with their DPI at time of drawing.
|
||||
|
||||
"""
|
||||
|
||||
for pdfimage, xobj in _image_xobjects(container):
|
||||
image = {}
|
||||
image['name'] = str(xobj)
|
||||
image['name'] = xobj
|
||||
image['width'] = pdfimage['/Width']
|
||||
image['height'] = pdfimage['/Height']
|
||||
if '/BitsPerComponent' in pdfimage:
|
||||
@@ -329,12 +346,12 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||
|
||||
image['dpi_w'] = image['dpi_h'] = 0
|
||||
|
||||
for raster in contentsinfo.raster_settings:
|
||||
for xobj in contentsinfo.xobject_settings:
|
||||
# Loop in case the same image is display multiple times on a page
|
||||
if raster.name != image['name']:
|
||||
if xobj.name != image['name']:
|
||||
continue
|
||||
|
||||
if raster.stack_depth == 0 and _is_unit_square(raster.shorthand):
|
||||
if xobj.stack_depth == 0 and _is_unit_square(xobj.shorthand):
|
||||
# At least one PDF in the wild (and test suite) draws an image
|
||||
# when the graphics stack depth is 0, meaning that the image
|
||||
# gets drawn into a square of 1x1 PDF units (or 1/72",
|
||||
@@ -343,7 +360,7 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||
continue
|
||||
|
||||
dpi_w, dpi_h = _get_dpi(
|
||||
raster.shorthand, (image['width'], image['height']))
|
||||
xobj.shorthand, (image['width'], image['height']))
|
||||
|
||||
# When image is used multiple times take the highest DPI it is
|
||||
# rendered at
|
||||
@@ -358,12 +375,91 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||
yield image
|
||||
|
||||
|
||||
def _find_page_images(page, pageinfo, contentsinfo):
|
||||
yield from _find_page_inline_images(page, pageinfo, contentsinfo)
|
||||
yield from _find_page_regular_images(page, pageinfo, contentsinfo)
|
||||
def _find_form_xobject_images(pdf, container, contentsinfo):
|
||||
"""Find any images that are in Form XObjects in the container
|
||||
|
||||
The container may be a page, or a parent Form XObject.
|
||||
|
||||
"""
|
||||
if '/Resources' not in container:
|
||||
return
|
||||
resources = container['/Resources']
|
||||
if '/XObject' not in resources:
|
||||
return
|
||||
for xobj in resources['/XObject']:
|
||||
candidate = resources['/XObject'][xobj]
|
||||
if candidate['/Subtype'] != '/Form':
|
||||
continue
|
||||
|
||||
form_xobject = candidate
|
||||
for settings in contentsinfo.xobject_settings:
|
||||
if settings.name != xobj:
|
||||
continue
|
||||
|
||||
# Find images once for each time this Form XObject is drawn.
|
||||
# This could be optimized to cache the multiple drawing events
|
||||
# but in practice both Form XObjects and multiple drawing of the
|
||||
# same object are both very rare.
|
||||
ctm_shorthand = settings.shorthand
|
||||
yield from _find_images(pdf, form_xobject, ctm_shorthand)
|
||||
|
||||
|
||||
def _find_images(pdf, container, shorthand=None):
|
||||
"""Find all individual instances of images drawn in the container
|
||||
|
||||
Usually the container is a page, but it may also be a Form XObject.
|
||||
|
||||
On a typical page images are stored inline or as regular images
|
||||
in an XObject.
|
||||
|
||||
Form XObjects may include inline images, XObject images,
|
||||
and recursively, other Form XObjects; and also vector drawing commands.
|
||||
|
||||
Every instance of an image being drawn somewhere is flattened and
|
||||
treated as a unique image, since if the same image is drawn multiple times
|
||||
on one page it may be drawn at differing resolutions, and our objective
|
||||
is to find the resolution at which the page can be rastered without
|
||||
downsampling.
|
||||
|
||||
"""
|
||||
|
||||
if container.get('/Type') == '/Page' and '/Contents' in container:
|
||||
# For a /Page the content stream is attached to the page's /Contents
|
||||
page = container
|
||||
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
|
||||
initial_shorthand = shorthand or UNIT_SQUARE
|
||||
elif container.get('/Type') == '/XObject' and \
|
||||
container['/Subtype'] == '/Form':
|
||||
# For a Form XObject that content stream is attached to the XObject
|
||||
contentstream = pypdf.pdf.ContentStream(container, pdf)
|
||||
|
||||
# Set the CTM to the state it was when the "Do" operator was
|
||||
# encountered that is drawing this instance of the Form XObject
|
||||
ctm = _matrix_from_shorthand(shorthand or UNIT_SQUARE)
|
||||
|
||||
# A Form XObject may provide its own matrix to map form space into
|
||||
# user space. Get this if one exists
|
||||
form_matrix = _matrix_from_shorthand(
|
||||
container.get('/Matrix', UNIT_SQUARE))
|
||||
|
||||
# Concatenate form matrix with CTM to ensure CTM is correct for
|
||||
# drawing this instance of the XObject
|
||||
ctm = matrix_mult(form_matrix, ctm)
|
||||
initial_shorthand = _shorthand_from_matrix(ctm)
|
||||
else:
|
||||
return
|
||||
|
||||
contentsinfo = _interpret_contents(contentstream, initial_shorthand)
|
||||
|
||||
yield from _find_inline_images(contentsinfo)
|
||||
yield from _find_regular_images(container, contentsinfo)
|
||||
yield from _find_form_xobject_images(pdf, container, contentsinfo)
|
||||
|
||||
|
||||
def _page_has_text(pdf, page):
|
||||
if not '/Contents' in page:
|
||||
return False
|
||||
|
||||
# Simple test
|
||||
text = page.extractText()
|
||||
if text.strip() != '':
|
||||
@@ -405,15 +501,8 @@ def _pdf_get_pageinfo(infile, pageno: int):
|
||||
except KeyError:
|
||||
pageinfo['rotate'] = 0
|
||||
|
||||
try:
|
||||
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
|
||||
except AttributeError as e:
|
||||
return pageinfo
|
||||
|
||||
contentsinfo = _interpret_contents(contentstream)
|
||||
pageinfo['images'] = [im for im in _find_page_images(
|
||||
page, pageinfo, contentsinfo)]
|
||||
|
||||
pageinfo['images'] = [im for im in
|
||||
_find_images(pdf, page)]
|
||||
if pageinfo['images']:
|
||||
xres = max(image['dpi_w'] for image in pageinfo['images'])
|
||||
yres = max(image['dpi_h'] for image in pageinfo['images'])
|
||||
|
||||
+18
-12
@@ -26,11 +26,7 @@ pdfa_def_template = u"""%!
|
||||
/ICCProfile ($icc_profile)
|
||||
def
|
||||
|
||||
[ /Title <$title>
|
||||
/Author <$author>
|
||||
/Subject <$subject>
|
||||
/Keywords <$keywords>
|
||||
/Creator <$creator>
|
||||
[$docinfo
|
||||
/DOCINFO pdfmark
|
||||
|
||||
% Define an ICC profile :
|
||||
@@ -89,20 +85,30 @@ def encode_text_string(s: str) -> str:
|
||||
|
||||
|
||||
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
|
||||
pdfmark_utf16 = {k: encode_text_string(v) for k, v in pdfmark.items()}
|
||||
# Ghostscript <= 9.21 has a bug where null entries in DOCINFO might produce
|
||||
# ERROR: VMerror (-25) on closing pdfwrite device.
|
||||
# https://bugs.ghostscript.com/show_bug.cgi?id=697684
|
||||
# Work around this by only adding keys that have a nontrivial value
|
||||
docinfo_keys = ('/Title', '/Author', '/Subject', '/Creator', '/Keywords')
|
||||
docinfo_line_template = ' {key} <{value}>'
|
||||
|
||||
def docinfo_gen():
|
||||
for key in docinfo_keys:
|
||||
if key in pdfmark and pdfmark[key].strip() != '':
|
||||
line = docinfo_line_template.format(
|
||||
key=key, value=encode_text_string(pdfmark[key]))
|
||||
yield line
|
||||
|
||||
docinfo = '\n'.join(docinfo_gen())
|
||||
|
||||
t = Template(pdfa_def_template)
|
||||
result = t.substitute(icc_profile=icc_profile,
|
||||
icc_identifier=icc_identifier,
|
||||
title=pdfmark_utf16.get('/Title', ''),
|
||||
author=pdfmark_utf16.get('/Author', ''),
|
||||
subject=pdfmark_utf16.get('/Subject', ''),
|
||||
creator=pdfmark_utf16.get('/Creator', ''),
|
||||
keywords=pdfmark_utf16.get('/Keywords', ''))
|
||||
docinfo=docinfo)
|
||||
return result
|
||||
|
||||
|
||||
def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'):
|
||||
def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB'):
|
||||
if icc == 'sRGB':
|
||||
icc_profile = SRGB_ICC_PROFILE
|
||||
else:
|
||||
|
||||
+129
-44
@@ -23,7 +23,7 @@ from ruffus import formatter, regex, Pipeline, suffix
|
||||
|
||||
from .hocrtransform import HocrTransform
|
||||
from .pageinfo import pdf_get_all_pageinfo
|
||||
from .pdfa import generate_pdfa_def, file_claims_pdfa
|
||||
from .pdfa import generate_pdfa_ps, file_claims_pdfa
|
||||
from .helpers import re_symlink, is_iterable_notstr, page_number
|
||||
from .exec import ghostscript, tesseract, qpdf
|
||||
from .exceptions import *
|
||||
@@ -159,17 +159,20 @@ def triage(
|
||||
log,
|
||||
context):
|
||||
|
||||
options = context.get_options()
|
||||
try:
|
||||
with open(input_file, 'rb') as f:
|
||||
signature = f.read(4)
|
||||
if signature == b'%PDF':
|
||||
if options.image_dpi:
|
||||
log.warning("Argument --image-dpi ignored because the "
|
||||
"input file is a PDF, not an image.")
|
||||
re_symlink(input_file, output_file, log)
|
||||
return
|
||||
except EnvironmentError as e:
|
||||
log.error(e)
|
||||
raise InputFileError() from e
|
||||
|
||||
options = context.get_options()
|
||||
triage_image_file(input_file, output_file, log, options)
|
||||
|
||||
|
||||
@@ -247,7 +250,7 @@ def is_ocr_required(pageinfo, log, options):
|
||||
"skipping all processing on this page"))
|
||||
ocr_required = False
|
||||
|
||||
if ocr_required and options.skip_big:
|
||||
if ocr_required and options.skip_big and pageinfo['images']:
|
||||
pixel_count = pageinfo['width_pixels'] * pageinfo['height_pixels']
|
||||
if pixel_count > (options.skip_big * 1000000):
|
||||
ocr_required = False
|
||||
@@ -393,15 +396,16 @@ def rasterize_with_ghostscript(
|
||||
pageinfo = get_pageinfo(input_file, context)
|
||||
|
||||
device = 'png16m' # 24-bit
|
||||
if all(image['comp'] == 1 for image in pageinfo['images']):
|
||||
if all(image['bpc'] == 1 for image in pageinfo['images']):
|
||||
device = 'pngmono'
|
||||
elif all(image['bpc'] > 1 and image['color'] == 'index'
|
||||
for image in pageinfo['images']):
|
||||
device = 'png256'
|
||||
elif all(image['bpc'] > 1 and image['color'] == 'gray'
|
||||
for image in pageinfo['images']):
|
||||
device = 'pnggray'
|
||||
if pageinfo['images']:
|
||||
if all(image['comp'] == 1 for image in pageinfo['images']):
|
||||
if all(image['bpc'] == 1 for image in pageinfo['images']):
|
||||
device = 'pngmono'
|
||||
elif all(image['bpc'] > 1 and image['color'] == 'index'
|
||||
for image in pageinfo['images']):
|
||||
device = 'png256'
|
||||
elif all(image['bpc'] > 1 and image['color'] == 'gray'
|
||||
for image in pageinfo['images']):
|
||||
device = 'pnggray'
|
||||
|
||||
log.debug("Rasterize {0} with {1}".format(
|
||||
os.path.basename(input_file), device))
|
||||
@@ -482,13 +486,13 @@ def select_ocr_image(
|
||||
|
||||
def ocr_tesseract_hocr(
|
||||
input_file,
|
||||
output_file,
|
||||
output_files,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
tesseract.generate_hocr(
|
||||
input_file=input_file,
|
||||
output_hocr=output_file,
|
||||
output_files=output_files,
|
||||
language=options.language,
|
||||
engine_mode=options.tesseract_oem,
|
||||
tessconfig=options.tesseract_config,
|
||||
@@ -517,7 +521,10 @@ def select_visible_page_image(
|
||||
image = next(ii for ii in infiles if ii.endswith(image_suffix))
|
||||
|
||||
pageinfo = get_pageinfo(image, context)
|
||||
if all(orig_image['enc'] == 'jpeg' for orig_image in pageinfo['images']):
|
||||
if pageinfo['images'] and \
|
||||
all(im['enc'] == 'jpeg' for im in pageinfo['images']):
|
||||
log.debug('{:4d}: JPEG input -> JPEG output'.format(
|
||||
page_number(image)))
|
||||
# If all images were JPEGs originally, produce a JPEG as output
|
||||
im = Image.open(image)
|
||||
|
||||
@@ -555,27 +562,32 @@ def select_image_layer(
|
||||
re_symlink(page_pdf, output_file, log)
|
||||
else:
|
||||
pageinfo = get_pageinfo(image, context)
|
||||
dpi = get_page_dpi(pageinfo, options)
|
||||
dpi = float(dpi[0]), float(dpi[1])
|
||||
layout_fun = img2pdf.get_fixed_dpi_layout_fun(dpi)
|
||||
|
||||
# We rasterize a square DPI version of each page because most image
|
||||
# processing tools don't support rectangular DPI. Use the square DPI
|
||||
# as it accurately describes the image. It would be possible to
|
||||
# resample the image at this stage back to non-square DPI to more
|
||||
# closely resemble the input, except that the hocr renderer does not
|
||||
# understand non-square DPI. The tess4 renderer would be fine.
|
||||
dpi = get_page_square_dpi(pageinfo, options)
|
||||
layout_fun = img2pdf.get_fixed_dpi_layout_fun((dpi, dpi))
|
||||
|
||||
with open(image, 'rb') as imfile, \
|
||||
open(output_file, 'wb') as pdf:
|
||||
rawdata = imfile.read()
|
||||
log.debug('{:4d}: convert'.format(page_number(page_pdf)))
|
||||
img2pdf.convert(
|
||||
rawdata, with_pdfrw=False,
|
||||
imfile, with_pdfrw=False,
|
||||
layout_fun=layout_fun, outputstream=pdf)
|
||||
log.debug('{:4d}: convert done'.format(page_number(page_pdf)))
|
||||
|
||||
|
||||
def render_hocr_page(
|
||||
input_file,
|
||||
infiles,
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
hocr = input_file
|
||||
hocr = next(ii for ii in infiles if ii.endswith('.hocr'))
|
||||
pageinfo = get_pageinfo(hocr, context)
|
||||
dpi = get_page_square_dpi(pageinfo, options)
|
||||
|
||||
@@ -584,14 +596,22 @@ def render_hocr_page(
|
||||
showBoundingboxes=False, invisibleText=True)
|
||||
|
||||
|
||||
def flatten_groups(groups):
|
||||
for obj in groups:
|
||||
if is_iterable_notstr(obj):
|
||||
yield from obj
|
||||
else:
|
||||
yield obj
|
||||
|
||||
|
||||
def render_hocr_debug_page(
|
||||
infiles,
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
hocr = next(ii for ii in infiles if ii.endswith('.hocr'))
|
||||
image = next(ii for ii in infiles if ii.endswith('.image'))
|
||||
hocr = next(ii for ii in flatten_groups(infiles) if ii.endswith('.hocr'))
|
||||
image = next(ii for ii in flatten_groups(infiles) if ii.endswith('.image'))
|
||||
|
||||
pageinfo = get_pageinfo(image, context)
|
||||
dpi = get_page_square_dpi(pageinfo, options)
|
||||
@@ -606,8 +626,10 @@ def combine_layers(
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
text = next(ii for ii in infiles if ii.endswith('.text.pdf'))
|
||||
image = next(ii for ii in infiles if ii.endswith('.image-layer.pdf'))
|
||||
text = next(ii for ii in flatten_groups(infiles)
|
||||
if ii.endswith('.text.pdf'))
|
||||
image = next(ii for ii in flatten_groups(infiles)
|
||||
if ii.endswith('.image-layer.pdf'))
|
||||
|
||||
pdf_text = pypdf.PdfFileReader(open(text, "rb"))
|
||||
pdf_image = pypdf.PdfFileReader(open(image, "rb"))
|
||||
@@ -673,21 +695,27 @@ def combine_layers(
|
||||
|
||||
def ocr_tesseract_and_render_pdf(
|
||||
infiles,
|
||||
output_file,
|
||||
outfiles,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
input_image = next((ii for ii in infiles if ii.endswith('.image')), '')
|
||||
input_pdf = next((ii for ii in infiles if ii.endswith('.pdf')))
|
||||
output_pdf = next((ii for ii in outfiles if ii.endswith('.pdf')))
|
||||
output_text = next((ii for ii in outfiles if ii.endswith('.txt')))
|
||||
|
||||
if not input_image:
|
||||
# Skipping this page
|
||||
re_symlink(input_pdf, output_file, log)
|
||||
re_symlink(input_pdf, output_pdf, log)
|
||||
with open(output_text, 'w') as f:
|
||||
f.write('[skipped page]')
|
||||
return
|
||||
|
||||
tesseract.generate_pdf(
|
||||
input_image=input_image,
|
||||
skip_pdf=input_pdf,
|
||||
output_pdf=output_file,
|
||||
output_pdf=output_pdf,
|
||||
output_text=output_text,
|
||||
language=options.language,
|
||||
engine_mode=options.tesseract_oem,
|
||||
text_only=False,
|
||||
@@ -699,19 +727,23 @@ def ocr_tesseract_and_render_pdf(
|
||||
|
||||
def ocr_tesseract_textonly_pdf(
|
||||
infiles,
|
||||
output_file,
|
||||
outfiles,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
input_image = next((ii for ii in infiles if ii.endswith('.ocr.png')), '')
|
||||
if not input_image:
|
||||
raise ValueError("No image rendered?")
|
||||
|
||||
skip_pdf = next((ii for ii in infiles if ii.endswith('.pdf')))
|
||||
|
||||
output_pdf = next((ii for ii in outfiles if ii.endswith('.pdf')))
|
||||
output_text = next((ii for ii in outfiles if ii.endswith('.txt')))
|
||||
|
||||
tesseract.generate_pdf(
|
||||
input_image=input_image,
|
||||
skip_pdf=skip_pdf,
|
||||
output_pdf=output_file,
|
||||
output_pdf=output_pdf,
|
||||
output_text=output_text,
|
||||
language=options.language,
|
||||
engine_mode=options.tesseract_oem,
|
||||
text_only=True,
|
||||
@@ -762,13 +794,14 @@ def generate_postscript_stub(
|
||||
options = context.get_options()
|
||||
pdf = pypdf.PdfFileReader(input_file)
|
||||
pdfmark = get_pdfmark(pdf, options)
|
||||
generate_pdfa_def(output_file, pdfmark)
|
||||
generate_pdfa_ps(output_file, pdfmark)
|
||||
|
||||
|
||||
def skip_page(
|
||||
input_file,
|
||||
output_file,
|
||||
log):
|
||||
log,
|
||||
context):
|
||||
# The purpose of this step is its filter to forward only the skipped
|
||||
# files (.skip.oriented.pdf) while disregarding the processed ones
|
||||
# (.ocr.oriented.pdf). Alternative would be for merge_pages to filter
|
||||
@@ -777,7 +810,7 @@ def skip_page(
|
||||
|
||||
|
||||
def merge_pages_ghostscript(
|
||||
input_files,
|
||||
input_files_groups,
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
@@ -795,17 +828,24 @@ def merge_pages_ghostscript(
|
||||
key += 1
|
||||
return key
|
||||
|
||||
input_files = (f for f in flatten_groups(input_files_groups)
|
||||
if not f.endswith('.txt'))
|
||||
pdf_pages = sorted(input_files, key=input_file_order)
|
||||
log.debug("Final pages: " + "\n".join(pdf_pages))
|
||||
ghostscript.generate_pdfa(pdf_pages, output_file, log, options.jobs or 1)
|
||||
ghostscript.generate_pdfa(
|
||||
pdf_pages, output_file, options.pdfa_image_compression,
|
||||
log, options.jobs or 1)
|
||||
|
||||
|
||||
def merge_pages_qpdf(
|
||||
input_files,
|
||||
input_files_groups,
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
|
||||
input_files = list(f for f in flatten_groups(input_files_groups)
|
||||
if not f.endswith('.txt'))
|
||||
metadata_file = next(
|
||||
(ii for ii in input_files if ii.endswith('.repaired.pdf')))
|
||||
input_files.remove(metadata_file)
|
||||
@@ -839,6 +879,40 @@ def merge_pages_qpdf(
|
||||
qpdf.merge(pdf_pages, output_file)
|
||||
|
||||
|
||||
def merge_sidecars(
|
||||
input_files_groups,
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
pdfinfo = context.get_pdfinfo()
|
||||
|
||||
txt_files = [None] * len(pdfinfo)
|
||||
|
||||
for infile in flatten_groups(input_files_groups):
|
||||
if infile.endswith('.txt'):
|
||||
idx = page_number(infile) - 1
|
||||
txt_files[idx] = infile
|
||||
|
||||
def write_pages(stream):
|
||||
for page_number, txt_file in enumerate(txt_files):
|
||||
if page_number != 0:
|
||||
stream.write('\f') # Form feed between pages
|
||||
if txt_file:
|
||||
with open(txt_file, 'r') as in_:
|
||||
stream.write(in_.read())
|
||||
else:
|
||||
stream.write('[OCR skipped on page {}]'.format(
|
||||
page_number + 1))
|
||||
|
||||
if output_file == '-':
|
||||
write_pages(sys.stdout)
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
with open(output_file, 'w', encoding='utf-8') as out:
|
||||
write_pages(out)
|
||||
|
||||
|
||||
def copy_final(
|
||||
input_files,
|
||||
output_file,
|
||||
@@ -943,7 +1017,7 @@ def build_pipeline(options, work_folder, log, context):
|
||||
task_func=ocr_tesseract_hocr,
|
||||
input=task_select_ocr_image,
|
||||
filter=suffix(".ocr.png"),
|
||||
output=".hocr",
|
||||
output=[".hocr", ".txt"],
|
||||
extras=[log, context])
|
||||
task_ocr_tesseract_hocr.graphviz(fillcolor='"#00cc66"')
|
||||
task_ocr_tesseract_hocr.active_if(options.pdf_renderer == 'hocr')
|
||||
@@ -975,8 +1049,8 @@ def build_pipeline(options, work_folder, log, context):
|
||||
task_render_hocr_page = main_pipeline.transform(
|
||||
task_func=render_hocr_page,
|
||||
input=task_ocr_tesseract_hocr,
|
||||
filter=suffix('.hocr'),
|
||||
output='.text.pdf',
|
||||
filter=regex(r".*/(\d{6})(?:\.hocr)"),
|
||||
output=os.path.join(work_folder, r'\1.text.pdf'),
|
||||
extras=[log, context])
|
||||
task_render_hocr_page.graphviz(fillcolor='"#00cc66"')
|
||||
task_render_hocr_page.active_if(options.pdf_renderer == 'hocr')
|
||||
@@ -996,7 +1070,8 @@ def build_pipeline(options, work_folder, log, context):
|
||||
task_func=ocr_tesseract_textonly_pdf,
|
||||
input=[task_select_ocr_image, task_orient_page],
|
||||
filter=regex(r".*/(\d{6})(?:\.ocr.png|\.ocr\.oriented\.pdf)"),
|
||||
output=os.path.join(work_folder, r'\1.text.pdf'),
|
||||
output=[os.path.join(work_folder, r'\1.text.pdf'),
|
||||
os.path.join(work_folder, r'\1.text.txt')],
|
||||
extras=[log, context])
|
||||
task_ocr_tesseract_textonly_pdf.graphviz(fillcolor='"#ff69b4"')
|
||||
task_ocr_tesseract_textonly_pdf.active_if(options.pdf_renderer == 'tess4')
|
||||
@@ -1019,7 +1094,8 @@ def build_pipeline(options, work_folder, log, context):
|
||||
task_func=ocr_tesseract_and_render_pdf,
|
||||
input=[task_select_visible_page_image, task_orient_page],
|
||||
filter=regex(r".*/(\d{6})(?:\.image|\.ocr\.oriented\.pdf)"),
|
||||
output=os.path.join(work_folder, r'\1.rendered.pdf'),
|
||||
output=[os.path.join(work_folder, r'\1.rendered.pdf'),
|
||||
os.path.join(work_folder, r'\1.rendered.txt')],
|
||||
extras=[log, context])
|
||||
task_ocr_tesseract_and_render_pdf.graphviz(fillcolor='"#66ccff"')
|
||||
task_ocr_tesseract_and_render_pdf.active_if(options.pdf_renderer == 'tesseract')
|
||||
@@ -1031,7 +1107,7 @@ def build_pipeline(options, work_folder, log, context):
|
||||
task_func=generate_postscript_stub,
|
||||
input=task_repair_pdf,
|
||||
filter=formatter(r'\.repaired\.pdf'),
|
||||
output=os.path.join(work_folder, 'pdfa_def.ps'),
|
||||
output=os.path.join(work_folder, 'pdfa.ps'),
|
||||
extras=[log, context])
|
||||
task_generate_postscript_stub.active_if(options.output_type == 'pdfa')
|
||||
|
||||
@@ -1043,7 +1119,7 @@ def build_pipeline(options, work_folder, log, context):
|
||||
filter=suffix('.skip.oriented.pdf'),
|
||||
output='.done.pdf',
|
||||
output_dir=work_folder,
|
||||
extras=[log])
|
||||
extras=[log, context])
|
||||
|
||||
# Merge pages
|
||||
task_merge_pages_ghostscript = main_pipeline.merge(
|
||||
@@ -1068,6 +1144,15 @@ def build_pipeline(options, work_folder, log, context):
|
||||
extras=[log, context])
|
||||
task_merge_pages_qpdf.active_if(options.output_type == 'pdf')
|
||||
|
||||
task_merge_sidecars = main_pipeline.merge(
|
||||
task_func=merge_sidecars,
|
||||
input=[task_ocr_tesseract_hocr,
|
||||
task_ocr_tesseract_and_render_pdf,
|
||||
task_ocr_tesseract_textonly_pdf],
|
||||
output=options.sidecar,
|
||||
extras=[log, context])
|
||||
task_merge_sidecars.active_if(options.sidecar)
|
||||
|
||||
# Finalize
|
||||
task_copy_final = main_pipeline.merge(
|
||||
task_func=copy_final,
|
||||
|
||||
+4
-4
@@ -2,8 +2,8 @@
|
||||
# setup.py lists a separate set of requirements that are looser to simplify
|
||||
# installation
|
||||
ruffus == 2.6.3
|
||||
Pillow == 4.0.0
|
||||
reportlab == 3.3.0
|
||||
PyPDF2 == 1.26
|
||||
Pillow == 4.1.1
|
||||
reportlab == 3.4.0
|
||||
PyPDF2 == 1.26.0
|
||||
img2pdf == 0.2.3
|
||||
cffi == 1.9.1
|
||||
cffi == 1.10.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[bdist_wheel]
|
||||
python-tag = py34
|
||||
python-tag = py35
|
||||
|
||||
[aliases]
|
||||
test=pytest
|
||||
@@ -11,3 +11,4 @@ ignore =
|
||||
[tool:pytest]
|
||||
norecursedirs = lib .pc .git output cache resources
|
||||
testpaths = tests
|
||||
addopts = -n auto
|
||||
@@ -5,8 +5,8 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
if sys.version_info < (3, 4):
|
||||
print("Python 3.4 or newer is required", file=sys.stderr)
|
||||
if sys.version_info < (3, 5):
|
||||
print("Python 3.5 or newer is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
from setuptools import setup, find_packages # nopep8
|
||||
@@ -155,7 +155,7 @@ if not forced and command.startswith('install') or \
|
||||
command in ['check', 'test', 'nosetests', 'easy_install']:
|
||||
check_external_program(
|
||||
program='tesseract',
|
||||
need_version='3.03', # limited by Travis CI / Ubuntu 14.04 backports
|
||||
need_version='3.04', # using backport for Travis CI
|
||||
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}
|
||||
)
|
||||
check_external_program(
|
||||
@@ -195,8 +195,6 @@ setup(
|
||||
packages=find_packages(exclude=["tests", "tests.*"]),
|
||||
keywords=['PDF', 'OCR', 'optical character recognition', 'PDF/A', 'scanning'],
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -224,7 +222,7 @@ setup(
|
||||
install_requires=[
|
||||
'ruffus==2.6.3', # pinned - ocrmypdf implements a 2.6.3 workaround
|
||||
'Pillow>=4.0.0', # Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
|
||||
'reportlab>=3.2.0', # oldest released version with sane image handling
|
||||
'reportlab>=3.3.0', # oldest released version with sane image handling
|
||||
'PyPDF2>=1.26', # pure Python, so track HEAD closely
|
||||
'img2pdf>=0.2.3', # pure Python, so track HEAD closely
|
||||
'cffi>=1.9.1' # must be a setup and install requirement
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
deb http://httpredir.debian.org/debian stretch main
|
||||
deb http://httpredir.debian.org/debian stretch-updates main
|
||||
deb http://security.debian.org stretch/updates main
|
||||
deb http://ftp.de.debian.org/debian sid main contrib non-free
|
||||
Binary file not shown.
@@ -1,2 +1,4 @@
|
||||
pytest >= 3.0
|
||||
pytest-helpers-namespace
|
||||
pytest-helpers-namespace
|
||||
pytest-xdist
|
||||
pytest-cov
|
||||
+22
-3
@@ -22,10 +22,21 @@ def is_linux():
|
||||
return platform.system() == 'Linux'
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
def is_macos():
|
||||
return platform.system() == 'Darwin'
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
def running_in_docker():
|
||||
# Docker creates a file named /.dockerinit
|
||||
return os.path.exists('/.dockerinit')
|
||||
# Docker creates a file named /.dockerenv (newer versions) or
|
||||
# /.dockerinit (older) -- this is undocumented, not an offical test
|
||||
return os.path.exists('/.dockerenv') or os.path.exists('/.dockerinit')
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
def running_in_travis():
|
||||
return os.environ.get('TRAVIS') == 'true'
|
||||
|
||||
|
||||
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
@@ -90,7 +101,7 @@ def check_ocrmypdf(input_file, output_file, *args, env=None):
|
||||
|
||||
p, out, err = run_ocrmypdf(input_file, output_file, *args, env=env)
|
||||
#print(err) # ensure py.test collects the output, use -s to view
|
||||
assert p.returncode == 0
|
||||
assert p.returncode == 0, "<stderr>\n" + err + "\n</stderr>"
|
||||
assert os.path.exists(str(output_file)), "Output file not created"
|
||||
assert os.stat(str(output_file)).st_size > 100, "PDF too small or empty"
|
||||
assert out == "", \
|
||||
@@ -114,3 +125,11 @@ def run_ocrmypdf(input_file, output_file, *args, env=None):
|
||||
#print(err)
|
||||
|
||||
return p, out, err
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
def first_page_dimensions(pdf):
|
||||
from ocrmypdf import pageinfo
|
||||
info = pageinfo.pdf_get_all_pageinfo(str(pdf))
|
||||
page0 = info[0]
|
||||
return (page0['width_inches'], page0['height_inches'])
|
||||
|
||||
@@ -34,6 +34,9 @@ In some cases they were converted from one image format to another without other
|
||||
* - typewriter.png, 2400dpi.pdf
|
||||
- `Wikimedia: Triumph typewrtier text Linzensoep`_
|
||||
* Creative Commons BY-SA 2.5
|
||||
* - baiona.png
|
||||
- `Wikimedia: Baionako udalerri mugakideak`_
|
||||
- Creative Commons BY-SA 4.0
|
||||
|
||||
|
||||
Files generated for this project
|
||||
@@ -64,6 +67,9 @@ under the terms of the license in LICENSE.rst.
|
||||
* - epson.pdf
|
||||
- @lowesjam
|
||||
- a linearized PDF containing some unusual indirect objects, created by an Epson printer; printout of a Wikipedia article (CC BY-SA)
|
||||
* - formxobject.pdf
|
||||
- @jbarlow83
|
||||
- hand-crafted exotic PDF containing an image inside a Form XObject
|
||||
* - francais.pdf
|
||||
- @jbarlow83
|
||||
- a page containing French accents (diacritics)
|
||||
@@ -82,6 +88,9 @@ under the terms of the license in LICENSE.rst.
|
||||
* - overlay.pdf
|
||||
- @maxandersen
|
||||
- PDF file generated by PDFPen pro that triggered content stream parse errors
|
||||
* - no_conentes.pdf
|
||||
- @jbarlow83
|
||||
- synthetic PDF with a blank page that has no /Contents entry
|
||||
|
||||
Assemblies
|
||||
==========
|
||||
@@ -112,4 +121,6 @@ These test resources are assemblies from other previously mentioned files, relea
|
||||
|
||||
.. _`Linux (Wikipedia Article)`: https://de.wikipedia.org/wiki/Linux
|
||||
|
||||
.. _`Wikimedia: Triumph typewrtier text Linzensoep`: https://commons.wikimedia.org/wiki/File:Triumph.typewriter_text_Linzensoep.gif
|
||||
.. _`Wikimedia: Triumph typewrtier text Linzensoep`: https://commons.wikimedia.org/wiki/File:Triumph.typewriter_text_Linzensoep.gif
|
||||
|
||||
.. _`Wikimedia: Baionako udalerri mugakideak`: https://commons.wikimedia.org/wiki/File:Baionako_udalerri_mugakideak.png
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
%PDF-1.3
|
||||
%¿÷¢þ
|
||||
1 0 obj
|
||||
<< /Pages 2 0 R /Type /Catalog >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /MediaBox [ 0 0 720 720 ] /Parent 2 0 R /Resources << >> /Type /Page >>
|
||||
endobj
|
||||
xref
|
||||
0 4
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000064 00000 n
|
||||
0000000123 00000 n
|
||||
trailer << /Root 1 0 R /Size 4 /ID [<52bba3c78160d0c6e851b59110e5d076><52bba3c78160d0c6e851b59110e5d076>] >>
|
||||
startxref
|
||||
213
|
||||
%%EOF
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
"""Replicate Ghostscript raster failure while allowing rendering"""
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
|
||||
|
||||
def main():
|
||||
if '--version' in sys.argv:
|
||||
print('9.20')
|
||||
print('SPOOFED: ' + os.path.basename(__filename__))
|
||||
sys.exit(0)
|
||||
|
||||
# For any rendering calls (device == pdfwrite) call real ghostscript
|
||||
if '-sDEVICE=pdfwrite' in sys.argv:
|
||||
real_ghostscript(sys.argv)
|
||||
return
|
||||
|
||||
# Fail
|
||||
print("ERROR: Ghost story archive not found")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
"""Replicate Ghostscript render failure while allowing rasterizing"""
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
|
||||
|
||||
def main():
|
||||
if '--version' in sys.argv:
|
||||
print('9.20')
|
||||
print('SPOOFED: ' + os.path.basename(__filename__))
|
||||
sys.exit(0)
|
||||
|
||||
# For any rasterize calls (device != pdfwrite) call real ghostscript
|
||||
if '-sDEVICE=pdfwrite' not in sys.argv:
|
||||
real_ghostscript(sys.argv)
|
||||
return
|
||||
|
||||
# Fail
|
||||
print("ERROR: Casper is not a friendly ghost")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -36,6 +36,10 @@ def real_tesseract():
|
||||
|
||||
def main():
|
||||
operation = sys.argv[-1]
|
||||
sidecar = False
|
||||
if sys.argv[-2] == 'txt':
|
||||
sidecar = True
|
||||
|
||||
# For anything unexpected operation, defer to real tesseract binary
|
||||
# Currently this includes all use of "--tesseract-config"
|
||||
if operation != 'hocr' and operation != 'pdf' and operation != 'stdout':
|
||||
@@ -53,11 +57,6 @@ def main():
|
||||
['tesseract', '--version'],
|
||||
stderr=subprocess.STDOUT)
|
||||
|
||||
if b'4.00.00alpha' in tess_version:
|
||||
# Tesseract 4.x alpha is a moving target, don't cache it
|
||||
real_tesseract()
|
||||
return
|
||||
|
||||
m.update(tess_version)
|
||||
|
||||
# Insert this source file into the hash function, to ensure that any
|
||||
@@ -92,16 +91,22 @@ def main():
|
||||
return
|
||||
|
||||
if operation == 'stdout':
|
||||
# tesseract [--options] ... input stdout
|
||||
input_file = sys.argv[-2]
|
||||
output_file = 'stdout'
|
||||
sidecar_file = ''
|
||||
else:
|
||||
input_file = sys.argv[-3]
|
||||
output_file = sys.argv[-2]
|
||||
# tesseract [--options] ... input output txt hocr|pdf
|
||||
input_file = sys.argv[-4]
|
||||
output_file = sys.argv[-3]
|
||||
sidecar_file = sys.argv[-3]
|
||||
|
||||
if operation == 'hocr':
|
||||
output_file += '.hocr'
|
||||
sidecar_file += '.txt'
|
||||
elif operation == 'pdf':
|
||||
output_file += '.pdf'
|
||||
sidecar_file += '.txt'
|
||||
|
||||
with open(input_file, 'rb') as f:
|
||||
m.update(f.read())
|
||||
@@ -112,6 +117,8 @@ def main():
|
||||
print("Tesseract cache hit", file=sys.stderr)
|
||||
if operation != 'stdout':
|
||||
shutil.copy(cache_name, output_file)
|
||||
if sidecar:
|
||||
shutil.copy(cache_name + '.sidecar', sidecar_file)
|
||||
|
||||
# Replicate output
|
||||
with open(cache_name + '.stdout', 'rb') as f:
|
||||
@@ -149,6 +156,8 @@ def main():
|
||||
shutil.copy(output_file, cache_name)
|
||||
else:
|
||||
print("Could not find output file", file=sys.stderr)
|
||||
if sidecar and os.path.exists(sidecar_file):
|
||||
shutil.copy(sidecar_file, cache_name + '.sidecar')
|
||||
else:
|
||||
open(cache_name, 'w').close()
|
||||
|
||||
|
||||
@@ -53,18 +53,22 @@ def main():
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
inputf = sys.argv[-3]
|
||||
output = sys.argv[-2]
|
||||
inputf = sys.argv[-4]
|
||||
output = sys.argv[-3]
|
||||
with Image.open(inputf) as im, \
|
||||
open(output + '.hocr', 'w', encoding='utf-8') as f:
|
||||
w, h = im.size
|
||||
f.write(HOCR_TEMPLATE.format(str(w), str(h)))
|
||||
with open(output + '.txt', 'w') as f:
|
||||
f.write('')
|
||||
elif sys.argv[-1] == 'pdf':
|
||||
inputf = sys.argv[-3]
|
||||
output = sys.argv[-2]
|
||||
inputf = sys.argv[-4]
|
||||
output = sys.argv[-3]
|
||||
pdf_bytes = img2pdf.convert([inputf], dpi=300)
|
||||
with open(output + '.pdf', 'wb') as f:
|
||||
f.write(pdf_bytes)
|
||||
with open(output + '.txt', 'w') as f:
|
||||
f.write('')
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
inputf = sys.argv[-2]
|
||||
print("""Orientation: 0
|
||||
|
||||
+272
-21
@@ -12,6 +12,7 @@ import PyPDF2 as pypdf
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
from ocrmypdf import leptonica
|
||||
from ocrmypdf.pdfa import file_claims_pdfa
|
||||
from ocrmypdf.exec import ghostscript
|
||||
|
||||
|
||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||
@@ -50,6 +51,16 @@ def spoof_no_tess_pdfa_warning():
|
||||
return spoof(tesseract='tesseract_noop.py', gs='gs_feature_elision.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_gs_render_fail():
|
||||
return spoof(tesseract='tesseract_noop.py', gs='gs_render_failure.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_gs_raster_fail():
|
||||
return spoof(tesseract='tesseract_noop.py', gs='gs_raster_failure.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_qpdf_always_error():
|
||||
return spoof(qpdf='qpdf_dummy_return2.py')
|
||||
@@ -136,14 +147,18 @@ def test_remove_background(spoof_tesseract_noop, resources, outdir):
|
||||
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
|
||||
def test_exotic_image(spoof_tesseract_cache, pdf, renderer, output_type,
|
||||
resources, outdir):
|
||||
outfile = outdir / 'test_{0}_{1}.pdf'.format(pdf, renderer)
|
||||
check_ocrmypdf(
|
||||
resources / pdf,
|
||||
outdir / 'test_{0}_{1}.pdf'.format(pdf, renderer),
|
||||
outfile,
|
||||
'-dc',
|
||||
'-v', '1',
|
||||
'--output-type', output_type,
|
||||
'--sidecar',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
assert outfile.with_suffix('.pdf.txt').exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("output_type", [
|
||||
'pdfa', 'pdf'
|
||||
@@ -166,48 +181,50 @@ def test_preserve_metadata(spoof_tesseract_noop, output_type,
|
||||
assert pdfa_info['output'] == output_type
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
pytest.helpers.is_linux() and not pytest.helpers.running_in_docker(),
|
||||
reason="likely to fail if Linux locale is not configured correctly")
|
||||
@pytest.mark.parametrize("output_type", [
|
||||
'pdfa', 'pdf'
|
||||
])
|
||||
def test_override_metadata(spoof_tesseract_noop, output_type, resources,
|
||||
outpdf):
|
||||
input_file = resources / 'c02-22.pdf'
|
||||
|
||||
german = 'Du siehst den Wald vor lauter Bäumen nicht.'
|
||||
chinese = '孔子'
|
||||
high_unicode = 'U+1030C is: 𐌌'
|
||||
|
||||
p, out, err = run_ocrmypdf(
|
||||
input_file, outpdf,
|
||||
'--title', german,
|
||||
'--author', chinese,
|
||||
'--subject', high_unicode,
|
||||
'--output-type', output_type,
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
assert p.returncode == ExitCode.ok, err
|
||||
|
||||
pdf = str(outpdf)
|
||||
reader = pypdf.PdfFileReader(outpdf)
|
||||
|
||||
out_pdfinfo = check_output(['pdfinfo', pdf], universal_newlines=True)
|
||||
lines_pdfinfo = out_pdfinfo.splitlines()
|
||||
pdfinfo = {}
|
||||
for line in lines_pdfinfo:
|
||||
k, v = line.strip().split(':', maxsplit=1)
|
||||
pdfinfo[k.strip()] = v.strip()
|
||||
assert reader.documentInfo['/Title'] == german
|
||||
assert reader.documentInfo['/Author'] == chinese
|
||||
assert reader.documentInfo.get('/Keywords', '') == ''
|
||||
|
||||
assert pdfinfo['Title'] == german
|
||||
assert pdfinfo['Author'] == chinese
|
||||
assert pdfinfo['Subject'] == high_unicode
|
||||
assert pdfinfo.get('Keywords', '') == ''
|
||||
|
||||
pdfa_info = file_claims_pdfa(pdf)
|
||||
pdfa_info = file_claims_pdfa(outpdf)
|
||||
assert pdfa_info['output'] == output_type
|
||||
|
||||
|
||||
def test_high_unicode(spoof_tesseract_noop, resources, no_outpdf):
|
||||
|
||||
# Ghostscript doesn't support high Unicode, so neither do we, to be
|
||||
# safe
|
||||
input_file = resources / 'c02-22.pdf'
|
||||
high_unicode = 'U+1030C is: 𐌌'
|
||||
|
||||
p, out, err = run_ocrmypdf(
|
||||
input_file, no_outpdf,
|
||||
'--subject', high_unicode,
|
||||
'--output-type', 'pdfa',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
assert p.returncode == ExitCode.bad_args, err
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
@@ -409,10 +426,14 @@ def test_force_ocr_on_pdf_with_no_images(spoof_tesseract_crash, resources,
|
||||
assert not os.path.exists(no_outpdf)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
pytest.helpers.is_macos() and pytest.helpers.running_in_travis(),
|
||||
reason="takes too long to install language packs in Travis macOS homebrew")
|
||||
def test_french(spoof_tesseract_cache, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'francais.pdf', outpdf, '-l', 'fra',
|
||||
env=spoof_tesseract_cache)
|
||||
print(os.environ)
|
||||
assert p.returncode == ExitCode.ok, \
|
||||
"This test may fail if Tesseract language packs are missing"
|
||||
|
||||
@@ -564,6 +585,45 @@ def test_non_square_resolution(renderer, spoof_tesseract_cache,
|
||||
assert in_pageinfo[0]['yres'] == out_pageinfo[0]['yres']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract'
|
||||
])
|
||||
def test_convert_to_square_resolution(renderer, spoof_tesseract_cache,
|
||||
resources, outpdf):
|
||||
from math import isclose
|
||||
|
||||
# Confirm input image is non-square resolution
|
||||
in_pageinfo = pdf_get_all_pageinfo(str(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 = pdf_get_all_pageinfo(str(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]['dpi_w']
|
||||
out_im_h = out_p0['images'][0]['height'] / out_p0['images'][0]['dpi_h']
|
||||
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 / 'LinnSequencer.jpg', outpdf, '--image-dpi', '200',
|
||||
@@ -614,6 +674,23 @@ def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||
assert qpdf.check(output_file, log=None)
|
||||
|
||||
|
||||
def test_closed_streams(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||
input_file = str(resources / 'francais.pdf')
|
||||
output_file = str(outpdf)
|
||||
|
||||
def evil_closer():
|
||||
os.close(0)
|
||||
os.close(1)
|
||||
|
||||
p_args = ocrmypdf_exec + [input_file, output_file]
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=None, stderr=PIPE, stdin=None,
|
||||
env=spoof_tesseract_noop, preexec_fn=evil_closer)
|
||||
out, err = p.communicate()
|
||||
print(err.decode())
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
|
||||
def test_masks(spoof_tesseract_noop, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'masks.pdf', outpdf, env=spoof_tesseract_noop)
|
||||
|
||||
@@ -675,6 +752,11 @@ 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)
|
||||
pdfinfo = pdf_get_all_pageinfo(outpdf)
|
||||
|
||||
image = pdfinfo[0]['images'][0]
|
||||
assert image['dpi_w'] == image['dpi_h']
|
||||
assert image['dpi_w'] == 2400
|
||||
|
||||
|
||||
def test_overlay(spoof_tesseract_noop, resources, outpdf):
|
||||
@@ -682,6 +764,10 @@ def test_overlay(spoof_tesseract_noop, resources, outpdf):
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.getuid() == 0 or os.geteuid() == 0,
|
||||
reason="root can write to anything"
|
||||
)
|
||||
def test_destination_not_writable(spoof_tesseract_noop, resources, outdir):
|
||||
protected_file = outdir / 'protected.pdf'
|
||||
protected_file.touch()
|
||||
@@ -738,3 +824,168 @@ THIS FILE IS INVALID
|
||||
'--tesseract-config', str(cfg_file))
|
||||
assert "parameter not found" in err, "No error message"
|
||||
assert p.returncode == ExitCode.invalid_config
|
||||
|
||||
|
||||
def test_form_xobject(spoof_tesseract_noop, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'formxobject.pdf', outpdf,
|
||||
'--force-ocr',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_pagesize_consistency(renderer, resources, outpdf):
|
||||
from math import isclose
|
||||
|
||||
first_page_dimensions = pytest.helpers.first_page_dimensions
|
||||
|
||||
infile = resources / 'linn.pdf'
|
||||
|
||||
before_dims = first_page_dimensions(infile)
|
||||
|
||||
check_ocrmypdf(
|
||||
infile,
|
||||
outpdf, '--pdf-renderer', renderer,
|
||||
'--clean', '--deskew', '--remove-background', '--clean-final')
|
||||
|
||||
after_dims = first_page_dimensions(outpdf)
|
||||
|
||||
assert isclose(before_dims[0], after_dims[0])
|
||||
assert isclose(before_dims[1], after_dims[1])
|
||||
|
||||
|
||||
def test_skip_big_with_no_images(spoof_tesseract_noop, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'blank.pdf', outpdf,
|
||||
'--skip-big', '5',
|
||||
'--force-ocr',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
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_no_contents(spoof_tesseract_noop, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'no_contents.pdf', outpdf, '--force-ocr',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('image', [
|
||||
'baiona.png',
|
||||
'baiona_gray.png',
|
||||
'congress.jpg'
|
||||
])
|
||||
def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
|
||||
resources, image, outpdf):
|
||||
from PIL import Image
|
||||
|
||||
input_file = str(resources / image)
|
||||
output_file = str(outpdf)
|
||||
|
||||
im = Image.open(input_file)
|
||||
|
||||
# Runs: ocrmypdf - output.pdf < testfile
|
||||
with open(input_file, 'rb') as input_stream:
|
||||
p_args = ocrmypdf_exec + [
|
||||
'--image-dpi', '150', '--output-type', 'pdf', '-', output_file]
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
stdin=input_stream, env=spoof_tesseract_noop)
|
||||
out, err = p.communicate()
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
pdfinfo = pdf_get_all_pageinfo(output_file)
|
||||
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
|
||||
if input_file.endswith('.png'):
|
||||
assert pdfimage['enc'] != 'jpeg', \
|
||||
"Lossless compression changed to lossy!"
|
||||
elif input_file.endswith('.jpg'):
|
||||
assert pdfimage['enc'] == 'jpeg', \
|
||||
"Lossy compression changed to lossless!"
|
||||
if im.mode.startswith('RGB') or im.mode.startswith('BGR'):
|
||||
assert pdfimage['color'] == 'rgb', \
|
||||
"Colorspace changed"
|
||||
elif im.mode.startswith('L'):
|
||||
assert pdfimage['color'] == 'gray', \
|
||||
"Colorspace changed"
|
||||
|
||||
|
||||
@pytest.mark.parametrize('image,compression', [
|
||||
('baiona.png', 'jpeg'),
|
||||
('baiona_gray.png', 'lossless'),
|
||||
('congress.jpg', 'lossless')
|
||||
])
|
||||
def test_compression_changed(spoof_tesseract_noop, ocrmypdf_exec,
|
||||
resources, image, compression, outpdf):
|
||||
from PIL import Image
|
||||
|
||||
input_file = str(resources / image)
|
||||
output_file = str(outpdf)
|
||||
|
||||
im = Image.open(input_file)
|
||||
|
||||
# Runs: ocrmypdf - output.pdf < testfile
|
||||
with open(input_file, 'rb') as input_stream:
|
||||
p_args = ocrmypdf_exec + [
|
||||
'--image-dpi', '150', '--output-type', 'pdfa',
|
||||
'--pdfa-image-compression', compression,
|
||||
'-', output_file]
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
stdin=input_stream, env=spoof_tesseract_noop)
|
||||
out, err = p.communicate()
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
pdfinfo = pdf_get_all_pageinfo(output_file)
|
||||
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
|
||||
if compression == 'jpeg':
|
||||
assert pdfimage['enc'] == 'jpeg'
|
||||
elif compression == 'lossless':
|
||||
assert pdfimage['enc'] == 'image'
|
||||
|
||||
if im.mode.startswith('RGB') or im.mode.startswith('BGR'):
|
||||
assert pdfimage['color'] == 'rgb', \
|
||||
"Colorspace changed"
|
||||
elif im.mode.startswith('L'):
|
||||
assert pdfimage['color'] == 'gray', \
|
||||
"Colorspace changed"
|
||||
|
||||
|
||||
def test_sidecar_pagecount(spoof_tesseract_cache, resources, outpdf):
|
||||
sidecar = outpdf + '.txt'
|
||||
check_ocrmypdf(
|
||||
resources / 'multipage.pdf', outpdf,
|
||||
'--skip-text',
|
||||
'--sidecar', sidecar,
|
||||
env=spoof_tesseract_cache)
|
||||
|
||||
pdfinfo = pdf_get_all_pageinfo(str(resources / 'multipage.pdf'))
|
||||
num_pages = len(pdfinfo)
|
||||
|
||||
with open(sidecar, 'r') as f:
|
||||
ocr_text = f.read()
|
||||
|
||||
# There should a formfeed between each pair of pages, so the count of
|
||||
# formfeeds is the page count less one
|
||||
assert ocr_text.count('\f') == num_pages - 1, \
|
||||
"Sidecar page count does not match PDF page count"
|
||||
|
||||
+15
-1
@@ -35,7 +35,6 @@ def test_single_page_text(outdir):
|
||||
assert len(page['images']) == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs Path.read_bytes")
|
||||
def test_single_page_image(outdir):
|
||||
filename = outdir / 'image-mono.pdf'
|
||||
|
||||
@@ -101,3 +100,18 @@ def test_jpeg(resources, outdir):
|
||||
assert pdfimage['enc'] == 'jpeg'
|
||||
assert (pdfimage['dpi_w'] - 150) < 1e-5
|
||||
|
||||
|
||||
def test_form_xobject(resources):
|
||||
filename = resources / 'formxobject.pdf'
|
||||
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
assert pdfimage['width'] == 50
|
||||
|
||||
|
||||
def test_no_contents(resources):
|
||||
filename = resources / 'no_contents.pdf'
|
||||
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
|
||||
assert len(pdfinfo[0]['images']) == 0
|
||||
assert pdfinfo[0]['has_text'] == False
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
import pytest
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
from ocrmypdf.exec import tesseract
|
||||
|
||||
|
||||
# Skip all tests in this file if not tesseract 3
|
||||
pytestmark = pytest.mark.skipif(tesseract.v4(),
|
||||
reason="tesseract 3.x required")
|
||||
|
||||
|
||||
def test_textonly_pdf_on_tess3(resources, no_outpdf):
|
||||
p, _, _ = pytest.helpers.run_ocrmypdf(
|
||||
resources / 'linn.pdf',
|
||||
no_outpdf, '--pdf-renderer', 'tess4')
|
||||
|
||||
assert p.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
def test_oem_on_tess3(resources, no_outpdf):
|
||||
p, _, err = pytest.helpers.run_ocrmypdf(
|
||||
resources / 'aspect.pdf',
|
||||
no_outpdf, '--tesseract-oem', '1')
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
assert 'argument ignored' in err
|
||||
+105
-16
@@ -1,31 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from subprocess import Popen, PIPE, check_output, check_call, DEVNULL
|
||||
import os
|
||||
import shutil
|
||||
from contextlib import suppress
|
||||
import sys
|
||||
import pytest
|
||||
from ocrmypdf.pageinfo import pdf_get_all_pageinfo
|
||||
import PyPDF2 as pypdf
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
from ocrmypdf import leptonica
|
||||
from ocrmypdf.pdfa import file_claims_pdfa
|
||||
from ocrmypdf.exec import tesseract
|
||||
from ocrmypdf import pageinfo
|
||||
import sys
|
||||
import os
|
||||
import PyPDF2 as pypdf
|
||||
|
||||
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ensure_tess4():
|
||||
if tesseract.v4():
|
||||
# Either "tesseract" on $PATH is already v4, or
|
||||
# OCRMYPDF_TESSERACT is tess4 already
|
||||
return os.environ.copy()
|
||||
|
||||
if os.environ.get('OCRMYPDF_TESS4'):
|
||||
# OCRMYPDF_TESS4 is a hint environment variable that tells us to look
|
||||
# somewhere special for tess4 if and only if we need it. This allows
|
||||
# setting OCRMYPDF_TESS4 to test tess4 and PATH to point to tess3
|
||||
# on a system with both installed.
|
||||
env = os.environ.copy()
|
||||
env['OCRMYPDF_TESSERACT'] = env['OCRMYPDF_TESS4']
|
||||
return env
|
||||
|
||||
raise EnvironmentError("Can't find Tesseract 4")
|
||||
|
||||
|
||||
def tess4_available():
|
||||
"""Check if a tesseract 4 binary is available, even if it's not the
|
||||
official "tesseract" on PATH
|
||||
|
||||
"""
|
||||
try:
|
||||
ensure_tess4()
|
||||
return True
|
||||
except EnvironmentError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
# Skip all tests in this file if not tesseract 4
|
||||
pytestmark = pytest.mark.skipif(not tesseract.v4(),
|
||||
reason="tesseract 4.0 required")
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not tess4_available(),
|
||||
reason="tesseract 4.0 with textonly_pdf feature required")
|
||||
|
||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||
run_ocrmypdf = pytest.helpers.run_ocrmypdf
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
@pytest.mark.skipif(not tesseract.has_textonly_pdf(),
|
||||
reason="requires textonly_pdf feature")
|
||||
def test_textonly_pdf(resources, outdir):
|
||||
pytest.helpers.check_ocrmypdf(
|
||||
def test_textonly_pdf(ensure_tess4, resources, outdir):
|
||||
check_ocrmypdf(
|
||||
resources / 'linn.pdf',
|
||||
outdir / 'linn_textonly.pdf', '--pdf-renderer', 'tess4')
|
||||
outdir / 'linn_textonly.pdf', '--pdf-renderer', 'tess4',
|
||||
'--sidecar', 'foo',
|
||||
env=ensure_tess4)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs math.isclose")
|
||||
def test_pagesize_consistency_tess4(ensure_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', 'tess4',
|
||||
'--clean', '--deskew', '--remove-background', '--clean-final',
|
||||
env=ensure_tess4)
|
||||
|
||||
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(
|
||||
ensure_tess4, resources, basename, outdir):
|
||||
infile = resources / basename
|
||||
outpdf = outdir / basename
|
||||
|
||||
check_ocrmypdf(
|
||||
infile,
|
||||
outpdf, '--pdf-renderer', 'tess4', '--force-ocr',
|
||||
'--tesseract-timeout', '0',
|
||||
env=ensure_tess4
|
||||
)
|
||||
|
||||
info_in = pageinfo.pdf_get_all_pageinfo(str(infile))
|
||||
|
||||
info = pageinfo.pdf_get_all_pageinfo(str(outpdf))
|
||||
for page in info:
|
||||
assert len(page['images']) == 1, "skipped page was replicated"
|
||||
|
||||
for n in range(len(info_in)):
|
||||
assert info[n]['width_inches'] == info_in[n]['width_inches']
|
||||
|
||||
|
||||
def test_content_preservation(ensure_tess4, resources, outpdf):
|
||||
infile = resources / 'masks.pdf'
|
||||
|
||||
check_ocrmypdf(
|
||||
infile,
|
||||
outpdf, '--pdf-renderer', 'tess4', '--tesseract-timeout', '0',
|
||||
env=ensure_tess4
|
||||
)
|
||||
|
||||
info = pageinfo.pdf_get_all_pageinfo(str(outpdf))
|
||||
page = info[0]
|
||||
assert len(page['images']) > 1, "masked were rasterized"
|
||||
Reference in New Issue
Block a user