Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9cd5bf253 | ||
|
|
8102ca1075 | ||
|
|
6b5934ff4e | ||
|
|
e38b30af1c | ||
|
|
f64712322e | ||
|
|
1d09061130 | ||
|
|
a2203b2447 | ||
|
|
6a302fdb88 | ||
|
|
1d9cc239ee | ||
|
|
d240fc1ea6 | ||
|
|
e7d21dd826 | ||
|
|
e774b4650b | ||
|
|
8f8e6dcdd4 | ||
|
|
5252b88f0f | ||
|
|
ea69883386 | ||
|
|
eb343b1e37 | ||
|
|
9f02de55be | ||
|
|
7394a4cf49 | ||
|
|
ed9fb110b1 | ||
|
|
4650074428 | ||
|
|
70aa644c10 | ||
|
|
2ccb3edc58 | ||
|
|
1f40a70554 | ||
|
|
e14ffbf03f | ||
|
|
25a1dde57c | ||
|
|
1d10eac764 | ||
|
|
3f868118cd | ||
|
|
c7cf041e4a | ||
|
|
38ab03655b | ||
|
|
9226f8a5d1 | ||
|
|
5c8a007f3e | ||
|
|
b3ad3e297d | ||
|
|
d607553e48 | ||
|
|
7cf83c77ca | ||
|
|
8a9f174f63 | ||
|
|
98a0786c32 | ||
|
|
df1129724c | ||
|
|
383e726d65 | ||
|
|
2131ad4670 | ||
|
|
219fe2155b | ||
|
|
4209034d20 | ||
|
|
abcae0c2a4 | ||
|
|
0934905493 | ||
|
|
11cd6201d9 | ||
|
|
df87e21c85 | ||
|
|
d761d80750 |
+7
-11
@@ -1,22 +1,18 @@
|
|||||||
# OCRmyPDF
|
# OCRmyPDF
|
||||||
#
|
#
|
||||||
FROM ubuntu:17.10
|
FROM ubuntu:18.04
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
software-properties-common python-software-properties \
|
ocrmypdf \
|
||||||
python3-wheel \
|
|
||||||
python3-reportlab \
|
|
||||||
python3-venv \
|
|
||||||
ghostscript \
|
|
||||||
qpdf \
|
|
||||||
poppler-utils \
|
|
||||||
unpaper \
|
unpaper \
|
||||||
libffi-dev \
|
|
||||||
tesseract-ocr \
|
|
||||||
tesseract-ocr-eng \
|
tesseract-ocr-eng \
|
||||||
tesseract-ocr-fra \
|
tesseract-ocr-fra \
|
||||||
|
tesseract-ocr-deu \
|
||||||
tesseract-ocr-spa \
|
tesseract-ocr-spa \
|
||||||
tesseract-ocr-deu
|
tesseract-ocr-por \
|
||||||
|
tesseract-ocr-chi-sim \
|
||||||
|
python3-venv \
|
||||||
|
python3-pip
|
||||||
|
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
# OCRmyPDF
|
|
||||||
#
|
|
||||||
FROM ubuntu:17.10
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
ENV LANG=C.UTF-8
|
|
||||||
|
|
||||||
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[fitz]
|
|
||||||
|
|
||||||
# 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/docker-wrapper.sh"]
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
Please include the command line and (if needed) a test file with your issue report.
|
Please include the command line and a test file with your issue report.
|
||||||
|
|
||||||
If possible, please use a test file that we can include in future test cases (no personal information, no copyrighted material).
|
If possible, please use a test file that we can include in future test cases (no personal information, no copyrighted material).
|
||||||
|
|
||||||
|
If you wish to encrypt a test file for the OCRmyPDF maintainer only, see the [Wiki](https://github.com/jbarlow83/OCRmyPDF/wiki).
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.sublime-*
|
*.sublime-*
|
||||||
venv*/
|
venv*/
|
||||||
|
.venv/
|
||||||
pyvenv.cfg
|
pyvenv.cfg
|
||||||
tasks.py
|
tasks.py
|
||||||
.bash_history
|
.bash_history
|
||||||
.ruffus_history.sqlite
|
.ruffus_history.sqlite
|
||||||
.idea/
|
.idea/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
.pylintrc
|
||||||
|
|
||||||
# Package building
|
# Package building
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
@@ -42,3 +44,4 @@ pdfbox-app*.jar
|
|||||||
.vscode/
|
.vscode/
|
||||||
IDEAS
|
IDEAS
|
||||||
_Dockerfile.local
|
_Dockerfile.local
|
||||||
|
/scratch.py
|
||||||
|
|||||||
+31
-36
@@ -1,32 +1,51 @@
|
|||||||
dist: trusty
|
dist: trusty
|
||||||
language: python
|
language: python
|
||||||
cache:
|
cache:
|
||||||
ccache: true
|
|
||||||
pip: true
|
pip: true
|
||||||
directories:
|
directories:
|
||||||
- $HOME/Library/Caches/Homebrew
|
- $HOME/Library/Caches/Homebrew
|
||||||
|
|
||||||
env:
|
addons:
|
||||||
global:
|
apt:
|
||||||
- secure: "hsf6MT+n2x3OiDM2fQyJZdV0/PWYmv81LdVqC6cfnHBE/8N3DloJRqQ7WfO14TxhiK9PEC7MpyCj0lSabUHEO7gSH6Vks6I1asoSkt8S9/bSMlhT4hei+pwVpeGEiU5xHVATNjY+D919VC3IFvc3XmjT74h/2SLhaZ+jhEmDggM=" # HOMEBREW_OCRMYPDF_TOKEN
|
update: true
|
||||||
|
sources:
|
||||||
|
- sourceline: 'ppa:alex-p/tesseract-ocr'
|
||||||
|
- sourceline: 'ppa:heyarje/libav-11'
|
||||||
|
- sourceline: 'ppa:vshn/ghostscript'
|
||||||
|
packages:
|
||||||
|
- ghostscript
|
||||||
|
- libavcodec56
|
||||||
|
- libavformat56
|
||||||
|
- libavutil54
|
||||||
|
- libffi-dev
|
||||||
|
- poppler-utils
|
||||||
|
- qpdf
|
||||||
|
- tesseract-ocr
|
||||||
|
- tesseract-ocr-deu
|
||||||
|
- tesseract-ocr-eng
|
||||||
|
- tesseract-ocr-fra
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: linux
|
- os: linux
|
||||||
sudo: required
|
sudo: required
|
||||||
language: python
|
language: python
|
||||||
python: 3.5
|
python: "3.5"
|
||||||
env: EXTRAS=
|
env: EXTRAS=
|
||||||
- os: linux
|
- os: linux
|
||||||
sudo: required
|
sudo: required
|
||||||
language: python
|
language: python
|
||||||
python: 3.6
|
python: "3.6"
|
||||||
env: EXTRAS=
|
env: EXTRAS=
|
||||||
- os: linux
|
- os: linux
|
||||||
sudo: required
|
sudo: required
|
||||||
language: python
|
language: python
|
||||||
python: 3.6
|
python: "3.6"
|
||||||
env: EXTRAS=[fitz]
|
env: EXTRAS=[fitz]
|
||||||
|
- os: linux
|
||||||
|
sudo: required
|
||||||
|
language: python
|
||||||
|
python: "3.7-dev"
|
||||||
- os: osx
|
- os: osx
|
||||||
osx_image: xcode8
|
osx_image: xcode8
|
||||||
language: generic
|
language: generic
|
||||||
@@ -41,7 +60,10 @@ before_cache:
|
|||||||
|
|
||||||
before_install: |
|
before_install: |
|
||||||
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
|
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
|
||||||
bash .travis/linux_before_install.sh
|
pip install --upgrade pip
|
||||||
|
mkdir -p packages
|
||||||
|
wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O packages/unpaper_6.1-1.deb
|
||||||
|
sudo dpkg -i packages/unpaper_6.1-1.deb
|
||||||
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||||
brew update && brew bundle --file=.travis/Brewfile
|
brew update && brew bundle --file=.travis/Brewfile
|
||||||
pip3 install --upgrade pip
|
pip3 install --upgrade pip
|
||||||
@@ -49,6 +71,7 @@ before_install: |
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
- pip3 install pycparser # py3.7 workaround for https://github.com/eliben/pycparser/issues/251
|
||||||
- pip3 install ".$EXTRAS"
|
- pip3 install ".$EXTRAS"
|
||||||
- pip3 install -r test_requirements.txt
|
- pip3 install -r test_requirements.txt
|
||||||
|
|
||||||
@@ -73,31 +96,3 @@ deploy:
|
|||||||
tags: true
|
tags: true
|
||||||
condition: $TRAVIS_PYTHON_VERSION == "3.6" && $TRAVIS_OS_NAME == "linux" && $EXTRAS == ""
|
condition: $TRAVIS_PYTHON_VERSION == "3.6" && $TRAVIS_OS_NAME == "linux" && $EXTRAS == ""
|
||||||
skip_upload_docs: true
|
skip_upload_docs: true
|
||||||
|
|
||||||
# test pypi
|
|
||||||
- provider: pypi
|
|
||||||
server: https://testpypi.pypi.org/legacy/
|
|
||||||
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: /usr/bin/true
|
|
||||||
on:
|
|
||||||
branch: master
|
|
||||||
tags: true
|
|
||||||
condition: $TRAVIS_OS_NAME == "osx"
|
|
||||||
|
|
||||||
after_deploy: |
|
|
||||||
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
|
||||||
bash .travis/osx_brew.sh
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# © 2017-18 James R. Barlow: github.com/jbarlow83
|
|
||||||
|
|
||||||
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 "pkg-config" => :build
|
|
||||||
depends_on "mupdf-tools" => :build # statically links libmupdf.a
|
|
||||||
depends_on "freetype"
|
|
||||||
depends_on "ghostscript"
|
|
||||||
depends_on "jpeg"
|
|
||||||
depends_on "libpng"
|
|
||||||
depends_on "python"
|
|
||||||
depends_on "qpdf"
|
|
||||||
depends_on "tesseract"
|
|
||||||
depends_on "unpaper"
|
|
||||||
|
|
||||||
${resources}
|
|
||||||
def install
|
|
||||||
venv = virtualenv_create(libexec, "python3")
|
|
||||||
|
|
||||||
resource("Pillow").stage do
|
|
||||||
inreplace "setup.py" do |s|
|
|
||||||
sdkprefix = MacOS::CLT.installed? ? "" : MacOS.sdk_path
|
|
||||||
s.gsub! "openjpeg.h", "probably_not_a_header_called_this_eh.h"
|
|
||||||
s.gsub! "ZLIB_ROOT = None", "ZLIB_ROOT = ('#{sdkprefix}/usr/lib', '#{sdkprefix}/usr/include')"
|
|
||||||
s.gsub! "JPEG_ROOT = None", "JPEG_ROOT = ('#{Formula["jpeg"].opt_prefix}/lib', '#{Formula["jpeg"].opt_prefix}/include')"
|
|
||||||
s.gsub! "FREETYPE_ROOT = None", "FREETYPE_ROOT = ('#{Formula["freetype"].opt_prefix}/lib', '#{Formula["freetype"].opt_prefix}/include')"
|
|
||||||
end
|
|
||||||
|
|
||||||
# avoid triggering "helpful" distutils code that doesn't recognize Xcode 7 .tbd stubs
|
|
||||||
ENV.append "CFLAGS", "-I#{MacOS.sdk_path}/System/Library/Frameworks/Tk.framework/Versions/8.5/Headers" unless MacOS::CLT.installed?
|
|
||||||
venv.pip_install Pathname.pwd
|
|
||||||
end
|
|
||||||
|
|
||||||
res = resources.map(&:name).to_set - ["Pillow"]
|
|
||||||
|
|
||||||
res.each do |r|
|
|
||||||
venv.pip_install resource(r)
|
|
||||||
end
|
|
||||||
|
|
||||||
venv.pip_install_and_link buildpath
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
# Since we use Python 3, we require a UTF-8 locale
|
|
||||||
ENV["LC_ALL"] = "en_US.UTF-8"
|
|
||||||
|
|
||||||
system "#{bin}/ocrmypdf", "-f", "-q", "--deskew",
|
|
||||||
test_fixtures("test.pdf"), "ocr.pdf"
|
|
||||||
assert_predicate testpath/"ocr.pdf", :exist?
|
|
||||||
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()
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
|
||||||
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 \
|
|
||||||
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
|
|
||||||
wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O packages/unpaper_6.1-1.deb
|
|
||||||
sudo dpkg -i packages/unpaper_6.1-1.deb
|
|
||||||
|
|
||||||
if [ ! -f /usr/local/bin/qpdf ]; then
|
|
||||||
export QPDF_RELEASE='https://github.com/qpdf/qpdf/releases/download/release-qpdf-8.0.2/qpdf-8.0.2.tar.gz'
|
|
||||||
mkdir qpdf
|
|
||||||
wget -q $QPDF_RELEASE -O - | tar xz -C qpdf --strip-components=1
|
|
||||||
cd qpdf/
|
|
||||||
export PATH="/usr/local/opt/ccache/libexec:$PATH"
|
|
||||||
./configure --prefix=/usr
|
|
||||||
make -j 2
|
|
||||||
sudo make install
|
|
||||||
cd ..
|
|
||||||
fi
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
|
||||||
set -uo pipefail
|
|
||||||
set -x
|
|
||||||
|
|
||||||
pip3 install homebrew-pypi-poet
|
|
||||||
python3 .travis/autobrew.py
|
|
||||||
cat ocrmypdf.rb
|
|
||||||
|
|
||||||
# brew audit crashes Travis
|
|
||||||
#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
|
|
||||||
+1
-3
@@ -126,9 +126,7 @@ If you detect an issue, please:
|
|||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Runs on CPython 3.6, and requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
|
Runs on CPython 3.5, 3.6 and 3.7. Requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
|
||||||
|
|
||||||
Python 3.5 is also supported.
|
|
||||||
|
|
||||||
Press & Media
|
Press & Media
|
||||||
-------------
|
-------------
|
||||||
|
|||||||
+2
-2
@@ -65,8 +65,8 @@ By default OCRmyPDF assumes the document is English.
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
ocrmypdf -l fre LeParisien.pdf LeParisien.pdf
|
ocrmypdf -l fra LeParisien.pdf LeParisien.pdf
|
||||||
ocrmypdf -l eng+fre Bilingual-English-French.pdf Bilingual-English-French.pdf
|
ocrmypdf -l eng+fra Bilingual-English-French.pdf Bilingual-English-French.pdf
|
||||||
|
|
||||||
Language packs must be installed for all languages specified. See :ref:`Installing additional language packs <lang-packs>`.
|
Language packs must be installed for all languages specified. See :ref:`Installing additional language packs <lang-packs>`.
|
||||||
|
|
||||||
|
|||||||
+25
-8
@@ -77,25 +77,22 @@ Assuming you have a Docker engine running, you can download one of the three ava
|
|||||||
- Notes
|
- Notes
|
||||||
* - ocrmypdf
|
* - ocrmypdf
|
||||||
- ``docker pull jbarlow83/ocrmypdf``
|
- ``docker pull jbarlow83/ocrmypdf``
|
||||||
- Latest ocrmypdf with Tesseract 3.x. Includes English, French, German, Spanish.
|
- Latest ocrmypdf with Tesseract 4.0.0-beta1 on Ubuntu 18.04. Includes English, French, German, Spanish, Portugeuse and Simplified Chinese.
|
||||||
* - ocrmypdf-polyglot
|
* - ocrmypdf-polyglot
|
||||||
- ``docker pull jbarlow83/ocrmypdf-polyglot``
|
- ``docker pull jbarlow83/ocrmypdf-polyglot``
|
||||||
- As above, with all available language packs.
|
- As above, with all available language packs.
|
||||||
* - ocrmypdf-tess4
|
|
||||||
- ``docker pull jbarlow83/ocrmypdf-tess4``
|
|
||||||
- Latest ocrmypdf with Tesseract 4.x and English, French, German, Spanish, Portuguese, Chinese Simplified, Arabic and Russian (the top 8).
|
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker pull jbarlow83/ocrmypdf-tess4
|
docker pull jbarlow83/ocrmypdf
|
||||||
|
|
||||||
Then tag it to give a more convenient name, just ocrmypdf:
|
Then tag it to give a more convenient name, just ocrmypdf:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker tag jbarlow83/ocrmypdf-tess4 ocrmypdf
|
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||||
|
|
||||||
.. _docker-polyglot:
|
.. _docker-polyglot:
|
||||||
|
|
||||||
@@ -171,7 +168,7 @@ Install or upgrade the required Homebrew packages, if any are missing:
|
|||||||
brew install libxml2 libffi leptonica
|
brew install libxml2 libffi leptonica
|
||||||
brew install unpaper # optional
|
brew install unpaper # optional
|
||||||
|
|
||||||
Python 3.5 and 3.6 are supported.
|
Python 3.5, 3.6 and 3.7 are supported.
|
||||||
|
|
||||||
Install the required Tesseract OCR engine with the language packs you plan to use:
|
Install the required Tesseract OCR engine with the language packs you plan to use:
|
||||||
|
|
||||||
@@ -211,6 +208,26 @@ The command line program should now be available:
|
|||||||
|
|
||||||
ocrmypdf --help
|
ocrmypdf --help
|
||||||
|
|
||||||
|
Installing the latest version on Ubuntu 18.04 LTS
|
||||||
|
-------------------------------------------------
|
||||||
|
|
||||||
|
Ubuntu 18.04 includes ocrmypdf 6.1.2. To install a more recent version, first
|
||||||
|
install the system version to get all the dependencies:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install \
|
||||||
|
ocrmypdf \
|
||||||
|
python3-pip
|
||||||
|
|
||||||
|
Then install ocrmypdf 6.1.5 for the local user and set the user's ``PATH`` to check for the user's Python packages.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
export PATH=$HOME/.local/bin:$PATH
|
||||||
|
pip3 install --user ocrmypdf[fitz]
|
||||||
|
|
||||||
|
|
||||||
Installing on Ubuntu 16.04 LTS
|
Installing on Ubuntu 16.04 LTS
|
||||||
------------------------------
|
------------------------------
|
||||||
@@ -383,7 +400,7 @@ The following dependencies are recommended:
|
|||||||
|
|
||||||
These are in addition to the Python packaging dependencies, meaning that unfortunately, the ``pip install`` command cannot satisfy all of them.
|
These are in addition to the Python packaging dependencies, meaning that unfortunately, the ``pip install`` command cannot satisfy all of them.
|
||||||
|
|
||||||
Python 3.6 and Tesseract 4.x are recommended for best OCR results and best performance.
|
Python 3.6 and Tesseract 4.0.0-beta.1 are recommended for best OCR results and best performance.
|
||||||
|
|
||||||
The library PyMuPDF is not widely available in platform distributions, and it improves OCRmyPDF in certain conditions. Consider installing OCRmyPDF from the Python binary wheels, which include a precompiled version of this library.
|
The library PyMuPDF is not widely available in platform distributions, and it improves OCRmyPDF in certain conditions. Consider installing OCRmyPDF from the Python binary wheels, which include a precompiled version of this library.
|
||||||
|
|
||||||
|
|||||||
+43
-1
@@ -9,10 +9,52 @@ The OCRmyPDF package itself does not contain a public API, although it is fairly
|
|||||||
find: [^`]\#([0-9]{1,3})[^0-9]
|
find: [^`]\#([0-9]{1,3})[^0-9]
|
||||||
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
|
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
|
||||||
|
|
||||||
|
|
||||||
|
v6.2.4
|
||||||
|
------
|
||||||
|
|
||||||
|
- Backport Ghostscript 9.25 compatibility fixes, which removes support for setting Unicode metadata
|
||||||
|
- Backport blacklisting Ghostscript 9.24
|
||||||
|
- Older versions of Ghostscript are still supported
|
||||||
|
|
||||||
|
|
||||||
|
v6.2.3
|
||||||
|
------
|
||||||
|
|
||||||
|
- Fix compatibility with img2pdf >= 0.3.0 by rejecting input images that have an alpha channel
|
||||||
|
|
||||||
|
|
||||||
|
v6.2.2
|
||||||
|
------
|
||||||
|
|
||||||
|
- Backport compatibility fixes for Python 3.7 and ruffus 2.7.0 from v7.0.0
|
||||||
|
- Backport fix to ignore masks when deciding what colors are on a page
|
||||||
|
- Backport some minor improvements from v7.0.0: better argument validation and warnings about the Tesseract 4.0.0 ``--user-words`` regression
|
||||||
|
|
||||||
|
v6.2.1
|
||||||
|
------
|
||||||
|
|
||||||
|
- Fix recent versions of Tesseract (after 4.0.0-beta1) not being detected as supporting the ``sandwich`` renderer (`#271 <https://github.com/ppjbarlow83/OCRmyPDF/issues/271>`_).
|
||||||
|
|
||||||
|
|
||||||
|
v6.2.0
|
||||||
|
------
|
||||||
|
|
||||||
|
- **Docker**: The Docker image ``ocrmypdf-tess4`` has been removed. The main Docker images, ``ocrmypdf`` and ``ocrmypdf-polyglot`` now use Ubuntu 18.04 as a base image, and as such Tesseract 4.0.0-beta1 is now the Tesseract version they use. There is no Docker image based on Tesseract 3.05 anymore.
|
||||||
|
|
||||||
|
- Creation of PDF/A-3 is now supported. However, there is no ability to attach files to PDF/A-3.
|
||||||
|
|
||||||
|
- List more reasons why the file size might grow.
|
||||||
|
|
||||||
|
- Fix issue `#262 <https://github.com/ppjbarlow83/OCRmyPDF/issues/262>`_, ``--remove-background`` error on PDFs contained colormapped (paletted) images.
|
||||||
|
|
||||||
|
- Fix another XMP metadata validation issue, in cases where the input file's creation date has no timezone and the creation date is not overridden.
|
||||||
|
|
||||||
|
|
||||||
v6.1.5
|
v6.1.5
|
||||||
------
|
------
|
||||||
|
|
||||||
- Fix issue `#253 <https://github.com/jbarlow83/OCRmyPDF/issues/248>`_, a possible division by zero when using the ``hocr`` renderer.
|
- Fix issue `#253 <https://github.com/jbarlow83/OCRmyPDF/issues/253>`_, a possible division by zero when using the ``hocr`` renderer.
|
||||||
|
|
||||||
- Fix incorrectly formatted ``<xmp:ModifyDate>`` field inside XMP metadata for PDF/As. veraPDF flags this as a PDF/A validation failure. The error is caused the timezone and final digit of the seconds of modified time to be omitted, so at worst the modification time stamp is rounded to the nearest 10 seconds.
|
- Fix incorrectly formatted ``<xmp:ModifyDate>`` field inside XMP metadata for PDF/As. veraPDF flags this as a PDF/A validation failure. The error is caused the timezone and final digit of the seconds of modified time to be omitted, so at worst the modification time stamp is rounded to the nearest 10 seconds.
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -1,10 +1,11 @@
|
|||||||
# requirements.txt can be used to replicate the developer's build environment
|
# requirements.txt can be used to replicate the developer's build environment
|
||||||
# setup.py lists a separate set of requirements that are looser to simplify
|
# setup.py lists a separate set of requirements that are looser to simplify
|
||||||
# installation
|
# installation
|
||||||
ruffus == 2.6.3
|
ruffus == 2.7.0
|
||||||
Pillow == 5.1.0
|
Pillow == 5.2.0
|
||||||
reportlab == 3.4.0
|
reportlab == 3.4.0
|
||||||
PyPDF2 == 1.26.0
|
PyPDF2 == 1.26.0
|
||||||
img2pdf == 0.2.4
|
img2pdf == 0.2.4
|
||||||
cffi == 1.11.5
|
cffi == 1.11.5
|
||||||
PyMuPDF == 1.12.5
|
PyMuPDF == 1.12.5
|
||||||
|
defusedxml == 0.5.0
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ setup(
|
|||||||
classifiers=[
|
classifiers=[
|
||||||
"Programming Language :: Python :: 3.5",
|
"Programming Language :: Python :: 3.5",
|
||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: End Users/Desktop",
|
"Intended Audience :: End Users/Desktop",
|
||||||
@@ -248,7 +249,7 @@ setup(
|
|||||||
# block 5.1.0, broken wheels
|
# block 5.1.0, broken wheels
|
||||||
'PyPDF2 >= 1.26', # pure Python, so track HEAD closely
|
'PyPDF2 >= 1.26', # pure Python, so track HEAD closely
|
||||||
'reportlab >= 3.3.0', # oldest released version with sane image handling
|
'reportlab >= 3.3.0', # oldest released version with sane image handling
|
||||||
'ruffus == 2.6.3', # pinned - ocrmypdf implements a 2.6.3 workaround
|
'ruffus >= 2.7.0',
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'fitz': ['PyMuPDF >= 1.12.5'] # for table of contents bug
|
'fitz': ['PyMuPDF >= 1.12.5'] # for table of contents bug
|
||||||
|
|||||||
+76
-30
@@ -85,6 +85,20 @@ if tesseract.version() < MINIMUM_TESS_VERSION:
|
|||||||
# -------------
|
# -------------
|
||||||
# Parser
|
# Parser
|
||||||
|
|
||||||
|
def numeric(basetype, min_=None, max_=None):
|
||||||
|
"Validator for numeric params"
|
||||||
|
min_ = basetype(min_) if min_ is not None else None
|
||||||
|
max_ = basetype(max_) if max_ is not None else None
|
||||||
|
def _numeric(string):
|
||||||
|
value = basetype(string)
|
||||||
|
if (min_ is not None and value < min_
|
||||||
|
or max_ is not None and value > max_):
|
||||||
|
msg = "%r not in valid range %r" % (string, (min_, max_))
|
||||||
|
raise argparse.ArgumentTypeError(msg)
|
||||||
|
return value
|
||||||
|
return _numeric
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog=PROGRAM_NAME,
|
prog=PROGRAM_NAME,
|
||||||
fromfile_prefix_chars='@',
|
fromfile_prefix_chars='@',
|
||||||
@@ -156,14 +170,15 @@ parser.add_argument(
|
|||||||
'--image-dpi', metavar='DPI', type=int,
|
'--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(
|
parser.add_argument(
|
||||||
'--output-type', choices=['pdfa', 'pdf', 'pdfa-1', 'pdfa-2'],
|
'--output-type', choices=['pdfa', 'pdf', 'pdfa-1', 'pdfa-2', 'pdfa-3'],
|
||||||
default='pdfa',
|
default='pdfa',
|
||||||
help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for "
|
help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for "
|
||||||
"long term archiving (default, recommended) but may not suitable "
|
"long term archiving (default, recommended) but may not suitable "
|
||||||
"for users who want their file altered as little as possible. 'pdfa' "
|
"for users who want their file altered as little as possible. 'pdfa' "
|
||||||
"also has problems with full Unicode text. 'pdf' attempts to "
|
"also has problems with full Unicode text. 'pdf' attempts to "
|
||||||
"preserve file contents as much as possible. 'pdf-a1' creates a "
|
"preserve file contents as much as possible. 'pdf-a1' creates a "
|
||||||
"PDF/A1-b file. 'pdf-a2' is equivalent to 'pdfa'."
|
"PDF/A1-b file. 'pdf-a2' is equivalent to 'pdfa'. 'pdf-a3' creates a "
|
||||||
|
"PDF/A3-b file."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use null string '\0' as sentinel to indicate the user supplied no argument,
|
# Use null string '\0' as sentinel to indicate the user supplied no argument,
|
||||||
@@ -232,7 +247,7 @@ preprocessing.add_argument(
|
|||||||
help="Clean page as above, and incorporate the cleaned image in the final "
|
help="Clean page as above, and incorporate the cleaned image in the final "
|
||||||
"PDF. Might remove desired content.")
|
"PDF. Might remove desired content.")
|
||||||
preprocessing.add_argument(
|
preprocessing.add_argument(
|
||||||
'--oversample', metavar='DPI', type=int, default=0,
|
'--oversample', metavar='DPI', type=numeric(int, 0, 5000), 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")
|
"results slightly")
|
||||||
|
|
||||||
@@ -254,7 +269,7 @@ ocrsettings.add_argument(
|
|||||||
# "pages")
|
# "pages")
|
||||||
|
|
||||||
ocrsettings.add_argument(
|
ocrsettings.add_argument(
|
||||||
'--skip-big', type=float, metavar='MPixels',
|
'--skip-big', type=numeric(float, 0, 5000), 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")
|
"but include skipped pages in final output")
|
||||||
|
|
||||||
@@ -262,7 +277,7 @@ advanced = parser.add_argument_group(
|
|||||||
"Advanced",
|
"Advanced",
|
||||||
"Advanced options to control Tesseract's OCR behavior")
|
"Advanced options to control Tesseract's OCR behavior")
|
||||||
advanced.add_argument(
|
advanced.add_argument(
|
||||||
'--max-image-mpixels', action='store', type=float, metavar='MPixels',
|
'--max-image-mpixels', action='store', type=numeric(float, 0), metavar='MPixels',
|
||||||
help="Set maximum number of pixels to unpack before treating an image as a "
|
help="Set maximum number of pixels to unpack before treating an image as a "
|
||||||
"decompression bomb",
|
"decompression bomb",
|
||||||
default=128.0)
|
default=128.0)
|
||||||
@@ -295,11 +310,11 @@ advanced.add_argument(
|
|||||||
" of Ghostscript; deprecated"
|
" of Ghostscript; deprecated"
|
||||||
)
|
)
|
||||||
advanced.add_argument(
|
advanced.add_argument(
|
||||||
'--tesseract-timeout', default=180.0, type=float, metavar='SECONDS',
|
'--tesseract-timeout', default=180.0, type=numeric(float, 0), 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')
|
'into the final output')
|
||||||
advanced.add_argument(
|
advanced.add_argument(
|
||||||
'--rotate-pages-threshold', default=14.0, type=float, metavar='CONFIDENCE',
|
'--rotate-pages-threshold', default=14.0, type=numeric(float, max_=1000), 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)")
|
"units reported by tesseract)")
|
||||||
advanced.add_argument(
|
advanced.add_argument(
|
||||||
@@ -394,6 +409,14 @@ def check_options_output(options, log):
|
|||||||
log.info(
|
log.info(
|
||||||
"Ignoring --debug-rendering because it requires --pdf-renderer=hocr")
|
"Ignoring --debug-rendering because it requires --pdf-renderer=hocr")
|
||||||
|
|
||||||
|
if options.output_type == 'pdfa':
|
||||||
|
options.output_type = 'pdfa-2'
|
||||||
|
|
||||||
|
if options.output_type == 'pdfa-3' and ghostscript.version() < '9.19':
|
||||||
|
raise MissingDependencyError(
|
||||||
|
"--output-type pdfa-3 requires Ghostscript 9.19 or later"
|
||||||
|
)
|
||||||
|
|
||||||
lossless_reconstruction = False
|
lossless_reconstruction = False
|
||||||
if options.pdf_renderer in ('hocr', 'sandwich'):
|
if options.pdf_renderer in ('hocr', 'sandwich'):
|
||||||
if not any((options.deskew, options.clean_final, options.force_ocr,
|
if not any((options.deskew, options.clean_final, options.force_ocr,
|
||||||
@@ -482,6 +505,10 @@ def check_options_advanced(options, log):
|
|||||||
"--output-type is not 'pdfa', 'pdfa-1', or 'pdfa-2'"
|
"--output-type is not 'pdfa', 'pdfa-1', or 'pdfa-2'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if tesseract.v4() and (options.user_words or options.user_patterns):
|
||||||
|
log.warning(
|
||||||
|
'Tesseract 4.x ignores --user-words, so this has no effect')
|
||||||
|
|
||||||
|
|
||||||
def check_options_metadata(options, log):
|
def check_options_metadata(options, log):
|
||||||
import unicodedata
|
import unicodedata
|
||||||
@@ -629,33 +656,31 @@ def do_ruffus_exception(ruffus_five_tuple, options, log):
|
|||||||
return ExitCode.other_error
|
return ExitCode.other_error
|
||||||
|
|
||||||
|
|
||||||
def traverse_ruffus_exception(e_args, options, log):
|
def traverse_ruffus_exception(exceptions, options, log):
|
||||||
"""Walk through a RethrownJobError and find the first exception.
|
"""Traverse a RethrownJobError and output the exceptions
|
||||||
|
|
||||||
Ruffus flattens exception to 5 element tuples. Because of a bug
|
Ruffus presents exceptions as 5 element tuples. The RethrownJobException
|
||||||
in <= 2.6.3 it may present either the single:
|
has a list of exceptions like
|
||||||
(task, job, exc, value, stack)
|
e.job_exceptions = [(5-tuple), (5-tuple), ...]
|
||||||
or something like:
|
|
||||||
[[(task, job, exc, value, stack)]]
|
|
||||||
|
|
||||||
Generally cross-process exception marshalling doesn't work well
|
ruffus < 2.7.0 had a bug with exception marshalling that would give
|
||||||
and ruffus doesn't support because BaseException has its own
|
different output whether the main or child process raised the exception.
|
||||||
implementation of __reduce__ that attempts to reconstruct the
|
We no longer support this.
|
||||||
exception based on e.__init__(e.args).
|
|
||||||
|
|
||||||
Attempting to log the exception directly marshalls it to the logger
|
Attempting to log the exception itself will re-marshall it to the logger
|
||||||
which is probably in another process, so it's better to log only
|
which is normally running in another process. It's better to avoid re-
|
||||||
data from the exception at this point.
|
marshalling.
|
||||||
|
|
||||||
The exit code will be based on this, even if multiple exceptions occurred
|
The exit code will be based on this, even if multiple exceptions occurred
|
||||||
at the same time."""
|
at the same time."""
|
||||||
|
|
||||||
if isinstance(e_args, Sequence) and isinstance(e_args[0], str) and \
|
exit_codes = []
|
||||||
len(e_args) == 5:
|
for exc in exceptions:
|
||||||
return do_ruffus_exception(e_args, options, log)
|
exit_code = do_ruffus_exception(exc, options, log)
|
||||||
elif is_iterable_notstr(e_args):
|
exit_codes.append(exit_code)
|
||||||
for exc in e_args:
|
|
||||||
return traverse_ruffus_exception(exc, options, log)
|
return exit_codes[0] # Multiple codes are rare so take the first one
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def check_closed_streams(options):
|
def check_closed_streams(options):
|
||||||
@@ -787,8 +812,20 @@ def report_output_file_size(options, _log, input_file, output_file):
|
|||||||
reasons = []
|
reasons = []
|
||||||
if not fitz:
|
if not fitz:
|
||||||
reasons.append("The optional dependency PyMuPDF is not installed.")
|
reasons.append("The optional dependency PyMuPDF is not installed.")
|
||||||
if options.force_ocr:
|
image_preproc = {
|
||||||
reasons.append("The argument --force-ocr was issued.")
|
'deskew',
|
||||||
|
'clean_final',
|
||||||
|
'remove_background',
|
||||||
|
'oversample',
|
||||||
|
'force_ocr'
|
||||||
|
}
|
||||||
|
for arg in image_preproc:
|
||||||
|
attr = getattr(options, arg, None)
|
||||||
|
if not attr:
|
||||||
|
continue
|
||||||
|
reasons.append(
|
||||||
|
"The argument --{} was issued, causing transcoding.".format(
|
||||||
|
arg.replace('_', '-')))
|
||||||
|
|
||||||
if reasons:
|
if reasons:
|
||||||
explanation = (
|
explanation = (
|
||||||
@@ -826,6 +863,14 @@ def run_pipeline():
|
|||||||
"security vulnerabilities with certain malformed PDFs. Consider "
|
"security vulnerabilities with certain malformed PDFs. Consider "
|
||||||
"upgrading to version 7.0.0 or newer.".format(qpdf.version()))
|
"upgrading to version 7.0.0 or newer.".format(qpdf.version()))
|
||||||
|
|
||||||
|
if ghostscript.version() == '9.24':
|
||||||
|
complain(
|
||||||
|
"Ghostscript 9.24 contains serious regressions and is not "
|
||||||
|
"supported. Please upgrade to Ghostscript 9.25 or use an older "
|
||||||
|
"version."
|
||||||
|
)
|
||||||
|
return ExitCode.missing_dependency
|
||||||
|
|
||||||
# Any changes to options will not take effect for options that are already
|
# Any changes to options will not take effect for options that are already
|
||||||
# bound to function parameters in the pipeline. (For example
|
# bound to function parameters in the pipeline. (For example
|
||||||
# options.input_file, options.pdf_renderer are already bound.)
|
# options.input_file, options.pdf_renderer are already bound.)
|
||||||
@@ -865,7 +910,8 @@ def run_pipeline():
|
|||||||
except ruffus_exceptions.RethrownJobError as e:
|
except ruffus_exceptions.RethrownJobError as e:
|
||||||
if options.verbose:
|
if options.verbose:
|
||||||
_log.debug(str(e)) # stringify exception so logger doesn't have to
|
_log.debug(str(e)) # stringify exception so logger doesn't have to
|
||||||
exitcode = traverse_ruffus_exception(e.args, options, _log)
|
exceptions = e.job_exceptions
|
||||||
|
exitcode = traverse_ruffus_exception(exceptions, options, _log)
|
||||||
if exitcode is None:
|
if exitcode is None:
|
||||||
_log.error("Unexpected ruffus exception: " + str(e))
|
_log.error("Unexpected ruffus exception: " + str(e))
|
||||||
_log.error(repr(e))
|
_log.error(repr(e))
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ import codecs
|
|||||||
|
|
||||||
def verify_python3_env():
|
def verify_python3_env():
|
||||||
"""Ensures that the environment is good for unicode on Python 3."""
|
"""Ensures that the environment is good for unicode on Python 3."""
|
||||||
|
|
||||||
|
# PEP 538 changes in Python 3.7 should make this wrangling unnecessary
|
||||||
|
if sys.version_info[0:3] >= (3, 7, 0):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import locale
|
import locale
|
||||||
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
|
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ def get_version(program, *,
|
|||||||
args_prog, close_fds=True, universal_newlines=True,
|
args_prog, close_fds=True, universal_newlines=True,
|
||||||
stdout=PIPE, stderr=STDOUT, check=True)
|
stdout=PIPE, stderr=STDOUT, check=True)
|
||||||
output = proc.stdout
|
output = proc.stdout
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
raise MissingDependencyError(
|
||||||
|
"Could not find program '{}' on the PATH".format(
|
||||||
|
program)) from e
|
||||||
except CalledProcessError as e:
|
except CalledProcessError as e:
|
||||||
if e.returncode < 0:
|
if e.returncode < 0:
|
||||||
raise MissingDependencyError(
|
raise MissingDependencyError(
|
||||||
|
|||||||
@@ -34,12 +34,20 @@ def version():
|
|||||||
|
|
||||||
def jpeg_passthrough_available():
|
def jpeg_passthrough_available():
|
||||||
"""
|
"""
|
||||||
Ghostscript 9.23 introduced JPEG passthrough but it seems to corrupt the
|
Returns True if the installed version of Ghostscript supports JPEG passthru
|
||||||
last two bytes of certain images, for now we disable it for 9.23 and
|
|
||||||
do not mention it for < 9.23.
|
|
||||||
|
|
||||||
|
Prior to 9.23, Ghostscript decode and re-encoded JPEGs internally. In 9.23
|
||||||
|
it gained the ability to keep JPEGs unmodified. However, the 9.23
|
||||||
|
implementation was buggy and would deletes the last two bytes of images in
|
||||||
|
some cases, as reported here.
|
||||||
https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
||||||
|
|
||||||
|
The issue was fixed for 9.24, hence that is the first version we consider
|
||||||
|
the feature available. (However, we don't use 9.24 at all, so the first
|
||||||
|
version that allows JPEG passthrough is 9.25.
|
||||||
|
|
||||||
|
Regardless, in ocrmypdf 6.x we are ignoring this new feature entirely to
|
||||||
|
avoid new behavior.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -144,10 +152,12 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
|
|||||||
# git commit fe1c025d.
|
# git commit fe1c025d.
|
||||||
strategy = 'RGB' if version() >= '9.19' else '/RGB'
|
strategy = 'RGB' if version() >= '9.19' else '/RGB'
|
||||||
|
|
||||||
if version() == '9.23':
|
if version() >= '9.23':
|
||||||
# 9.23: new feature JPEG passthrough is broken in some cases, best to
|
# 9.23: new feature JPEG passthrough is broken in some cases, best to
|
||||||
# disable it always
|
# disable it always
|
||||||
# https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
# https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
||||||
|
# fixed in 9.24, but to avoid changing expected behavior we disable it
|
||||||
|
# for ocrmypdf 6.x
|
||||||
compression_args.append('-dPassThroughJPEGImages=false')
|
compression_args.append('-dPassThroughJPEGImages=false')
|
||||||
|
|
||||||
with NamedTemporaryFile(delete=True) as gs_pdf:
|
with NamedTemporaryFile(delete=True) as gs_pdf:
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ def has_textonly_pdf():
|
|||||||
"""
|
"""
|
||||||
args_tess = [
|
args_tess = [
|
||||||
'tesseract',
|
'tesseract',
|
||||||
'--print-parameters'
|
'--print-parameters',
|
||||||
|
'pdf'
|
||||||
]
|
]
|
||||||
params = ''
|
params = ''
|
||||||
try:
|
try:
|
||||||
@@ -159,7 +160,7 @@ def get_orientation(input_file, language: list, engine_mode, timeout: float,
|
|||||||
assert 'Rotate' not in osd
|
assert 'Rotate' not in osd
|
||||||
angle = -angle % 360
|
angle = -angle % 360
|
||||||
else:
|
else:
|
||||||
# Tesseract == 3.04.01, hopefully also Tesseract > 3.04.01
|
# Tesseract >= 3.04.01
|
||||||
# reports "Orientation in degrees" as a clockwise angle
|
# reports "Orientation in degrees" as a clockwise angle
|
||||||
assert 'Rotate' in osd
|
assert 'Rotate' in osd
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from contextlib import suppress, contextmanager
|
from contextlib import suppress, contextmanager
|
||||||
@@ -22,6 +24,7 @@ from pathlib import Path
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
def re_symlink(input_file, soft_link_name, log=None):
|
def re_symlink(input_file, soft_link_name, log=None):
|
||||||
@@ -70,7 +73,7 @@ def is_iterable_notstr(thing):
|
|||||||
|
|
||||||
def page_number(input_file):
|
def page_number(input_file):
|
||||||
"Get one-based page number implied by filename (000002.pdf -> 2)"
|
"Get one-based page number implied by filename (000002.pdf -> 2)"
|
||||||
return int(os.path.basename(input_file)[0:6])
|
return int(os.path.basename(fspath(input_file))[0:6])
|
||||||
|
|
||||||
|
|
||||||
def available_cpu_count():
|
def available_cpu_count():
|
||||||
@@ -85,7 +88,7 @@ def available_cpu_count():
|
|||||||
except (ImportError, AttributeError):
|
except (ImportError, AttributeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
complain(
|
warnings.warn(
|
||||||
"Could not get CPU count. Assuming one (1) CPU."
|
"Could not get CPU count. Assuming one (1) CPU."
|
||||||
"Use -j N to set manually.")
|
"Use -j N to set manually.")
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
+16
-17
@@ -108,13 +108,6 @@ class LeptonicaIOError(LeptonicaError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RemoveColormap(Enum):
|
|
||||||
to_binary = 0
|
|
||||||
to_grayscale = 1
|
|
||||||
to_full_color = 2
|
|
||||||
based_on_src = 3
|
|
||||||
|
|
||||||
|
|
||||||
class Pix:
|
class Pix:
|
||||||
"""Wrapper around leptonica's PIX object.
|
"""Wrapper around leptonica's PIX object.
|
||||||
|
|
||||||
@@ -141,9 +134,10 @@ class Pix:
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self._pix:
|
if self._pix:
|
||||||
s = "<leptonica.Pix image size={0}x{1} depth={2} at 0x{3:x}>"
|
s = "<leptonica.Pix image size={0}x{1} depth={2}{4} at 0x{3:x}>"
|
||||||
return s.format(self._pix.w, self._pix.h, self._pix.d,
|
return s.format(self._pix.w, self._pix.h, self._pix.d,
|
||||||
int(ffi.cast("intptr_t", self._pix)))
|
int(ffi.cast('intptr_t', self._pix)),
|
||||||
|
'(colormapped)' if self._pix.colormap else '')
|
||||||
else:
|
else:
|
||||||
return "<leptonica.Pix image NULL>"
|
return "<leptonica.Pix image NULL>"
|
||||||
|
|
||||||
@@ -328,13 +322,16 @@ class Pix:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def remove_colormap(self, removal_type):
|
def remove_colormap(self, removal_type):
|
||||||
"""Remove a palette
|
"""Remove a palette (colormap); if no colormap, returns a copy of this
|
||||||
|
image
|
||||||
|
|
||||||
|
removal_type - any of lept.REMOVE_CMAP_*
|
||||||
|
|
||||||
removal_type - RemovalColormap()
|
|
||||||
"""
|
"""
|
||||||
|
return self
|
||||||
with LeptonicaErrorTrap():
|
# with LeptonicaErrorTrap():
|
||||||
return Pix(lept.pixRemoveColormap(self._pix, removal_type))
|
# return Pix(lept.pixRemoveColormapGeneral(
|
||||||
|
# self._pix, removal_type, lept.L_COPY))
|
||||||
|
|
||||||
def otsu_adaptive_threshold(
|
def otsu_adaptive_threshold(
|
||||||
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
|
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
|
||||||
@@ -426,9 +423,11 @@ class Pix:
|
|||||||
def background_norm(
|
def background_norm(
|
||||||
self, mask=None, grayscale=None, tile_size=(10, 15), fg_threshold=60,
|
self, mask=None, grayscale=None, tile_size=(10, 15), fg_threshold=60,
|
||||||
min_count=40, bg_val=200, smooth_kernel=(2, 1)):
|
min_count=40, bg_val=200, smooth_kernel=(2, 1)):
|
||||||
|
# Background norm doesn't work on color mapped Pix, so remove colormap
|
||||||
|
target_pix = self.remove_colormap(lept.REMOVE_CMAP_BASED_ON_SRC)
|
||||||
with LeptonicaErrorTrap():
|
with LeptonicaErrorTrap():
|
||||||
return Pix(lept.pixBackgroundNorm(
|
return Pix(lept.pixBackgroundNorm(
|
||||||
self._pix,
|
target_pix._pix,
|
||||||
mask or ffi.NULL,
|
mask or ffi.NULL,
|
||||||
grayscale or ffi.NULL,
|
grayscale or ffi.NULL,
|
||||||
tile_size[0],
|
tile_size[0],
|
||||||
@@ -450,8 +449,8 @@ class Pix:
|
|||||||
if get_leptonica_version() < 'leptonica-1.72':
|
if get_leptonica_version() < 'leptonica-1.72':
|
||||||
# Older versions of Leptonica (pre-1.72) have a buggy
|
# Older versions of Leptonica (pre-1.72) have a buggy
|
||||||
# implementation of pixCorrelationBinary that overflows on larger
|
# implementation of pixCorrelationBinary that overflows on larger
|
||||||
# images. Ubuntu trusty has 1.70. Ubuntu PPA
|
# images. Ubuntu 14.04/trusty has 1.70. Ubuntu PPA
|
||||||
# ppa:rebuntu16/avidemux+unofficial has "leptonlib" 1.73.
|
# ppa:alex-p/tesseract-ocr has leptonlib 1.75.
|
||||||
pix1_count = ffi.new('l_int32 *')
|
pix1_count = ffi.new('l_int32 *')
|
||||||
pix2_count = ffi.new('l_int32 *')
|
pix2_count = ffi.new('l_int32 *')
|
||||||
pixn_count = ffi.new('l_int32 *')
|
pixn_count = ffi.new('l_int32 *')
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import _cffi_backend
|
|||||||
|
|
||||||
ffi = _cffi_backend.FFI('ocrmypdf.lib._leptonica',
|
ffi = _cffi_backend.FFI('ocrmypdf.lib._leptonica',
|
||||||
_version = 0x2601,
|
_version = 0x2601,
|
||||||
_types = b'\x00\x00\x0F\x0D\x00\x00\xBB\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\xBE\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\xBA\x03\x00\x00\x0F\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x02\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x0D\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x08\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\xC7\x03\x00\x00\x1C\x01\x00\x00\x00\x0F\x00\x00\xBD\x0D\x00\x00\x00\x0F\x00\x00\x49\x0D\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x1F\x03\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x49\x11\x00\x00\x49\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x49\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x01\x03\x00\x00\x9D\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\xC6\x03\x00\x00\x65\x03\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x08\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\xC4\x03\x00\x00\xA3\x11\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x00\x0F\x00\x00\xCA\x0D\x00\x00\x10\x11\x00\x00\x00\x0F\x00\x00\xCA\x0D\x00\x00\x9D\x11\x00\x00\x00\x0F\x00\x00\xCA\x0D\x00\x00\xCA\x03\x00\x00\x00\x0F\x00\x00\x00\x09\x00\x00\x01\x09\x00\x00\x02\x09\x00\x00\xBE\x03\x00\x00\x02\x01\x00\x00\x0E\x01\x00\x00\x0B\x01\x00\x00\x05\x01\x00\x00\x03\x01\x00\x00\xBC\x03\x00\x00\xC5\x03\x00\x00\x04\x01\x00\x00\xC7\x03\x00\x00\x08\x01\x00\x00\x0C\x01\x00\x00\x06\x01\x00\x00\x00\x01',
|
_types = b'\x00\x00\x0F\x0D\x00\x00\xBB\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\xBE\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\xBA\x03\x00\x00\x0F\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x02\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x0D\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x08\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\xC9\x03\x00\x00\x1C\x01\x00\x00\x00\x0F\x00\x00\xBD\x0D\x00\x00\x00\x0F\x00\x00\x49\x0D\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x1F\x03\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x49\x11\x00\x00\x49\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x49\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x01\x03\x00\x00\x9D\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\xC8\x03\x00\x00\x65\x03\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x08\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\xC6\x03\x00\x00\xA3\x11\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x00\x0F\x00\x00\xCC\x0D\x00\x00\x10\x11\x00\x00\x00\x0F\x00\x00\xCC\x0D\x00\x00\x9D\x11\x00\x00\x00\x0F\x00\x00\xCC\x0D\x00\x00\xCC\x03\x00\x00\x00\x0F\x00\x00\x00\x09\x00\x00\x01\x09\x00\x00\x02\x09\x00\x00\xBE\x03\x00\x00\x02\x01\x00\x00\x0E\x01\x00\x00\x00\x0B\x00\x00\x01\x0B\x00\x00\x0B\x01\x00\x00\x05\x01\x00\x00\x03\x01\x00\x00\xBC\x03\x00\x00\xC7\x03\x00\x00\x04\x01\x00\x00\xC9\x03\x00\x00\x08\x01\x00\x00\x0C\x01\x00\x00\x06\x01\x00\x00\x00\x01',
|
||||||
_globals = (b'\x00\x00\xB1\x23boxDestroy',0,b'\x00\x00\x67\x23getLeptonicaVersion',0,b'\x00\x00\xB7\x23lept_free',0,b'\x00\x00\x69\x23makePixelSumTab8',0,b'\x00\x00\x16\x23pixAnd',0,b'\x00\x00\x23\x23pixBackgroundNorm',0,b'\x00\x00\x1B\x23pixCleanBackgroundToWhite',0,b'\x00\x00\x0D\x23pixClipRectangle',0,b'\x00\x00\x8D\x23pixColorFraction',0,b'\x00\x00\x59\x23pixColorMagnitude',0,b'\x00\x00\x0A\x23pixConvertRGBToLuminance',0,b'\x00\x00\x6B\x23pixCorrelationBinary',0,b'\x00\x00\x80\x23pixCountPixels',0,b'\x00\x00\x63\x23pixDeserializeFromMemory',0,b'\x00\x00\x50\x23pixDeskew',0,b'\x00\x00\xB4\x23pixDestroy',0,b'\x00\x00\x0A\x23pixEndianByteSwapNew',0,b'\x00\x00\x00\x23pixFindPageForeground',0,b'\x00\x00\x7B\x23pixFindSkew',0,b'\x00\x00\x2F\x23pixGammaTRC',0,b'\x00\x00\x70\x23pixGetAverageMaskedRGB',0,b'\x00\x00\x36\x23pixGlobalNormRGB',0,b'\x00\x00\x12\x23pixInvert',0,b'\x00\x00\x54\x23pixMaskOverColorPixels',0,b'\x00\x00\x85\x23pixNumSignificantGrayColors',0,b'\x00\x00\x96\x23pixOtsuAdaptiveThreshold',0,b'\x00\x00\x3E\x23pixOtsuThreshOnBackgroundNorm',0,b'\x00\x00\x60\x23pixRead',0,b'\x00\x00\x50\x23pixRemoveColormap',0,b'\x00\x00\x12\x23pixRotate180',0,b'\x00\x00\x50\x23pixRotateOrth',0,b'\x00\x00\x4B\x23pixScale',0,b'\x00\x00\xA0\x23pixSerializeToMemory',0,b'\x00\x00\xA5\x23pixWriteImpliedFormat',0,b'\x00\x00\xAB\x23pixWriteMemPng',0),
|
_globals = (b'\xFF\xFF\xFF\x0BL_CLONE',2,b'\xFF\xFF\xFF\x0BL_COPY',1,b'\xFF\xFF\xFF\x0BL_COPY_CLONE',3,b'\xFF\xFF\xFF\x0BL_INSERT',0,b'\xFF\xFF\xFF\x0BL_NOCOPY',0,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_BASED_ON_SRC',4,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_TO_BINARY',0,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_TO_FULL_COLOR',2,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_TO_GRAYSCALE',1,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_WITH_ALPHA',3,b'\x00\x00\xB1\x23boxDestroy',0,b'\x00\x00\x67\x23getLeptonicaVersion',0,b'\x00\x00\xB7\x23lept_free',0,b'\x00\x00\x69\x23makePixelSumTab8',0,b'\x00\x00\x16\x23pixAnd',0,b'\x00\x00\x23\x23pixBackgroundNorm',0,b'\x00\x00\x1B\x23pixCleanBackgroundToWhite',0,b'\x00\x00\x0D\x23pixClipRectangle',0,b'\x00\x00\x8D\x23pixColorFraction',0,b'\x00\x00\x59\x23pixColorMagnitude',0,b'\x00\x00\x0A\x23pixConvertRGBToLuminance',0,b'\x00\x00\x6B\x23pixCorrelationBinary',0,b'\x00\x00\x80\x23pixCountPixels',0,b'\x00\x00\x63\x23pixDeserializeFromMemory',0,b'\x00\x00\x50\x23pixDeskew',0,b'\x00\x00\xB4\x23pixDestroy',0,b'\x00\x00\x0A\x23pixEndianByteSwapNew',0,b'\x00\x00\x00\x23pixFindPageForeground',0,b'\x00\x00\x7B\x23pixFindSkew',0,b'\x00\x00\x2F\x23pixGammaTRC',0,b'\x00\x00\x70\x23pixGetAverageMaskedRGB',0,b'\x00\x00\x36\x23pixGlobalNormRGB',0,b'\x00\x00\x12\x23pixInvert',0,b'\x00\x00\x54\x23pixMaskOverColorPixels',0,b'\x00\x00\x85\x23pixNumSignificantGrayColors',0,b'\x00\x00\x96\x23pixOtsuAdaptiveThreshold',0,b'\x00\x00\x3E\x23pixOtsuThreshOnBackgroundNorm',0,b'\x00\x00\x60\x23pixRead',0,b'\x00\x00\x50\x23pixRemoveColormap',0,b'\x00\x00\x54\x23pixRemoveColormapGeneral',0,b'\x00\x00\x12\x23pixRotate180',0,b'\x00\x00\x50\x23pixRotateOrth',0,b'\x00\x00\x4B\x23pixScale',0,b'\x00\x00\xA0\x23pixSerializeToMemory',0,b'\x00\x00\xA5\x23pixWriteImpliedFormat',0,b'\x00\x00\xAB\x23pixWriteMemPng',0),
|
||||||
_struct_unions = ((b'\x00\x00\x00\xBA\x00\x00\x00\x02Box',b'\x00\x00\x02\x11x',b'\x00\x00\x02\x11y',b'\x00\x00\x02\x11w',b'\x00\x00\x02\x11h',b'\x00\x00\xC7\x11refcount'),(b'\x00\x00\x00\xBB\x00\x00\x00\x02Pix',b'\x00\x00\xC7\x11w',b'\x00\x00\xC7\x11h',b'\x00\x00\xC7\x11d',b'\x00\x00\xC7\x11spp',b'\x00\x00\xC7\x11wpl',b'\x00\x00\xC7\x11refcount',b'\x00\x00\x02\x11xres',b'\x00\x00\x02\x11yres',b'\x00\x00\x02\x11informat',b'\x00\x00\x02\x11special',b'\x00\x00\xBD\x11text',b'\x00\x00\xC3\x11colormap',b'\x00\x00\xC6\x11data'),(b'\x00\x00\x00\xBC\x00\x00\x00\x02PixColormap',b'\x00\x00\xB8\x11array',b'\x00\x00\x02\x11depth',b'\x00\x00\x02\x11nalloc',b'\x00\x00\x02\x11n')),
|
_struct_unions = ((b'\x00\x00\x00\xBA\x00\x00\x00\x02Box',b'\x00\x00\x02\x11x',b'\x00\x00\x02\x11y',b'\x00\x00\x02\x11w',b'\x00\x00\x02\x11h',b'\x00\x00\xC9\x11refcount'),(b'\x00\x00\x00\xBB\x00\x00\x00\x02Pix',b'\x00\x00\xC9\x11w',b'\x00\x00\xC9\x11h',b'\x00\x00\xC9\x11d',b'\x00\x00\xC9\x11spp',b'\x00\x00\xC9\x11wpl',b'\x00\x00\xC9\x11refcount',b'\x00\x00\x02\x11xres',b'\x00\x00\x02\x11yres',b'\x00\x00\x02\x11informat',b'\x00\x00\x02\x11special',b'\x00\x00\xBD\x11text',b'\x00\x00\xC5\x11colormap',b'\x00\x00\xC8\x11data'),(b'\x00\x00\x00\xBC\x00\x00\x00\x02PixColormap',b'\x00\x00\xB8\x11array',b'\x00\x00\x02\x11depth',b'\x00\x00\x02\x11nalloc',b'\x00\x00\x02\x11n')),
|
||||||
_typenames = (b'\x00\x00\x00\xBABOX',b'\x00\x00\x00\xBBPIX',b'\x00\x00\x00\xBCPIXCMAP',b'\x00\x00\x00\x1Fl_float32',b'\x00\x00\x00\xBFl_float64',b'\x00\x00\x00\xC1l_int16',b'\x00\x00\x00\x02l_int32',b'\x00\x00\x00\xC0l_int64',b'\x00\x00\x00\xC2l_int8',b'\x00\x00\x00\xC9l_uint16',b'\x00\x00\x00\xC7l_uint32',b'\x00\x00\x00\xC8l_uint64',b'\x00\x00\x00\xC5l_uint8'),
|
_enums = (b'\x00\x00\x00\xC0\x00\x00\x00\x16$1\x00REMOVE_CMAP_TO_BINARY,REMOVE_CMAP_TO_GRAYSCALE,REMOVE_CMAP_TO_FULL_COLOR,REMOVE_CMAP_WITH_ALPHA,REMOVE_CMAP_BASED_ON_SRC',b'\x00\x00\x00\xC1\x00\x00\x00\x16$2\x00L_NOCOPY,L_INSERT,L_COPY,L_CLONE,L_COPY_CLONE'),
|
||||||
|
_typenames = (b'\x00\x00\x00\xBABOX',b'\x00\x00\x00\xBBPIX',b'\x00\x00\x00\xBCPIXCMAP',b'\x00\x00\x00\x1Fl_float32',b'\x00\x00\x00\xBFl_float64',b'\x00\x00\x00\xC3l_int16',b'\x00\x00\x00\x02l_int32',b'\x00\x00\x00\xC2l_int64',b'\x00\x00\x00\xC4l_int8',b'\x00\x00\x00\xCBl_uint16',b'\x00\x00\x00\xC9l_uint32',b'\x00\x00\x00\xCAl_uint64',b'\x00\x00\x00\xC7l_uint8'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -72,6 +72,24 @@ struct Box
|
|||||||
};
|
};
|
||||||
typedef struct Box BOX;
|
typedef struct Box BOX;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
REMOVE_CMAP_TO_BINARY = 0, /*!< remove colormap for conv to 1 bpp */
|
||||||
|
REMOVE_CMAP_TO_GRAYSCALE = 1, /*!< remove colormap for conv to 8 bpp */
|
||||||
|
REMOVE_CMAP_TO_FULL_COLOR = 2, /*!< remove colormap for conv to 32 bpp */
|
||||||
|
REMOVE_CMAP_WITH_ALPHA = 3, /*!< remove colormap and alpha */
|
||||||
|
REMOVE_CMAP_BASED_ON_SRC = 4 /*!< remove depending on src format */
|
||||||
|
};
|
||||||
|
|
||||||
|
/*! Access and storage flags */
|
||||||
|
enum {
|
||||||
|
L_NOCOPY = 0, /*!< do not copy the object; do not delete the ptr */
|
||||||
|
L_INSERT = L_NOCOPY, /*!< stuff it in; do not copy or clone */
|
||||||
|
L_COPY = 1, /*!< make/use a copy of the object */
|
||||||
|
L_CLONE = 2, /*!< make/use clone (ref count) of the object */
|
||||||
|
L_COPY_CLONE = 3 /*!< make a new array object (e.g., pixa) and fill */
|
||||||
|
/*!< the array with clones (e.g., pix) */
|
||||||
|
};
|
||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
ffibuilder.cdef("""
|
ffibuilder.cdef("""
|
||||||
@@ -227,6 +245,11 @@ PIX *
|
|||||||
pixInvert(PIX * pixd,
|
pixInvert(PIX * pixd,
|
||||||
PIX * pixs);
|
PIX * pixs);
|
||||||
|
|
||||||
|
PIX *
|
||||||
|
pixRemoveColormapGeneral(PIX *pixs,
|
||||||
|
l_int32 type,
|
||||||
|
l_int32 ifnocmap);
|
||||||
|
|
||||||
void
|
void
|
||||||
boxDestroy(BOX **pbox);
|
boxDestroy(BOX **pbox);
|
||||||
|
|
||||||
|
|||||||
+91
-28
@@ -15,7 +15,21 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# Generate a PDFA_def.ps file for Ghostscript >= 9.14
|
"""
|
||||||
|
Generate a PDFMARK file for Ghostscript >= 9.14, for PDF/A conversion
|
||||||
|
|
||||||
|
pdfmark is an extension to the Postscript language that describes some PDF
|
||||||
|
features like bookmarks and annotations. It was originally specified Adobe
|
||||||
|
Distiller, for Postscript to PDF conversion:
|
||||||
|
https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
|
||||||
|
|
||||||
|
Ghostscript uses pdfmark for PDF to PDF/A conversion as well. To use Ghostscript
|
||||||
|
to create a PDF/A, we need to create a pdfmark file with the necessary metadata.
|
||||||
|
|
||||||
|
This takes care of the many version-specific bugs and pecularities in
|
||||||
|
Ghostscript's handling of pdfmark.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
from string import Template
|
from string import Template
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
@@ -78,7 +92,8 @@ def
|
|||||||
|
|
||||||
|
|
||||||
def encode_text_string(s: str) -> str:
|
def encode_text_string(s: str) -> str:
|
||||||
'''Encode text string to hex string for use in a PDF
|
"""
|
||||||
|
Encode text string to hex string for use in a PDF
|
||||||
|
|
||||||
From PDF 32000-1:2008 a string object may be included in hexademical form
|
From PDF 32000-1:2008 a string object may be included in hexademical form
|
||||||
if it is enclosed in angle brackets. For general Unicode the string should
|
if it is enclosed in angle brackets. For general Unicode the string should
|
||||||
@@ -86,7 +101,7 @@ def encode_text_string(s: str) -> str:
|
|||||||
ASCII strings could be encoded as PdfDocEncoding literals provided
|
ASCII strings could be encoded as PdfDocEncoding literals provided
|
||||||
that certain Postscript sequences are escaped. But it's far simpler to
|
that certain Postscript sequences are escaped. But it's far simpler to
|
||||||
encode everything as UTF-16.
|
encode everything as UTF-16.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# Sometimes lazy C programmers leave their NULs at the end of strings they
|
# Sometimes lazy C programmers leave their NULs at the end of strings they
|
||||||
# insert into PDFs
|
# insert into PDFs
|
||||||
@@ -102,8 +117,28 @@ def encode_text_string(s: str) -> str:
|
|||||||
return ascii_hex_str
|
return ascii_hex_str
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_ascii(s: str) -> str:
|
||||||
|
"""
|
||||||
|
Aggressively strip non-ASCII and PDF escape sequences
|
||||||
|
|
||||||
|
Ghostscript 9.24+ lost support for UTF-16BE in pdfmark files for reasons
|
||||||
|
given in GhostPDL commit e997c683. Our temporary workaround is use ASCII
|
||||||
|
and drop all non-ASCII characters. A slightly improved alternative would
|
||||||
|
be to implement PdfDocEncoding in pikepdf and encode to that, or handle
|
||||||
|
metadata there.
|
||||||
|
"""
|
||||||
|
trans = str.maketrans({
|
||||||
|
'(': '',
|
||||||
|
')': '',
|
||||||
|
'\\': '',
|
||||||
|
'\0': ''
|
||||||
|
})
|
||||||
|
return s.translate(trans).encode('ascii', errors='replace').decode()
|
||||||
|
|
||||||
|
|
||||||
def encode_pdf_date(d: datetime) -> str:
|
def encode_pdf_date(d: datetime) -> str:
|
||||||
"""Encode Python datetime object as PDF date string
|
"""
|
||||||
|
Encode Python datetime object as PDF date string
|
||||||
|
|
||||||
From Adobe pdfmark manual:
|
From Adobe pdfmark manual:
|
||||||
(D:YYYYMMDDHHmmSSOHH'mm')
|
(D:YYYYMMDDHHmmSSOHH'mm')
|
||||||
@@ -125,31 +160,38 @@ def encode_pdf_date(d: datetime) -> str:
|
|||||||
s = d.strftime(pdfmark_date_fmt)
|
s = d.strftime(pdfmark_date_fmt)
|
||||||
|
|
||||||
tz = d.strftime('%z')
|
tz = d.strftime('%z')
|
||||||
if tz == 'Z':
|
if tz == 'Z' or tz == '':
|
||||||
|
# Ghostscript <= 9.23 handles missing timezones incorrectly, so if
|
||||||
|
# timezone is missing, move it into GMT.
|
||||||
|
# https://bugs.ghostscript.com/show_bug.cgi?id=699182
|
||||||
s += "+00'00'"
|
s += "+00'00'"
|
||||||
elif tz != '':
|
else:
|
||||||
sign, tz_hours, tz_mins = tz[0], tz[1:3], tz[3:5]
|
sign, tz_hours, tz_mins = tz[0], tz[1:3], tz[3:5]
|
||||||
s += "{}{}'{}'".format(sign, tz_hours, tz_mins)
|
s += "{}{}'{}'".format(sign, tz_hours, tz_mins)
|
||||||
else:
|
|
||||||
raise ValueError("Naive timezone not supported")
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def decode_pdf_date(s: str) -> datetime:
|
def decode_pdf_date(s: str) -> datetime:
|
||||||
pdfmark_date_fmts = (
|
"""
|
||||||
r'%Y%m%d%H%M%S%z', # +0430 etc
|
Decode a pdfmark date to a Python datetime object
|
||||||
r'%Y%m%d%H%M%S', # no time zone
|
|
||||||
r'%Y%m%d%H%M%SZ') # trailing Z
|
|
||||||
|
|
||||||
|
A pdfmark date is a string in a paritcular format. See the pdfmark
|
||||||
|
Reference for the specification.
|
||||||
|
|
||||||
|
"""
|
||||||
if s.startswith('D:'):
|
if s.startswith('D:'):
|
||||||
s = s[2:]
|
s = s[2:]
|
||||||
|
|
||||||
|
# Literal Z00'00', is incorrect but found in the wild,
|
||||||
|
# probably made by OS X Quartz -- standardize
|
||||||
|
if s.endswith("Z00'00'"):
|
||||||
|
s = s.replace("Z00'00'", '+0000')
|
||||||
|
elif s.endswith('Z'):
|
||||||
|
s = s.replace('Z', '+0000')
|
||||||
|
|
||||||
s = s.replace("'", "") # Remove apos from PDF time strings
|
s = s.replace("'", "") # Remove apos from PDF time strings
|
||||||
for fmt in pdfmark_date_fmts:
|
|
||||||
try:
|
return datetime.strptime(s, r'%Y%m%d%H%M%S%z')
|
||||||
return datetime.strptime(s, fmt)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pdfmark_dates(pdfmark):
|
def _get_pdfmark_dates(pdfmark):
|
||||||
@@ -170,27 +212,49 @@ def _get_pdfmark_dates(pdfmark):
|
|||||||
date_str = pdfmark[key]
|
date_str = pdfmark[key]
|
||||||
if date_str.startswith('D:'):
|
if date_str.startswith('D:'):
|
||||||
date_str = date_str[2:]
|
date_str = date_str[2:]
|
||||||
yield ' {} (D:{})'.format(key, date_str)
|
try:
|
||||||
|
yield ' {} (D:{})'.format(
|
||||||
|
key,
|
||||||
|
encode_pdf_date(decode_pdf_date(date_str)))
|
||||||
|
except ValueError:
|
||||||
|
yield ' {} null'.format(key)
|
||||||
|
|
||||||
|
|
||||||
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
|
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark, ascii_docinfo=False):
|
||||||
"""Create a Postscript file for Ghostscript. pdfmark contains the various
|
"""Create a Postscript pdfmark file for Ghostscript.
|
||||||
objects as strings; these must be encoded in ASCII, and dates have a
|
|
||||||
special format."""
|
pdfmark contains the various objects as strings; these must be encoded in
|
||||||
|
ASCII, and dates have a special format.
|
||||||
|
|
||||||
|
:param icc_profile: filename of the ICC profile to include in pdfmark
|
||||||
|
:param icc_identifier: ICC identifier such as 'sRGB'
|
||||||
|
:param pdfmark: a dictionary containing keys to include the pdfmark
|
||||||
|
:param ascii_docinfo: if True, the docinfo block must be encoded in pure
|
||||||
|
ASCII and may not contain UTF-16BE-BOM-hex encoded strings, as
|
||||||
|
required for Ghostscript 9.24+
|
||||||
|
|
||||||
|
:returns: a string containing the entire pdfmark
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
# Ghostscript <= 9.21 has a bug where null entries in DOCINFO might produce
|
# Ghostscript <= 9.21 has a bug where null entries in DOCINFO might produce
|
||||||
# ERROR: VMerror (-25) on closing pdfwrite device.
|
# ERROR: VMerror (-25) on closing pdfwrite device.
|
||||||
# https://bugs.ghostscript.com/show_bug.cgi?id=697684
|
# https://bugs.ghostscript.com/show_bug.cgi?id=697684
|
||||||
# Work around this by only adding keys that have a nontrivial value
|
# Work around this by only adding keys that have a nontrivial value
|
||||||
docinfo_keys = ('/Title', '/Author', '/Subject', '/Creator', '/Keywords')
|
docinfo_keys = ('/Title', '/Author', '/Subject', '/Creator', '/Keywords')
|
||||||
docinfo_line_template = ' {key} <{value}>'
|
|
||||||
|
|
||||||
def docinfo_gen():
|
def docinfo_gen():
|
||||||
|
if not ascii_docinfo:
|
||||||
|
docinfo_line_template = ' {key} <{value}>'
|
||||||
|
encode = encode_text_string
|
||||||
|
else:
|
||||||
|
docinfo_line_template = ' {key} ({value})'
|
||||||
|
encode = _encode_ascii
|
||||||
yield from _get_pdfmark_dates(pdfmark)
|
yield from _get_pdfmark_dates(pdfmark)
|
||||||
for key in docinfo_keys:
|
for key in docinfo_keys:
|
||||||
if key in pdfmark and pdfmark[key].strip() != '':
|
if key in pdfmark and pdfmark[key].strip() != '':
|
||||||
line = docinfo_line_template.format(
|
line = docinfo_line_template.format(
|
||||||
key=key, value=encode_text_string(pdfmark[key]))
|
key=key, value=encode(pdfmark[key]))
|
||||||
yield line
|
yield line
|
||||||
docinfo = '\n'.join(docinfo_gen())
|
docinfo = '\n'.join(docinfo_gen())
|
||||||
|
|
||||||
@@ -201,13 +265,13 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB'):
|
def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
|
||||||
if icc == 'sRGB':
|
if icc == 'sRGB':
|
||||||
icc_profile = SRGB_ICC_PROFILE
|
icc_profile = SRGB_ICC_PROFILE
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Only supporting sRGB")
|
raise NotImplementedError("Only supporting sRGB")
|
||||||
|
|
||||||
ps = _get_pdfa_def(icc_profile, icc, pdfmark)
|
ps = _get_pdfa_def(icc_profile, icc, pdfmark, ascii_docinfo=ascii_docinfo)
|
||||||
|
|
||||||
# We should have encoded everything to pure ASCII by this point, and
|
# We should have encoded everything to pure ASCII by this point, and
|
||||||
# to be safe, only allow ASCII in PostScript
|
# to be safe, only allow ASCII in PostScript
|
||||||
@@ -258,4 +322,3 @@ def file_claims_pdfa(filename):
|
|||||||
pdfa_dict['conformance'] = conformance
|
pdfa_dict['conformance'] = conformance
|
||||||
|
|
||||||
return pdfa_dict
|
return pdfa_dict
|
||||||
|
|
||||||
|
|||||||
+42
-14
@@ -19,6 +19,7 @@ from contextlib import suppress
|
|||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from io import BytesIO
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -144,6 +145,13 @@ def triage_image_file(input_file, output_file, log, options):
|
|||||||
"image was scanned and specify it using --image-dpi.")
|
"image was scanned and specify it using --image-dpi.")
|
||||||
raise DpiError()
|
raise DpiError()
|
||||||
|
|
||||||
|
if im.mode in ('RGBA', 'LA'):
|
||||||
|
log.error(
|
||||||
|
"The input image has an alpha channel. Remove the alpha "
|
||||||
|
"channel first."
|
||||||
|
)
|
||||||
|
raise UnsupportedImageFormatError()
|
||||||
|
|
||||||
if 'iccprofile' not in im.info:
|
if 'iccprofile' not in im.info:
|
||||||
if im.mode == 'RGB':
|
if im.mode == 'RGB':
|
||||||
log.info('Input image has no ICC profile, assuming sRGB')
|
log.info('Input image has no ICC profile, assuming sRGB')
|
||||||
@@ -222,7 +230,7 @@ def repair_and_parse_pdf(
|
|||||||
|
|
||||||
pdfinfo = PdfInfo(output_file)
|
pdfinfo = PdfInfo(output_file)
|
||||||
|
|
||||||
if pdfinfo.has_userunit and options.output_type == 'pdfa':
|
if pdfinfo.has_userunit and options.output_type.startswith('pdfa'):
|
||||||
log.error(
|
log.error(
|
||||||
"This input file uses a PDF feature that is not supported "
|
"This input file uses a PDF feature that is not supported "
|
||||||
"by Ghostscript, so you cannot use --output-type=pdfa for this "
|
"by Ghostscript, so you cannot use --output-type=pdfa for this "
|
||||||
@@ -496,17 +504,23 @@ def rasterize_with_ghostscript(
|
|||||||
options = context.get_options()
|
options = context.get_options()
|
||||||
pageinfo = get_pageinfo(input_file, context)
|
pageinfo = get_pageinfo(input_file, context)
|
||||||
|
|
||||||
device = 'png16m' # 24-bit
|
colorspaces = ['pngmono', 'pnggray', 'png256', 'png16m']
|
||||||
if pageinfo.images:
|
device_idx = 0
|
||||||
if all(image.comp == 1 for image in pageinfo.images):
|
def at_least(cs):
|
||||||
if all(image.bpc == 1 for image in pageinfo.images):
|
return max(device_idx, colorspaces.index(cs))
|
||||||
device = 'pngmono'
|
|
||||||
elif all(image.bpc > 1 and image.color == Colorspace.index
|
for image in pageinfo.images:
|
||||||
for image in pageinfo.images):
|
if image.type_ != 'image':
|
||||||
device = 'png256'
|
continue # ignore masks
|
||||||
elif all(image.bpc > 1 and image.color == Colorspace.gray
|
if image.bpc > 1:
|
||||||
for image in pageinfo.images):
|
if image.color == Colorspace.index:
|
||||||
device = 'pnggray'
|
device_idx = at_least('png256')
|
||||||
|
elif image.color == Colorspace.gray:
|
||||||
|
device_idx = at_least('pnggray')
|
||||||
|
else:
|
||||||
|
device_idx = at_least('png16m')
|
||||||
|
|
||||||
|
device = colorspaces[device_idx]
|
||||||
|
|
||||||
log.debug("Rasterize {0} with {1}".format(
|
log.debug("Rasterize {0} with {1}".format(
|
||||||
os.path.basename(input_file), device))
|
os.path.basename(input_file), device))
|
||||||
@@ -912,7 +926,20 @@ def generate_postscript_stub(
|
|||||||
options = context.get_options()
|
options = context.get_options()
|
||||||
pdf = pypdf.PdfFileReader(input_file)
|
pdf = pypdf.PdfFileReader(input_file)
|
||||||
pdfmark = get_pdfmark(pdf, options)
|
pdfmark = get_pdfmark(pdf, options)
|
||||||
generate_pdfa_ps(output_file, pdfmark)
|
|
||||||
|
ascii_docinfo = False
|
||||||
|
if ghostscript.version() >= '9.24':
|
||||||
|
ascii_docinfo = True
|
||||||
|
try:
|
||||||
|
for v in pdfmark.values():
|
||||||
|
v.encode('ascii', errors='strict')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
log.warning(
|
||||||
|
"Ghostscript 9.24+ does not support Unicode strings in "
|
||||||
|
" metadata. These will be converted to ASCII if possible."
|
||||||
|
)
|
||||||
|
|
||||||
|
generate_pdfa_ps(output_file, pdfmark, ascii_docinfo=ascii_docinfo)
|
||||||
|
|
||||||
|
|
||||||
def skip_page(
|
def skip_page(
|
||||||
@@ -970,6 +997,7 @@ def merge_pages_ghostscript(
|
|||||||
pdf_pages, _ = _merge_pages_common(
|
pdf_pages, _ = _merge_pages_common(
|
||||||
input_files_groups, output_file, log, context)
|
input_files_groups, output_file, log, context)
|
||||||
input_pdfinfo = context.get_pdfinfo()
|
input_pdfinfo = context.get_pdfinfo()
|
||||||
|
|
||||||
ghostscript.generate_pdfa(
|
ghostscript.generate_pdfa(
|
||||||
pdf_version=input_pdfinfo.min_version,
|
pdf_version=input_pdfinfo.min_version,
|
||||||
pdf_pages=pdf_pages,
|
pdf_pages=pdf_pages,
|
||||||
@@ -977,7 +1005,7 @@ def merge_pages_ghostscript(
|
|||||||
compression=options.pdfa_image_compression,
|
compression=options.pdfa_image_compression,
|
||||||
log=log,
|
log=log,
|
||||||
threads=options.jobs or 1,
|
threads=options.jobs or 1,
|
||||||
pdfa_part=('1' if options.output_type == 'pdfa-1' else '2'))
|
pdfa_part=options.output_type[-1]) # is pdfa-1, pdfa-2, or pdfa-3
|
||||||
if fitz:
|
if fitz:
|
||||||
doc = fitz.Document(output_file + '_toc.pdf')
|
doc = fitz.Document(output_file + '_toc.pdf')
|
||||||
doc.setToC(input_pdfinfo.table_of_contents)
|
doc.setToC(input_pdfinfo.table_of_contents)
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ pytest >= 3.2
|
|||||||
pytest-helpers-namespace
|
pytest-helpers-namespace
|
||||||
pytest-xdist
|
pytest-xdist
|
||||||
pytest-cov
|
pytest-cov
|
||||||
pytest-timeout
|
pytest-timeout == 1.2.1
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ Assemblies
|
|||||||
These test resources are assemblies or derivatives from other previously mentioned files, released under the same license terms as their input files.
|
These test resources are assemblies or derivatives from other previously mentioned files, released under the same license terms as their input files.
|
||||||
|
|
||||||
- baiona_gray.png (from baiona.png)
|
- baiona_gray.png (from baiona.png)
|
||||||
|
- baiona_colormapped.png (from baiona.png)
|
||||||
- cardinal.pdf (four cardinal directions, baked-in rotated copies of LinnSequencer.jpg)
|
- cardinal.pdf (four cardinal directions, baked-in rotated copies of LinnSequencer.jpg)
|
||||||
- ccitt.pdf (LinnSequencer.jpg, converted to CCITT encoding)
|
- ccitt.pdf (LinnSequencer.jpg, converted to CCITT encoding)
|
||||||
- encrypted_algo4.pdf (congress.jpg, encrypted with algorithm 4 - not supported by PyPDF2)
|
- encrypted_algo4.pdf (congress.jpg, encrypted with algorithm 4 - not supported by PyPDF2)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 147 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -106,6 +106,8 @@ def main():
|
|||||||
source = os.environ['_OCRMYPDF_TEST_INFILE'] # required
|
source = os.environ['_OCRMYPDF_TEST_INFILE'] # required
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
cache_disabled = os.environ.get('_OCRMYPDF_CACHE_DISABLED', False)
|
||||||
|
|
||||||
if args.imagename == 'stdin':
|
if args.imagename == 'stdin':
|
||||||
real_tesseract()
|
real_tesseract()
|
||||||
|
|
||||||
@@ -128,7 +130,7 @@ def main():
|
|||||||
print("Tesseract cache folder {} - ".format(cache_folder), end='',
|
print("Tesseract cache folder {} - ".format(cache_folder), end='',
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
|
|
||||||
if (cache_folder / 'stderr.bin').exists():
|
if (cache_folder / 'stderr.bin').exists() and not cache_disabled:
|
||||||
# Cache hit
|
# Cache hit
|
||||||
print("HIT", file=sys.stderr)
|
print("HIT", file=sys.stderr)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# © 2018 James R. Barlow: github.com/jbarlow83
|
||||||
|
#
|
||||||
|
# This file is part of OCRmyPDF.
|
||||||
|
#
|
||||||
|
# OCRmyPDF is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# OCRmyPDF is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import ocrmypdf.leptonica as lept
|
||||||
|
|
||||||
|
|
||||||
|
def test_colormap_backgroundnorm(resources):
|
||||||
|
# Issue #262 - unclear how to reproduce exactly, so just ensure leptonica
|
||||||
|
# can handle that case
|
||||||
|
pix = lept.Pix.read(resources / 'baiona-colormapped.png')
|
||||||
|
pix.background_norm()
|
||||||
+21
-16
@@ -300,7 +300,8 @@ def test_autorotate_threshold(
|
|||||||
@pytest.mark.parametrize('renderer',RENDERERS)
|
@pytest.mark.parametrize('renderer',RENDERERS)
|
||||||
def test_ocr_timeout(renderer, resources, outpdf):
|
def test_ocr_timeout(renderer, resources, outpdf):
|
||||||
out = check_ocrmypdf(resources / 'skew.pdf', outpdf,
|
out = check_ocrmypdf(resources / 'skew.pdf', outpdf,
|
||||||
'--tesseract-timeout', '1.0')
|
'--tesseract-timeout', '0.01',
|
||||||
|
'--pdf-renderer', renderer)
|
||||||
pdfinfo = PdfInfo(out)
|
pdfinfo = PdfInfo(out)
|
||||||
assert not pdfinfo[0].has_text
|
assert not pdfinfo[0].has_text
|
||||||
|
|
||||||
@@ -718,7 +719,7 @@ def test_tesseract_config_notfound(renderer, resources, outdir):
|
|||||||
'--pdf-renderer', renderer,
|
'--pdf-renderer', renderer,
|
||||||
'--tesseract-config', cfg_file)
|
'--tesseract-config', cfg_file)
|
||||||
assert "Can't open" in err, "No error message about missing config file"
|
assert "Can't open" in err, "No error message about missing config file"
|
||||||
assert p.returncode == ExitCode.ok
|
assert p.returncode == ExitCode.ok, err
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||||
@@ -828,6 +829,7 @@ def test_no_contents(spoof_tesseract_noop, resources, outpdf):
|
|||||||
@pytest.mark.parametrize('image', [
|
@pytest.mark.parametrize('image', [
|
||||||
'baiona.png',
|
'baiona.png',
|
||||||
'baiona_gray.png',
|
'baiona_gray.png',
|
||||||
|
'baiona_alpha.png',
|
||||||
'congress.jpg'
|
'congress.jpg'
|
||||||
])
|
])
|
||||||
def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
|
def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
|
||||||
@@ -838,7 +840,6 @@ def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
|
|||||||
output_file = str(outpdf)
|
output_file = str(outpdf)
|
||||||
|
|
||||||
im = Image.open(input_file)
|
im = Image.open(input_file)
|
||||||
|
|
||||||
# Runs: ocrmypdf - output.pdf < testfile
|
# Runs: ocrmypdf - output.pdf < testfile
|
||||||
with open(input_file, 'rb') as input_stream:
|
with open(input_file, 'rb') as input_stream:
|
||||||
p_args = ocrmypdf_exec + [
|
p_args = ocrmypdf_exec + [
|
||||||
@@ -848,7 +849,12 @@ def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
|
|||||||
stdin=input_stream, env=spoof_tesseract_noop)
|
stdin=input_stream, env=spoof_tesseract_noop)
|
||||||
out, err = p.communicate()
|
out, err = p.communicate()
|
||||||
|
|
||||||
assert p.returncode == ExitCode.ok
|
if im.mode in ('RGBA', 'LA'):
|
||||||
|
# If alpha image is input, expect an error
|
||||||
|
assert p.returncode != ExitCode.ok and b'alpha' in err
|
||||||
|
return
|
||||||
|
|
||||||
|
assert p.returncode == ExitCode.ok, err.decode('utf-8')
|
||||||
|
|
||||||
pdfinfo = PdfInfo(output_file)
|
pdfinfo = PdfInfo(output_file)
|
||||||
|
|
||||||
@@ -893,7 +899,7 @@ def test_compression_changed(spoof_tesseract_noop, ocrmypdf_exec,
|
|||||||
stdin=input_stream, env=spoof_tesseract_noop)
|
stdin=input_stream, env=spoof_tesseract_noop)
|
||||||
out, err = p.communicate()
|
out, err = p.communicate()
|
||||||
|
|
||||||
assert p.returncode == ExitCode.ok
|
assert p.returncode == ExitCode.ok, err
|
||||||
|
|
||||||
pdfinfo = PdfInfo(output_file)
|
pdfinfo = PdfInfo(output_file)
|
||||||
|
|
||||||
@@ -902,13 +908,7 @@ def test_compression_changed(spoof_tesseract_noop, ocrmypdf_exec,
|
|||||||
if compression == "jpeg":
|
if compression == "jpeg":
|
||||||
assert pdfimage.enc == Encoding.jpeg
|
assert pdfimage.enc == Encoding.jpeg
|
||||||
else:
|
else:
|
||||||
if ghostscript.jpeg_passthrough_available():
|
assert pdfimage.enc not in (Encoding.jpeg, Encoding.jpeg2000)
|
||||||
# Ghostscript 9.23 adds JPEG passthrough, which allows a JPEG to be
|
|
||||||
# copied without transcoding - so report
|
|
||||||
if image.endswith('jpg'):
|
|
||||||
assert pdfimage.enc == Encoding.jpeg
|
|
||||||
else:
|
|
||||||
assert pdfimage.enc not in (Encoding.jpeg, Encoding.jpeg2000)
|
|
||||||
|
|
||||||
if im.mode.startswith('RGB') or im.mode.startswith('BGR'):
|
if im.mode.startswith('RGB') or im.mode.startswith('BGR'):
|
||||||
assert pdfimage.color == Colorspace.rgb, \
|
assert pdfimage.color == Colorspace.rgb, \
|
||||||
@@ -951,17 +951,22 @@ def test_sidecar_nonempty(spoof_tesseract_cache, resources, outpdf):
|
|||||||
assert 'the' in ocr_text
|
assert 'the' in ocr_text
|
||||||
|
|
||||||
|
|
||||||
def test_pdfa_1(spoof_tesseract_cache, resources, outpdf):
|
@pytest.mark.parametrize('pdfa_level', ['1', '2', '3'])
|
||||||
|
def test_pdfa_n(spoof_tesseract_cache, pdfa_level, resources, outpdf):
|
||||||
|
if pdfa_level == '3' and ghostscript.version() < '9.19':
|
||||||
|
pytest.xfail(reason='Ghostscript >= 9.19 required')
|
||||||
|
|
||||||
check_ocrmypdf(
|
check_ocrmypdf(
|
||||||
resources / 'ccitt.pdf', outpdf,
|
resources / 'ccitt.pdf', outpdf,
|
||||||
'--output-type', 'pdfa-1',
|
'--output-type', 'pdfa-' + pdfa_level,
|
||||||
env=spoof_tesseract_cache
|
env=spoof_tesseract_cache
|
||||||
)
|
)
|
||||||
|
|
||||||
pdfa_info = file_claims_pdfa(outpdf)
|
pdfa_info = file_claims_pdfa(outpdf)
|
||||||
assert pdfa_info['conformance'] == 'PDF/A-1B'
|
assert pdfa_info['conformance'] == 'PDF/A-{}B'.format(pdfa_level)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info >= (3, 7, 0), reason='fixed')
|
||||||
def test_bad_locale():
|
def test_bad_locale():
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env['LC_ALL'] = 'C'
|
env['LC_ALL'] = 'C'
|
||||||
@@ -1012,7 +1017,7 @@ def test_rotate_deskew_timeout(resources, outdir):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
PIL.PILLOW_VERSION < '5.0.0',
|
PIL.__version__ < '5.0.0',
|
||||||
reason="Pillow < 5.0.0 doesn't raise the exception")
|
reason="Pillow < 5.0.0 doesn't raise the exception")
|
||||||
def test_decompression_bomb(resources, outpdf):
|
def test_decompression_bomb(resources, outpdf):
|
||||||
p, out, err = run_ocrmypdf(
|
p, out, err = run_ocrmypdf(
|
||||||
|
|||||||
+18
-6
@@ -24,6 +24,12 @@ from datetime import timezone
|
|||||||
from ocrmypdf.pdfa import file_claims_pdfa, encode_pdf_date, decode_pdf_date
|
from ocrmypdf.pdfa import file_claims_pdfa, encode_pdf_date, decode_pdf_date
|
||||||
from ocrmypdf.exceptions import ExitCode
|
from ocrmypdf.exceptions import ExitCode
|
||||||
from ocrmypdf.lib import fitz
|
from ocrmypdf.lib import fitz
|
||||||
|
from ocrmypdf.helpers import fspath
|
||||||
|
from ocrmypdf.pdfa import (
|
||||||
|
file_claims_pdfa, encode_pdf_date, decode_pdf_date, generate_pdfa_ps,
|
||||||
|
SRGB_ICC_PROFILE
|
||||||
|
)
|
||||||
|
from ocrmypdf.exec import ghostscript
|
||||||
|
|
||||||
# pytest.helpers is dynamic
|
# pytest.helpers is dynamic
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
@@ -73,11 +79,19 @@ def test_override_metadata(spoof_tesseract_noop, output_type, resources,
|
|||||||
|
|
||||||
assert p.returncode == ExitCode.ok, err
|
assert p.returncode == ExitCode.ok, err
|
||||||
|
|
||||||
reader = pypdf.PdfFileReader(outpdf)
|
before = pypdf.PdfFileReader(str(input_file))
|
||||||
|
after = pypdf.PdfFileReader(outpdf)
|
||||||
|
|
||||||
assert reader.documentInfo['/Title'] == german
|
if ghostscript.version() >= '9.24':
|
||||||
assert reader.documentInfo['/Author'] == chinese
|
pytest.xfail('Ghostscript 9.24+ does not support Unicode DOCINFO')
|
||||||
assert reader.documentInfo.get('/Keywords', '') == ''
|
|
||||||
|
assert after.documentInfo['/Title'] == german
|
||||||
|
assert after.documentInfo['/Author'] == chinese
|
||||||
|
assert after.documentInfo.get('/Keywords', '') == ''
|
||||||
|
|
||||||
|
before_date = decode_pdf_date(before.documentInfo['/CreationDate'])
|
||||||
|
after_date = decode_pdf_date(after.documentInfo['/CreationDate'])
|
||||||
|
assert before_date == after_date
|
||||||
|
|
||||||
pdfa_info = file_claims_pdfa(outpdf)
|
pdfa_info = file_claims_pdfa(outpdf)
|
||||||
assert pdfa_info['output'] == output_type
|
assert pdfa_info['output'] == output_type
|
||||||
@@ -152,5 +166,3 @@ def test_creation_date_preserved(spoof_tesseract_noop, output_type, resources,
|
|||||||
date_after = decode_pdf_date(after['/ModDate'])
|
date_after = decode_pdf_date(after['/ModDate'])
|
||||||
assert seconds_between_dates(
|
assert seconds_between_dates(
|
||||||
date_after, datetime.datetime.now(timezone.utc)) < 1000
|
date_after, datetime.datetime.now(timezone.utc)) < 1000
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import pytest
|
import pytest
|
||||||
import img2pdf
|
import img2pdf
|
||||||
import pytest
|
|
||||||
import sys
|
import sys
|
||||||
import PyPDF2 as pypdf
|
import PyPDF2 as pypdf
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user