Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff78d7c56c | ||
|
|
ff092c8629 | ||
|
|
fe14cb57c0 | ||
|
|
507fbc01d5 | ||
|
|
325479e5be | ||
|
|
e926ecb8b2 | ||
|
|
d0cb6c0e92 | ||
|
|
5b7c8cf5d3 | ||
|
|
40baab32ac | ||
|
|
e877d37ac8 | ||
|
|
5a9f77e438 | ||
|
|
8ddd67d1e2 | ||
|
|
1605408c23 | ||
|
|
2d3b1ebf6e | ||
|
|
c74eaab7f5 | ||
|
|
c21d231388 | ||
|
|
a73afc4e76 | ||
|
|
76c364150d | ||
|
|
94a3e447cc | ||
|
|
12868b461a | ||
|
|
322085933b | ||
|
|
3fed94bb79 | ||
|
|
8c877482bd | ||
|
|
b17d589e84 | ||
|
|
368252a243 | ||
|
|
ccefda1bee | ||
|
|
3d0e8c9629 | ||
|
|
313bbbb94c | ||
|
|
0360f078de | ||
|
|
c8901666c4 | ||
|
|
7430006596 | ||
|
|
f3e06b2dbd | ||
|
|
e97df307ff | ||
|
|
1443354aa2 | ||
|
|
250e68c1cd | ||
|
|
6a380ee99c | ||
|
|
3c90bd96a9 | ||
|
|
06a7ceb25a | ||
|
|
733a8e7d58 | ||
|
|
570bbe9a05 | ||
|
|
5cc3adb39a | ||
|
|
3957a0606c | ||
|
|
11a561dbce | ||
|
|
dad2198394 | ||
|
|
e40fdc502d | ||
|
|
d446fe5922 | ||
|
|
4ca90c106d | ||
|
|
7c5e58a497 | ||
|
|
323b9a5f8e | ||
|
|
cab381a339 | ||
|
|
fe4d4c39cd | ||
|
|
ad188d7ae1 | ||
|
|
8246cc0538 | ||
|
|
6f3ac46b1c | ||
|
|
ac71c3be63 | ||
|
|
ecc0ac9b19 | ||
|
|
ea4e6bf67d | ||
|
|
46c204f533 | ||
|
|
71fbda8bf6 | ||
|
|
9b79b4a7c8 | ||
|
|
c04cc853d7 | ||
|
|
dd41e70ccc | ||
|
|
4206e74f42 | ||
|
|
68c3ce56a9 | ||
|
|
ab0e5fa425 | ||
|
|
f3b0434a87 | ||
|
|
aa394440db | ||
|
|
3b98a1a04b | ||
|
|
fcb89b0c58 |
@@ -0,0 +1 @@
|
||||
ref-names: $Format:%D$
|
||||
+3
-1
@@ -5,4 +5,6 @@
|
||||
# (binary is a macro for -text -diff)
|
||||
*.jar binary
|
||||
*.pdf binary
|
||||
*.PDF binary
|
||||
*.PDF binary
|
||||
|
||||
.git_archival.txt export-subst
|
||||
|
||||
@@ -10,6 +10,7 @@ pyvenv.cfg
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
wheelhouse/
|
||||
|
||||
# Automatically generated files
|
||||
ocrmypdf/lib/_*.py
|
||||
|
||||
+5
-7
@@ -5,7 +5,7 @@ cache: pip
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.ccache
|
||||
- tarballs
|
||||
- packages
|
||||
- tests/cache
|
||||
|
||||
python:
|
||||
@@ -21,17 +21,15 @@ before_install:
|
||||
- sudo add-apt-repository ppa:b-eltzner/qpdfview-exp -y # for QPDF 5
|
||||
- sudo add-apt-repository ppa:itachi-san/ffmpeg -y # for libav 11.2 (for unpaper)
|
||||
- sudo apt-get update -qq # must go after all add-apt-repo
|
||||
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils gcc libavformat-dev libavcodec-dev libavutil-dev automake make pkg-config xsltproc libffi-dev
|
||||
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils libavformat56 libavcodec56 libavutil54 libffi-dev
|
||||
|
||||
# pip
|
||||
- pip install --upgrade pip
|
||||
|
||||
# Download, make and install unpaper (using ccache)
|
||||
- mkdir -p tarballs
|
||||
- "[ -f tarballs/unpaper-6.1.tar.xz ] || wget -q https://www.flameeyes.eu/files/unpaper-6.1.tar.xz -O tarballs/unpaper-6.1.tar.xz"
|
||||
- tar -xvf tarballs/unpaper-6.1.tar.xz
|
||||
- export PATH="/usr/lib/ccache:$PATH"
|
||||
- pushd unpaper-6.1 && ./configure --prefix=/usr && make -j && sudo make install && popd
|
||||
- mkdir -p packages
|
||||
- "[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb"
|
||||
- sudo dpkg -i packages/unpaper_6.1-1.deb
|
||||
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
|
||||
+12
-6
@@ -23,11 +23,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
unpaper \
|
||||
ghostscript \
|
||||
qpdf \
|
||||
poppler-utils
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
poppler-utils \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu tesseract-ocr-spa tesseract-ocr-eng tesseract-ocr-fra
|
||||
|
||||
@@ -36,6 +33,14 @@ RUN apt-get install -qy --no-install-recommends \
|
||||
libpython3-dev \
|
||||
gcc
|
||||
|
||||
# Install Ghostscript from Debian sid to work around JPEG 2000 issue in
|
||||
# Debian stretch libgs9 or gs 9.16~dfsg-2.1
|
||||
|
||||
COPY ./share/etc-apt-sources.list /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y ghostscript/sid
|
||||
|
||||
|
||||
# Enforce UTF-8
|
||||
# Borrowed from https://index.docker.io/u/crosbymichael/python/
|
||||
RUN dpkg-reconfigure locales && \
|
||||
@@ -63,9 +68,11 @@ RUN chmod 644 /usr/share/tesseract-ocr/tessdata/pdf.ttf
|
||||
|
||||
# Install application and dependencies
|
||||
# In this arrangement Pillow and reportlab will be provided by the system
|
||||
# Even though ocrmypdf is locally present, pull from PyPI because
|
||||
# Dockerhub and setuptools_scm clash
|
||||
RUN . /appenv/bin/activate; \
|
||||
pip install --upgrade pip \
|
||||
&& pip install /application \
|
||||
&& pip install ocrmypdf \
|
||||
&& pip install --no-cache-dir -r /application/test_requirements.txt
|
||||
|
||||
# Remove the junk
|
||||
@@ -77,7 +84,6 @@ USER docker
|
||||
WORKDIR /home/docker
|
||||
|
||||
ENV OCRMYPDF_TEST_OUTPUT=/tmp/test-output
|
||||
ENV OCRMYPDF_IN_DOCKER=1
|
||||
ENV OCRMYPDF_SHARP_TTF=1
|
||||
|
||||
# Must use array form of ENTRYPOINT
|
||||
|
||||
@@ -18,3 +18,9 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
----
|
||||
|
||||
sRGB_IEC61966-2-1_black_scaled.icc
|
||||
|
||||
To anyone who acknowledges that the file "sRGB_IEC61966-2-1_black scaled.icc" is provided "AS IS" WITH NO EXPRESS OR IMPLIED WARRANTY, permission to use, copy and distribute these file for any purpose is hereby granted without fee, provided that the file is not changed including the ICC copyright notice tag, and that the name of ICC shall not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. ICC makes no representations about the suitability of this software for any purpose.
|
||||
+11
-4
@@ -48,6 +48,12 @@ Download OCRmyPDF here: https://github.com/jbarlow83/OCRmyPDF/releases
|
||||
|
||||
You can install it to a Python virtual environment or system-wide.
|
||||
|
||||
Debian and Ubuntu
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
|
||||
``apt-get install ocrmypdf``.
|
||||
|
||||
Installing the Docker image
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -116,8 +122,8 @@ Install or upgrade the required Homebrew packages, if any are missing::
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2
|
||||
brew install leptonica
|
||||
brew install libxml2 libffi leptonica
|
||||
brew install unpaper # optional
|
||||
brew install tesseract
|
||||
|
||||
Update the homebrew pip and install Pillow::
|
||||
@@ -148,6 +154,7 @@ Install system dependencies::
|
||||
sudo apt-get install \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
libffi-dev \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
@@ -252,11 +259,11 @@ In case you detect an issue, please:
|
||||
Press & Media
|
||||
-------------
|
||||
|
||||
- `c't 1-2014, page 59 <http://www.heise.de/ct/inhalt/2014/1/58/>`__:
|
||||
- `c't 1-2014, page 59 <http://heise.de/-2279695>`__:
|
||||
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
|
||||
magazine c't
|
||||
- `heise Open Source, 09/2014: Texterkennung mit
|
||||
OCRmyPDF <http://www.heise.de/-2356670>`__
|
||||
OCRmyPDF <http://heise.de/-2356670>`__
|
||||
|
||||
Disclaimer
|
||||
----------
|
||||
|
||||
+81
-4
@@ -6,6 +6,83 @@ Please always read this file before installing the package
|
||||
Download software here: https://github.com/jbarlow83/OCRmyPDF/tags
|
||||
|
||||
|
||||
v4.1:
|
||||
=====
|
||||
|
||||
- ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold``
|
||||
- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time
|
||||
- Fixed problems with "RethrownJobError" errors during error handling that suppressed the useful error messages
|
||||
|
||||
|
||||
v4.0.7:
|
||||
=======
|
||||
|
||||
- Minor correction to Ghostscript output settings
|
||||
|
||||
|
||||
v4.0.6:
|
||||
=======
|
||||
|
||||
- Update install instructions
|
||||
- Provide a sRGB profile instead of using Ghostscript's
|
||||
|
||||
|
||||
v4.0.5:
|
||||
=======
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Remove some verbose debug messages from v4.0.4
|
||||
- Fixed temporary that wasn't being deleted
|
||||
- DPI is now calculated correctly for cropped images, along with other image transformations
|
||||
- Inline images are now checked during DPI calculation instead of rejecting the image
|
||||
|
||||
v4.0.4:
|
||||
=======
|
||||
|
||||
Released with verbose debug message turned on. Do not use. Skip to v4.0.5.
|
||||
|
||||
|
||||
v4.0.3:
|
||||
=======
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Page orientations detected are now reported in a summary comment
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Show stack trace if unexpect errors occur
|
||||
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
|
||||
abort the file
|
||||
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
|
||||
|
||||
|
||||
v4.0.2:
|
||||
=======
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
|
||||
orientation information
|
||||
- Improved handling of Tesseract errors and crashes
|
||||
- Fixed use of chmod on Docker that broke most test cases
|
||||
|
||||
|
||||
v4.0.1:
|
||||
=======
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed a KeyError if tesseract fails to find page orientation information
|
||||
|
||||
|
||||
v4.0:
|
||||
=====
|
||||
|
||||
@@ -15,15 +92,15 @@ New features
|
||||
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information
|
||||
on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
|
||||
fairly reliable but some false positives occur especially if there is not much text to work with. (#4)
|
||||
- Deskewing is now performed using Leptonica instead of unpaper. Leptonica deskews images faster and
|
||||
more reliably than unpaper.
|
||||
- Deskewing is now performed using Leptonica instead of unpaper. Leptonica is faster and more reliable
|
||||
at image deskewing than unpaper.
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly
|
||||
rotated in the page had a /Rotate tag applied (#50)
|
||||
if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
|
||||
- Fixed an issue where lossless reconstruction could misalign the graphics layer with respect to
|
||||
text layer if the page had been cropped such that its origin is not (0, 0) (#49)
|
||||
|
||||
@@ -31,7 +108,7 @@ Fixes
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Logging output much is now better
|
||||
- Logging output is now much easier to read
|
||||
- ``--deskew`` is now performed by Leptonica instead of unpaper (#25)
|
||||
- libffi is now required
|
||||
- Some changes were made to the Docker and Travis build environments to support libffi
|
||||
|
||||
@@ -10,6 +10,7 @@ class ExitCode(IntEnum):
|
||||
invalid_output_pdfa = 4
|
||||
file_access_error = 5
|
||||
already_done_ocr = 6
|
||||
child_process_error = 7
|
||||
other_error = 15
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -5,6 +5,7 @@ from tempfile import NamedTemporaryFile
|
||||
from subprocess import Popen, PIPE, check_call
|
||||
from shutil import copy
|
||||
from . import get_program
|
||||
from .pdfa import SRGB_ICC_PROFILE
|
||||
|
||||
|
||||
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
@@ -52,7 +53,6 @@ def generate_pdfa(pdf_pages, output_file, threads=1):
|
||||
"-dJPEGQ=95",
|
||||
"-dPDFA=2",
|
||||
"-sPDFACompatibilityPolicy=2",
|
||||
"-sOutputICCProfile=srgb.icc",
|
||||
"-sOutputFile=" + gs_pdf.name,
|
||||
]
|
||||
args_gs.extend(pdf_pages)
|
||||
|
||||
+73
-46
@@ -84,6 +84,20 @@ class LeptonicaIOError(LeptonicaError):
|
||||
|
||||
|
||||
class Pix:
|
||||
"""Wrapper around leptonica's PIX object.
|
||||
|
||||
Leptonica uses referencing counting on PIX objects. Also, many Leptonica
|
||||
functions return the original object with an increased reference count
|
||||
if the operation had no effect (for example, image skew was found to be 0).
|
||||
This has complications for memory management in Python. Whenever Leptonica
|
||||
returns a PIX object (new or old), we wrap it in this class, which
|
||||
registers it with the FFI garbage collector. pixDestroy() decrements the
|
||||
reference count and only destroys when the last reference is removed.
|
||||
|
||||
Leptonica's reference counting is not threadsafe. This class can be used
|
||||
in a threadsafe manner if a Python threading.Lock protects the data.
|
||||
"""
|
||||
|
||||
def __init__(self, cpix):
|
||||
self.cpix = ffi.gc(cpix, Pix._pix_destroy)
|
||||
|
||||
@@ -95,6 +109,34 @@ class Pix:
|
||||
else:
|
||||
return "<leptonica.Pix image NULL>"
|
||||
|
||||
def __getstate__(self):
|
||||
data = ffi.new('l_uint32 **')
|
||||
size = ffi.new('size_t *')
|
||||
|
||||
err = lept.pixSerializeToMemory(self.cpix, data, size)
|
||||
if err != 0:
|
||||
raise LeptonicaIOError("pixSerializeToMemory")
|
||||
|
||||
char_data = ffi.cast('char *', data[0])
|
||||
data_bytes = ffi.buffer(char_data, size[0])[:]
|
||||
lept.lept_free(char_data)
|
||||
return dict(data=data_bytes)
|
||||
|
||||
def __setstate__(self, state):
|
||||
cdata_bytes = ffi.new('char[]', state['data'])
|
||||
cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes)
|
||||
|
||||
self.cpix = lept.pixDeserializeFromMemory(
|
||||
cdata_uint32, len(state['data']))
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.cpix.w
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self.cpix.h
|
||||
|
||||
@classmethod
|
||||
def read(cls, filename):
|
||||
"""Load an image file into a PIX object.
|
||||
@@ -113,23 +155,11 @@ class Pix:
|
||||
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
|
||||
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
|
||||
"""
|
||||
fileroot, extension = os.path.splitext(filename)
|
||||
fix_pnm = False
|
||||
if extension.lower() in ('.pbm', '.pgm', '.ppm'):
|
||||
# Leptonica does not process handle these extensions correctly, but
|
||||
# does handle .pnm correctly. Add another .pnm suffix.
|
||||
filename += '.pnm'
|
||||
fix_pnm = True
|
||||
|
||||
with LeptonicaErrorTrap():
|
||||
lept.pixWriteImpliedFormat(
|
||||
filename.encode(sys.getfilesystemencoding()),
|
||||
self.cpix, jpeg_quality, jpeg_progressive)
|
||||
|
||||
if fix_pnm:
|
||||
from shutil import move
|
||||
move(filename, filename[:-4]) # Remove .pnm suffix
|
||||
|
||||
def deskew(self, reduction_factor=0):
|
||||
"""Returns the deskewed pix object.
|
||||
|
||||
@@ -165,16 +195,40 @@ class Pix:
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
def otsu_adaptive_threshold(
|
||||
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
|
||||
with LeptonicaErrorTrap():
|
||||
sx, sy = tile_size
|
||||
smoothx, smoothy = kernel_size
|
||||
p_cpix = ffi.new('PIX **')
|
||||
|
||||
result = lept.pixOtsuAdaptiveThreshold(
|
||||
self.cpix,
|
||||
sx, sy,
|
||||
smoothx, smoothy,
|
||||
scorefract,
|
||||
ffi.NULL,
|
||||
p_cpix)
|
||||
if result == 0:
|
||||
return Pix(p_cpix[0])
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=1)
|
||||
def make_pixel_sum_tab8():
|
||||
return lept.makePixelSumTab8()
|
||||
|
||||
@staticmethod
|
||||
def correlation_binary(pix1, pix2):
|
||||
if getLeptonicaVersion() < 'leptonica-1.72':
|
||||
if get_leptonica_version() < 'leptonica-1.72':
|
||||
# Older versions of Leptonica (pre-1.72) have a buggy
|
||||
# implementation of pixCorrelationBinary that overflows on larger
|
||||
# images.
|
||||
pix1_count = ffi.new('l_int32 *', 0)
|
||||
pix2_count = ffi.new('l_int32 *', 0)
|
||||
pixn_count = ffi.new('l_int32 *', 0)
|
||||
tab8 = lept.makePixelSumTab8() # Small memory leak on each call
|
||||
pix1_count = ffi.new('l_int32 *')
|
||||
pix2_count = ffi.new('l_int32 *')
|
||||
pixn_count = ffi.new('l_int32 *')
|
||||
tab8 = Pix.make_pixel_sum_tab8()
|
||||
|
||||
lept.pixCountPixels(pix1.cpix, pix1_count, tab8)
|
||||
lept.pixCountPixels(pix2.cpix, pix2_count, tab8)
|
||||
@@ -204,7 +258,7 @@ class Pix:
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def getLeptonicaVersion():
|
||||
def get_leptonica_version():
|
||||
"""Get Leptonica version string.
|
||||
|
||||
Caveat: Leptonica expects the caller to free this memory. We don't,
|
||||
@@ -248,39 +302,12 @@ if __name__ == '__main__':
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if getLeptonicaVersion() != u'leptonica-1.69':
|
||||
if get_leptonica_version() != u'leptonica-1.69':
|
||||
print("Unexpected leptonica version: %s" % getLeptonicaVersion())
|
||||
|
||||
args.func(args)
|
||||
|
||||
|
||||
def _test_output(mode, extension, im_format):
|
||||
from PIL import Image
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
with NamedTemporaryFile(prefix='test-lept-pnm', suffix=extension, delete=True) as tmpfile:
|
||||
im = Image.new(mode=mode, size=(100, 100))
|
||||
im.save(tmpfile)
|
||||
|
||||
pix = pixRead(tmpfile.name)
|
||||
pixWriteImpliedFormat(tmpfile.name, pix)
|
||||
pixDestroy(pix)
|
||||
|
||||
im_roundtrip = Image.open(tmpfile.name)
|
||||
assert im_roundtrip.mode == im.mode, "leptonica mode differs"
|
||||
assert im_roundtrip.format == im_format, \
|
||||
"{0}: leptonica produced a {1}".format(
|
||||
extension,
|
||||
im_roundtrip.format)
|
||||
|
||||
|
||||
def test_pnm_output():
|
||||
params = [['1', '.pbm', 'PPM'], ['L', '.pgm', 'PPM'],
|
||||
['RGB', '.ppm', 'PPM']]
|
||||
for param in params:
|
||||
_test_output(*param)
|
||||
|
||||
|
||||
def test_skew_angle():
|
||||
from PIL import Image, ImageDraw
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
@@ -58,7 +58,23 @@ PIX *pixRotate180(PIX *pixd, PIX *pixs);
|
||||
l_int32 pixCountPixels ( PIX *pix, l_int32 *pcount, l_int32 *tab8 );
|
||||
PIX * pixAnd ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
|
||||
l_int32 * makePixelSumTab8 ( void );
|
||||
|
||||
PIX * pixDeserializeFromMemory ( const l_uint32 *data, size_t nbytes );
|
||||
l_int32 pixSerializeToMemory ( PIX *pixs, l_uint32 **pdata, size_t *pnbytes );
|
||||
|
||||
l_int32
|
||||
pixOtsuAdaptiveThreshold(PIX *pixs,
|
||||
l_int32 sx,
|
||||
l_int32 sy,
|
||||
l_int32 smoothx,
|
||||
l_int32 smoothy,
|
||||
l_float32 scorefract,
|
||||
PIX **ppixth,
|
||||
PIX **ppixd);
|
||||
|
||||
void lept_free(void *ptr);
|
||||
""")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ffi.compile()
|
||||
|
||||
+127
-35
@@ -188,6 +188,10 @@ advanced.add_argument(
|
||||
'--tesseract-timeout', default=180.0, type=float, metavar='SECONDS',
|
||||
help='give up on OCR after the timeout, but copy the preprocessed page '
|
||||
'into the final output')
|
||||
advanced.add_argument(
|
||||
'--rotate-pages-threshold', default=14.0, type=float, metavar='CONFIDENCE',
|
||||
help="only rotate pages when confidence is above this value (arbitrary "
|
||||
"units reported by tesseract)")
|
||||
|
||||
debugging = parser.add_argument_group(
|
||||
"Debugging",
|
||||
@@ -234,13 +238,18 @@ if options.pdf_renderer == 'tesseract' and tesseract.version() < '3.04.01' \
|
||||
"Some PDF viewers will fail to find searchable text.\n"
|
||||
"--pdf-renderer=tesseract is not recommended.")
|
||||
|
||||
if any((options.deskew, options.clean, options.clean_final)):
|
||||
if any((options.clean, options.clean_final)):
|
||||
try:
|
||||
from . import unpaper
|
||||
except ImportError:
|
||||
if unpaper.version() < '6.1':
|
||||
complain(
|
||||
"The installed 'unpaper' is not supported. "
|
||||
"Install version 6.1 or newer.")
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
except FileNotFoundError:
|
||||
complain(
|
||||
"Install the 'unpaper' program to use --deskew or --clean.")
|
||||
sys.exit(ExitCode.bad_args)
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
else:
|
||||
unpaper = None
|
||||
|
||||
@@ -378,7 +387,7 @@ def cleanup_working_files(*args):
|
||||
@transform(
|
||||
input=options.input_file,
|
||||
filter=formatter('(?i)\.pdf'),
|
||||
output=work_folder + '{basename[0]}.repaired.pdf',
|
||||
output=os.path.join(work_folder, '{basename[0]}.repaired.pdf'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def repair_pdf(
|
||||
input_file,
|
||||
@@ -524,32 +533,52 @@ def orient_page(
|
||||
|
||||
direction = {
|
||||
0: '⇧',
|
||||
90: '⇦',
|
||||
90: '⇨',
|
||||
180: '⇩',
|
||||
270: '⇨'
|
||||
270: '⇦'
|
||||
}
|
||||
|
||||
apply_correction = False
|
||||
description = ''
|
||||
if orient_conf.confidence >= options.rotate_pages_threshold:
|
||||
if orient_conf.angle != 0:
|
||||
apply_correction = True
|
||||
description = ' - will rotate'
|
||||
else:
|
||||
description = ' - rotation appears correct'
|
||||
else:
|
||||
if orient_conf.angle != 0:
|
||||
description = ' - confidence too low to rotate'
|
||||
else:
|
||||
description = ' - no change'
|
||||
|
||||
log.info(
|
||||
'{0:4d}: page is facing {1}, confidence {2:.2f}{3}'.format(
|
||||
page_number(preview),
|
||||
direction.get(orient_conf.angle, '?'),
|
||||
orient_conf.confidence,
|
||||
' - correcting rotation' if orient_conf.angle != 0 else '')
|
||||
description)
|
||||
)
|
||||
|
||||
if orient_conf.angle == 0:
|
||||
if not apply_correction:
|
||||
re_symlink(page_pdf, output_file)
|
||||
else:
|
||||
writer = pypdf.PdfFileWriter()
|
||||
reader = pypdf.PdfFileReader(page_pdf)
|
||||
page = reader.pages[0]
|
||||
|
||||
# Rotate opposite of orientation
|
||||
rotated_page = page.rotateClockwise(orient_conf.angle)
|
||||
# angle is a clockwise angle, so rotating ccw will correct the error
|
||||
rotated_page = page.rotateCounterClockwise(orient_conf.angle)
|
||||
writer.addPage(rotated_page)
|
||||
with open(output_file, 'wb') as out:
|
||||
writer.write(out)
|
||||
|
||||
with pdfinfo_lock:
|
||||
pageno = int(os.path.basename(page_pdf)[0:6]) - 1
|
||||
pageinfo = pdfinfo[pageno].copy()
|
||||
pageinfo['rotated'] = orient_conf.angle
|
||||
pdfinfo[pageno] = pageinfo
|
||||
|
||||
|
||||
@transform(
|
||||
input=orient_page,
|
||||
@@ -788,11 +817,12 @@ def add_text_layer(
|
||||
|
||||
page_text = pdf_text.getPage(0)
|
||||
|
||||
# The text page always will be oriented up
|
||||
# The text page always will be oriented up by this stage
|
||||
# but if lossless_reconstruction, pdf_image may have a rotation applied
|
||||
# we can't just merge the pages, because a page can only have one /Rotate
|
||||
# tag, so the differential rotation must be corrected.
|
||||
# Also, pdf_image may not have its mediabox nailed to (0, 0)
|
||||
# We have to eliminate the /Rotate tag (because it applies to the whole
|
||||
# page) and rotate the image layer to match the text page
|
||||
# Also, pdf_image may not have its mediabox nailed to (0, 0), so may need
|
||||
# translation
|
||||
page_image = pdf_image.getPage(0)
|
||||
rotation = page_image.get('/Rotate', 0)
|
||||
|
||||
@@ -1003,42 +1033,104 @@ def cleanup_ruffus_error_message(msg):
|
||||
return msg
|
||||
|
||||
|
||||
def do_ruffus_exception(ruffus_five_tuple):
|
||||
"""Replace the elaborate ruffus stack trace with a user friendly
|
||||
description of the error message that occurred."""
|
||||
|
||||
task_name, job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
|
||||
if exc_name == 'builtins.SystemExit':
|
||||
match = re.search(r"\.(.+?)\)", exc_value)
|
||||
exit_code_name = match.groups()[0]
|
||||
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
|
||||
return exit_code
|
||||
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
|
||||
_log.error(cleanup_ruffus_error_message(exc_value))
|
||||
return ExitCode.input_file
|
||||
elif exc_name == 'builtins.TypeError':
|
||||
# Even though repair_pdf will fail, ruffus will still try
|
||||
# to call split_pages with no input files, likely due to a bug
|
||||
if task_name == 'split_pages':
|
||||
_log.error("Input file '{0}' is not a valid PDF".format(
|
||||
options.input_file))
|
||||
return ExitCode.input_file
|
||||
elif exc_name == 'subprocess.CalledProcessError':
|
||||
# It's up to the subprocess handler to report something useful
|
||||
msg = "Error occurred while running this command:"
|
||||
_log.error(msg + '\n' + exc_value)
|
||||
return ExitCode.child_process_error
|
||||
elif not options.verbose:
|
||||
_log.error(exc_stack)
|
||||
return ExitCode.other_error
|
||||
|
||||
|
||||
def traverse_ruffus_exception(e):
|
||||
"""Walk through a RethrownJobError and find the first exception.
|
||||
|
||||
The exit code will be based on this, even if multiple exceptions occurred
|
||||
at the same time."""
|
||||
|
||||
if isinstance(e[0], str) and len(e) == 5:
|
||||
return do_ruffus_exception(e)
|
||||
elif hasattr(e, '__iter__'):
|
||||
for exc in e:
|
||||
return traverse_ruffus_exception(exc)
|
||||
|
||||
|
||||
def run_pipeline():
|
||||
if not options.jobs:
|
||||
options.jobs = available_cpu_count()
|
||||
try:
|
||||
options.history_file = os.path.join(work_folder, 'ruffus_history.sqlite')
|
||||
options.history_file = os.path.join(
|
||||
work_folder, 'ruffus_history.sqlite')
|
||||
cmdline.run(options)
|
||||
except ruffus_exceptions.RethrownJobError as e:
|
||||
if options.verbose:
|
||||
print(e)
|
||||
_log.debug(str(e)) # stringify exception so logger doesn't have to
|
||||
|
||||
# Yuck. Hunt through the ruffus exception to find out what the
|
||||
# return code is supposed to be.
|
||||
for exc in e.args:
|
||||
task_name, job_name, exc_name, exc_value, exc_stack = exc
|
||||
if exc_name == 'builtins.SystemExit':
|
||||
match = re.search(r"\.(.+?)\)", exc_value)
|
||||
exit_code_name = match.groups()[0]
|
||||
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
|
||||
return exit_code
|
||||
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
|
||||
print(cleanup_ruffus_error_message(exc_value))
|
||||
return ExitCode.input_file
|
||||
elif exc_name == 'builtins.TypeError':
|
||||
# Even though repair_pdf will fail, ruffus will still try
|
||||
# to call split_pages with no input files, likely due to a bug
|
||||
if task_name == 'split_pages':
|
||||
print("Input file '{0}' is not a valid PDF".format(
|
||||
options.input_file))
|
||||
return ExitCode.input_file
|
||||
# Ruffus flattens exception to 5 element tuples. Because of a bug
|
||||
# in <= 2.6.3 it may present either the single:
|
||||
# (task, job, exc, value, stack)
|
||||
# or something like:
|
||||
# [[(task, job, exc, value, stack)]]
|
||||
#
|
||||
# Generally cross-process exception marshalling doesn't work well
|
||||
# and ruffus doesn't support because BaseException has its own
|
||||
# implementation of __reduce__ that attempts to reconstruct the
|
||||
# exception based on e.__init__(e.args).
|
||||
#
|
||||
# Attempting to log the exception directly marshalls it to the logger
|
||||
# which is probably in another process, so it's better to log only
|
||||
# data from the exception at this point.
|
||||
|
||||
exitcode = traverse_ruffus_exception(e.args)
|
||||
if exitcode is None:
|
||||
_log.error("Unexpected ruffus exception: " + str(e))
|
||||
_log.error(repr(e))
|
||||
return ExitCode.other_error
|
||||
else:
|
||||
return exitcode
|
||||
except Exception as e:
|
||||
_log.error(e)
|
||||
return ExitCode.other_error
|
||||
|
||||
if not validate_pdfa(options.output_file, _log):
|
||||
_log.warning('Output file: The generated PDF/A file is INVALID')
|
||||
return ExitCode.invalid_output_pdfa
|
||||
|
||||
with _pdfinfo_lock:
|
||||
_log.debug(_pdfinfo)
|
||||
direction = {0: 'n', 90: 'e',
|
||||
180: 's', 270: 'w'}
|
||||
orientations = []
|
||||
for n, page in enumerate(_pdfinfo):
|
||||
angle = _pdfinfo[n].get('rotated', 0)
|
||||
if angle != 0:
|
||||
orientations.append('{0}{1}'.format(
|
||||
n + 1,
|
||||
direction.get(angle, '')))
|
||||
if orientations:
|
||||
_log.info('Page orientations detected: ' + ' '.join(orientations))
|
||||
|
||||
return ExitCode.ok
|
||||
|
||||
|
||||
|
||||
+205
-36
@@ -3,10 +3,13 @@
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from decimal import Decimal, getcontext
|
||||
from math import hypot
|
||||
import re
|
||||
import sys
|
||||
import PyPDF2 as pypdf
|
||||
from collections import namedtuple
|
||||
|
||||
matrix_mult = pypdf.pdf.utils.matrixMultiply
|
||||
|
||||
FRIENDLY_COLORSPACE = {
|
||||
'/DeviceGray': 'gray',
|
||||
@@ -19,7 +22,11 @@ FRIENDLY_COLORSPACE = {
|
||||
'/Indexed': 'index',
|
||||
'/Separation': 'sep',
|
||||
'/DeviceN': 'devn',
|
||||
'/Pattern': '-'
|
||||
'/Pattern': '-',
|
||||
'/G': 'gray', # Abbreviations permitted in inline images
|
||||
'/RGB': 'rgb',
|
||||
'/CMYK': 'cmyk',
|
||||
'/I': 'index',
|
||||
}
|
||||
|
||||
FRIENDLY_ENCODING = {
|
||||
@@ -27,6 +34,13 @@ FRIENDLY_ENCODING = {
|
||||
'/DCTDecode': 'jpeg',
|
||||
'/JPXDecode': 'jpx',
|
||||
'/JBIG2Decode': 'jbig2',
|
||||
'/CCF': 'ccitt', # Abbreviations permitted in inline images
|
||||
'/DCT': 'jpeg',
|
||||
'/AHx': 'asciihex',
|
||||
'/A85': 'ascii85',
|
||||
'/LZW': 'lzw',
|
||||
'/Fl': 'flate',
|
||||
'/RL': 'runlength'
|
||||
}
|
||||
|
||||
FRIENDLY_COMP = {
|
||||
@@ -38,47 +52,165 @@ FRIENDLY_COMP = {
|
||||
}
|
||||
|
||||
|
||||
def _page_has_inline_images(page):
|
||||
# PDF always uses \r\n for separator regardless of platform
|
||||
# Really basic heuristic that might trigger the odd false positive
|
||||
# This is only finds the first image and is not quite spec compliant
|
||||
try:
|
||||
contents = page.getContents()
|
||||
data = contents.getData()
|
||||
except AttributeError:
|
||||
# If we can't access the contents or data (empty page?) then there
|
||||
# are no inline images
|
||||
return False
|
||||
def _matrix_from_shorthand(shorthand):
|
||||
"""Convert from PDF matrix shorthand to full matrix
|
||||
|
||||
begin_image, image_data, end_image = False, False, False
|
||||
for data in re.split(b'\s+', data):
|
||||
if data == b'BI':
|
||||
begin_image = True
|
||||
elif data == b'ID':
|
||||
image_data = True
|
||||
elif data == b'EI':
|
||||
end_image = True
|
||||
if all((begin_image, image_data, end_image)):
|
||||
return True
|
||||
return False
|
||||
PDF 1.7 spec defines a shorthand for describing the entries of a matrix
|
||||
since the last column is always (0, 0, 1).
|
||||
"""
|
||||
|
||||
a, b, c, d, e, f = map(float, shorthand)
|
||||
return ((a, b, 0),
|
||||
(c, d, 0),
|
||||
(e, f, 1))
|
||||
|
||||
|
||||
def _find_page_images(page, pageinfo):
|
||||
def _shorthand_from_matrix(matrix):
|
||||
"""Convert from transformation matrix to PDF shorthand."""
|
||||
a, b = matrix[0][0], matrix[0][1]
|
||||
c, d = matrix[1][0], matrix[1][1]
|
||||
e, f = matrix[2][0], matrix[2][1]
|
||||
return tuple(map(float, (a, b, c, d, e, f)))
|
||||
|
||||
|
||||
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
|
||||
|
||||
|
||||
def _interpret_contents(contentstream):
|
||||
operations = contentstream.operations
|
||||
stack = []
|
||||
ctm = _matrix_from_shorthand((1, 0, 0, 1, 0, 0))
|
||||
image_raster_settings = []
|
||||
inline_images = []
|
||||
|
||||
for op in operations:
|
||||
operands, command = op
|
||||
if command == b'q':
|
||||
stack.append(ctm)
|
||||
if len(stack) > 32:
|
||||
raise RuntimeError("PDF graphics stack overflow")
|
||||
elif command == b'Q':
|
||||
ctm = stack.pop()
|
||||
elif command == b'cm':
|
||||
ctm = matrix_mult(
|
||||
ctm, _matrix_from_shorthand(operands))
|
||||
elif command == b'Do':
|
||||
image_name = operands[0]
|
||||
image_raster_settings.append(
|
||||
(image_name, _shorthand_from_matrix(ctm)))
|
||||
elif command == b'INLINE IMAGE':
|
||||
settings = operands['settings']
|
||||
inline_images.append(
|
||||
(settings, _shorthand_from_matrix(ctm)))
|
||||
|
||||
return ContentsInfo(
|
||||
raster_settings=image_raster_settings,
|
||||
inline_images=inline_images)
|
||||
|
||||
|
||||
def _get_dpi(ctm_shorthand, image_size):
|
||||
"""Given the transformation matrix and image size, find the image DPI.
|
||||
|
||||
PDFs do not include image resolution information within image data.
|
||||
Instead, the PDF page content stream describes the location where the
|
||||
image will be rasterized, and the effective resolution is the ratio of the
|
||||
pixel size to raster target size.
|
||||
|
||||
Normally a scanned PDF has the paper size set appropriately but this is
|
||||
not guaranteed. The most common case is a cropped image will change the
|
||||
page size (/CropBox) without altering the page content stream. That means
|
||||
it is not sufficient to assume that the image fills the page, even though
|
||||
that is the most common case.
|
||||
|
||||
A PDF image may be scaled (always), cropped, translated, rotated in place
|
||||
to an arbitrary angle (rarely) and skewed. Only equal area mappings can
|
||||
be expressed, that is, it is not necessary to consider distortions where
|
||||
the effective DPI varies with position.
|
||||
|
||||
To determine the image scale, transform an offset axis vector v0 (0, 0),
|
||||
width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix,
|
||||
which gives the dimensions of the image in PDF units. From there we can
|
||||
compare to actual image dimensions. PDF uses
|
||||
row vector * matrix_tranposed unlike the traditional
|
||||
matrix * column vector.
|
||||
|
||||
The offset, width and height vectors can be combined in a matrix and
|
||||
multiplied by the transform matrix. Then we want to calculated
|
||||
magnitude(width_vector - offset_vector)
|
||||
and
|
||||
magnitude(height_vector - offset_vector)
|
||||
|
||||
When the above is worked out algebraically, the effect of translation
|
||||
cancels out, and the vector magnitudes become functions of the nonzero
|
||||
transformation matrix indices. The results of the derivation are used
|
||||
in this code.
|
||||
|
||||
pdfimages -list does calculate the DPI in some way that is not completely
|
||||
naive, but it does not get the DPI of rotated images right, so cannot be
|
||||
used anymore to validate this. Photoshop works, or using Acrobat to
|
||||
rotate the image back to normal.
|
||||
|
||||
It does not matter if the image is partially cropped, or even out of the
|
||||
/MediaBox.
|
||||
|
||||
"""
|
||||
|
||||
a, b, c, d, _, _ = ctm_shorthand
|
||||
|
||||
# Calculate the width and height of the image in PDF units
|
||||
image_drawn_width = hypot(a, b)
|
||||
image_drawn_height = hypot(c, d)
|
||||
|
||||
# The scale of the image is pixels per PDF unit (1/72")
|
||||
scale_w = image_size[0] / image_drawn_width
|
||||
scale_h = image_size[1] / image_drawn_height
|
||||
|
||||
# DPI = scale * 72
|
||||
dpi_w = scale_w * 72.0
|
||||
dpi_h = scale_h * 72.0
|
||||
|
||||
return (dpi_w, dpi_h)
|
||||
|
||||
|
||||
def _find_page_inline_images(page, pageinfo, contentsinfo):
|
||||
"Find inline images on the page"
|
||||
|
||||
for n, im in enumerate(contentsinfo.inline_images):
|
||||
settings, shorthand = im
|
||||
image = {}
|
||||
image['name'] = str('inline-%02d' % n)
|
||||
image['width'] = settings['/W']
|
||||
image['height'] = settings['/H']
|
||||
image['bpc'] = settings['/BPC']
|
||||
image['color'] = FRIENDLY_COLORSPACE.get(settings['/CS'], '-')
|
||||
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
||||
if '/F' in settings:
|
||||
filter_ = settings['/F']
|
||||
if isinstance(filter_, pypdf.generic.ArrayObject):
|
||||
filter_ = filter_[0]
|
||||
image['enc'] = FRIENDLY_ENCODING.get(filter_, 'image')
|
||||
else:
|
||||
image['enc'] = 'image'
|
||||
|
||||
dpi_w, dpi_h = _get_dpi(shorthand, (image['width'], image['height']))
|
||||
image['dpi_w'], image['dpi_h'] = Decimal(dpi_w), Decimal(dpi_h)
|
||||
yield image
|
||||
|
||||
|
||||
def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||
"Find images stored in XObject resources"
|
||||
|
||||
try:
|
||||
page['/Resources']['/XObject']
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
# Look for XObject (out of line images)
|
||||
for xobj in page['/Resources']['/XObject']:
|
||||
# PyPDF2 returns the keys as an iterator
|
||||
pdfimage = page['/Resources']['/XObject'][xobj]
|
||||
if pdfimage['/Subtype'] != '/Image':
|
||||
continue
|
||||
if '/ImageMask' in pdfimage:
|
||||
if pdfimage['/ImageMask']:
|
||||
continue
|
||||
image = {}
|
||||
image['name'] = str(xobj)
|
||||
image['width'] = pdfimage['/Width']
|
||||
image['height'] = pdfimage['/Height']
|
||||
image['bpc'] = pdfimage['/BitsPerComponent']
|
||||
@@ -98,12 +230,33 @@ def _find_page_images(page, pageinfo):
|
||||
image['color'] = 'jpx' if image['enc'] == 'jpx' else '?'
|
||||
|
||||
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
||||
image['dpi_w'] = image['width'] / pageinfo['width_inches']
|
||||
image['dpi_h'] = image['height'] / pageinfo['height_inches']
|
||||
image['dpi_w'] = image['dpi_h'] = 0
|
||||
|
||||
for raster in contentsinfo.raster_settings:
|
||||
# Loop in case the same image is display multiple times on a page
|
||||
if raster[0] != image['name']:
|
||||
continue
|
||||
shorthand = raster[1]
|
||||
|
||||
dpi_w, dpi_h = _get_dpi(
|
||||
shorthand, (image['width'], image['height']))
|
||||
|
||||
# When image is used multiple times take the highest DPI it is
|
||||
# rendered at
|
||||
image['dpi_w'] = max(dpi_w, image.get('dpi_w', 0))
|
||||
image['dpi_h'] = max(dpi_h, image.get('dpi_h', 0))
|
||||
|
||||
image['dpi_w'] = Decimal(image['dpi_w'])
|
||||
image['dpi_h'] = Decimal(image['dpi_h'])
|
||||
image['dpi'] = (image['dpi_w'] * image['dpi_h']) ** Decimal(0.5)
|
||||
yield image
|
||||
|
||||
|
||||
def _find_page_images(page, pageinfo, contentsinfo):
|
||||
yield from _find_page_inline_images(page, pageinfo, contentsinfo)
|
||||
yield from _find_page_regular_images(page, pageinfo, contentsinfo)
|
||||
|
||||
|
||||
def _page_has_text(pdf, page):
|
||||
# Simple test
|
||||
text = page.extractText()
|
||||
@@ -141,12 +294,14 @@ def _pdf_get_pageinfo(infile, pageno: int):
|
||||
pageinfo['width_inches'] = width_pt / Decimal(72.0)
|
||||
pageinfo['height_inches'] = height_pt / Decimal(72.0)
|
||||
|
||||
pageinfo['images'] = [im for im in _find_page_images(page, pageinfo)]
|
||||
try:
|
||||
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
|
||||
except AttributeError as e:
|
||||
return pageinfo
|
||||
|
||||
# Look for inline images
|
||||
if _page_has_inline_images(page):
|
||||
raise NotImplementedError(
|
||||
"Warning: input PDF contains inline images - not supported")
|
||||
contentsinfo = _interpret_contents(contentstream)
|
||||
pageinfo['images'] = [im for im in _find_page_images(
|
||||
page, pageinfo, contentsinfo)]
|
||||
|
||||
if pageinfo['images']:
|
||||
xres = max(image['dpi_w'] for image in pageinfo['images'])
|
||||
@@ -164,3 +319,17 @@ def pdf_get_all_pageinfo(infile):
|
||||
pdf = pypdf.PdfFileReader(infile)
|
||||
getcontext().prec = 6
|
||||
return [_pdf_get_pageinfo(infile, n) for n in range(pdf.numPages)]
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('infile')
|
||||
args = parser.parse_args()
|
||||
info = pdf_get_all_pageinfo(args.infile)
|
||||
from pprint import pprint
|
||||
pprint(info)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
+7
-33
@@ -5,10 +5,13 @@
|
||||
|
||||
from __future__ import print_function, absolute_import, division
|
||||
from string import Template
|
||||
from subprocess import Popen, PIPE
|
||||
import os
|
||||
import codecs
|
||||
from . import get_program
|
||||
import pkg_resources
|
||||
|
||||
ICC_PROFILE_RELPATH = 'data/sRGB_IEC61966-2-1_black_scaled.icc'
|
||||
|
||||
SRGB_ICC_PROFILE = pkg_resources.resource_filename(
|
||||
'ocrmypdf', ICC_PROFILE_RELPATH)
|
||||
|
||||
|
||||
# This is a template written in PostScript which is needed to create PDF/A
|
||||
@@ -93,38 +96,9 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
|
||||
return result
|
||||
|
||||
|
||||
def _get_postscript_icc_path():
|
||||
"Parse Ghostscript's help message to find where iccprofiles are stored"
|
||||
|
||||
p_gs = Popen([get_program('gs'), '--help'], close_fds=True,
|
||||
universal_newlines=True,
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
out, _ = p_gs.communicate()
|
||||
lines = out.splitlines()
|
||||
|
||||
def search_paths(lines):
|
||||
seeking = True
|
||||
for line in lines:
|
||||
if seeking:
|
||||
if line.startswith('Search path'):
|
||||
seeking = False
|
||||
continue
|
||||
else:
|
||||
if line.strip().startswith('/'):
|
||||
yield from (
|
||||
path.strip() for path in line.split(':')
|
||||
if path.strip() != '')
|
||||
for root in search_paths(lines):
|
||||
path = os.path.realpath(os.path.join(root, '../iccprofiles'))
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
raise FileNotFoundError("Could not find Ghostscript's iccprofiles")
|
||||
|
||||
|
||||
def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'):
|
||||
if icc == 'sRGB':
|
||||
icc_profile = os.path.join(_get_postscript_icc_path(), 'srgb.icc')
|
||||
icc_profile = SRGB_ICC_PROFILE
|
||||
else:
|
||||
raise NotImplementedError("Only supporting sRGB")
|
||||
|
||||
|
||||
+59
-25
@@ -90,14 +90,18 @@ def get_orientation(input_file, language: list, timeout: float, log):
|
||||
'stdout'
|
||||
]
|
||||
|
||||
p = Popen(args_tesseract, close_fds=True, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
try:
|
||||
stdout, _ = p.communicate(timeout=timeout)
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
p.kill()
|
||||
stdout, _ = p.communicate()
|
||||
return OrientationConfidence(angle=0, confidence=0.0)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_file)
|
||||
if ('Too few characters. Skipping this page' in e.output or
|
||||
'Image too large' in e.output):
|
||||
return OrientationConfidence(0, 0)
|
||||
raise e from e
|
||||
else:
|
||||
osd = {}
|
||||
for line in stdout.splitlines():
|
||||
@@ -106,9 +110,21 @@ def get_orientation(input_file, language: list, timeout: float, log):
|
||||
if len(parts) == 2:
|
||||
osd[parts[0].strip()] = parts[1].strip()
|
||||
|
||||
angle = int(osd.get('Orientation in degrees', 0))
|
||||
if 'Orientation' in osd:
|
||||
# Tesseract < 3.04.01
|
||||
# reports "Orientation in degrees" as a counterclockwise angle
|
||||
# We keep it clockwise
|
||||
assert 'Rotate' not in osd
|
||||
angle = -angle % 360
|
||||
else:
|
||||
# Tesseract == 3.04.01, hopefully also Tesseract > 3.04.01
|
||||
# reports "Orientation in degrees" as a clockwise angle
|
||||
assert 'Rotate' in osd
|
||||
|
||||
oc = OrientationConfidence(
|
||||
angle=int(osd['Orientation in degrees']),
|
||||
confidence=float(osd['Orientation confidence']))
|
||||
angle=angle,
|
||||
confidence=float(osd.get('Orientation confidence', 0)))
|
||||
return oc
|
||||
|
||||
|
||||
@@ -124,10 +140,24 @@ def tesseract_log_output(log, stdout, input_file):
|
||||
log.warning(prefix + "lots of diacritics - possibly poor OCR")
|
||||
elif line.startswith('OSD: Weak margin'):
|
||||
log.warning(prefix + "unsure about page orientation")
|
||||
elif 'error' in line.lower() or 'exception' in line.lower():
|
||||
log.error(prefix + line.strip())
|
||||
else:
|
||||
log.info(prefix + line.strip())
|
||||
|
||||
|
||||
def page_timedout(log, input_file):
|
||||
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
|
||||
log.warning(prefix + " took too long to OCR - skipping")
|
||||
|
||||
|
||||
def _generate_null_hocr(output_hocr, pageinfo):
|
||||
with open(output_hocr, 'w', encoding="utf-8") as f:
|
||||
f.write(HOCR_TEMPLATE.format(
|
||||
pageinfo['width_pixels'],
|
||||
pageinfo['height_pixels']))
|
||||
|
||||
|
||||
def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
|
||||
timeout: float, pageinfo_getter, pagesegmode: int, log):
|
||||
|
||||
@@ -146,26 +176,25 @@ def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
|
||||
badxml,
|
||||
'hocr'
|
||||
] + tessconfig)
|
||||
p = Popen(args_tesseract, close_fds=True, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
try:
|
||||
stdout, _ = p.communicate(timeout=timeout)
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
p.kill()
|
||||
stdout, _ = p.communicate()
|
||||
# Generate a HOCR file with no recognized text if tesseract times out
|
||||
# Temporary workaround to hocrTransform not being able to function if
|
||||
# it does not have a valid hOCR file.
|
||||
with open(output_hocr, 'w', encoding="utf-8") as f:
|
||||
pageinfo = pageinfo_getter()
|
||||
f.write(HOCR_TEMPLATE.format(
|
||||
pageinfo['width_pixels'],
|
||||
pageinfo['height_pixels']))
|
||||
page_timedout(log, input_file)
|
||||
_generate_null_hocr(output_hocr, pageinfo_getter())
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_file)
|
||||
if 'Image too large' in e.output:
|
||||
_generate_null_hocr(output_hocr, pageinfo_getter())
|
||||
return
|
||||
|
||||
raise e from e
|
||||
else:
|
||||
tesseract_log_output(log, stdout, input_file)
|
||||
if p.returncode != 0:
|
||||
raise CalledProcessError(p.returncode, args_tesseract)
|
||||
|
||||
if os.path.exists(badxml + '.html'):
|
||||
# Tesseract 3.02 appends suffix ".html" on its own (.badxml.html)
|
||||
shutil.move(badxml + '.html', badxml)
|
||||
@@ -213,14 +242,19 @@ def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
|
||||
os.path.splitext(output_pdf)[0], # Tesseract appends suffix
|
||||
'pdf'
|
||||
] + tessconfig)
|
||||
p = Popen(args_tesseract, close_fds=True, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
|
||||
try:
|
||||
stdout, _ = p.communicate()
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
p.kill()
|
||||
log.info("Tesseract - page timed out")
|
||||
page_timedout(log, input_image)
|
||||
shutil.copy(skip_pdf, output_pdf)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_image)
|
||||
if 'Image too large' in e.output:
|
||||
shutil.copy(skip_pdf, output_pdf)
|
||||
return
|
||||
raise e from e
|
||||
else:
|
||||
tesseract_log_output(log, stdout, input_image)
|
||||
|
||||
+15
-14
@@ -3,7 +3,7 @@
|
||||
# unpaper documentation:
|
||||
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import CalledProcessError, STDOUT, check_output, check_call
|
||||
from tempfile import NamedTemporaryFile
|
||||
import sys
|
||||
import os
|
||||
@@ -17,10 +17,9 @@ def version():
|
||||
get_program('unpaper'),
|
||||
'--version'
|
||||
]
|
||||
p_unpaper = Popen(args_unpaper, close_fds=True, universal_newlines=True,
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
version, _ = p_unpaper.communicate(timeout=5)
|
||||
|
||||
version = check_output(
|
||||
args_unpaper, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT, timeout=5)
|
||||
return version.strip()
|
||||
|
||||
|
||||
@@ -68,15 +67,17 @@ def run(input_file, output_file, dpi, log, mode_args):
|
||||
os.unlink(output_pnm.name)
|
||||
|
||||
args_unpaper.extend([input_pnm.name, output_pnm.name])
|
||||
p_unpaper = Popen(
|
||||
args_unpaper, close_fds=True,
|
||||
universal_newlines=True, stdout=PIPE, stderr=PIPE
|
||||
)
|
||||
out, err = p_unpaper.communicate()
|
||||
log.debug(out)
|
||||
log.debug(err)
|
||||
|
||||
Image.open(output_pnm.name).save(output_file)
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_unpaper, close_fds=True,
|
||||
universal_newlines=True, stderr=STDOUT,
|
||||
)
|
||||
except CalledProcessError as e:
|
||||
log.debug(e.output)
|
||||
raise e from e
|
||||
else:
|
||||
log.debug(stdout)
|
||||
Image.open(output_pnm.name).save(output_file)
|
||||
|
||||
|
||||
def deskew(input_file, output_file, dpi, log):
|
||||
|
||||
@@ -209,7 +209,6 @@ setup(
|
||||
],
|
||||
setup_requires=[
|
||||
'setuptools_scm',
|
||||
'setuptools_scm_git_archive',
|
||||
'cffi>=1.5.0',
|
||||
'pytest-runner'
|
||||
],
|
||||
@@ -218,7 +217,7 @@ setup(
|
||||
'ocrmypdf/lib/compile_leptonica.py:ffi'
|
||||
],
|
||||
install_requires=[
|
||||
'ruffus>=2.6.3',
|
||||
'ruffus==2.6.3',
|
||||
'Pillow>=3.0.0',
|
||||
'reportlab>=3.1.44',
|
||||
'PyPDF2>=1.25.1',
|
||||
@@ -231,5 +230,6 @@ setup(
|
||||
'ocrmypdf = ocrmypdf.main:run_pipeline'
|
||||
],
|
||||
},
|
||||
package_data={'ocrmypdf': ['data/sRGB_IEC61966-2-1_black_scaled.icc']},
|
||||
include_package_data=True,
|
||||
zip_safe=False)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
deb http://httpredir.debian.org/debian stretch main
|
||||
deb http://httpredir.debian.org/debian stretch-updates main
|
||||
deb http://security.debian.org stretch/updates main
|
||||
deb http://ftp.de.debian.org/debian sid main contrib non-free
|
||||
@@ -18,9 +18,12 @@ In some cases they were converted from one image format to another without other
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| graph.pdf | `Wikimedia: Pandas text analysis.png`_ |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| LinnSequencer.jpg | `Wikimedia: LinnSequencer`_ (Creative Commons Attribution-ShareAlike 3.0) |
|
||||
| lichtenstein.pdf | `Wikimedia: JPEG2000 Lichtenstein`_ (Creative Commons BY-SA 3.0) |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
|
||||
| LinnSequencer.jpg, | `Wikimedia: LinnSequencer`_ (Creative Commons Attribution-ShareAlike 3.0) |
|
||||
| linn.pdf, linn.txt | |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
|
||||
|
||||
Files generated for this project
|
||||
================================
|
||||
@@ -32,6 +35,7 @@ under the terms of the license in LICENSE.rst.
|
||||
- cmyk.pdf (a CMYK image created in Photoshop)
|
||||
- enormous.pdf (a very lage page)
|
||||
- francais.pdf (a page containing French accented characters)
|
||||
- hugemono.pdf (large monochrome JBIG2 page with pixel dimensions of 35000x35000)
|
||||
- invalid.pdf (a PDF file header followed by EOF marker)
|
||||
- missing_docinfo.pdf (PDF file with no /DocumentInfo section)
|
||||
|
||||
@@ -57,4 +61,6 @@ These test resources are assemblies from other previously mentioned files, relea
|
||||
|
||||
.. _`US Congressional Records`: http://www.baxleystamps.com/litho/meiji/courts_1871.jpg
|
||||
|
||||
.. _`Wikimedia: Pandas text analysis.png`: https://en.wikipedia.org/wiki/File:Pandas_text_analysis.png
|
||||
.. _`Wikimedia: Pandas text analysis.png`: https://en.wikipedia.org/wiki/File:Pandas_text_analysis.png
|
||||
|
||||
.. _`Wikimedia: JPEG2000 Lichtenstein`: https://en.wikipedia.org/wiki/JPEG_2000#/media/File:Jpeg2000_2-level_wavelet_transform-lichtenstein.png
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,123 @@
|
||||
The LinnSequencer
|
||||
32 Track MIDI Sequence Recorder
|
||||
|
||||
The LinnSequencer is a state—of—the-art composition and performance tool for the professional musician. It is
|
||||
|
||||
extremely powerful, yet amazingly simple to learn and use. It’s many remarkable features include:
|
||||
|
||||
0 Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
|
||||
FORWARD, REWIND, and LOCATE controls.
|
||||
|
||||
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
|
||||
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
|
||||
|
||||
synthesizers !
|
||||
|
||||
0 Ultra-fast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
|
||||
|
||||
per disk!
|
||||
|
||||
0 One or all tracks may be TRANSPOSED at the touch of a key.
|
||||
0 Exclusive real—time ERASE function makes editing FAST.
|
||||
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
|
||||
|
||||
rhythmic value.
|
||||
|
||||
0 TIMING CORRECTION works during playback and operates without ‘chopping’ notes.
|
||||
|
||||
0 Optional SMPTE time code synchronization.
|
||||
|
||||
0 Optional remote control.
|
||||
|
||||
Recording a Sequence
|
||||
|
||||
To record a sequence, simply press RECORD and PLAY,
|
||||
then play your MIDI keyboard in time to the Sequencer’s
|
||||
click track. When the sequence loops back around to bar 1,
|
||||
you’ll hear what you played—only all timing errors will be
|
||||
|
||||
corrected! (Timing correction may be adjusted 0r defeated).
|
||||
|
||||
Any additional notes played will be added into the track
|
||||
—existing notes are not erased while recording!
|
||||
|
||||
FAST FORWARD, REWIND, and LOCATE controls
|
||||
may be used at any time to quickly access any location in
|
||||
your sequence for spot-recording. To overdub a new part,
|
||||
select a different track and start recording—while you
|
||||
record, the first‘track will play in perfect sync (unless you
|
||||
MUTE it, or SOLO another track). In this way, up to 32
|
||||
tracks may be overdubbed! All MIDI effects are recorded
|
||||
including pitch bend, modulation, velocity, aftertouch,
|
||||
sustain pedal, and program changes!
|
||||
|
||||
Editing
|
||||
|
||||
To erase a wrong note, simply hold ERASE and press
|
||||
the note to be erased just before it plays in the sequence-—
|
||||
when played back, it will be gone. Notes may also be
|
||||
|
||||
added, erased, or changed using the SINGLE STEP func-
|
||||
tion. To overdub notes at specific points within a sequence,
|
||||
|
||||
Additional Features
|
||||
|
||||
simply use LOCATE, FAST FORWARD, or REWIND to
|
||||
find the desired bar number, then start recording.
|
||||
|
||||
The INSERT/ COPY function allows you to move bars
|
||||
from one location to another—in the same sequence or a
|
||||
different one. For example, you might insert a copy of the
|
||||
first verse between the second chorus and the bridge.
|
||||
DELETE BARS operates the same way to remove
|
||||
unwanted sections.
|
||||
|
||||
Creating a Song
|
||||
|
||||
One way to create a song is to record each track all the
|
||||
way through (up to 999 bars). Another way is to record
|
||||
each basic section (verse, chorus, etc.) in individual
|
||||
sequences, then use the CREATE SONG function to “chain”
|
||||
them together. CREATE SONG will then automatically
|
||||
copy all the parts into a new sequence. If desired, you can
|
||||
even set the last few bars to repeat infinitely, for a fadeout.
|
||||
|
||||
Composition Without Compromise
|
||||
|
||||
The technology you use should never be so complex that
|
||||
it interferes with the creative process. That’s precisely why
|
||||
the LinnSequencer is designed to let you compose, record
|
||||
and edit while devoting your undivided attention to your
|
||||
music. See your Linn dealer today for a demonstration!
|
||||
|
||||
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
|
||||
|
||||
HELP button displays additional explanations.
|
||||
|
||||
0 Non-destructive recording—existing notes are not erased while recording.
|
||||
0 Two FOOTSWIT CH INPUTS may be assigned to remotely control many of the commonly used functions, including
|
||||
|
||||
ERASE, REPEAT, PLAY/ STOP, or LOCATE.
|
||||
|
||||
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
|
||||
|
||||
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
|
||||
|
||||
0 Utilizes ultra high—speed, 8 MHZ 80186 16 bit computer internally for FAST operation.
|
||||
0 TEMPO may be specified in BEATS-PER—MINUTE or FRAMES-PER—BEAT at 24, 25, or 30 frames per second,
|
||||
|
||||
(even drop frame!)
|
||||
|
||||
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
|
||||
|
||||
on the TAP TEMPO button.
|
||||
|
||||
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
|
||||
0 Any TIME SIGNATURE may be used, and may be changed within a song.
|
||||
|
||||
EDI]
|
||||
Linn Electronics, Inc.
|
||||
|
||||
18720 Oxnard Street, Tarzana, CA 91356
|
||||
(818) 708-8131 TELEX #298949 LINN UR
|
||||
|
||||
Binary file not shown.
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
|
||||
VERSION_STRING = '''tesseract 3.04.00
|
||||
leptonica-1.72
|
||||
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
|
||||
SPOOFED: return error claiming image too big
|
||||
'''
|
||||
|
||||
"""Simulates a Tesseract crash
|
||||
|
||||
It isn't strictly necessary to crash the process and that has unwanted
|
||||
side effects like triggering core dumps or error reporting, logging and such.
|
||||
It's enough to dump some text to stderr and return an error code.
|
||||
|
||||
Follows the POSIX? convention of returning 128 + signal number.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
if sys.argv[1] == '--version':
|
||||
print(VERSION_STRING, file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[1] == '--list-langs':
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif sys.argv[-1] == 'pdf':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Spoof doesn't understand arguments", file=sys.stderr)
|
||||
print(sys.argv, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -90,6 +90,13 @@ def main():
|
||||
universal_newlines=True)
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
if p.returncode != 0:
|
||||
# Do not cache errors or crashes
|
||||
print("Tesseract error", file=sys.stderr)
|
||||
print(stdout, end='')
|
||||
print(stderr, end='', file=sys.stderr)
|
||||
return p.returncode
|
||||
|
||||
with open(cache_name + '.stdout', 'w') as f:
|
||||
f.write(stdout)
|
||||
with open(cache_name + '.stderr', 'w') as f:
|
||||
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
|
||||
|
||||
VERSION_STRING = '''tesseract 3.04.00
|
||||
leptonica-1.72
|
||||
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
|
||||
SPOOFED: CRASH ON OCR or -psm 0
|
||||
'''
|
||||
|
||||
"""Simulates a Tesseract crash
|
||||
|
||||
It isn't strictly necessary to crash the process and that has unwanted
|
||||
side effects like triggering core dumps or error reporting, logging and such.
|
||||
It's enough to dump some text to stderr and return an error code.
|
||||
|
||||
Follows the POSIX? convention of returning 128 + signal number.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
if sys.argv[1] == '--version':
|
||||
print(VERSION_STRING, file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[1] == '--list-langs':
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGSEGV)
|
||||
elif sys.argv[-1] == 'pdf':
|
||||
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGSEGV)
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
print("libc++abi.dylib: terminating with uncaught exception of type "
|
||||
"std::bad_alloc: std::bad_alloc", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGABRT)
|
||||
else:
|
||||
print("Spoof doesn't understand arguments", file=sys.stderr)
|
||||
print(sys.argv, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
def main():
|
||||
if sys.argv[1] == '--version':
|
||||
print('0.5')
|
||||
sys.exit(0)
|
||||
|
||||
print("Only supports --version")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+124
-16
@@ -28,6 +28,11 @@ TEST_OUTPUT = os.environ.get(
|
||||
default=os.path.join(PROJECT_ROOT, 'tests', 'output', 'main'))
|
||||
|
||||
|
||||
def running_in_docker():
|
||||
# Docker creates a file named /.dockerinit
|
||||
return os.path.exists('/.dockerinit')
|
||||
|
||||
|
||||
def setup_module():
|
||||
with suppress(FileNotFoundError):
|
||||
shutil.rmtree(TEST_OUTPUT)
|
||||
@@ -83,22 +88,41 @@ def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
|
||||
return p, out, err
|
||||
|
||||
|
||||
def spoof(replace_program, with_spoof):
|
||||
"""Modify environment variables to override subprocess executables
|
||||
|
||||
Before running any executable, ocrmypdf checks the environment variable
|
||||
OCRMYPDF_PROGRAMNAME to override default program name/location, e.g.
|
||||
OCRMYPDF_GS redirects from the system path Ghostscript ("gs") to elsewhere.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
spoofer = os.path.join(SPOOF_PATH, with_spoof)
|
||||
if not os.access(spoofer, os.X_OK):
|
||||
os.chmod(spoofer, 0o755)
|
||||
env['OCRMYPDF_' + replace_program.upper()] = spoofer
|
||||
return env
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_noop():
|
||||
env = os.environ.copy()
|
||||
program = os.path.join(SPOOF_PATH, 'tesseract_noop.py')
|
||||
check_call(['chmod', "+x", program])
|
||||
env['OCRMYPDF_TESSERACT'] = program
|
||||
return env
|
||||
return spoof('tesseract', 'tesseract_noop.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_cache():
|
||||
env = os.environ.copy()
|
||||
program = os.path.join(SPOOF_PATH, "tesseract_cache.py")
|
||||
check_call(['chmod', '+x', program])
|
||||
env['OCRMYPDF_TESSERACT'] = program
|
||||
return env
|
||||
if running_in_docker():
|
||||
return os.environ.copy()
|
||||
return spoof('tesseract', "tesseract_cache.py")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_crash():
|
||||
return spoof('tesseract', 'tesseract_crash.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_big_image_error():
|
||||
return spoof('tesseract', 'tesseract_big_image_error.py')
|
||||
|
||||
|
||||
def test_quick(spoof_tesseract_noop):
|
||||
@@ -146,7 +170,9 @@ def test_clean(spoof_tesseract_noop):
|
||||
('ccitt.pdf', 'hocr'),
|
||||
('ccitt.pdf', 'tesseract'),
|
||||
('jbig2.pdf', 'hocr'),
|
||||
('jbig2.pdf', 'tesseract')
|
||||
('jbig2.pdf', 'tesseract'),
|
||||
('lichtenstein.pdf', 'hocr'),
|
||||
('lichtenstein.pdf', 'tesseract')
|
||||
])
|
||||
def test_exotic_image(spoof_tesseract_cache, pdf, renderer):
|
||||
check_ocrmypdf(
|
||||
@@ -285,6 +311,13 @@ def test_monochrome_correlation():
|
||||
test_pageno=3, # south facing page
|
||||
)
|
||||
assert corr < 0.10
|
||||
corr = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=2,
|
||||
test_pdf=_infile('cardinal.pdf'),
|
||||
test_pageno=2,
|
||||
)
|
||||
assert corr > 0.90
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
@@ -292,11 +325,6 @@ def test_monochrome_correlation():
|
||||
'tesseract',
|
||||
])
|
||||
def test_autorotate(spoof_tesseract_cache, renderer):
|
||||
import ocrmypdf.ghostscript as ghostscript
|
||||
import logging
|
||||
|
||||
gslog = logging.getLogger()
|
||||
|
||||
# cardinal.pdf contains four copies of an image rotated in each cardinal
|
||||
# direction - these ones are "burned in" not tagged with /Rotate
|
||||
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_%s.pdf' % renderer,
|
||||
@@ -310,6 +338,36 @@ def test_autorotate(spoof_tesseract_cache, renderer):
|
||||
assert correlation > 0.80
|
||||
|
||||
|
||||
def test_autorotate_threshold_low(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_threshold_low.pdf',
|
||||
'--rotate-pages-threshold', '1',
|
||||
'-r', '-v', '1', env=spoof_tesseract_cache)
|
||||
|
||||
# Low threshold -> always rotate -> expect high correlation between
|
||||
# reference page and test page
|
||||
correlation = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=1,
|
||||
test_pdf=out,
|
||||
test_pageno=3)
|
||||
assert correlation > 0.80
|
||||
|
||||
|
||||
def test_autorotate_threshold_high(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_threshold_high.pdf',
|
||||
'--rotate-pages-threshold', '99',
|
||||
'-r', '-v', '1', env=spoof_tesseract_cache)
|
||||
|
||||
# High threshold -> never rotate -> expect low correlation since
|
||||
# test page will not be rotated
|
||||
correlation = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=1,
|
||||
test_pdf=out,
|
||||
test_pageno=3)
|
||||
assert correlation < 0.10
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
@@ -382,6 +440,8 @@ def test_missing_docinfo(spoof_tesseract_noop):
|
||||
assert p.returncode == ExitCode.ok, err
|
||||
|
||||
|
||||
@pytest.mark.skipif(running_in_docker(),
|
||||
reason="writes to tests/resources")
|
||||
def test_uppercase_extension(spoof_tesseract_noop):
|
||||
shutil.copy(_infile("skew.pdf"), _infile("UPPERCASE.PDF"))
|
||||
try:
|
||||
@@ -438,4 +498,52 @@ def test_pagesegmode(renderer, spoof_tesseract_cache):
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_tesseract_crash(renderer, spoof_tesseract_crash):
|
||||
sh, out, err = run_ocrmypdf_env(
|
||||
'ccitt.pdf', 'wontwork.pdf', '-v', '1',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_crash)
|
||||
assert sh.returncode == ExitCode.child_process_error
|
||||
assert not os.path.exists(_outfile('wontwork.pdf'))
|
||||
assert "ERROR" in err
|
||||
|
||||
|
||||
def test_tesseract_crash_autorotate(spoof_tesseract_crash):
|
||||
sh, out, err = run_ocrmypdf_env(
|
||||
'ccitt.pdf', 'wontwork.pdf',
|
||||
'-r', env=spoof_tesseract_crash)
|
||||
assert sh.returncode == ExitCode.child_process_error
|
||||
assert not os.path.exists(_outfile('wontwork.pdf'))
|
||||
assert "ERROR" in err
|
||||
print(out)
|
||||
print(err)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error):
|
||||
check_ocrmypdf(
|
||||
'hugemono.pdf', 'hugemono_%s.pdf' % renderer, '-r',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_big_image_error)
|
||||
|
||||
|
||||
def test_no_unpaper():
|
||||
env = os.environ.copy()
|
||||
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/no_unpaper_here.py')
|
||||
sh, out, err = run_ocrmypdf_env(
|
||||
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
|
||||
assert sh.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
def test_old_unpaper():
|
||||
env = os.environ.copy()
|
||||
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/unpaper_oldversion.py')
|
||||
sh, out, err = run_ocrmypdf_env(
|
||||
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
|
||||
assert sh.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
@@ -103,8 +103,8 @@ def test_single_page_image():
|
||||
assert pdfimage['bpc'] == 8
|
||||
|
||||
# DPI in a 1"x1" is the image width
|
||||
assert pdfimage['dpi_w'] == 8
|
||||
assert pdfimage['dpi_h'] == 8
|
||||
assert abs(pdfimage['dpi_w'] - 8) < 1e-5
|
||||
assert abs(pdfimage['dpi_h'] - 8) < 1e-5
|
||||
|
||||
|
||||
def test_single_page_inline_image():
|
||||
@@ -120,8 +120,12 @@ def test_single_page_inline_image():
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
pageinfo.pdf_get_all_pageinfo(filename)
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(filename)
|
||||
print(pdfinfo)
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
assert (pdfimage['dpi_w'] - 8) < 1e-5
|
||||
assert pdfimage['color'] != '-'
|
||||
assert pdfimage['width'] == 8
|
||||
|
||||
|
||||
def test_jpeg():
|
||||
@@ -131,4 +135,5 @@ def test_jpeg():
|
||||
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
assert pdfimage['enc'] == 'jpeg'
|
||||
assert (pdfimage['dpi_w'] - 150) < 1e-5
|
||||
|
||||
|
||||
Reference in New Issue
Block a user