Compare commits

...
27 Commits
Author SHA1 Message Date
James R. Barlow 11a561dbce v4.0.3 release notes 2016-02-26 01:12:15 -08:00
James R. Barlow dad2198394 Log information about detected page orientations in a summary line 2016-02-26 01:07:59 -08:00
James R. Barlow e40fdc502d Always dump stack trace for unexpected errors 2016-02-26 01:06:59 -08:00
James R. Barlow d446fe5922 Fix "too few characters" reported as error by tesseract -psm 0 2016-02-21 08:53:34 -08:00
James R. Barlow 4ca90c106d Docker: fix blank JPEG2000 PDF issue 2016-02-21 04:24:21 -08:00
James R. Barlow 7c5e58a497 Fix test cases that break in Docker, improve test for running in Docker 2016-02-20 23:47:37 -08:00
James R. Barlow 323b9a5f8e Add other missing files 2016-02-20 05:34:21 -08:00
James R. Barlow cab381a339 Add JPEG 2000 test case 2016-02-20 05:13:19 -08:00
James R. Barlow fe4d4c39cd Merge commit '6f3ac46b1c176d48782347cfa14d9ef6ce773f37' into develop 2016-02-20 04:56:12 -08:00
James R. Barlow ad188d7ae1 Docker: supply openjpeg to address JPXDecode errors 2016-02-20 04:54:55 -08:00
James R. Barlow 8246cc0538 Gracefully recover from tesseract's failure to process very large images
And test cases to check this
2016-02-20 04:53:23 -08:00
James R. Barlow 6f3ac46b1c Gracefully recover from tesseract's failure to process very large images
And test cases to check this
2016-02-20 04:53:02 -08:00
James R. Barlow ac71c3be63 4.0.2rc1 - release notes, add missing file caught by Travis 2016-02-20 03:36:37 -08:00
James R. Barlow ecc0ac9b19 Fix error on --tesseract-timeout timing out 2016-02-20 03:13:23 -08:00
James R. Barlow ea4e6bf67d leptonica: serialization tweaks, memory handling 2016-02-20 02:54:53 -08:00
James R. Barlow 46c204f533 Fix leptonica pickling 2016-02-20 02:35:34 -08:00
James R. Barlow 71fbda8bf6 Adjust page orientation parsing to deal with change in Tess 3.04.01 2016-02-20 01:32:56 -08:00
James R. Barlow 9b79b4a7c8 Leptonica: documentation, helper functions 2016-02-20 01:20:06 -08:00
James R. Barlow c04cc853d7 leptonica: remove special PNM handling
We no longer use PNM as an intermediate format, so there's no need to
handle leptonica's PNM quirks.
2016-02-19 15:13:14 -08:00
James R. Barlow dd41e70ccc leptonica: nit 2016-02-19 15:11:48 -08:00
James R. Barlow 4206e74f42 tests: also check that monochrome correlation correctly detects matches 2016-02-19 14:35:31 -08:00
James R. Barlow 68c3ce56a9 Don't do chmod unless necessarily (breaks py.test on Docker) 2016-02-19 14:09:56 -08:00
James R. Barlow ab0e5fa425 Improve error checking for tesseract -psm 0 (orientation) errors 2016-02-19 03:58:39 -08:00
James R. Barlow f3b0434a87 Improve ability to capture error messages from tesseract on a crash 2016-02-19 03:48:49 -08:00
James R. Barlow aa394440db Just use the PyPI version of ocrmypdf in dockerfile
Apparently setuptools_scm_git_archive is ineffective on hub.docker.com
automatic build, it still can't find a version.
2016-02-17 15:14:23 -08:00
James R. Barlow 3b98a1a04b Fix KeyError on unexpected tess output 2016-02-17 06:05:27 -08:00
James R. Barlow fcb89b0c58 Forgot to save release notes 2016-02-17 01:48:25 -08:00
18 changed files with 534 additions and 102 deletions
+1
View File
@@ -10,6 +10,7 @@ pyvenv.cfg
.eggs/
build/
dist/
wheelhouse/
# Automatically generated files
ocrmypdf/lib/_*.py
+12 -6
View File
@@ -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
+43 -4
View File
@@ -6,6 +6,45 @@ Please always read this file before installing the package
Download software here: https://github.com/jbarlow83/OCRmyPDF/tags
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 +54,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 +70,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
+1
View File
@@ -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
+45 -42
View File
@@ -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 *[]', 1)
size = ffi.new('size_t *', 0)
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.
@@ -167,7 +197,7 @@ class Pix:
@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.
@@ -204,7 +234,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 +278,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
+6
View File
@@ -58,7 +58,13 @@ 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 );
void lept_free(void *ptr);
""")
if __name__ == '__main__':
ffi.compile()
+49 -11
View File
@@ -524,9 +524,9 @@ def orient_page(
direction = {
0: '',
90: '',
90: '',
180: '',
270: ''
270: ''
}
log.info(
@@ -544,12 +544,18 @@ def orient_page(
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 +794,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)
@@ -1011,10 +1018,17 @@ def run_pipeline():
cmdline.run(options)
except ruffus_exceptions.RethrownJobError as e:
if options.verbose:
print(e)
_log.debug(e)
# Yuck. Hunt through the ruffus exception to find out what the
# return code is supposed to be.
# Ruffus flattens the exception to a string, throwing away all kinds
# of helpful details
# task_name, job_name - ruffus status
# exc_name - class name of exception
# exc_value - irritating string that makes impossible to recover
# exception object
# exc_stack - string that contains traceback of exception
for exc in e.args:
task_name, job_name, exc_name, exc_value, exc_stack = exc
if exc_name == 'builtins.SystemExit':
@@ -1023,22 +1037,46 @@ def run_pipeline():
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))
_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':
print("Input file '{0}' is not a valid PDF".format(
_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(e)
return ExitCode.other_error
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
+59 -25
View File
@@ -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)
+4
View File
@@ -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
+9 -3
View File
@@ -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.
+123
View File
@@ -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. Its 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 Sequencers
click track. When the sequence loops back around to bar 1,
youll 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 firsttrack 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. Thats 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
+50
View File
@@ -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()
+7
View File
@@ -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:
+50
View File
@@ -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()
+75 -11
View File
@@ -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', [
@@ -382,6 +415,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 +473,33 @@ 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
@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)