Compare commits

..
53 Commits
Author SHA1 Message Date
James R. Barlow b8cd3acd9e v8.0.1 notes 2019-01-17 00:57:28 -08:00
James R. Barlow 03779e33da docs: Update some install procedures for v8 changes
[ci skip]
2019-01-12 00:33:36 -08:00
James R. Barlow c466483e82 docs: Explain intermediate files 2019-01-11 14:52:05 -08:00
James R. Barlow e3a58219d1 Ensure XObjects with no subtype don't cause an exception
Closes #325
2019-01-08 16:46:08 -08:00
James R. Barlow 72337094ca v8.0.0 release notes 2019-01-05 23:35:47 -08:00
James R. Barlow f472587d22 Bump pikepdf version, point to release notes 2019-01-05 16:48:13 -08:00
James R. Barlow f34b3015b2 Prevent Ghostscript from generating invalid XMP metadata
If DocumentInfo contains NULs Ghostscript will generate XMP with
NULs which is not allowed. Repair DocumentInfo before Ghostscript sees it.
2019-01-04 13:20:41 -08:00
James R. Barlow 089ece2715 use pikepdf 0.10.2 2019-01-03 12:08:43 -08:00
James R. Barlow 6438465e3f Add fish completions 2019-01-02 17:08:30 -08:00
James R. Barlow 7d330afd81 Delinting 2019-01-02 13:34:45 -08:00
James R. Barlow 68fbd9fcc9 pikepdf: version bump 2018-12-31 15:37:31 -08:00
James R. Barlow c771938907 Convert to f-strings where it makes sense 2018-12-31 15:01:19 -08:00
James R. Barlow c2a947acf4 travis: fix 2018-12-31 01:18:30 -08:00
James R. Barlow 8c0009c5c8 Make pdfminer.six optional
Mainly since the current release of pdfminer.six lacks a sdist, blocking
homebrew packaging. Also in case other distros don't accept pdfminer.six.
2018-12-31 01:08:43 -08:00
James R. Barlow cfc5cdf47d pdfa: remove a pile of deprecated code
It's now handled in pikepdf.
2018-12-31 00:05:13 -08:00
James R. Barlow 05152a8af9 Remove always-false Tess v3 tests 2018-12-30 02:01:05 -08:00
James R. Barlow 0880b16491 Sort imports with isort 2018-12-30 01:28:15 -08:00
James R. Barlow 06308a22ce Reformat with black 2018-12-30 01:27:49 -08:00
James R. Barlow 80bd7de580 Generate test cache 2018-12-30 01:02:37 -08:00
James R. Barlow 8b90c45437 Drop support for Tesseract 3 2018-12-30 00:47:12 -08:00
James R. Barlow 72b920eb16 Drop support for Python 3.5 2018-12-30 00:23:26 -08:00
James R. Barlow b4a51907d6 Detect when metadata is dropped during PDF/A conversion 2018-12-30 00:13:25 -08:00
James R. Barlow 1ca1221432 leptonica.py: Fix exception on certain types of barcode failures
Closes #322
2018-12-19 17:23:23 -08:00
James R. Barlow 40b72b0fa8 v7.4.1 release notes 2018-12-19 16:41:09 -08:00
James R. Barlow 0e55b4ad52 Travis: remove Brewfile 2018-12-19 16:40:48 -08:00
James R. Barlow 7b4f5a8fc4 docs: try to fix readthedocs
[ci skip]
2018-12-19 15:30:07 -08:00
James R. Barlow 9261a38493 Readme: more media 2018-12-19 15:27:54 -08:00
James R. Barlow cc8ff318ed New issue template 2018-12-19 15:27:44 -08:00
James R. Barlow ad15e845f9 docs: Ghostscript PDF/A XMP metadata loss; ocrmypdf-webservice
[ci skip]
2018-12-17 23:20:49 -08:00
James R. Barlow ab632f57cd v7.4.0 release notes 2018-12-15 15:27:23 -08:00
James R. Barlow 13d20bd993 pdfinfo: tolerate PDFs that overflow and underflow the graphics stack 2018-12-15 15:10:29 -08:00
James R. Barlow b973208137 Require pikepdf 0.9.1 2018-12-15 14:23:10 -08:00
James R. Barlow 942abf8074 Fix reqs/main.txt for pikepdf 0.9.0 2018-12-14 23:29:26 -08:00
James R. Barlow ed9bb985e2 Fix pikepdf 0.9.0 2018-12-14 23:21:13 -08:00
James R. Barlow 5a7a8e573b Require pikepdf 0.9.0 2018-12-14 23:06:57 -08:00
James R. Barlow ce878db913 Rename to polyglot.dockerfile 2018-12-14 23:06:29 -08:00
James R. Barlow a3d58683b2 Update webservice.py with separate license 2018-12-14 23:05:54 -08:00
James R. Barlow 039e8ca7e7 Merge branches 'feature/newer-pike' and 'feature/webapp' 2018-12-14 18:08:31 -08:00
James R. Barlow 0ebbd4e21b Don't open encrypted files, even if password is empty 2018-12-13 22:48:00 -08:00
James R. Barlow 2cb75f6076 Refactor pipeline to make PDF/A conversion a separate step 2018-12-13 20:48:48 -08:00
James R. Barlow 857d871364 Fix regression on Ghostscript path 2018-12-13 20:36:41 -08:00
James R. Barlow 632dab2cc0 Replace Ghostscript DOCINFO and fix 9.25 metadata date regression
We no longer use Ghostscript to manage PDF metadata, instead
omitting the DOCINFO segment from the pdfmark file we generate.

Instead all of the relevant metadata code has been migrated to pikepdf,
and we use that API. This should be more consistent and fixes the
Ghostscript version-depedent quirks.

Also removes our python-xmp-toolkit dependency, except for
testing.
2018-12-13 18:13:30 -08:00
James R. Barlow 7647918f2d setup: suppress XMLParser() warning - defusedxml related 2018-12-12 22:13:32 -08:00
James R. Barlow 75c5d8055c pdfinfo: fix FutureWarning 2018-12-12 22:12:14 -08:00
James R. Barlow a938bbea55 Remove more libxmp dependencies 2018-12-12 22:02:35 -08:00
James R. Barlow 414407fbd6 Deprecate encode/decode_pdf_date and remap to pikepdf version 2018-12-12 22:01:21 -08:00
James R. Barlow 076fc717df pdfa: replace PDF/A checking with pikepdf implementation 2018-12-12 21:41:16 -08:00
James R. Barlow 2a04b2d82b Rename webapp to webservice 2018-12-12 21:29:05 -08:00
James R. Barlow 065db414c0 webapp docker: Build from polyglot 2018-12-12 21:24:04 -08:00
James R. Barlow 19a054a78b Add webapp stuff 2018-12-10 20:03:52 -08:00
James R. Barlow 9df24a81b7 Fix comment in layout.py 2018-11-28 15:16:34 -08:00
James R. Barlow 40c0acd3f2 Support using --force-ocr and --threshold or --mask-barcodes together 2018-11-28 15:16:24 -08:00
James R. Barlow 20db7f0a8f leptonica: delete file junkpixt.png if created 2018-11-28 13:47:55 -08:00
193 changed files with 11350 additions and 4683 deletions
+19
View File
@@ -0,0 +1,19 @@
# OCRmyPDF webservice
#
FROM jbarlow83/ocrmypdf-polyglot:latest
USER root
# Update system and install our dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
python3-flask
RUN apt-get autoremove -y && apt-get clean -y
EXPOSE 5000
COPY .docker/webservice.py /application
USER docker
ENTRYPOINT ["python3", "/application/webservice.py"]
+94
View File
@@ -0,0 +1,94 @@
# webservice.py wrapper for OCRmyPDF
# Copyright (C) 2018 James R. Barlow: github.com/jbarlow83
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""This is a simple web service/HTTP wrapper for OCRmyPDF
This may be more convenient than the command line tool for some Docker users.
Note that OCRmyPDF uses Ghostscript, which is licensed under AGPL3+. While
OCRmyPDF is under GPL3, this file is distributed under the Affero GPL3+ license,
to emphasize that SaaS deployments should make sure they comply with
Ghostscript's license as well as OCRmyPDF's.
"""
from flask import Flask, Response, flash, request, redirect, url_for, abort, send_from_directory
from subprocess import run, PIPE
from tempfile import TemporaryDirectory
from werkzeug.utils import secure_filename
import os
import shlex
app = Flask(__name__)
app.secret_key = "secret"
uploaddir = TemporaryDirectory(prefix="ocrmypdf-upload")
downloaddir = TemporaryDirectory(prefix="ocrmypdf-download")
app.config["UPLOAD_FOLDER"] = uploaddir
ALLOWED_EXTENSIONS = set(["pdf"])
def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
def do_ocrmypdf(file):
filename = secure_filename(file.filename)
up_file = os.path.join(uploaddir.name, filename)
file.save(up_file)
down_file = os.path.join(downloaddir.name, filename)
cmd_args = [arg for arg in shlex.split(request.form["params"])]
if "--sidecar" in cmd_args:
return Response("--sidecar not supported", 501, mimetype='text/plain')
ocrmypdf_args = ["ocrmypdf", *cmd_args, up_file, down_file]
proc = run(ocrmypdf_args, stdout=PIPE, stderr=PIPE, encoding="utf-8")
if proc.returncode != 0:
stderr = proc.stderr
return Response(stderr, 400, mimetype='text/plain')
return send_from_directory(downloaddir.name, filename)
@app.route("/", methods=["GET", "POST"])
def upload_file():
if request.method == "POST":
if "file" not in request.files:
return Response("No file in POST", 400, mimetype='text/plain')
file = request.files["file"]
if file.filename == "":
return Response("Empty filename", 400, mimetype='text/plain')
if not allowed_file(file.filename):
return Response("Invalid filename", 400, mimetype='text/plain')
if file and allowed_file(file.filename):
return do_ocrmypdf(file)
return Response("Some other problem", 400, mimetype='text/plain')
return """
<!doctype html>
<title>OCRmyPDF webapp</title>
<h1>Upload a PDF (debug UI)</h1>
<form method=post enctype=multipart/form-data>
<label for="args">Command line parameters</label>
<input type=textbox name=params>
<label for="file">File to upload</label>
<input type=file name=file>
<input type=submit value=Upload>
</form>
"""
if __name__ == "__main__":
app.run(host='0.0.0.0')
-5
View File
@@ -1,5 +0,0 @@
Please include the command line and a test file with your issue report.
If possible, please use a test file that we can include in future test cases (no personal information, no copyrighted material).
If you wish to encrypt a test file for the OCRmyPDF maintainer only, see the [Wiki](https://github.com/jbarlow83/OCRmyPDF/wiki).
+33
View File
@@ -0,0 +1,33 @@
**Describe the issue**
A clear and concise description of what the issue is.
**To Reproduce**
What command line were you trying to run?
```bash
ocrmypdf ...arguments... input.pdf output.pdf
```
**Example file**
Please include an example *input* PDF (or image). The input file is more helpful.
Please check any or all that apply about the test file:
- [ ] This is the input file
- [ ] The file contains no personal or confidential information
- [ ] I am the copyright holder for this file
- [ ] I permit this file to be included in the OCRmyPDF test suite under the CC-BY-SA 4.0 license
- [ ] I am not the copyright holder, but this file is available under a free software license
Files that are not free for inclusion in this project are quite welcome, but we like to collect free files for our test suite when possible. Please do *not* submit files with confidential information. At your option you may encrypt files for OCRmyPDF's author only.
**Expected behavior**
A clear and concise description of what you expected to happen. Include screenshots if applicable.
**System:**
- OS: [e.g. Linux, macOS]
- OCRmyPDF Version: [e.g. v7.4.0]
**Additional context**
Add any other context about the problem here.
+2
View File
@@ -39,3 +39,5 @@ log/
tests/output/
tests/resources/private/
tmp/
/debug_tests.py
*.traineddata
+38 -10
View File
@@ -9,7 +9,7 @@ matrix:
dist: trusty
sudo: required
language: python
python: "3.5"
python: "3.6"
env:
- DIST=trusty
addons: &trusty_apt
@@ -33,14 +33,6 @@ matrix:
- tesseract-ocr-deu
- tesseract-ocr-eng
- tesseract-ocr-fra
- os: linux
dist: trusty
sudo: required
language: python
python: "3.6"
env:
- DIST=trusty
addons: *trusty_apt
- os: linux
dist: xenial
sudo: required
@@ -68,6 +60,39 @@ matrix:
- os: osx
osx_image: xcode9.2
language: generic
addons:
homebrew:
update: true
packages:
- exempi
- ghostscript
- jbig2enc
- leptonica
- openjpeg
- pngquant
- python
- qpdf
- tesseract
- unpaper
- os: osx
osx_image: xcode9.2
language: generic
env:
- ADD_PDFMINER=1
addons:
homebrew:
update: true
packages:
- exempi
- ghostscript
- jbig2enc
- leptonica
- openjpeg
- pngquant
- python
- qpdf
- tesseract
- unpaper
before_cache:
- rm -f $HOME/.cache/pip/log/debug.log
@@ -83,7 +108,6 @@ before_install: |
sudo dpkg -i packages/unpaper_6.1-1.deb
fi
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
brew update --quiet && brew bundle --file=.travis/Brewfile --quiet
pip3 install --upgrade pip
pip3 install wheel
fi
@@ -93,6 +117,10 @@ install:
- pip3 install pycparser # py3.7 workaround for https://github.com/eliben/pycparser/issues/251
- pip3 install -r requirements/main.txt
- pip3 install --no-deps .
- |
if [[ "$ADD_PDFMINER" == "1" ]]; then
pip3 install --no-deps .[pdfminer]
fi
- pip3 install -r requirements/test.txt
script:
-11
View File
@@ -1,11 +0,0 @@
brew 'python'
brew 'exempi'
brew 'ghostscript'
brew 'jbig2enc'
brew 'leptonica'
brew 'openjpeg'
brew 'pngquant'
brew 'qpdf'
brew 'tesseract'
brew 'unpaper'
+29 -25
View File
@@ -25,20 +25,22 @@ ocrmypdf # it's a scriptable command line program
output_searchable.pdf # produces validated PDF output
```
[See the release notes for details on the latest changes](https://ocrmypdf.readthedocs.io/en/latest/release_notes.html).
Main features
-------------
- Generates a searchable [PDF/A](https://en.wikipedia.org/?title=PDF/A) file from a regular PDF
- Places OCR text accurately below the image to ease copy / paste
- Keeps the exact resolution of the original embedded images
- When possible, inserts OCR information as a "lossless" operation without disrupting any other content
- Optimizes PDF images, often producing files smaller than the input file
- If requested deskews and/or cleans the image before performing OCR
- Validates input and output files
- Distributes work across all available CPU cores
- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine
- Supports more than [100 languages](https://github.com/tesseract-ocr/tessdata) recognized by Tesseract
- Battle-tested on thousands of PDFs, a test suite and continuous integration
- Generates a searchable [PDF/A](https://en.wikipedia.org/?title=PDF/A) file from a regular PDF
- Places OCR text accurately below the image to ease copy / paste
- Keeps the exact resolution of the original embedded images
- When possible, inserts OCR information as a "lossless" operation without disrupting any other content
- Optimizes PDF images, often producing files smaller than the input file
- If requested deskews and/or cleans the image before performing OCR
- Validates input and output files
- Distributes work across all available CPU cores
- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine
- Supports more than [100 languages](https://github.com/tesseract-ocr/tessdata) recognized by Tesseract
- Battle-tested on thousands of PDFs, a test suite and continuous integration
For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/en/latest/).
@@ -47,13 +49,13 @@ Motivation
I searched the web for a free command line tool to OCR PDF files on Linux/UNIX: I found many, but none of them were really satisfying.
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
- Or they did not handle accents and multilingual characters
- Or they changed the resolution of the embedded images
- Or they generated ridiculously large PDF files
- Or they crashed when trying to OCR
- Or they did not produce valid PDF files
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
- Or they did not handle accents and multilingual characters
- Or they changed the resolution of the embedded images
- Or they generated ridiculously large PDF files
- Or they crashed when trying to OCR
- Or they did not produce valid PDF files
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
...so I decided to develop my own tool.
@@ -110,11 +112,11 @@ Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/e
If you detect an issue, please:
- Check whether your issue is already known
- If no problem report exists on github, please create one here: <https://github.com/jbarlow83/OCRmyPDF/issues>
- Describe your problem thoroughly
- Append the console output of the script when running the debug mode (`-v 1` option)
- If possible provide your input PDF file as well as the content of the temporary folder (using a file sharing service like Dropbox)
- Check whether your issue is already known
- If no problem report exists on github, please create one here: <https://github.com/jbarlow83/OCRmyPDF/issues>
- Describe your problem thoroughly
- Append the console output of the script when running the debug mode (`-v 1` option)
- If possible provide your input PDF file as well as the content of the temporary folder (using a file sharing service like Dropbox)
Requirements
------------
@@ -124,8 +126,10 @@ Runs on CPython 3.5, 3.6 and 3.7. Requires external program installations of Gho
Press & Media
-------------
- [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://heise.de/-2356670)
- [Going paperless with OCRmyPDF](https://medium.com/@ikirichenko/going-paperless-with-ocrmypdf-e2f36143f46a)
- [Converting a scanned document into a compressed searchable PDF with redactions](https://medium.com/@treyharris/converting-a-scanned-document-into-a-compressed-searchable-pdf-with-redactions-63f61c34fe4c)
- [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://heise.de/-2356670)
License
-------
+6
View File
@@ -95,6 +95,12 @@ Files: tests/resources/vector.pdf
Copyright: (C) 2018 Catscratch
License: Expat
Files: test/resources/enron*.pdf
Copyright: EnronData.org
License: CC-BY-3.0
See: https://enrondata.readthedocs.io/en/latest/data/edo-enron-email-pst-dataset/
Comment: Unprocessed.
Files: src/ocrmypdf/data/sRGB.icc
Copyright: Kai-Uwe Behrmann <www.behrmann.name>
Marti Maria <www.littlecms.com>
+28 -8
View File
@@ -47,16 +47,16 @@ Some relevant environment variables that influence Tesseract's behavior include:
Controls the number of threads Tesseract will use. OCRmyPDF will manage this environment if it is not already set. (Currently, it will set it to 1 because this gives the best results in testing.)
For example, if you are testing tesseract 4.00 and don't wish to use an existing tesseract 3.04 installation, you can launch OCRmyPDF as follows:
For example, if you have a development build of Tesseract don't wish to use the system installation, you can launch OCRmyPDF as follows:
.. code-block:: bash
env \
PATH=/home/user/src/tesseract4/api:$PATH \
TESSDATA_PREFIX=/home/user/src/tesseract4 \
ocrmypdf --tesseract-oem 2 input.pdf output.pdf
PATH=/home/user/src/tesseract/api:$PATH \
TESSDATA_PREFIX=/home/user/src/tesseract \
ocrmypdf input.pdf output.pdf
In this example ``TESSDATA_PREFIX`` directs Tesseract 4.0 to use LSTM training data. ``--tesseract-oem 1`` requests tesseract 4.0's new LSTM engine. (Tesseract 4.0 only.)
In this example ``TESSDATA_PREFIX`` is required to redirect Tesseract to an alternate folder for its "tessdata" files.
Overriding other support programs
@@ -107,7 +107,7 @@ rendering
Creating a new PDF from other data (such as an existing PDF).
OCRmyPDF has these PDF renderers: ``sandwich`` and ``hocr``. The renderer may be selected using ``--pdf-renderer``. The default is ``auto`` which lets OCRmyPDF select the renderer to use. Currently, ``auto`` selects ``sandwich`` for Tesseract 3.05.01 or newer, or ``hocr`` for older versions of Tesseract.
OCRmyPDF has these PDF renderers: ``sandwich`` and ``hocr``. The renderer may be selected using ``--pdf-renderer``. The default is ``auto`` which lets OCRmyPDF select the renderer to use. Currently, ``auto`` always selects ``sandwich``.
The ``sandwich`` renderer
"""""""""""""""""""""""""
@@ -118,8 +118,6 @@ Currently this is the best renderer for most uses, however it is implemented in
When image preprocessing features like ``--deskew`` are used, the original PDF will be rendered as a full page and the OCR layer will be placed on top.
If a PDF created with this renderer using Tesseract versions older than 3.05.00 is then passed through Ghostscript's pdfwrite feature, the OCR text *may* be corrupted. The ``--output-type=pdfa`` argument will produce a warning in this situation. For this reason, OCRmyPDF automatically selects the ``hocr`` for older Tesseract versions.
The ``hocr`` renderer
"""""""""""""""""""""
@@ -191,3 +189,25 @@ user interface. They may be imported from ``ocrmypdf.exceptions``.
* - 130
- ``ExitCode.ctrl_c``
- The program was interrupted by pressing Ctrl+C.
Debugging the intermediate files
--------------------------------
OCRmyPDF normally saves its intermediate results to a temporary folder and deletes this folder when it exits, whether it succeeded or failed.
If the ``-k`` argument is issued on the command line, OCRmyPDF will keep the temporary folder and print the location, whether it succeeded or failed (provided the Python interpreter did not crash). An example message is:
.. code-block::
Temporary working files saved at:
/tmp/com.github.ocrmypdf.u20wpz07
The organization of this folder is an implementation detail and subject to change between releases. However the general organization is that working files on a per page basis have the page number as a prefix (starting with page 1), an infix indicates the processing stage, and a suffix indicates the file type. Some important files include:
* ``.page.png`` - what the input page looks like
* ``.image`` - the image we will show the user if we are in a mode that changes the final appearance; may be in one of several image formats
* ``.text.pdf`` - the OCR file; this will load as a blank page but should have visible text if checked with a tool like pdftotext or pdfminder.six
* ``.ocr.png`` - the file that is sent to Tesseract for OCR; depending on arguments this may differ from the presentation image
* ``layers.rendered.pdf`` - the composite PDF, before metadata repair and optimization
* ``images/*`` - images extracted during the optimization process; here the prefix indicates a PDF object ID not a page number
+6 -1
View File
@@ -74,7 +74,12 @@ if on_rtd:
def __getattr__(cls, name):
return MagicMock()
MOCK_MODULES = ['pikepdf', 'libxmp', 'libxmp.utils', 'ocrmypdf.leptonica']
MOCK_MODULES = [
'pikepdf',
'pikepdf.models',
'pikepdf.models.metadata',
'ocrmypdf.leptonica'
]
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
+2 -2
View File
@@ -81,7 +81,7 @@ This produces a file named "output.pdf" and a companion text file named "output.
OCR images, not PDFs
--------------------
If you are starting with images, you can just use Tesseract 3.04 or later directly to convert images to PDFs:
If you are starting with images, you can just use Tesseract directly to convert images to PDFs:
.. code-block:: bash
@@ -129,7 +129,7 @@ OCRmyPDF perform some image processing on each page of a PDF, if desired. The s
* ``--clean-final`` uses unpaper to clean up pages before OCR and inserts the page into the final output. You will want to review each page to ensure that unpaper did not remove something important.
* ``-mask-barcodes`` will "cover up" any barcodes detected in the image of a page. Barcodes are known to confuse Tesseract OCR and interfere with the recognition of text on the same baseline as a barcode. The output file will contain the unaltered image of the barcode.
* ``--mask-barcodes`` will "cover up" any barcodes detected in the image of a page. Barcodes are known to confuse Tesseract OCR and interfere with the recognition of text on the same baseline as a barcode. The output file will contain the unaltered image of the barcode.
.. note::
+61 -41
View File
@@ -6,9 +6,12 @@ Installation
|latest|
The easiest way to install OCRmyPDF to follow the steps for your operating system/platform.
The easiest way to install OCRmyPDF is to follow the steps for your operating
system/platform, although sometimes this version may be out of date.
If you want to use the latest version of OCRmyPDF, your best bet is to install the most recent version your platform provides, and then upgrade that version by installing the Python binary wheels.
If you want to use the latest version of OCRmyPDF, your best bet is to install
the most recent version your platform provides, and then upgrade that version by
installing the Python binary wheels.
.. contents:: Platform-specific steps
:depth: 2
@@ -136,22 +139,56 @@ To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 16.04 LTS
^^^^^^^^^^^^^^^^
No package is currently available for Ubuntu 16.04, but you can install the dependencies manually:
No package is available for Ubuntu 16.04. OCRmyPDF 8.0 and newer require Python
3.6. Ubuntu 16.04 ships Python 3.5, but you can install Python 3.6 on it. Or,
you can skip Python 3.6 and install OCRmyPDF 7.x or older - for that procedure,
please see the installation documentation for the version of OCRmyPDF you plan
to use.
**Install system packages for OCRmyPDF**
.. code-block:: bash
sudo apt-get update
sudo apt-get install \
sudo apt-get install -y software-properties-common python-software-properties
sudo add-apt-repository -y \
ppa:jonathonf/python-3.6 \
ppa:alex-p/tesseract-ocr
sudo apt-get update
sudo apt-get install -y \
ghostscript \
libexempi3 \
libffi6 \
pngquant \
python3-cffi \
python3-pip \
python3.6 \
qpdf \
tesseract-ocr \
unpaper
If you wish install OCRmyPDF for the current user, and ensure that the ``PATH``
This will install a Python 3.6 binary at ``/usr/bin/python3.6`` alongside the
system's Python 3.5. Do not remove the system Python. This will also install
Tesseract 4.0 from a PPA, since the version available in Ubuntu 16.04 is too old
for OCRmyPDF.
Now install pip for Python 3.6. This will install the Python 3.6 version of
``pip`` at ``/usr/local/bin/pip``.
.. code-block:: bash
curl https://bootstrap.pypa.io/get-pip.py | sudo python3.6
**Install OCRmyPDF**
OCRmyPDF requires the locale to be set for UTF-8. **On some minimal Ubuntu
installations systems**, it may be necessary to set the locale.
.. code-block:: bash
# Optional: Only need to set these if they are not already set
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
Now install OCRmyPDF for the current user, and ensure that the ``PATH``
environment variable contains ``$HOME/.local/bin``.
.. code-block:: bash
@@ -159,38 +196,20 @@ environment variable contains ``$HOME/.local/bin``.
export PATH=$HOME/.local/bin:$PATH
pip3 install --user ocrmypdf
Alternately, you can install ocrmypdf system-wide. (Not recommended.)
.. code-block:: bash
sudo pip3 install ocrmypdf
At your option, you may upgrade Ubuntu 16.04 LTS to Tesseract 4.0 for improved OCR results.
.. code-block:: bash
sudo apt-get install -y software-properties-common python-software-properties
sudo add-apt-repository ppa:alex-p/tesseract-ocr -y
sudo apt-get update
sudo apt-get upgrade tesseract-ocr
To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 14.04 LTS
^^^^^^^^^^^^^^^^
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because it is older and does not provide ``pip``.
Update apt-get:
.. code-block:: bash
sudo apt-get update
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other
options, because of its age. Several backports are required. For explanations of
some steps of this procedure, see the similar steps for Ubuntu 16.04.
Install system dependencies:
.. code-block:: bash
sudo apt-get update
sudo apt-get install \
software-properties-common python-software-properties \
zlib1g-dev \
@@ -200,9 +219,13 @@ Install system dependencies:
pngquant \
qpdf
We will need backports of Ghostscript 9.16, libav-11 (for unpaper 6.1), Tesseract 4.00 (alpha), and Python 3.6. This will replace Ghostscript and Tesseract 3.x on your system. Python 3.6 will be installed alongside the system Python 3.4.
We will need backports of Ghostscript 9.16, libav-11 (for unpaper 6.1),
Tesseract 4.00 (alpha), and Python 3.6. This will replace Ghostscript and
Tesseract 3.x on your system. Python 3.6 will be installed alongside the system
Python 3.4.
If you prefer to not modify your system in this matter, consider using a Docker container.
If you prefer to not modify your system in this matter, consider using a Docker
container.
.. code-block:: bash
@@ -228,8 +251,6 @@ Now we need to install ``pip`` and let it install ocrmypdf:
curl https://bootstrap.pypa.io/ez_setup.py -o - | python3.6 && python3.6 -m easy_install pip
pip3.6 install ocrmypdf
The ``wget`` command will download a program and run it.
These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper``, and created a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
.. code-block:: bash
@@ -377,6 +398,9 @@ Assuming you have a Docker engine running, you can download one of the three ava
* - ocrmypdf-polyglot
- ``docker pull jbarlow83/ocrmypdf-polyglot``
- As above, with all available language packs.
* - ocrmypdf-webservice
- ``docker pull jbarlow83/ocrmypdf-polyglot``
- All language packs, and a simple HTTP wrapper allowing OCRmyPDF to be used as a web service. Note that this component is licensed under AGPLv3.
For example:
@@ -490,21 +514,17 @@ Requirements for pip and HEAD install
OCRmyPDF currently requires these external programs and libraries to be installed, and must be satisfied using the operating system package manager. ``pip`` cannot provide them.
- Python 3.5 or newer
- Python 3.6 or newer
- Ghostscript 9.15 or newer
- libexempi3 2.2.0 or newer
- qpdf 8.1.0 or newer
- Tesseract 3.04 or newer
Using Python 3.5 in production is discouraged. Python 3.6 and 3.7 give much better performance.
- Tesseract 4.0.0-alpha or newer
As of ocrmypdf 7.2.1, the following versions are recommended:
- Python 3.7
- Ghostscript 9.23 or newer
- libexempi3 2.4.5 or newer
- qpdf 8.2.1
- Tesseract 4.0.0-rc1
- Tesseract 4.0.0 or newer
- jbig2enc 0.29 or newer
- pngquant 2.5 or newer
- unpaper 6.1
@@ -523,7 +543,7 @@ These are in addition to the Python packaging dependencies, meaning that unfortu
Installing HEAD revision from sources
-------------------------------------
If you have ``git`` and Python 3.5 or newer installed, you can install from source. When the ``pip`` installer runs, it will alert you if dependencies are missing.
If you have ``git`` and Python 3.6 or newer installed, you can install from source. When the ``pip`` installer runs, it will alert you if dependencies are missing.
If you prefer to build every from source, you will need to `build pikepdf from source <https://pikepdf.readthedocs.io/en/latest/installation.html#building-from-source>`_. First ensure you can build and install pikepdf.
+7 -3
View File
@@ -90,6 +90,7 @@ Ghostscript also imposes some limitations:
* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled.
* PDFs containing JPEG 2000-encoded content will be converted to JPEG encoding, which may introduce compression artifacts, if Ghostscript PDF/A is enabled.
* Ghostscript may transcode grayscale and color images, either lossy to lossless or lossless to lossy, based on an internal algorithm. This behavior can be suppressed by setting ``--pdfa-image-compression`` to ``jpeg`` or ``lossless`` to set all images to one type or the other. Ghostscript has no option to maintain the input image's format. (Ghostscript 9.25+ can copy JPEG images without transcoding them; earlier versions will transcode.)
* Ghostscript's PDF/A conversion removes any XMP metadata that is not one of the standard XMP metadata namespaces for PDFs. In particular, PRISM Metdata is removed.
Regarding OCRmyPDF itself:
@@ -109,7 +110,10 @@ To the author's knowledge, OCRmyPDF is the most feature-rich and thoroughly test
Web front-ends
--------------
* `Nextcloud OCR <https://github.com/janis91/ocr>`_ is a free software plugin for the Nextcloud private cloud software
* `OCRmyPDF-web <https://github.com/sseemayer/OCRmyPDF-web>`_, a micro web-frontend for OCRmyPDF (third-party, not actively maintained)
The Docker image ocrmypdf-webservice provides a web service front-end that allows files to submitted over HTTP and the results "downloaded". This is an HTTP server intended to simplify web services deployments; it is not intended to be deployed on the public internet and no real security measures to speak of.
Bear in mind that OCRmyPDF is not designed to be secure against malware-bearing PDFs (see `Using OCRmyPDF online`_).
In addition, the following integrations are available:
* `Nextcloud OCR <https://github.com/janis91/ocr>`_ is a free software plugin for the Nextcloud private cloud software
Bear in mind that OCRmyPDF is not designed to be secure against malware-bearing PDFs (see `Using OCRmyPDF online`_). Users should ensure they comply with OCRmyPDF's licenses and the licenses of all dependencies. In particular, OCRmyPDF requires Ghostscript, which is licensed under AGPLv3.
+53
View File
@@ -14,6 +14,59 @@ Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and ar
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
v8.0.1
------
- Fixed an exception when parsing PDFs that are missing a required field. `#325 <https://github.com/jbarlow83/OCRmyPDF/issues/325>`_
- pikepdf 1.0.5 is now required, to address some other PDF parsing issues.
v8.0.0
------
No major features. The intent of this release is to sever support for older versions of certain dependencies.
**Breaking changes**
- Dropped support for Tesseract 3.x. Tesseract 4.0 or newer is now required.
- Dropped support for Python 3.5.
- Some ``ocrmypdf.pdfa`` APIs that were deprecated in v7.x were removed. This functionality has been moved to pikepdf.
**Other changes**
- Fixed an unhandled exception when attempting to mask barcodes. `#322 <https://github.com/jbarlow83/OCRmyPDF/issues/322>`_
- It is now possible to use ocrmypdf without pdfminer.six, to support distributions that do not have it or cannot currently use it (e.g. Homebrew). Downstream maintainers should include pdfminer.six if possible.
- A warning is now issue when PDF/A conversion removes some XMP metadata from the input PDF. (Only a "whitelist" of certain XMP metadata types are allowed in PDF/A.)
- Fixed several issues that caused PDF/As to be produced with nonconforming XMP metadata (would fail validation with veraPDF).
- Fixed some instances where invalid DocumentInfo from a PDF cause XMP metadata creation to fail.
- Fixed a few documentation problems.
- pikepdf 1.0.2 is now required.
v7.4.0
------
- ``--force-ocr`` may now be used with the new ``--threshold`` and ``--mask-barcodes`` features
- pikepdf >= 0.9.1 is now required.
- Changed metadata handling to pikepdf 0.9.1. As a result, metadata handling of non-ASCII characters in Ghostscript 9.25 or later is fixed.
- chardet >= 3.0.4 is temporarily listed as required. pdfminer.six depends on it, but the most recent release does not specify this requirement. (`#326 <https://github.com/jbarlow83/OCRmyPDF/issues/326>`_)
- python-xmp-toolkit and libexempi are no longer required.
- A new Docker image is now being provided for users who wish to access OCRmyPDF over a simple HTTP interface, instead of the command line.
- Increase tolerance of PDFs that overflow or underflow the PDF graphics stack. (`#325 <https://github.com/jbarlow83/OCRmyPDF/issues/325>`_)
v7.3.1
------
+70
View File
@@ -0,0 +1,70 @@
complete -c ocrmypdf -l version
complete -c ocrmypdf -l help
complete -c ocrmypdf -l sidecar -r -d "write OCR to text file"
complete -c ocrmypdf -s q -l quiet
complete -c ocrmypdf -s r -l rotate-pages -d "rotate pages to correct orientation"
complete -c ocrmypdf -s d -l deskew -d "fix small horizontal alignment skew"
complete -c ocrmypdf -s c -l clean -d "clean document images before OCR"
complete -c ocrmypdf -s i -l clean-final -d "clean document images and keep result"
complete -c ocrmypdf -l remove-vectors -d "don't send vector objects to OCR"
complete -c ocrmypdf -l mask-barcodes -d "mask barcodes from OCR"
complete -c ocrmypdf -l threshold -d "threshold images before OCR"
complete -c ocrmypdf -s f -l force-ocr -d "OCR documents that already have printable text"
complete -c ocrmypdf -s s -l skip-ocr -d "skip OCR on pages that text, otherwise try OCR"
complete -c ocrmypdf -l redo-ocr -d "redo OCR on any pages that seem to have OCR already"
complete -c ocrmypdf -s k -l keep-temporary-files -d "keep temporary files (debug)"
complete -c ocrmypdf -x -s l -l language -d 'language'
complete -c ocrmypdf -x -s l -l language -a '(tesseract --list-langs)'
complete -c ocrmypdf -x -l image-dpi -d "assume this DPI if input image DPI is unknown"
function __fish_ocrmypdf_output_type
echo -e "pdfa\t"(_ "output a PDF/A (default)")
echo -e "pdf\t"(_ "output a standard PDF")
echo -e "pdfa-1\t"(_ "output a PDF/A-1b")
echo -e "pdfa-2\t"(_ "output a PDF/A-2b")
echo -e "pdfa-3\t"(_ "output a PDF/A-3b")
end
complete -c ocrmypdf -x -l output-type -a '(__fish_ocrmypdf_output_type)' -d "select PDF output options"
function __fish_ocrmypdf_pdf_renderer
echo -e "auto\t"(_ "auto select PDF renderer")
echo -e "hocr\t"(_ "use hocr renderer")
echo -e "sandwich\t"(_ "use sandwich renderer")
end
complete -c ocrmypdf -x -l pdf-render -a '(__fish_ocrmypdf_pdf_renderer)' -d "select PDF renderer options"
function __fish_ocrmypdf_optimize
echo -e "0\t"(_ "do not optimize")
echo -e "1\t"(_ "do safe, lossless optimizations (default)")
echo -e "2\t"(_ "do some lossy optimizations")
echo -e "3\t"(_ "do aggressive lossy optimizations (including lossy JBIG2)")
end
complete -c ocrmypdf -x -s O -l optimize -a '(__fish_ocrmypdf_optimize)' -d "select optimization level"
complete -c ocrmypdf -x -s j -l jobs -d "how many worker processes to use"
complete -c ocrmypdf -x -s v -a '(seq 1 9)'
complete -c ocrmypdf -x -l title -d "set metadata"
complete -c ocrmypdf -x -l author -d "set metadata"
complete -c ocrmypdf -x -l subject -d "set metadata"
complete -c ocrmypdf -x -l keywords -d "set metadata"
complete -c ocrmypdf -x -l oversample -d "oversample images to this DPI"
complete -c ocrmypdf -x -l skip-big -d "skip OCR on pages larger than this many MPixels"
complete -c ocrmypdf -x -l jpeg-quality -d "JPEG quality [0..100]"
complete -c ocrmypdf -x -l png-quality -d "PNG quality [0..100]"
complete -c ocrmypdf -x -l jbig2-lossy -d "enable lossy JBIG2 (see docs)"
complete -c ocrmypdf -x -l max-image-mpixels -d "image decompression bomb threshold"
complete -c ocrmypdf -x -l tesseract-config -d "set custom tesseract config file"
complete -c ocrmypdf -x -l tesseract-pagesegmode -d "set tesseract --psm"
complete -c ocrmypdf -x -l tesseract-oem -d "set tesseract --oem"
complete -c ocrmypdf -x -l tesseract-timeout -d "maximum number of seconds to wait for OCR"
complete -c ocrmypdf -x -l rotate-pages-threshold -d "page rotation confidence"
complete -c ocrmypdf -x -l pdfa-image-compression -a 'auto jpeg lossless' -d "set PDF/A image compression options"
complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf)"
+1 -1
View File
@@ -5,7 +5,7 @@ chardet == 3.0.4
cffi == 1.11.5
img2pdf == 0.3.1
pdfminer.six == 20181108
pikepdf == 0.3.7
pikepdf == 1.0.2
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
pycparser == 2.19
python-xmp-toolkit == 2.0.1
+10 -1
View File
@@ -11,6 +11,15 @@ ignore =
[tool:pytest]
norecursedirs = lib .pc .git output cache resources
testpaths = tests
filterwarnings =
ignore:.*XMLParser.*:DeprecationWarning
[isort]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
[metadata]
license_file = LICENSE
license_file = LICENSE
+19 -20
View File
@@ -20,15 +20,16 @@
from __future__ import print_function, unicode_literals
import sys
if sys.version_info < (3, 5):
print("Python 3.5 or newer is required", file=sys.stderr)
if sys.version_info < (3, 6):
print("Python 3.6 or newer is required", file=sys.stderr)
sys.exit(1)
from setuptools import setup, find_packages # nopep8
from subprocess import STDOUT, check_output, CalledProcessError # nopep8
from collections.abc import Mapping # nopep8
import re # nopep8
from setuptools import setup, find_packages
from subprocess import STDOUT, check_output, CalledProcessError
from collections.abc import Mapping
import re
# pylint: disable=w0613
missing_program = '''
The program '{program}' could not be executed or was not found on your
@@ -131,12 +132,12 @@ def check_external_program(
program,
need_version,
package,
version_check_args=['--version'],
version_check_args=None,
version_scrape_regex=re.compile(r'(\d+\.\d+(?:\.\d+)?)'),
optional=False):
print('Checking for {program} >= {need_version}...'.format(
program=program, need_version=need_version))
if not version_check_args:
version_check_args = ['--version']
print(f'Checking for {program} >= {need_version}...')
try:
result = check_output(
[program] + version_check_args,
@@ -145,7 +146,7 @@ def check_external_program(
error_missing_program(program, package, optional)
if not optional:
sys.exit(1)
print('Continuing install without {program}'.format(program=program))
print(f'Continuing install without {program}')
return
try:
@@ -158,8 +159,7 @@ def check_external_program(
error_old_version(program, package, optional, need_version,
found_version)
print('Found {program} {found_version}'.format(
program=program, found_version=found_version))
print(f'Found {program} {found_version}')
command = next((arg for arg in sys.argv[1:] if not arg.startswith('-')), '')
@@ -170,7 +170,7 @@ if not forced and command.startswith('install') or \
command in ['check', 'test', 'nosetests', 'easy_install']:
check_external_program(
program='tesseract',
need_version='3.04', # using backport for Travis CI
need_version='4.0.0', # using backport for Travis CI
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}
)
check_external_program(
@@ -219,7 +219,6 @@ setup(
package_dir={'': 'src'},
keywords=['PDF', 'OCR', 'optical character recognition', 'PDF/A', 'scanning'],
classifiers=[
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Development Status :: 5 - Production/Stable",
@@ -236,7 +235,7 @@ setup(
"Topic :: Text Processing :: Indexing",
"Topic :: Text Processing :: Linguistic",
],
python_requires=' >= 3.5',
python_requires=' >= 3.6',
setup_requires=[
'cffi >= 1.9.1', # to build the leptonica module
'pytest-runner', # to enable python setup.py test
@@ -248,19 +247,19 @@ setup(
'src/ocrmypdf/lib/compile_leptonica.py:ffibuilder'
],
install_requires=[
'chardet >= 3.0.4, < 4', # unlisted requirement of pdfminer.six 20181108
'cffi >= 1.9.1', # must be a setup and install requirement
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
'pdfminer.six == 20181108',
'pikepdf >= 0.3.7, < 0.4',
'pdfminer.six == 20181108 ; sys_platform != "darwin"',
'pikepdf >= 1.0.5, < 2',
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
# block 5.1.0, broken wheels
'python-xmp-toolkit >= 2, < 3',
'reportlab >= 3.3.0', # oldest released version with sane image handling
'ruffus >= 2.7.0',
],
extras_require={
'fitz': [], # Backward compatibility
'pdfminer': ['pdfminer.six == 20181108'],
},
tests_require=tests_require,
entry_points={
+12 -4
View File
@@ -25,10 +25,18 @@ __version__ = pkg_resources.get_distribution('ocrmypdf').version
VERSION = __version__
from .exceptions import (
ExitCode, BadArgsError, PdfMergeFailedError, MissingDependencyError,
UnsupportedImageFormatError, DpiError, OutputFileAccessError,
PriorOcrFoundError, InputFileError, SubprocessOutputError,
EncryptedPdfError, TesseractConfigError
ExitCode,
BadArgsError,
PdfMergeFailedError,
MissingDependencyError,
UnsupportedImageFormatError,
DpiError,
OutputFileAccessError,
PriorOcrFoundError,
InputFileError,
SubprocessOutputError,
EncryptedPdfError,
TesseractConfigError,
)
from . import helpers
+397 -291
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -15,10 +15,10 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import shutil
import sys
from contextlib import suppress
from multiprocessing.managers import SyncManager
import sys
import shutil
from .pdfinfo import PdfInfo
@@ -77,8 +77,7 @@ class JobContextManager(SyncManager):
def cleanup_working_files(work_folder, options):
if options.keep_temporary_files:
print("Temporary working files saved at:\n{0}".format(work_folder),
file=sys.stderr)
print(f"Temporary working files saved at:\n{work_folder}", file=sys.stderr)
else:
with suppress(FileNotFoundError):
shutil.rmtree(work_folder)
+355 -350
View File
File diff suppressed because it is too large Load Diff
+11 -6
View File
@@ -33,9 +33,9 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import codecs
import os
import sys
import codecs
def verify_python3_env(): # pragma: no cover
@@ -47,6 +47,7 @@ def verify_python3_env(): # pragma: no cover
try:
import locale
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
except Exception:
fs_enc = 'ascii'
@@ -56,8 +57,10 @@ def verify_python3_env(): # pragma: no cover
extra = ''
if os.name == 'posix':
import subprocess
rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE).communicate()[0]
rv = subprocess.Popen(
['locale', '-a'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
).communicate()[0]
good_locales = set()
has_c_utf8 = False
@@ -108,6 +111,8 @@ def verify_python3_env(): # pragma: no cover
'is not supported'
) % bad_locale
raise RuntimeError('ocrmypdf will abort further execution because Python 3 '
'was configured to use ASCII as encoding for the '
'environment.' + extra)
raise RuntimeError(
'ocrmypdf will abort further execution because Python 3 '
'was configured to use ASCII as encoding for the '
'environment.' + extra
)
+33 -38
View File
@@ -15,13 +15,13 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from pathlib import Path
from itertools import groupby
from pathlib import Path
import pikepdf
from .helpers import flatten_groups, page_number
from .exec import tesseract
from .helpers import flatten_groups, page_number
def _update_page_resources(*, page, font, font_key, procset):
@@ -89,8 +89,8 @@ def strip_invisible_text(pdf, page, log):
def _weave_layers_graft(
*, pdf_base, page_num, text, font, font_key, procset, rotation,
strip_old_text, log):
*, pdf_base, page_num, text, font, font_key, procset, rotation, strip_old_text, log
):
"""Insert the text layer from text page 0 on to pdf_base at page_num"""
log.debug("Grafting")
@@ -108,7 +108,7 @@ def _weave_layers_graft(
stream = bytearray(pdf_text_contents)
pattern = b'/Im1 Do'
idx = stream.find(pattern)
stream[idx:(idx + len(pattern))] = b' ' * len(pattern)
stream[idx : (idx + len(pattern))] = b' ' * len(pattern)
pdf_text_contents = bytes(stream)
base_page = pdf_base.pages.p(page_num)
@@ -117,12 +117,10 @@ def _weave_layers_graft(
# content may have a rotation applied. Wrap the text stream with a rotation
# so it will be oriented the same way as the rest of the page content.
# (Previous versions OCRmyPDF rotated the content layer to match the text.)
mediabox = [float(pdf_text.pages[0].MediaBox[v])
for v in range(4)]
mediabox = [float(pdf_text.pages[0].MediaBox[v]) for v in range(4)]
wt, ht = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
mediabox = [float(base_page.MediaBox[v])
for v in range(4)]
mediabox = [float(base_page.MediaBox[v]) for v in range(4)]
wp, hp = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
translate = pikepdf.PdfMatrix().translated(-wt / 2, -ht / 2)
@@ -147,11 +145,7 @@ def _weave_layers_graft(
# for a size different between initial and text PDF, then untranslate
ctm = translate @ rotate @ scale @ untranslate
pdf_text_contents = (
b'q %s cm\n' % ctm.encode() +
pdf_text_contents +
b'\nQ\n'
)
pdf_text_contents = b'q %s cm\n' % ctm.encode() + pdf_text_contents + b'\nQ\n'
new_text_layer = pikepdf.Stream(pdf_base, pdf_text_contents)
@@ -254,8 +248,7 @@ def _fix_toc(pdf_base, pageref_remap, log):
if not isinstance(dest_node, pikepdf.Array):
return
pageref = dest_node[0]
if pageref['/Type'] == '/Page' and \
pageref.objgen in pageref_remap:
if pageref['/Type'] == '/Page' and pageref.objgen in pageref_remap:
new_objgen = pageref_remap[pageref.objgen]
dest_node[0] = pdf_base.get_object(new_objgen)
@@ -278,11 +271,7 @@ def _fix_toc(pdf_base, pageref_remap, log):
_traverse_toc(pdf_base, visit_remap_dest, log)
def weave_layers(
infiles,
output_file,
log,
context):
def weave_layers(infiles, output_file, log, context):
"""Apply text layer and/or image layer changes to baseline file
This is where the magic happens. infiles will be the main PDF to modify,
@@ -313,6 +302,7 @@ def weave_layers(
return page_number(key)
except ValueError:
return -1
flat_inputs = sorted(flatten_groups(infiles), key=input_sorter)
groups = groupby(flat_inputs, key=input_sorter)
@@ -333,7 +323,8 @@ def weave_layers(
_traverse_toc(pdf_base, None, log)
procset = pdf_base.make_indirect(
pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]'))
pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]')
)
# Iterate rest
for page_num, layers in groups:
@@ -341,12 +332,8 @@ def weave_layers(
log.debug(page_num)
log.debug(layers)
text = next(
(ii for ii in layers if ii.endswith('.text.pdf')), None
)
image = next(
(ii for ii in layers if ii.endswith('.image-layer.pdf')), None
)
text = next((ii for ii in layers if ii.endswith('.text.pdf')), None)
image = next((ii for ii in layers if ii.endswith('.image-layer.pdf')), None)
if text and not font:
font, font_key = _find_font(text, pdf_base)
@@ -378,23 +365,30 @@ def weave_layers(
content_rotation = autorotate_correction
text_rotation = autorotate_correction
text_misaligned = (text_rotation - content_rotation) % 360
log.debug('%r', [
text_rotation, autorotate_correction, text_misaligned,
content_rotation]
log.debug(
'%r',
[text_rotation, autorotate_correction, text_misaligned, content_rotation],
)
if text and font:
# Graft the text layer onto this page, whether new or old
strip_old = context.get_options().redo_ocr
_weave_layers_graft(
pdf_base=pdf_base, page_num=page_num, text=text, font=font,
font_key=font_key, rotation=text_misaligned, procset=procset,
strip_old_text=strip_old, log=log
pdf_base=pdf_base,
page_num=page_num,
text=text,
font=font,
font_key=font_key,
rotation=text_misaligned,
procset=procset,
strip_old_text=strip_old,
log=log,
)
# Correct the rotation if applicable
pdf_base.pages[page_num - 1].Rotate = \
(content_rotation - autorotate_correction) % 360
pdf_base.pages[page_num - 1].Rotate = (
content_rotation - autorotate_correction
) % 360
if len(keep_open) > 100:
# qpdf limitations require us to keep files open when we intend
@@ -404,8 +398,9 @@ def weave_layers(
# even if page 1 doesn't use it, so we have a way to get it back.
page0 = pdf_base.pages[0]
_update_page_resources(
page=page0, font=font, font_key=font_key, procset=procset)
interim = output_file + '_working{}.pdf'.format(page_num)
page=page0, font=font, font_key=font_key, procset=procset
)
interim = output_file + f'_working{page_num}.pdf'
pdf_base.save(interim)
del pdf_base
keep_open = []
+10 -4
View File
@@ -19,6 +19,7 @@
from enum import IntEnum
from textwrap import dedent
class ExitCode(IntEnum):
ok = 0
bad_args = 1
@@ -52,7 +53,8 @@ class BadArgsError(ExitCodeException):
class PdfMergeFailedError(ExitCodeException):
exit_code = ExitCode.input_file
message = dedent('''\
message = dedent(
'''\
Failed to merge PDF image layer with OCR layer
Usually this happens because the input PDF file is malformed and
@@ -60,7 +62,9 @@ class PdfMergeFailedError(ExitCodeException):
Try using
ocrmypdf --pdf-renderer sandwich [..other args..]
''')
'''
)
class MissingDependencyError(ExitCodeException):
exit_code = ExitCode.missing_dependency
@@ -92,7 +96,8 @@ class SubprocessOutputError(ExitCodeException):
class EncryptedPdfError(ExitCodeException):
exit_code = ExitCode.encrypted_pdf
message = dedent('''\
message = dedent(
'''\
Input PDF is encrypted. The encryption must be removed to
perform OCR.
@@ -101,7 +106,8 @@ class EncryptedPdfError(ExitCodeException):
You can remove the encryption using
qpdf --decrypt [--password=[password]] infilename
''')
'''
)
class TesseractConfigError(ExitCodeException):
+17 -16
View File
@@ -24,36 +24,37 @@ from subprocess import run, STDOUT, PIPE, CalledProcessError
from ..exceptions import MissingDependencyError
def get_version(program, *,
version_arg='--version', regex=r'(\d+(\.\d+)*)'):
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)'):
"Get the version of the specified program"
args_prog = [
program,
version_arg
]
args_prog = [program, version_arg]
try:
proc = run(
args_prog, close_fds=True, universal_newlines=True,
stdout=PIPE, stderr=STDOUT, check=True)
args_prog,
close_fds=True,
universal_newlines=True,
stdout=PIPE,
stderr=STDOUT,
check=True,
)
output = proc.stdout
except FileNotFoundError as e:
raise MissingDependencyError(
"Could not find program '{}' on the PATH".format(
program)) from e
f"Could not find program '{program}' on the PATH"
) from e
except CalledProcessError as e:
if e.returncode < 0:
raise MissingDependencyError(
"Ran program '{}' but it exited with an error:\n{}".format(
program, e.output)) from e
f"Ran program '{program}' but it exited with an error:\n{e.output}"
) from e
raise MissingDependencyError(
"Could not find program '{}' on the PATH".format(
program)) from e
f"Could not find program '{program}' on the PATH"
) from e
try:
version = re.match(regex, output.strip()).group(1)
except AttributeError as e:
raise MissingDependencyError(
("The program '{}' did not report its version. "
"Message was:\n{}").format(program, output)
f"The program '{program}' did not report its version. "
f"Message was:\n{output}"
)
return version
+94 -69
View File
@@ -15,15 +15,17 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from tempfile import NamedTemporaryFile
from subprocess import run, PIPE, STDOUT
from shutil import copy
from functools import lru_cache
import re
from functools import lru_cache
from os import fspath
from shutil import copy
from subprocess import PIPE, STDOUT, run
from tempfile import NamedTemporaryFile
from PIL import Image
from . import get_version
from ..exceptions import SubprocessOutputError
from ..helpers import fspath
@lru_cache(maxsize=1)
@@ -69,39 +71,46 @@ def extract_text(input_file, pageno=1):
"""
if pageno is not None:
pages = [
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno
]
pages = ['-dFirstPage=%i' % pageno, '-dLastPage=%i' % pageno]
else:
pages = []
args_gs = [
'gs',
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
'-sDEVICE=txtwrite',
'-dTextFormat=0',
] + pages + [
'-o', '-',
fspath(input_file)
]
args_gs = (
[
'gs',
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
'-sDEVICE=txtwrite',
'-dTextFormat=0',
]
+ pages
+ ['-o', '-', fspath(input_file)]
)
p = run(args_gs, stdout=PIPE, stderr=PIPE)
if p.returncode != 0:
raise SubprocessOutputError(
'Ghostscript text extraction failed\n%s\n%s\n%s' % (
input_file, p.stdout.decode(), p.stderr.decode()
)
'Ghostscript text extraction failed\n%s\n%s\n%s'
% (input_file, p.stdout.decode(), p.stderr.decode())
)
return p.stdout
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
pageno=1, page_dpi=None, rotation=None, filter_vector=False):
def rasterize_pdf(
input_file,
output_file,
xres,
yres,
raster_device,
log,
pageno=1,
page_dpi=None,
rotation=None,
filter_vector=False,
):
"""Rasterize one page of a PDF at resolution (xres, yres) in canvas units.
The image is sized to match the integer pixels dimensions implied by
@@ -126,26 +135,30 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
page_dpi = res
with NamedTemporaryFile(delete=True) as tmp:
args_gs = [
'gs',
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
'-sDEVICE=%s' % raster_device,
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno,
'-r{0}x{1}'.format(str(int_res[0]), str(int_res[1])),
] + (['-dFILTERVECTOR'] if filter_vector else []) + [
'-o', tmp.name,
'-dAutoRotatePages=/None', # Probably has no effect on raster
'-f',
fspath(input_file)
]
args_gs = (
[
'gs',
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
f'-sDEVICE={raster_device}',
f'-dFirstPage={pageno}',
f'-dLastPage={pageno}',
f'-r{str(int_res[0])}x{str(int_res[1])}',
]
+ (['-dFILTERVECTOR'] if filter_vector else [])
+ [
'-o',
tmp.name,
'-dAutoRotatePages=/None', # Probably has no effect on raster
'-f',
fspath(input_file),
]
)
log.debug(args_gs)
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
universal_newlines=True)
p = run(args_gs, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
if _gs_error_reported(p.stdout):
log.error(p.stdout)
else:
@@ -162,12 +175,14 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
tmp.seek(0)
with Image.open(tmp) as im:
expected_size = round(im.size[0] / int_res[0] * res[0]), \
round(im.size[1] / int_res[1] * res[1])
expected_size = (
round(im.size[0] / int_res[0] * res[0]),
round(im.size[1] / int_res[1] * res[1]),
)
if expected_size != im.size or page_dpi != (xres, yres):
log.debug(
"Ghostscript: resize output image {} -> {}".format(
im.size, expected_size))
f"Ghostscript: resize output image {im.size} -> {expected_size}"
)
im = im.resize(expected_size)
if rotation is not None:
@@ -185,8 +200,15 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
im.save(fspath(output_file), dpi=page_dpi)
def generate_pdfa(pdf_pages, output_file, compression, log,
threads=1, pdf_version='1.5', pdfa_part='2'):
def generate_pdfa(
pdf_pages,
output_file,
compression,
log,
threads=1,
pdf_version='1.5',
pdfa_part='2',
):
"""Generate a PDF/A.
The pdf_pages, a list files, will be merged into output_file. One or more
@@ -240,26 +262,29 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
# nb no need to specify ProcessColorModel when ColorConversionStrategy
# is set; see:
# https://bugs.ghostscript.com/show_bug.cgi?id=699392
args_gs = [
"gs",
"-dQUIET",
"-dBATCH",
"-dNOPAUSE",
"-dCompatibilityLevel=" + str(pdf_version),
"-dNumRenderingThreads=" + str(threads),
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=" + strategy
] + compression_args + [
"-dJPEGQ=95",
"-dPDFA=" + pdfa_part,
"-dPDFACompatibilityPolicy=1",
"-sOutputFile=" + gs_pdf.name,
]
args_gs = (
[
"gs",
"-dQUIET",
"-dBATCH",
"-dNOPAUSE",
"-dCompatibilityLevel=" + str(pdf_version),
"-dNumRenderingThreads=" + str(threads),
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=" + strategy,
]
+ compression_args
+ [
"-dJPEGQ=95",
"-dPDFA=" + pdfa_part,
"-dPDFACompatibilityPolicy=1",
"-sOutputFile=" + gs_pdf.name,
]
)
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
log.debug(args_gs)
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
universal_newlines=True)
p = run(args_gs, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
if _gs_error_reported(p.stdout):
log.error(p.stdout)
@@ -270,7 +295,7 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
log.debug(
"Ghostscript had to remove PDF 'overprinting' from the "
"input file to complete PDF/A conversion. "
)
)
else:
log.debug(p.stdout)
+3 -7
View File
@@ -15,8 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from subprocess import run, PIPE
from functools import lru_cache
from subprocess import PIPE, run
from . import get_version
from ..exceptions import MissingDependencyError
@@ -42,7 +42,7 @@ def convert_group(*, cwd, infiles, out_prefix):
out_prefix,
'-s', # symbol mode (lossy)
# '-r', # refinement mode (lossless symbol mode, currently disabled in
# jbig2)
# jbig2)
'-p',
]
args.extend(infiles)
@@ -52,11 +52,7 @@ def convert_group(*, cwd, infiles, out_prefix):
def convert_single(*, cwd, infile, outfile):
args = [
'jbig2',
'-p',
infile
]
args = ['jbig2', '-p', infile]
with open(outfile, 'wb') as fstdout:
proc = run(args, cwd=cwd, stdout=fstdout, stderr=PIPE)
proc.check_returncode()
+6 -4
View File
@@ -15,8 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from subprocess import run
from functools import lru_cache
from subprocess import run
from . import get_version
from ..exceptions import MissingDependencyError
@@ -40,10 +40,12 @@ def quantize(input_file, output_file, quality_min, quality_max):
'pngquant',
'--force',
'--skip-if-larger',
'--output', output_file,
'--quality', '{}-{}'.format(quality_min, quality_max),
'--output',
output_file,
'--quality',
f'{quality_min}-{quality_max}',
'--',
input_file
input_file,
]
proc = run(args)
proc.check_returncode()
+6 -12
View File
@@ -15,11 +15,11 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from subprocess import CalledProcessError, STDOUT, PIPE, run
from functools import lru_cache
from os import fspath
from subprocess import PIPE, STDOUT, CalledProcessError, run
from . import get_version
from ..helpers import fspath
from . import get_version
@lru_cache(maxsize=1)
@@ -28,22 +28,16 @@ def version():
def check(input_file, log=None):
args_qpdf = [
'qpdf',
'--check',
fspath(input_file)
]
args_qpdf = ['qpdf', '--check', fspath(input_file)]
if log is None:
import logging as log
try:
run(args_qpdf, stderr=STDOUT, stdout=PIPE, universal_newlines=True,
check=True)
run(args_qpdf, stderr=STDOUT, stdout=PIPE, universal_newlines=True, check=True)
except CalledProcessError as e:
if e.returncode == 2:
log.error("%s: not a valid PDF, and could not repair it.",
input_file)
log.error("%s: not a valid PDF, and could not repair it.", input_file)
log.error("Details:")
log.error(e.output)
elif e.returncode == 3:
+80 -92
View File
@@ -15,22 +15,28 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import shutil
from functools import lru_cache
import sys
from collections import namedtuple
from textwrap import dedent
from subprocess import CalledProcessError, TimeoutExpired, check_output, STDOUT, run, PIPE
from contextlib import suppress
from functools import lru_cache
from os import fspath
from subprocess import (
PIPE,
STDOUT,
CalledProcessError,
TimeoutExpired,
check_output,
run,
)
from textwrap import dedent
from ..exceptions import MissingDependencyError, TesseractConfigError
from ..helpers import page_number, fspath
from . import get_version
from ..exceptions import MissingDependencyError, TesseractConfigError
from ..helpers import page_number
OrientationConfidence = namedtuple(
'OrientationConfidence',
('angle', 'confidence'))
OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence'))
HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
@@ -39,8 +45,8 @@ HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name='ocr-system' content='tesseract 3.05.00' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
<meta name='ocr-system' content='tesseract 4.0.0' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "_blank.png"; bbox 0 0 {0} {1}; ppageno 0'>
@@ -64,50 +70,37 @@ def v4():
def has_textonly_pdf():
"""Does Tesseract have textonly_pdf capability?
Available in 3.05.01, and v4.00.00alpha since January 2017. Best to
Available in v4.00.00alpha since January 2017. Best to
parse the parameter list
"""
args_tess = [
'tesseract',
'--print-parameters',
'pdf'
]
args_tess = ['tesseract', '--print-parameters', 'pdf']
params = ''
try:
params = check_output(
args_tess, universal_newlines=True, stderr=STDOUT)
params = check_output(args_tess, universal_newlines=True, stderr=STDOUT)
except CalledProcessError as e:
print("Could not --print-parameters from tesseract",
file=sys.stderr)
print("Could not --print-parameters from tesseract", file=sys.stderr)
raise MissingDependencyError from e
if 'textonly_pdf' in params:
return True
return False
def psm():
"If Tesseract 4.0, use argument --psm instead of -psm"
return '--psm' if v4() else '-psm'
@lru_cache(maxsize=1)
def languages():
def lang_error(output):
msg = dedent("""Tesseract failed to report available languages.
msg = dedent(
"""Tesseract failed to report available languages.
Output from Tesseract:
-----------
""")
"""
)
msg += output
print(msg, file=sys.stderr)
args_tess = [
'tesseract',
'--list-langs'
]
args_tess = ['tesseract', '--list-langs']
try:
proc = run(
args_tess, universal_newlines=True, stdout=PIPE, stderr=STDOUT,
check=True
args_tess, universal_newlines=True, stdout=PIPE, stderr=STDOUT, check=True
)
output = proc.stdout
except CalledProcessError as e:
@@ -122,9 +115,7 @@ def languages():
def tess_base_args(langs, engine_mode):
args = [
'tesseract',
]
args = ['tesseract']
if langs:
args.extend(['-l', '+'.join(langs)])
if engine_mode is not None and v4():
@@ -134,20 +125,22 @@ def tess_base_args(langs, engine_mode):
def get_orientation(input_file, engine_mode, timeout: float, log):
args_tesseract = tess_base_args(['osd'], engine_mode) + [
psm(), '0',
'--psm',
'0',
fspath(input_file),
'stdout'
'stdout',
]
try:
stdout = check_output(
args_tesseract, stderr=STDOUT, timeout=timeout)
stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout)
except TimeoutExpired:
return OrientationConfidence(angle=0, confidence=0.0)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
if (b'Too few characters. Skipping this page' in e.output or
b'Image too large' in e.output):
if (
b'Too few characters. Skipping this page' in e.output
or b'Image too large' in e.output
):
return OrientationConfidence(0, 0)
raise e from e
else:
@@ -159,32 +152,24 @@ def get_orientation(input_file, engine_mode, timeout: float, log):
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
# reports "Orientation in degrees" as a clockwise angle
assert 'Rotate' in osd
oc = OrientationConfidence(
angle=angle,
confidence=float(osd.get('Orientation confidence', 0)))
angle=angle, confidence=float(osd.get('Orientation confidence', 0))
)
return oc
def tesseract_log_output(log, stdout, input_file):
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
prefix = f"{(page_number(input_file)):4d}: [tesseract] "
try:
text = stdout.decode()
except UnicodeDecodeError:
log.error(prefix + "command line output was not utf-8. " +
"This usually means Tesseract's language packs do not match "
"the installed version of Tesseract.")
log.error(
prefix
+ "command line output was not utf-8. "
+ "This usually means Tesseract's language packs do not match "
"the installed version of Tesseract."
)
text = stdout.decode('utf-8', 'backslashreplace')
lines = text.splitlines()
@@ -216,7 +201,7 @@ def tesseract_log_output(log, stdout, input_file):
def page_timedout(log, input_file):
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
prefix = f"{(page_number(input_file)):4d}: [tesseract] "
log.warning(prefix + " took too long to OCR - skipping")
@@ -234,10 +219,18 @@ def _generate_null_hocr(output_hocr, output_sidecar, image):
f.write('[skipped page]')
def generate_hocr(input_file, output_files, language: list, engine_mode,
tessconfig: list,
timeout: float, pagesegmode: int, user_words, user_patterns,
log):
def generate_hocr(
input_file,
output_files,
language: list,
engine_mode,
tessconfig: list,
timeout: float,
pagesegmode: int,
user_words,
user_patterns,
log,
):
output_hocr = next(o for o in output_files if o.endswith('.hocr'))
output_sidecar = next(o for o in output_files if o.endswith('.txt'))
@@ -246,7 +239,7 @@ def generate_hocr(input_file, output_files, language: list, engine_mode,
args_tesseract = tess_base_args(language, engine_mode)
if pagesegmode is not None:
args_tesseract.extend([psm(), str(pagesegmode)])
args_tesseract.extend(['--psm', str(pagesegmode)])
if user_words:
args_tesseract.extend(['--user-words', user_words])
@@ -256,20 +249,10 @@ def generate_hocr(input_file, output_files, language: list, engine_mode,
# Reminder: test suite tesseract spoofers will break after any changes
# to the number of order parameters here
# Tesseract 3.04 requires the order here to be "hocr txt" and will fail
# on "txt hocr"
args_tesseract.extend([
input_file,
prefix,
'hocr',
'txt'
] + tessconfig)
args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig)
try:
log.debug(args_tesseract)
stdout = check_output(
args_tesseract, stderr=STDOUT,
timeout=timeout)
stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout)
except TimeoutExpired:
# Generate a HOCR file with no recognized text if tesseract times out
# Temporary workaround to hocrTransform not being able to function if
@@ -307,10 +290,22 @@ def use_skip_page(text_only, skip_pdf, output_pdf, output_text):
out.write(b'')
def generate_pdf(*, input_image, skip_pdf=None, output_pdf, output_text,
language: list, engine_mode, text_only: bool,
tessconfig: list, timeout: float, pagesegmode: int,
user_words, user_patterns, log):
def generate_pdf(
*,
input_image,
skip_pdf=None,
output_pdf,
output_text,
language: list,
engine_mode,
text_only: bool,
tessconfig: list,
timeout: float,
pagesegmode: int,
user_words,
user_patterns,
log,
):
'''Use Tesseract to render a PDF.
input_image -- image to analyze
@@ -328,7 +323,7 @@ def generate_pdf(*, input_image, skip_pdf=None, output_pdf, output_text,
args_tesseract = tess_base_args(language, engine_mode)
if pagesegmode is not None:
args_tesseract.extend([psm(), str(pagesegmode)])
args_tesseract.extend(['--psm', str(pagesegmode)])
if text_only and has_textonly_pdf():
args_tesseract.extend(['-c', 'textonly_pdf=1'])
@@ -344,18 +339,11 @@ def generate_pdf(*, input_image, skip_pdf=None, output_pdf, output_text,
# Reminder: test suite tesseract spoofers might break after any changes
# to the number of order parameters here
args_tesseract.extend([
input_image,
prefix,
'pdf',
'txt'
] + tessconfig)
args_tesseract.extend([input_image, prefix, 'pdf', 'txt'] + tessconfig)
try:
log.debug(args_tesseract)
stdout = check_output(
args_tesseract, stderr=STDOUT,
timeout=timeout)
stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout)
if os.path.exists(prefix + '.txt'):
shutil.move(prefix + '.txt', output_text)
except TimeoutExpired:
+28 -24
View File
@@ -18,14 +18,14 @@
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
from subprocess import CalledProcessError, STDOUT, check_output
from tempfile import NamedTemporaryFile
import sys
import os
import sys
from functools import lru_cache
from ..exceptions import MissingDependencyError
from . import get_version
from subprocess import STDOUT, CalledProcessError, check_output
from tempfile import NamedTemporaryFile
from . import get_version
from ..exceptions import MissingDependencyError
try:
from PIL import Image
@@ -40,11 +40,7 @@ def version():
def run(input_file, output_file, dpi, log, mode_args):
args_unpaper = [
'unpaper',
'-v',
'--dpi', str(dpi)
] + mode_args
args_unpaper = ['unpaper', '-v', '--dpi', str(dpi)] + mode_args
SUFFIXES = {'1': '.pbm', 'L': '.pgm', 'RGB': '.ppm'}
@@ -68,8 +64,9 @@ def run(input_file, output_file, dpi, log, mode_args):
im.close()
raise MissingDependencyError() from e
with NamedTemporaryFile(suffix=suffix) as input_pnm, \
NamedTemporaryFile(suffix=suffix, mode="r+b") as output_pnm:
with NamedTemporaryFile(suffix=suffix) as input_pnm, NamedTemporaryFile(
suffix=suffix, mode="r+b"
) as output_pnm:
im.save(input_pnm, format='PPM')
im.close()
@@ -78,9 +75,8 @@ def run(input_file, output_file, dpi, log, mode_args):
args_unpaper.extend([input_pnm.name, output_pnm.name])
try:
stdout = check_output(
args_unpaper, close_fds=True,
universal_newlines=True, stderr=STDOUT,
)
args_unpaper, close_fds=True, universal_newlines=True, stderr=STDOUT
)
except CalledProcessError as e:
log.debug(e.output)
raise e from e
@@ -91,12 +87,20 @@ def run(input_file, output_file, dpi, log, mode_args):
def clean(input_file, output_file, dpi, log):
run(input_file, output_file, dpi, log, [
'--layout', 'none',
'--mask-scan-size', '100', # don't blank out narrow columns
'--no-border-align', # don't align visible content to borders
'--no-mask-center', # don't center visible content within page
'--no-grayfilter', # don't remove light gray areas
'--no-blackfilter', # don't remove solid black areas
'--no-deskew', # don't deskew
])
run(
input_file,
output_file,
dpi,
log,
[
'--layout',
'none',
'--mask-scan-size',
'100', # don't blank out narrow columns
'--no-border-align', # don't align visible content to borders
'--no-mask-center', # don't center visible content within page
'--no-grayfilter', # don't remove light gray areas
'--no-blackfilter', # don't remove solid black areas
'--no-deskew', # don't deskew
],
)
+40 -63
View File
@@ -15,22 +15,22 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from functools import partial
import multiprocessing
import os
import sys
import warnings
from collections.abc import Iterable
from contextlib import suppress
from functools import partial, wraps
from pathlib import Path
import sys
import os
import multiprocessing
import warnings
def re_symlink(input_file, soft_link_name, log=None):
"""
Helper function: relinks soft symbolic link if necessary
"""
input_file = fspath(input_file) # For Py3.5
soft_link_name = fspath(soft_link_name)
input_file = os.fspath(input_file)
soft_link_name = os.fspath(soft_link_name)
if log is None:
prdebug = partial(print, file=sys.stderr)
else:
@@ -38,32 +38,29 @@ def re_symlink(input_file, soft_link_name, log=None):
# Guard against soft linking to oneself
if input_file == soft_link_name:
prdebug("Warning: No symbolic link made. You are using " +
"the original data directory as the working directory.")
prdebug(
"Warning: No symbolic link made. You are using "
+ "the original data directory as the working directory."
)
return
# Soft link already exists: delete for relink?
if os.path.lexists(soft_link_name):
# do not delete or overwrite real (non-soft link) file
if not os.path.islink(soft_link_name):
raise FileExistsError(
"%s exists and is not a link" % soft_link_name)
raise FileExistsError("%s exists and is not a link" % soft_link_name)
try:
os.unlink(soft_link_name)
except OSError:
prdebug("Can't unlink %s" % (soft_link_name))
if not os.path.exists(input_file):
raise FileNotFoundError(
"trying to create a broken symlink to %s" % input_file)
raise FileNotFoundError("trying to create a broken symlink to %s" % input_file)
prdebug("os.symlink(%s, %s)" % (input_file, soft_link_name))
# Create symbolic link using absolute path
os.symlink(
os.path.abspath(input_file),
soft_link_name
)
os.symlink(os.path.abspath(input_file), soft_link_name)
def is_iterable_notstr(thing):
@@ -72,7 +69,7 @@ def is_iterable_notstr(thing):
def page_number(input_file):
"""Get one-based page number implied by filename (000002.pdf -> 2)"""
return int(os.path.basename(fspath(input_file))[0:6])
return int(os.path.basename(os.fspath(input_file))[0:6])
def available_cpu_count():
@@ -83,13 +80,14 @@ def available_cpu_count():
try:
import psutil
return psutil.cpu_count()
except (ImportError, AttributeError):
pass
warnings.warn(
"Could not get CPU count. Assuming one (1) CPU."
"Use -j N to set manually.")
"Could not get CPU count. Assuming one (1) CPU." "Use -j N to set manually."
)
return 1
@@ -103,20 +101,15 @@ def is_file_writable(test_file):
p = Path(test_file)
if p.is_symlink():
# Python 3.5 does not accept parameters for Path.resolve() and behaves
# as if strict=True (throws an exception on failure). Python 3.6
# defaults to strict=False. This implements strict=False like behavior
# for Python 3.5.
if sys.version_info[0:2] <= (3, 5):
p = Path(os.path.realpath(fspath(p)))
else:
p = p.resolve(strict=False)
p = p.resolve(strict=False)
# p.is_file() throws an exception in some cases
if p.exists() and p.is_file():
return os.access(
fspath(p), os.W_OK,
effective_ids=(os.access in os.supports_effective_ids))
os.fspath(p),
os.W_OK,
effective_ids=(os.access in os.supports_effective_ids),
)
else:
try:
fp = p.open('wb')
@@ -129,42 +122,26 @@ def is_file_writable(test_file):
return True
if sys.version_info[0:2] <= (3, 5):
def fspath(path):
"""https://www.python.org/dev/peps/pep-0519/#os"""
import pathlib
if isinstance(path, (str, bytes)):
return path
# Work from the object's type to match method resolution of other magic
# methods.
path_type = type(path)
try:
path = path_type.__fspath__(path)
except AttributeError:
# Added for Python 3.5 support.
if isinstance(path, pathlib.Path):
return str(path)
elif hasattr(path_type, '__fspath__'):
raise
else:
if isinstance(path, (str, bytes)):
return path
else:
raise TypeError("expected __fspath__() to return str or bytes, "
"not " + type(path).__name__)
raise TypeError(
"expected str, bytes, pathlib.Path or os.PathLike object, not "
+ path_type.__name__)
else:
fspath = os.fspath
def flatten_groups(groups):
for obj in groups:
if is_iterable_notstr(obj):
yield from obj
else:
yield obj
def deprecated(func):
"""Warn that function is deprecated"""
@wraps(func)
def new_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # turn off filter
warnings.warn(
"Call to deprecated function {}.".format(func.__name__),
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter('default', DeprecationWarning) # reset filter
return func(*args, **kwargs)
return new_func
+113 -73
View File
@@ -28,14 +28,14 @@
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.units import inch
from xml.etree import ElementTree
from collections import namedtuple
from math import atan, sin, cos
import re
import argparse
import re
from collections import namedtuple
from math import atan, cos, sin
from xml.etree import ElementTree
from reportlab.lib.units import inch
from reportlab.pdfgen.canvas import Canvas
Rect = namedtuple('Rect', ['x1', 'y1', 'x2', 'y2'])
@@ -44,7 +44,7 @@ class HocrTransformError(Exception):
pass
class HocrTransform():
class HocrTransform:
"""
A class for converting documents from the hOCR format.
@@ -53,17 +53,16 @@ class HocrTransform():
"""
box_pattern = re.compile(r'bbox((\s+\d+){4})')
baseline_pattern = re.compile(r'''
baseline_pattern = re.compile(
r'''
baseline \s+
([\-\+]?\d*\.?\d*) \s+ # +/- decimal float
([\-\+]?\d+) # +/- int''', re.VERBOSE)
ligatures = str.maketrans({
'': 'ff',
'': 'ffi',
'': 'ffl',
'': 'fi',
'': 'fl',
})
([\-\+]?\d+) # +/- int''',
re.VERBOSE,
)
ligatures = str.maketrans(
{'': 'ff', '': 'ffi', '': 'ffl', '': 'fi', '': 'fl'}
)
def __init__(self, hocrFileName, dpi):
self.dpi = dpi
@@ -78,8 +77,7 @@ class HocrTransform():
# get dimension in pt (not pixel!!!!) of the OCRed image
self.width, self.height = None, None
for div in self.hocr.findall(
".//%sdiv[@class='ocr_page']" % (self.xmlns)):
for div in self.hocr.findall(".//%sdiv[@class='ocr_page']" % (self.xmlns)):
coords = self.element_coordinates(div)
pt_coords = self.pt_from_pixel(coords)
self.width = pt_coords.x2 - pt_coords.x1
@@ -144,8 +142,7 @@ class HocrTransform():
"""
Returns the quantity in PDF units (pt) given quantity in pixels
"""
return Rect._make(
(c / self.dpi * inch) for c in pxl)
return Rect._make((c / self.dpi * inch) for c in pxl)
@classmethod
def replace_unsupported_chars(cls, s):
@@ -156,8 +153,15 @@ class HocrTransform():
"""
return s.translate(cls.ligatures)
def to_pdf(self, outFileName, imageFileName=None, showBoundingboxes=False,
fontname="Helvetica", invisibleText=False, interwordSpaces=False):
def to_pdf(
self,
outFileName,
imageFileName=None,
showBoundingboxes=False,
fontname="Helvetica",
invisibleText=False,
interwordSpaces=False,
):
"""
Creates a PDF file with an image superimposed on top of the text.
Text is positioned according to the bounding box of the lines in
@@ -168,17 +172,15 @@ class HocrTransform():
"""
# create the PDF file
# page size in points (1/72 in.)
pdf = Canvas(
outFileName, pagesize=(self.width, self.height), pageCompression=1)
pdf = Canvas(outFileName, pagesize=(self.width, self.height), pageCompression=1)
# draw bounding box for each paragraph
# light blue for bounding box of paragraph
pdf.setStrokeColorRGB(0, 1, 1)
# light blue for bounding box of paragraph
pdf.setFillColorRGB(0, 1, 1)
pdf.setLineWidth(0) # no line for bounding box
for elem in self.hocr.findall(
".//%sp[@class='%s']" % (self.xmlns, "ocr_par")):
pdf.setLineWidth(0) # no line for bounding box
for elem in self.hocr.findall(".//%sp[@class='%s']" % (self.xmlns, "ocr_par")):
elemtxt = self._get_element_text(elem).rstrip()
if len(elemtxt) == 0:
@@ -190,38 +192,58 @@ class HocrTransform():
# draw the bbox border
if showBoundingboxes:
pdf.rect(
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1,
fill=1)
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=1
)
found_lines = False
for line in self.hocr.findall(
".//%sspan[@class='%s']" % (self.xmlns, "ocr_line")):
".//%sspan[@class='%s']" % (self.xmlns, "ocr_line")
):
found_lines = True
self._do_line(pdf, line, "ocrx_word", fontname, invisibleText,
interwordSpaces, showBoundingboxes)
self._do_line(
pdf,
line,
"ocrx_word",
fontname,
invisibleText,
interwordSpaces,
showBoundingboxes,
)
if not found_lines:
# Tesseract did not report any lines (just words)
root = self.hocr.find(".//%sdiv[@class='%s']" % (self.xmlns, "ocr_page"))
self._do_line(pdf, root, "ocrx_word", fontname, invisibleText,
interwordSpaces, showBoundingboxes)
self._do_line(
pdf,
root,
"ocrx_word",
fontname,
invisibleText,
interwordSpaces,
showBoundingboxes,
)
# put the image on the page, scaled to fill the page
if imageFileName is not None:
pdf.drawImage(imageFileName, 0, 0,
width=self.width, height=self.height)
pdf.drawImage(imageFileName, 0, 0, width=self.width, height=self.height)
# finish up the page and save it
pdf.showPage()
pdf.save()
@classmethod
def polyval(cls, poly, x):
return x * poly[0] + poly[1]
def _do_line(self, pdf, line, elemclass, fontname, invisibleText,
interwordSpaces, showBoundingboxes):
def _do_line(
self,
pdf,
line,
elemclass,
fontname,
invisibleText,
interwordSpaces,
showBoundingboxes,
):
pxl_line_coords = self.element_coordinates(line)
line_box = self.pt_from_pixel(pxl_line_coords)
line_height = line_box.y2 - line_box.y1
@@ -254,23 +276,20 @@ class HocrTransform():
pdf.setLineWidth(0.5)
# negate slope because it is defined as a rise/run in pixel
# coordinates and page coordinates have the y axis flipped
pdf.line(line_box.x1,
baseline_y2,
line_box.x2,
self.polyval((-slope, baseline_y2),
line_box.x2 - line_box.x1))
pdf.line(
line_box.x1,
baseline_y2,
line_box.x2,
self.polyval((-slope, baseline_y2), line_box.x2 - line_box.x1),
)
# light green for bounding box of word/line
pdf.setDash(6, 3)
pdf.setStrokeColorRGB(1, 0, 0)
text.setTextTransform(
cos_a, -sin_a, sin_a, cos_a,
line_box.x1, baseline_y2
)
text.setTextTransform(cos_a, -sin_a, sin_a, cos_a, line_box.x1, baseline_y2)
pdf.setFillColorRGB(0, 0, 0) # text in black
elements = line.findall(
".//%sspan[@class='%s']" % (self.xmlns, elemclass))
elements = line.findall(".//%sspan[@class='%s']" % (self.xmlns, elemclass))
for elem in elements:
elemtxt = self._get_element_text(elem).strip()
elemtxt = self.replace_unsupported_chars(elemtxt)
@@ -287,22 +306,22 @@ class HocrTransform():
# though it would look better, because it will interfere with
# naive text extraction. \n does not work either.
elemtxt += ' '
box = Rect._make((
box.x1,
line_box.y1,
box.x2 + pdf.stringWidth(' ', fontname, line_height),
line_box.y2))
box = Rect._make(
(
box.x1,
line_box.y1,
box.x2 + pdf.stringWidth(' ', fontname, line_height),
line_box.y2,
)
)
box_width = box.x2 - box.x1
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
# draw the bbox border
if showBoundingboxes:
pdf.rect(
box.x1,
self.height - line_box.y2,
box_width,
line_height,
fill=0)
box.x1, self.height - line_box.y2, box_width, line_height, fill=0
)
# Adjust relative position of cursor
# This is equivalent to:
@@ -331,19 +350,40 @@ class HocrTransform():
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Convert hocr file to PDF')
parser.add_argument('-b', '--boundingboxes', action="store_true",
default=False, help='Show bounding boxes borders')
parser.add_argument('-r', '--resolution', type=int,
default=300,
help='Resolution of the image that was OCRed')
parser.add_argument('-i', '--image', default=None,
help='Path to the image to be placed above the text')
parser.add_argument('--interword-spaces', action='store_true',
default=False, help='Add spaces between words')
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
parser.add_argument(
'outputfile', help='Path to the PDF file to be generated')
'-b',
'--boundingboxes',
action="store_true",
default=False,
help='Show bounding boxes borders',
)
parser.add_argument(
'-r',
'--resolution',
type=int,
default=300,
help='Resolution of the image that was OCRed',
)
parser.add_argument(
'-i',
'--image',
default=None,
help='Path to the image to be placed above the text',
)
parser.add_argument(
'--interword-spaces',
action='store_true',
default=False,
help='Add spaces between words',
)
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
parser.add_argument('outputfile', help='Path to the PDF file to be generated')
args = parser.parse_args()
hocr = HocrTransform(args.hocrfile, args.resolution)
hocr.to_pdf(args.outputfile, args.image, args.boundingboxes, interwordSpaces=args.interword_spaces)
hocr.to_pdf(
args.outputfile,
args.image,
args.boundingboxes,
interwordSpaces=args.interword_spaces,
)
+161 -104
View File
@@ -20,20 +20,20 @@
#
# Python FFI wrapper for Leptonica library
from collections.abc import Sequence
from contextlib import suppress
from ctypes.util import find_library
from functools import lru_cache
from io import BytesIO
from tempfile import TemporaryFile
import argparse
import logging
import os
import sys
import warnings
from collections.abc import Sequence
from contextlib import suppress
from ctypes.util import find_library
from functools import lru_cache
from io import BytesIO
from os import fspath
from tempfile import TemporaryFile
from .lib._leptonica import ffi
from .helpers import fspath
# pylint: disable=protected-access
@@ -69,14 +69,14 @@ class _LeptonicaErrorTrap:
def __enter__(self):
from io import UnsupportedOperation
self.tmpfile = TemporaryFile()
# Save the old stderr, and redirect stderr to temporary file
sys.stderr.flush()
try:
self.copy_of_stderr = os.dup(sys.stderr.fileno())
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno(),
inheritable=False)
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno(), inheritable=False)
except UnsupportedOperation:
self.copy_of_stderr = None
return
@@ -185,9 +185,13 @@ class Pix(LeptonicaObject):
def __repr__(self):
if self._cdata:
s = "<leptonica.Pix image size={0}x{1} depth={2}{4} at 0x{3:x}>"
return s.format(self._cdata.w, self._cdata.h, self._cdata.d,
int(ffi.cast('intptr_t', self._cdata)),
'(colormapped)' if self._cdata.colormap else '')
return s.format(
self._cdata.w,
self._cdata.h,
self._cdata.d,
int(ffi.cast('intptr_t', self._cdata)),
'(colormapped)' if self._cdata.colormap else '',
)
else:
return "<leptonica.Pix image NULL>"
@@ -289,8 +293,7 @@ class Pix(LeptonicaObject):
with _LeptonicaErrorTrap():
return cls(lept.pixRead(os.fsencode(filename)))
def write_implied_format(
self, path, jpeg_quality=0, jpeg_progressive=0):
def write_implied_format(self, path, jpeg_quality=0, jpeg_progressive=0):
"""Write pix to the filename, with the extension indicating format.
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
@@ -299,8 +302,8 @@ class Pix(LeptonicaObject):
filename = fspath(path)
with _LeptonicaErrorTrap():
lept.pixWriteImpliedFormat(
os.fsencode(filename),
self._cdata, jpeg_quality, jpeg_progressive)
os.fsencode(filename), self._cdata, jpeg_quality, jpeg_progressive
)
@classmethod
def frompil(self, pillow_image):
@@ -401,11 +404,13 @@ class Pix(LeptonicaObject):
"""
with _LeptonicaErrorTrap():
return Pix(lept.pixRemoveColormapGeneral(
self._cdata, removal_type, lept.L_COPY))
return Pix(
lept.pixRemoveColormapGeneral(self._cdata, removal_type, lept.L_COPY)
)
def otsu_adaptive_threshold(
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1
):
with _LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
@@ -413,20 +418,23 @@ class Pix(LeptonicaObject):
pix = Pix(lept.pixConvertTo8(self._cdata, 0))
result = lept.pixOtsuAdaptiveThreshold(
pix._cdata,
sx, sy,
smoothx, smoothy,
scorefract,
ffi.NULL,
p_pix)
pix._cdata, sx, sy, smoothx, smoothy, scorefract, ffi.NULL, p_pix
)
if result == 0:
return Pix(p_pix[0])
else:
return None
def otsu_threshold_on_background_norm(
self, mask=None, tile_size=(10, 15), thresh=100, mincount=50,
bgval=255, kernel_size=(2, 2), scorefract=0.1):
self,
mask=None,
tile_size=(10, 15),
thresh=100,
mincount=50,
bgval=255,
kernel_size=(2, 2),
scorefract=0.1,
):
with _LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
@@ -438,17 +446,27 @@ class Pix(LeptonicaObject):
thresh_pix = lept.pixOtsuThreshOnBackgroundNorm(
pix._cdata,
mask,
sx, sy,
thresh, mincount, bgval,
smoothx, smoothy,
sx,
sy,
thresh,
mincount,
bgval,
smoothx,
smoothy,
scorefract,
ffi.NULL
ffi.NULL,
)
return Pix(thresh_pix)
def masked_threshold_on_background_norm(
self, mask=None, tile_size=(10, 15), thresh=100, mincount=50,
kernel_size=(2, 2), scorefract=0.1):
self,
mask=None,
tile_size=(10, 15),
thresh=100,
mincount=50,
kernel_size=(2, 2),
scorefract=0.1,
):
with _LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
@@ -460,74 +478,91 @@ class Pix(LeptonicaObject):
thresh_pix = lept.pixMaskedThreshOnBackgroundNorm(
pix._cdata,
mask,
sx, sy,
thresh, mincount,
smoothx, smoothy,
sx,
sy,
thresh,
mincount,
smoothx,
smoothy,
scorefract,
ffi.NULL
ffi.NULL,
)
return Pix(thresh_pix)
def crop_to_foreground(
self, threshold=128, mindist=70, erasedist=30, pagenum=0,
showmorph=0, display=0, pdfdir=ffi.NULL):
self,
threshold=128,
mindist=70,
erasedist=30,
pagenum=0,
showmorph=0,
display=0,
pdfdir=ffi.NULL,
):
with _LeptonicaErrorTrap():
cropbox = Box(lept.pixFindPageForeground(
self._cdata,
threshold,
mindist,
erasedist,
pagenum,
showmorph,
display,
pdfdir))
cropbox = Box(
lept.pixFindPageForeground(
self._cdata,
threshold,
mindist,
erasedist,
pagenum,
showmorph,
display,
pdfdir,
)
)
cropped_pix = lept.pixClipRectangle(
self._cdata,
cropbox._cdata,
ffi.NULL)
cropped_pix = lept.pixClipRectangle(self._cdata, cropbox._cdata, ffi.NULL)
return Pix(cropped_pix)
def clean_background_to_white(
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255):
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255
):
with _LeptonicaErrorTrap():
return Pix(lept.pixCleanBackgroundToWhite(
self._cdata,
mask or ffi.NULL,
grayscale or ffi.NULL,
gamma,
black,
white))
return Pix(
lept.pixCleanBackgroundToWhite(
self._cdata,
mask or ffi.NULL,
grayscale or ffi.NULL,
gamma,
black,
white,
)
)
def gamma_trc(self, gamma=1.0, minval=0, maxval=255):
with _LeptonicaErrorTrap():
return Pix(lept.pixGammaTRC(
ffi.NULL,
self._cdata,
gamma,
minval,
maxval
))
return Pix(lept.pixGammaTRC(ffi.NULL, self._cdata, gamma, minval, maxval))
def background_norm(
self, mask=None, grayscale=None, tile_size=(10, 15), fg_threshold=60,
min_count=40, bg_val=200, smooth_kernel=(2, 1)):
self,
mask=None,
grayscale=None,
tile_size=(10, 15),
fg_threshold=60,
min_count=40,
bg_val=200,
smooth_kernel=(2, 1),
):
# Background norm doesn't work on color mapped Pix, so remove colormap
target_pix = self.remove_colormap(lept.REMOVE_CMAP_BASED_ON_SRC)
with _LeptonicaErrorTrap():
return Pix(lept.pixBackgroundNorm(
target_pix._cdata,
mask or ffi.NULL,
grayscale or ffi.NULL,
tile_size[0],
tile_size[1],
fg_threshold,
min_count,
bg_val,
smooth_kernel[0],
smooth_kernel[1]
))
return Pix(
lept.pixBackgroundNorm(
target_pix._cdata,
mask or ffi.NULL,
grayscale or ffi.NULL,
tile_size[0],
tile_size[1],
fg_threshold,
min_count,
bg_val,
smooth_kernel[0],
smooth_kernel[1],
)
)
@staticmethod
@lru_cache(maxsize=1)
@@ -544,8 +579,7 @@ class Pix(LeptonicaObject):
raise LeptonicaError("Leptonica version is too old")
correlation = ffi.new('float *', 0.0)
result = lept.pixCorrelationBinary(pix1._cdata, pix2._cdata,
correlation)
result = lept.pixCorrelationBinary(pix1._cdata, pix2._cdata, correlation)
if result != 0:
raise LeptonicaError("Correlation failed")
return correlation[0]
@@ -553,8 +587,7 @@ class Pix(LeptonicaObject):
def generate_pdf_ci_data(self, type_, quality):
"Convert to PDF data, with transcoding"
p_compdata = ffi.new('L_COMP_DATA **')
result = lept.pixGenerateCIData(self._cdata, type_, quality, 0,
p_compdata)
result = lept.pixGenerateCIData(self._cdata, type_, quality, 0, p_compdata)
if result != 0:
raise LeptonicaError("Generate PDF data failed")
return CompressedData(p_compdata[0])
@@ -567,15 +600,23 @@ class Pix(LeptonicaObject):
with _LeptonicaErrorTrap():
pix = Pix(lept.pixConvertTo8(self._cdata, 0))
pixa_candidates = PixArray(lept.pixExtractBarcodes(pix._cdata, 0))
sarray = StringArray(lept.pixReadBarcodes(
pixa_candidates._cdata,
lept.L_BF_ANY,
lept.L_USE_WIDTHS,
ffi.NULL,
0
))
except (LeptonicaError, ValueError) as e:
if not pixa_candidates:
return
sarray = StringArray(
lept.pixReadBarcodes(
pixa_candidates._cdata,
lept.L_BF_ANY,
lept.L_USE_WIDTHS,
ffi.NULL,
0,
)
)
except (LeptonicaError, ValueError, IndexError):
return
finally:
with suppress(FileNotFoundError):
os.unlink('junkpixt.png') # leptonica may produce this
os.unlink('junkpixt')
for n, s in enumerate(sarray):
decoded = s.decode()
@@ -629,7 +670,8 @@ class CompressedData(LeptonicaObject):
p_compdata = ffi.new('L_COMP_DATA **')
result = lept.l_generateCIDataForPdf(
os.fsencode(filename), ffi.NULL, jpeg_quality, p_compdata)
os.fsencode(filename), ffi.NULL, jpeg_quality, p_compdata
)
if result != 0:
raise LeptonicaError("CompressedData.open")
return CompressedData(p_compdata[0])
@@ -683,7 +725,8 @@ class Box(LeptonicaObject):
def __repr__(self):
if self._cdata:
return '<leptonica.Box x={0} y={1} w={2} h={3}>'.format(
self.x, self.y, self.w, self.h)
self.x, self.y, self.w, self.h
)
return '<leptonica.Box NULL>'
@property
@@ -802,15 +845,22 @@ def deskew(infile, outfile, dpi):
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
def remove_background(infile, outfile, tile_size=(40, 60), gamma=1.0,
black_threshold=70, white_threshold=190):
def remove_background(
infile,
outfile,
tile_size=(40, 60),
gamma=1.0,
black_threshold=70,
white_threshold=190,
):
try:
pix = Pix.open(infile)
except LeptonicaIOError:
raise LeptonicaIOError("Failed to open file: %s" % infile)
pix = pix.background_norm(tile_size=tile_size).gamma_trc(
gamma, black_threshold, white_threshold)
gamma, black_threshold, white_threshold
)
try:
pix.write_implied_format(outfile)
@@ -819,15 +869,22 @@ def remove_background(infile, outfile, tile_size=(40, 60), gamma=1.0,
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="Python wrapper to access Leptonica")
parser = argparse.ArgumentParser(description="Python wrapper to access Leptonica")
subparsers = parser.add_subparsers(title='commands',
description='supported operations')
subparsers = parser.add_subparsers(
title='commands', description='supported operations'
)
parser_deskew = subparsers.add_parser('deskew')
parser_deskew.add_argument('-r', '--dpi', dest='dpi', action='store',
type=int, default=300, help='input resolution')
parser_deskew.add_argument(
'-r',
'--dpi',
dest='dpi',
action='store',
type=int,
default=300,
help='input resolution',
)
parser_deskew.add_argument('infile', help='image to deskew')
parser_deskew.add_argument('outfile', help='deskewed output image')
parser_deskew.set_defaults(func=deskew)
+8 -4
View File
@@ -19,7 +19,8 @@
from cffi import FFI
ffibuilder = FFI()
ffibuilder.cdef("""
ffibuilder.cdef(
"""
typedef signed char l_int8;
typedef unsigned char l_uint8;
typedef short l_int16;
@@ -202,9 +203,11 @@ enum {
SEL_MISS = 2
};
""")
"""
)
ffibuilder.cdef("""
ffibuilder.cdef(
"""
PIX * pixRead ( const char *filename );
PIX * pixReadMem ( const l_uint8 *data, size_t size );
PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
@@ -480,7 +483,8 @@ void selDestroy ( SEL **psel );
l_int32
setMsgSeverity(l_int32 newsev);
""")
"""
)
ffibuilder.set_source("ocrmypdf.lib._leptonica", None)
+72 -82
View File
@@ -15,27 +15,28 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from pathlib import Path
import concurrent.futures
from collections import defaultdict
import logging
import sys
from collections import defaultdict
from os import fspath
from pathlib import Path
from PIL import Image
import pikepdf
from ._jobcontext import JobContext
from . import leptonica
from .helpers import re_symlink, fspath
from .exec import pngquant, jbig2enc
from ._jobcontext import JobContext
from .exec import jbig2enc, pngquant
from .helpers import re_symlink
DEFAULT_JPEG_QUALITY = 75
DEFAULT_PNG_QUALITY = 70
def img_name(root, xref, ext):
return fspath(root / '{:08d}{}'.format(xref, ext))
return fspath(root / f'{xref:08d}{ext}')
def png_name(root, xref):
@@ -79,11 +80,13 @@ def extract_image_jbig2(*, pike, root, log, image, xref, options):
return None
pim, filtdp = result
if pim.bits_per_component == 1 \
and filtdp != '/JBIG2Decode' \
and jbig2enc.available():
if (
pim.bits_per_component == 1
and filtdp != '/JBIG2Decode'
and jbig2enc.available()
):
try:
imgname = Path(root / '{:08d}'.format(xref))
imgname = Path(root / f'{xref:08d}')
with imgname.open('wb') as f:
ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext))
@@ -99,8 +102,7 @@ def extract_image_generic(*, pike, root, log, image, xref, options):
return None
pim, filtdp = result
if filtdp[0] == '/DCTDecode' \
and options.optimize >= 2:
if filtdp[0] == '/DCTDecode' and options.optimize >= 2:
# This is a simple heuristic derived from some training data, that has
# about a 70% chance of guessing whether the JPEG is high quality,
# and possibly recompressible, or not. The number itself doesn't mean
@@ -118,16 +120,18 @@ def extract_image_generic(*, pike, root, log, image, xref, options):
# with Image.open(stream) as im:
# im.save(jpg_name(root, xref), icc_profile=iccbytes)
try:
imgname = Path(root / '{:08d}'.format(xref))
imgname = Path(root / f'{xref:08d}')
with imgname.open('wb') as f:
ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext))
except pikepdf.UnsupportedImageTypeError:
return None
return xref, ext
elif pim.indexed \
and pim.colorspace in pim.SIMPLE_COLORSPACES \
and options.optimize >= 3:
elif (
pim.indexed
and pim.colorspace in pim.SIMPLE_COLORSPACES
and options.optimize >= 3
):
# Try to improve on indexed images - these are far from low hanging
# fruit in most cases
pim.as_pil_image().save(png_name(root, xref))
@@ -141,11 +145,10 @@ def extract_image_generic(*, pike, root, log, image, xref, options):
return None
def extract_images(pike, root, log, options, extract_fn):
"""Extract image using extract_fn
extract_fn decides where the image is interesting in this case
extract_fn decides whether the image is interesting in this case
"""
include_xrefs = set()
@@ -171,8 +174,7 @@ def extract_images(pike, root, log, options, extract_fn):
image = pike.get_object((xref, 0))
try:
result = extract_fn(
pike=pike, root=root, log=log, image=image,
xref=xref, options=options
pike=pike, root=root, log=log, image=image, xref=xref, options=options
)
except Exception as e:
log.debug("Image xref %s", xref)
@@ -189,17 +191,13 @@ def extract_images_generic(pike, root, log, options):
jpegs = []
pngs = []
for _, xref, ext in extract_images(
pike, root, log, options, extract_image_generic):
for _, xref, ext in extract_images(pike, root, log, options, extract_image_generic):
log.debug('xref = %s ext = %s', xref, ext)
if ext == '.png':
pngs.append(xref)
elif ext == '.jpg':
jpegs.append(xref)
log.debug(
"Optimizable images: "
"JPEGs: %s PNGs: %s", len(jpegs), len(pngs)
)
log.debug("Optimizable images: " "JPEGs: %s PNGs: %s", len(jpegs), len(pngs))
return jpegs, pngs
@@ -208,17 +206,16 @@ def extract_images_jbig2(pike, root, log, options):
jbig2_groups = defaultdict(list)
for pageno, xref, ext in extract_images(
pike, root, log, options, extract_image_jbig2):
pike, root, log, options, extract_image_jbig2
):
group = pageno // options.jbig2_page_group_size
jbig2_groups[group].append((xref, ext))
# Elide empty groups
jbig2_groups = {group: xrefs for group, xrefs in jbig2_groups.items()
if len(xrefs) > 0}
log.debug(
"Optimizable images: "
"JBIG2 groups: %s", (len(jbig2_groups),)
)
jbig2_groups = {
group: xrefs for group, xrefs in jbig2_groups.items() if len(xrefs) > 0
}
log.debug("Optimizable images: " "JBIG2 groups: %s", (len(jbig2_groups),))
return jbig2_groups
@@ -227,18 +224,18 @@ def _produce_jbig2_images(jbig2_groups, root, log, options):
def jbig2_group_futures(executor, root, groups):
for group, xref_exts in groups.items():
prefix = 'group{:08d}'.format(group)
prefix = f'group{group:08d}'
future = executor.submit(
jbig2enc.convert_group,
cwd=fspath(root),
infiles=(img_name(root, xref, ext) for xref, ext in xref_exts),
out_prefix=prefix
out_prefix=prefix,
)
yield future
def jbig2_single_futures(executor, root, groups):
for group, xref_exts in groups.items():
prefix = 'group{:08d}'.format(group)
prefix = f'group{group:08d}'
# Second loop is to ensure multiple images per page are unpacked
for n, xref_ext in enumerate(xref_exts):
xref, ext = xref_ext
@@ -246,7 +243,7 @@ def _produce_jbig2_images(jbig2_groups, root, log, options):
jbig2enc.convert_single,
cwd=fspath(root),
infile=img_name(root, xref, ext),
outfile=root / ('{}.{:04d}'.format(prefix, n))
outfile=root / f'{prefix}.{n:04d}',
)
yield future
@@ -255,8 +252,7 @@ def _produce_jbig2_images(jbig2_groups, root, log, options):
else:
jbig2_futures = jbig2_single_futures
with concurrent.futures.ThreadPoolExecutor(
max_workers=options.jobs) as executor:
with concurrent.futures.ThreadPoolExecutor(max_workers=options.jobs) as executor:
futures = jbig2_futures(executor, root, jbig2_groups)
for future in concurrent.futures.as_completed(futures):
proc = future.result()
@@ -280,14 +276,12 @@ def convert_to_jbig2(pike, jbig2_groups, root, log, options):
_produce_jbig2_images(jbig2_groups, root, log, options)
for group, xref_exts in jbig2_groups.items():
prefix = 'group{:08d}'.format(group)
prefix = f'group{group:08d}'
jbig2_symfile = root / (prefix + '.sym')
if jbig2_symfile.exists():
jbig2_globals_data = jbig2_symfile.read_bytes()
jbig2_globals = pikepdf.Stream(pike, jbig2_globals_data)
jbig2_globals_dict = pikepdf.Dictionary({
'/JBIG2Globals': jbig2_globals
})
jbig2_globals_dict = pikepdf.Dictionary({'/JBIG2Globals': jbig2_globals})
elif options.jbig2_page_group_size == 1:
jbig2_globals_dict = None
else:
@@ -295,12 +289,13 @@ def convert_to_jbig2(pike, jbig2_groups, root, log, options):
for n, xref_ext in enumerate(xref_exts):
xref, _ = xref_ext
jbig2_im_file = root / (prefix + '.{:04d}'.format(n))
jbig2_im_file = root / (prefix + f'.{n:04d}')
jbig2_im_data = jbig2_im_file.read_bytes()
im_obj = pike.get_object(xref, 0)
im_obj.write(
jbig2_im_data, pikepdf.Name('/JBIG2Decode'),
jbig2_globals_dict
jbig2_im_data,
filter=pikepdf.Name('/JBIG2Decode'),
decode_parms=jbig2_globals_dict,
)
@@ -314,9 +309,7 @@ def transcode_jpegs(pike, jpegs, root, log, options):
# 'close'. Seems to be mostly harmless
# https://github.com/python-pillow/Pillow/issues/1144
with Image.open(fspath(in_jpg)) as im:
im.save(fspath(opt_jpg),
optimize=True,
quality=options.jpeg_quality)
im.save(fspath(opt_jpg), optimize=True, quality=options.jpeg_quality)
# pylint: disable=no-member
if opt_jpg.stat().st_size > in_jpg.stat().st_size:
log.debug("xref %s, jpeg, made larger - skip", xref)
@@ -324,24 +317,26 @@ def transcode_jpegs(pike, jpegs, root, log, options):
compdata = leptonica.CompressedData.open(opt_jpg)
im_obj = pike.get_object(xref, 0)
im_obj.write(
compdata.read(), filter=pikepdf.Name('/DCTDecode')
)
im_obj.write(compdata.read(), filter=pikepdf.Name('/DCTDecode'))
def transcode_pngs(pike, pngs, root, log, options):
if options.optimize >= 2:
png_quality = (
max(10, options.png_quality - 10),
min(100, options.png_quality + 10)
min(100, options.png_quality + 10),
)
with concurrent.futures.ThreadPoolExecutor(
max_workers=options.jobs) as executor:
max_workers=options.jobs
) as executor:
for xref in pngs:
executor.submit(
pngquant.quantize,
png_name(root, xref), png_name(root, xref),
png_quality[0], png_quality[1])
png_name(root, xref),
png_name(root, xref),
png_quality[0],
png_quality[1],
)
for xref in pngs:
im_obj = pike.get_object(xref, 0)
@@ -351,9 +346,7 @@ def transcode_pngs(pike, pngs, root, log, options):
pix = leptonica.Pix.open(png_name(root, xref))
if pix.depth == 1:
pix = pix.invert() # PDF assumes 1 is black for monochrome
compdata = pix.generate_pdf_ci_data(
leptonica.lept.L_FLATE_ENCODE, 0
)
compdata = pix.generate_pdf_ci_data(leptonica.lept.L_FLATE_ENCODE, 0)
except leptonica.LeptonicaError as e:
log.error(e)
continue
@@ -361,7 +354,7 @@ def transcode_pngs(pike, pngs, root, log, options):
# This is what we should be doing: open the compressed data without
# transcoding. However this shifts each pixel row by one for some
# reason.
#compdata = leptonica.CompressedData.open(png_name(root, xref))
# compdata = leptonica.CompressedData.open(png_name(root, xref))
if len(compdata) > int(im_obj.stream_dict.Length):
continue # If we produced a larger image, don't use
@@ -377,8 +370,12 @@ def transcode_pngs(pike, pngs, root, log, options):
palette_pdf_string = compdata.get_palette_pdf_string()
palette_data = pikepdf.Object.parse(palette_pdf_string)
palette_stream = pikepdf.Stream(pike, bytes(palette_data))
palette = [pikepdf.Name('/Indexed'), pikepdf.Name('/DeviceRGB'),
compdata.ncolors - 1, palette_stream]
palette = [
pikepdf.Name('/Indexed'),
pikepdf.Name('/DeviceRGB'),
compdata.ncolors - 1,
palette_stream,
]
cs = palette
else:
if compdata.spp == 1:
@@ -389,16 +386,11 @@ def transcode_pngs(pike, pngs, root, log, options):
cs = pikepdf.Name('/DeviceCMYK')
im_obj.ColorSpace = cs
im_obj.write(
compdata.read(),
filter=pikepdf.Name('/FlateDecode'), decode_parms=predictor
compdata.read(), filter=pikepdf.Name('/FlateDecode'), decode_parms=predictor
)
def optimize(
input_file,
output_file,
log,
context):
def optimize(input_file, output_file, log, context):
options = context.get_options()
if options.optimize == 0:
@@ -406,14 +398,11 @@ def optimize(
return
if options.jpeg_quality == 0:
options.jpeg_quality = \
DEFAULT_JPEG_QUALITY if options.optimize < 3 else 40
options.jpeg_quality = DEFAULT_JPEG_QUALITY if options.optimize < 3 else 40
if options.png_quality == 0:
options.png_quality = \
DEFAULT_PNG_QUALITY if options.optimize < 3 else 30
options.png_quality = DEFAULT_PNG_QUALITY if options.optimize < 3 else 30
if options.jbig2_page_group_size == 0:
options.jbig2_page_group_size = \
10 if options.jbig2_lossy else 1
options.jbig2_page_group_size = 10 if options.jbig2_lossy else 1
pike = pikepdf.Pdf.open(input_file)
@@ -429,15 +418,17 @@ def optimize(
target_file = Path(output_file).with_suffix('.opt.pdf')
pike.remove_unreferenced_resources()
pike.save(target_file, preserve_pdfa=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate)
pike.save(
target_file,
preserve_pdfa=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate,
)
input_size = Path(input_file).stat().st_size
output_size = Path(target_file).stat().st_size
ratio = input_size / output_size
savings = 1 - output_size / input_size
log.info("Optimize ratio: {:.2f} savings: {:.1f}%".format(
ratio, 100 * savings))
log.info(f"Optimize ratio: {ratio:.2f} savings: {(100 * savings):.1f}%")
if savings < 0:
log.info("Optimize did not improve the file - discarded")
@@ -453,8 +444,7 @@ def main(infile, outfile, level, jobs=1):
class OptimizeOptions:
"""Emulate ocrmypdf's options"""
def __init__(
self, jobs, optimize, jpeg_quality, png_quality, jb2lossy):
def __init__(self, jobs, optimize, jpeg_quality, png_quality, jb2lossy):
self.jobs = jobs
self.optimize = optimize
self.jpeg_quality = jpeg_quality
@@ -471,7 +461,7 @@ def main(infile, outfile, level, jobs=1):
optimize=int(level),
jpeg_quality=0, # Use default
png_quality=0,
jb2lossy=False
jb2lossy=False,
)
ctx.set_options(options)
+30 -214
View File
@@ -31,21 +31,18 @@ Ghostscript's handling of pdfmark.
"""
import os
from binascii import hexlify
from datetime import datetime
from pathlib import Path
from string import Template
import pkg_resources
import os
from libxmp.utils import file_to_dict
from libxmp import consts
import pikepdf
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
SRGB_ICC_PROFILE = pkg_resources.resource_filename(
'ocrmypdf', ICC_PROFILE_RELPATH)
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
@@ -56,9 +53,6 @@ pdfa_def_template = u"""%!
/ICCProfile $icc_profile
def
[$docinfo
/DOCINFO pdfmark
% Define an ICC profile :
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
@@ -89,190 +83,29 @@ def
"""
def encode_text_string(s: str) -> str:
"""
Encode text string to hex string for use in a PDF
def generate_pdfa_ps(target_filename, icc='sRGB'):
"""Create a Postscript pdfmark file for Ghostscript PDF/A conversion
From PDF 32000-1:2008 a string object may be included in hexademical form
if it is enclosed in angle brackets. For general Unicode the string should
be UTF-16 (big endian) with byte order marks. Many strings including all
ASCII strings could be encoded as PdfDocEncoding literals provided
that certain Postscript sequences are escaped. But it's far simpler to
encode everything as UTF-16.
"""
A pdfmark file is a small Postscript program that provides some information
Ghostscript needs to perform PDF/A conversion. The only information we put
in specifies that we want the file to be a PDF/A, and we want to Ghostscript
to convert objects to the sRGB colorspace if it runs into any object that
it decides must be converted.
# Sometimes lazy C programmers leave their NULs at the end of strings they
# insert into PDFs
# tests/resources/aspect.pdf is one example (created by ImageMagick)
s = s.replace('\x00', '')
See the Adobe pdfmark Reference for details:
https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
if s == '':
return ''
utf16_bytes = s.encode('utf-16be')
ascii_hex_bytes = hexlify(b'\xfe\xff' + utf16_bytes)
ascii_hex_str = ascii_hex_bytes.decode('ascii').lower()
return ascii_hex_str
def _encode_ascii(s: str) -> str:
"""
Aggressively strip non-ASCII and PDF escape sequences
Ghostscript 9.24+ lost support for UTF-16BE in pdfmark files for reasons
given in GhostPDL commit e997c683. Our temporary workaround is use ASCII
and drop all non-ASCII characters. A slightly improved alternative would
be to implement PdfDocEncoding in pikepdf and encode to that, or handle
metadata there.
"""
trans = str.maketrans({
'(': '',
')': '',
'\\': '',
'\0': ''
})
return s.translate(trans).encode('ascii', errors='replace').decode()
def encode_pdf_date(d: datetime) -> str:
"""
Encode Python datetime object as PDF date string
From Adobe pdfmark manual:
(D:YYYYMMDDHHmmSSOHH'mm')
D: is an optional prefix. YYYY is the year. All fields after the year are
optional. MM is the month (01-12), DD is the day (01-31), HH is the
hour (00-23), mm are the minutes (00-59), and SS are the seconds
(00-59). The remainder of the string defines the relation of local
time to GMT. O is either + for a positive difference (local time is
later than GMT) or - (minus) for a negative difference. HH' is the
absolute value of the offset from GMT in hours, and mm' is the
absolute value of the offset in minutes. If no GMT information is
specified, the relation between the specified time and GMT is
considered unknown. Regardless of whether or not GMT
information is specified, the remainder of the string should specify
the local time.
"""
pdfmark_date_fmt = r'%Y%m%d%H%M%S'
s = d.strftime(pdfmark_date_fmt)
tz = d.strftime('%z')
if tz == 'Z' or tz == '':
# Ghostscript <= 9.23 handles missing timezones incorrectly, so if
# timezone is missing, move it into GMT.
# https://bugs.ghostscript.com/show_bug.cgi?id=699182
s += "+00'00'"
else:
sign, tz_hours, tz_mins = tz[0], tz[1:3], tz[3:5]
s += "{}{}'{}'".format(sign, tz_hours, tz_mins)
return s
def decode_pdf_date(s: str) -> datetime:
"""
Decode a pdfmark date to a Python datetime object
A pdfmark date is a string in a paritcular format. See the pdfmark
Reference for the specification.
"""
if s.startswith('D:'):
s = s[2:]
# Literal Z00'00', is incorrect but found in the wild,
# probably made by OS X Quartz -- standardize
if s.endswith("Z00'00'"):
s = s.replace("Z00'00'", '+0000')
elif s.endswith('Z'):
s = s.replace('Z', '+0000')
s = s.replace("'", "") # Remove apos from PDF time strings
return datetime.strptime(s, r'%Y%m%d%H%M%S%z')
def _get_pdfmark_dates(pdfmark):
"""
Encode dates in the expected format for pdfmark Postscript
The best way to deal with amissing date entry is set it to null, because if
the key is omitted Ghostscript will set it to now - we do not want to erase
the fact that the value was unknown. Setting to an empty string breaks
Ghostscript 9.22 as reported here:
https://bugs.ghostscript.com/show_bug.cgi?id=699182
"""
for key in ('/CreationDate', '/ModDate'):
if key not in pdfmark:
continue
if pdfmark[key].strip() == '':
yield ' {} null'.format(key)
continue
date_str = pdfmark[key]
if date_str.startswith('D:'):
date_str = date_str[2:]
try:
yield ' {} (D:{})'.format(
key,
encode_pdf_date(decode_pdf_date(date_str)))
except ValueError:
yield ' {} null'.format(key)
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark, ascii_docinfo=False):
"""
Create a Postscript pdfmark file for Ghostscript.
pdfmark contains the various objects as strings; these must be encoded in
ASCII, and dates have a special format.
:param icc_profile: filename of the ICC profile to include in pdfmark
:param icc_identifier: ICC identifier such as 'sRGB'
:param pdfmark: a dictionary containing keys to include the pdfmark
:param ascii_docinfo: if True, the docinfo block must be encoded in pure
ASCII and may not contain UTF-16BE-BOM-hex encoded strings, as
required for Ghostscript 9.24+
:param target_filename: filename to save
:param icc: ICC identifier such as 'sRGB'
:returns: a string containing the entire pdfmark
"""
# Ghostscript <= 9.21 has a bug where null entries in DOCINFO might produce
# ERROR: VMerror (-25) on closing pdfwrite device.
# https://bugs.ghostscript.com/show_bug.cgi?id=697684
# Work around this by only adding keys that have a nontrivial value
docinfo_keys = ('/Title', '/Author', '/Subject', '/Creator', '/Keywords')
def docinfo_gen():
if not ascii_docinfo:
docinfo_line_template = ' {key} <{value}>'
encode = encode_text_string
else:
docinfo_line_template = ' {key} ({value})'
encode = _encode_ascii
yield from _get_pdfmark_dates(pdfmark)
for key in docinfo_keys:
if key in pdfmark and pdfmark[key].strip() != '':
line = docinfo_line_template.format(
key=key, value=encode(pdfmark[key]))
yield line
docinfo = '\n'.join(docinfo_gen())
t = Template(pdfa_def_template)
result = t.substitute(icc_profile=icc_profile,
icc_identifier=icc_identifier,
docinfo=docinfo)
return result
def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
if icc == 'sRGB':
icc_profile = SRGB_ICC_PROFILE
else:
raise NotImplementedError("Only supporting sRGB")
# pdfmark must contain the full path to the ICC profile, and pdfmark must
# pdfmark must contain the full path to the ICC profile, and pdfmark must be
# also encoded in ASCII. ocrmypdf can be installed anywhere, including to
# paths that have a non-ASCII character in the filename. Ghostscript
# accepts hex-encoded strings and converts them to byte strings, so
@@ -283,7 +116,8 @@ def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
hex_icc_profile = hexlify(bytes_icc_profile)
icc_profile = '<' + hex_icc_profile.decode('ascii') + '>'
ps = _get_pdfa_def(icc_profile, icc, pdfmark, ascii_docinfo=ascii_docinfo)
t = Template(pdfa_def_template)
ps = t.substitute(icc_profile=icc_profile, icc_identifier=icc)
# We should have encoded everything to pure ASCII by this point, and
# to be safe, only allow ASCII in PostScript
@@ -293,41 +127,23 @@ def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
def file_claims_pdfa(filename):
"""Determines if the file claims to be PDF/A compliant
Checking if a file is a truly compliant PDF/A is a massive undertaking
that no open source tool does properly. Some commercial tools are
generally reliable (Acrobat).
This checks if the XMP metadata contains a PDF/A marker.
This only checks if the XMP metadata contains a PDF/A marker. It does not
do full PDF/A validation.
"""
xmp = file_to_dict(filename)
if not xmp:
return {'pass': False, 'output': 'pdf',
'conformance': 'No XMP metadata'}
if not consts.XMP_NS_PDFA_ID in xmp:
return {'pass': False, 'output': 'pdf',
'conformance': 'No PDF/A metadata in XMP'}
pdfa_node = xmp[consts.XMP_NS_PDFA_ID]
def read_node(node, key):
return next(
(v for k, v, meta in node if k == key), ''
)
part = read_node(pdfa_node, 'pdfaid:part')
conformance = read_node(pdfa_node, 'pdfaid:conformance')
part_conformance = part + conformance
pdf = pikepdf.open(filename)
pdfmeta = pdf.open_metadata()
if not pdfmeta.pdfa_status:
return {
'pass': False,
'output': 'pdf',
'conformance': 'No PDF/A metadata in XMP',
}
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
conformance = 'PDF/A-{}'.format(
part_conformance)
conformance = f'PDF/A-{pdfmeta.pdfa_status}'
pdfa_dict = {}
if part_conformance in valid_part_conforms:
if pdfmeta.pdfa_status in valid_part_conforms:
pdfa_dict['pass'] = True
pdfa_dict['output'] = 'pdfa'
pdfa_dict['conformance'] = conformance
return pdfa_dict
+117 -80
View File
@@ -20,25 +20,25 @@ from collections import namedtuple
from decimal import Decimal
from enum import Enum
from math import hypot, isclose
from os import fspath
from pathlib import Path
from unittest.mock import Mock
from warnings import warn
import re
from pikepdf import PdfMatrix
import pikepdf
from . import ghosttext
from .layout import get_page_analysis, get_text_boxes
from ..helpers import fspath
from ..exceptions import EncryptedPdfError, MissingDependencyError
Colorspace = Enum('Colorspace',
'gray rgb cmyk lab icc index sep devn pattern jpeg2000')
Colorspace = Enum('Colorspace', 'gray rgb cmyk lab icc index sep devn pattern jpeg2000')
Encoding = Enum('Encoding',
'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate ' + \
'runlength')
Encoding = Enum(
'Encoding', 'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate ' + 'runlength'
)
FRIENDLY_COLORSPACE = {
'/DeviceGray': Colorspace.gray,
@@ -69,7 +69,7 @@ FRIENDLY_ENCODING = {
'/A85': Encoding.ascii85,
'/LZW': Encoding.lzw,
'/Fl': Encoding.flate,
'/RL': Encoding.runlength
'/RL': Encoding.runlength,
}
FRIENDLY_COMP = {
@@ -77,28 +77,28 @@ FRIENDLY_COMP = {
Colorspace.rgb: 3,
Colorspace.cmyk: 4,
Colorspace.lab: 3,
Colorspace.index: 1
Colorspace.index: 1,
}
UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
def _is_unit_square(shorthand):
values = map(float, shorthand)
pairwise = zip(values, UNIT_SQUARE)
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
XobjectSettings = namedtuple('XobjectSettings',
['name', 'shorthand', 'stack_depth'])
InlineSettings = namedtuple('InlineSettings',
['iimage', 'shorthand', 'stack_depth'])
XobjectSettings = namedtuple('XobjectSettings', ['name', 'shorthand', 'stack_depth'])
ContentsInfo = namedtuple('ContentsInfo',
['xobject_settings', 'inline_images', 'found_vector'])
InlineSettings = namedtuple('InlineSettings', ['iimage', 'shorthand', 'stack_depth'])
TextboxInfo = namedtuple('TextboxInfo',
['bbox', 'is_visible', 'is_corrupt'])
ContentsInfo = namedtuple(
'ContentsInfo', ['xobject_settings', 'inline_images', 'found_vector']
)
TextboxInfo = namedtuple('TextboxInfo', ['bbox', 'is_visible', 'is_corrupt'])
class VectorInfo:
@@ -110,9 +110,9 @@ def _normalize_stack(graphobjs):
"""Convert runs of qQ's in the stack into single graphobjs"""
for operands, operator in graphobjs:
operator = str(operator)
if re.match(r'Q*q+$', operator): # Zero or more Q, one or more q
for char in operator: # Split into individual
yield ([], char) # Yield individual
if re.match(r'Q*q+$', operator): # Zero or more Q, one or more q
for char in operator: # Split into individual
yield ([], char) # Yield individual
else:
yield (operands, operator)
@@ -135,6 +135,13 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
page.
PDF units suit our needs so we initialize ctm to the identity matrix.
According to the PDF specification, the maximum stack depth is 32. Other
viewers tolerate some amount beyond this. We issue a warning if the
stack depth exceeds the spec limit and set a hard limit beyond this to
bound our memory requirements. If the stack underflows behavior is
undefined in the spec, but we just pretend nothing happened and leave the
CTM unchanged.
"""
stack = []
@@ -146,34 +153,40 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
image_ops = set('BI ID EI q Q Do cm'.split())
operator_whitelist = ' '.join(vector_ops | image_ops)
for n, graphobj in enumerate(_normalize_stack(
pikepdf.parse_content_stream(contentstream, operator_whitelist))):
for n, graphobj in enumerate(
_normalize_stack(
pikepdf.parse_content_stream(contentstream, operator_whitelist)
)
):
operands, operator = graphobj
if operator == 'q':
stack.append(ctm)
if len(stack) > 32:
raise RuntimeError(
"PDF graphics stack overflow, operator %i" % n)
if len(stack) > 32: # See docstring
if len(stack) > 128:
raise RuntimeError(
"PDF graphics stack overflowed hard limit, operator %i" % n
)
warn("PDF graphics stack overflowed spec limit")
elif operator == 'Q':
try:
ctm = stack.pop()
except IndexError:
raise RuntimeError(
"PDF graphics stack underflow, operator %i" % n)
# Keeping the ctm the same seems to be the only sensible thing
# to do. Just pretend nothing happened, keep calm and carry on.
warn("PDF graphics stack underflowed - PDF may be malformed")
elif operator == 'cm':
ctm = PdfMatrix(operands) @ ctm
elif operator == 'Do':
image_name = operands[0]
settings = XobjectSettings(
name=image_name, shorthand=ctm.shorthand,
stack_depth=len(stack))
name=image_name, shorthand=ctm.shorthand, stack_depth=len(stack)
)
xobject_settings.append(settings)
elif operator == 'INLINE IMAGE': # BI/ID/EI are grouped into this
elif operator == 'INLINE IMAGE': # BI/ID/EI are grouped into this
iimage = operands[0]
inline = InlineSettings(
iimage=iimage, shorthand=ctm.shorthand,
stack_depth=len(stack))
iimage=iimage, shorthand=ctm.shorthand, stack_depth=len(stack)
)
inline_images.append(inline)
elif operator in vector_ops:
found_vector = True
@@ -181,7 +194,8 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
return ContentsInfo(
xobject_settings=xobject_settings,
inline_images=inline_images,
found_vector=found_vector)
found_vector=found_vector,
)
def _get_dpi(ctm_shorthand, image_size):
@@ -251,8 +265,7 @@ def _get_dpi(ctm_shorthand, image_size):
class ImageInfo:
DPI_PREC = Decimal('1.000')
def __init__(self, *, name='', pdfimage=None, inline=None,
shorthand=None):
def __init__(self, *, name='', pdfimage=None, inline=None, shorthand=None):
self._name = str(name)
self._shorthand = shorthand
@@ -337,19 +350,24 @@ class ImageInfo:
return _get_dpi(self._shorthand, (self._width, self._height))[1]
def __repr__(self):
class_locals = {attr: getattr(self, attr, None) for attr in dir(self)
if not attr.startswith('_')}
class_locals = {
attr: getattr(self, attr, None)
for attr in dir(self)
if not attr.startswith('_')
}
return (
"<ImageInfo '{name}' {type_} {width}x{height} {color} "
"{comp} {bpc} {enc} {xres}x{yres}>").format(**class_locals)
"{comp} {bpc} {enc} {xres}x{yres}>"
).format(**class_locals)
def _find_inline_images(contentsinfo):
"Find inline images in the contentstream"
for n, inline in enumerate(contentsinfo.inline_images):
yield ImageInfo(name='inline-%02d' % n, shorthand=inline.shorthand,
inline=inline)
yield ImageInfo(
name='inline-%02d' % n, shorthand=inline.shorthand, inline=inline
)
def _image_xobjects(container):
@@ -373,6 +391,8 @@ def _image_xobjects(container):
xobjs = resources['/XObject'].as_dict()
for xobj in xobjs:
candidate = xobjs[xobj]
if not '/Subtype' in candidate:
continue
if candidate['/Subtype'] == '/Image':
pdfimage = candidate
yield (pdfimage, xobj)
@@ -403,8 +423,7 @@ def _find_regular_images(container, contentsinfo):
# these from our DPI calculation for the page.
continue
yield ImageInfo(name=draw.name, pdfimage=pdfimage, shorthand=
draw.shorthand)
yield ImageInfo(name=draw.name, pdfimage=pdfimage, shorthand=draw.shorthand)
def _find_form_xobject_images(pdf, container, contentsinfo):
@@ -435,7 +454,8 @@ def _find_form_xobject_images(pdf, container, contentsinfo):
# same object are both very rare.
ctm_shorthand = settings.shorthand
yield from _process_content_streams(
pdf=pdf, container=form_xobject, shorthand=ctm_shorthand)
pdf=pdf, container=form_xobject, shorthand=ctm_shorthand
)
def _process_content_streams(*, pdf, container, shorthand=None):
@@ -459,8 +479,7 @@ def _process_content_streams(*, pdf, container, shorthand=None):
if container.get('/Type') == '/Page' and '/Contents' in container:
initial_shorthand = shorthand or UNIT_SQUARE
elif container.get('/Type') == '/XObject' and \
container['/Subtype'] == '/Form':
elif container.get('/Type') == '/XObject' and container['/Subtype'] == '/Form':
# Set the CTM to the state it was when the "Do" operator was
# encountered that is drawing this instance of the Form XObject
ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity()
@@ -494,9 +513,9 @@ def _page_has_text(text_blocks, page_width, page_height):
margin_ratio = 0.125
interior_bbox = (
margin_ratio * pw, # left
(1 - margin_ratio) * ph, # top
(1 - margin_ratio) * pw, # right
margin_ratio * ph # bottom (first quadrant: bottom < top)
(1 - margin_ratio) * ph, # top
(1 - margin_ratio) * pw, # right
margin_ratio * ph, # bottom (first quadrant: bottom < top)
)
def rects_intersect(a, b):
@@ -515,17 +534,17 @@ def _page_has_text(text_blocks, page_width, page_height):
return has_text
def simplify_textboxes(miner):
def simplify_textboxes(miner, textbox_getter):
"""Extract only limited content from text boxes
We do this to save memory and ensure that our objects are pickleable.
"""
for box in get_text_boxes(miner):
for box in textbox_getter(miner):
first_line = box._objs[0]
first_char = first_line._objs[0]
visible = (first_char.rendermode != 3)
corrupt = (first_char.get_text() == '\ufffd')
visible = first_char.rendermode != 3
corrupt = first_char.get_text() == '\ufffd'
yield TextboxInfo(box.bbox, visible, corrupt)
@@ -539,19 +558,26 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
width_pt = mediabox[2] - mediabox[0]
height_pt = mediabox[3] - mediabox[1]
if xmltext:
if xmltext is not None:
bboxes = ghosttext.page_get_textblocks(
fspath(infile), pageno, xmltext=xmltext, height=height_pt)
fspath(infile), pageno, xmltext=xmltext, height=height_pt
)
pageinfo['bboxes'] = bboxes
else:
pscript5_mode = str(pdf.metadata.get('/Creator')).startswith('PScript5')
# pdfminer required for this section
try:
from .layout import get_page_analysis, get_text_boxes
except ImportError:
raise MissingDependencyError(
"pdfminer is required for this feature. Your distribution "
"may not have installed it."
)
pscript5_mode = str(pdf.docinfo.get('/Creator')).startswith('PScript5')
miner = get_page_analysis(infile, pageno, pscript5_mode)
pageinfo['textboxes'] = list(simplify_textboxes(miner))
pageinfo['textboxes'] = list(simplify_textboxes(miner, get_text_boxes))
bboxes = (box.bbox for box in pageinfo['textboxes'])
pageinfo['has_text'] = _page_has_text(
bboxes, width_pt, height_pt
)
pageinfo['has_text'] = _page_has_text(bboxes, width_pt, height_pt)
userunit = page.get('/UserUnit', Decimal(1.0))
if not isinstance(userunit, Decimal):
@@ -566,24 +592,24 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
pageinfo['rotate'] = 0
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
contentsinfo = [ci for ci in
_process_content_streams(pdf=pdf, container=page,
shorthand=userunit_shorthand)]
contentsinfo = [
ci
for ci in _process_content_streams(
pdf=pdf, container=page, shorthand=userunit_shorthand
)
]
pageinfo['has_vector'] = False
if any(isinstance(ci, VectorInfo) for ci in contentsinfo):
pageinfo['has_vector'] = True
pageinfo['images'] = [im for im in contentsinfo
if isinstance(im, ImageInfo)]
pageinfo['images'] = [im for im in contentsinfo if isinstance(im, ImageInfo)]
if pageinfo['images']:
xres = Decimal(max(image.xres for image in pageinfo['images']))
yres = Decimal(max(image.yres for image in pageinfo['images']))
pageinfo['xres'], pageinfo['yres'] = xres, yres
pageinfo['width_pixels'] = \
int(round(xres * pageinfo['width_inches']))
pageinfo['height_pixels'] = \
int(round(yres * pageinfo['height_inches']))
pageinfo['width_pixels'] = int(round(xres * pageinfo['width_inches']))
pageinfo['height_pixels'] = int(round(yres * pageinfo['height_inches']))
return pageinfo
@@ -593,6 +619,8 @@ def _pdf_get_all_pageinfo(infile, detailed_analysis=False, log=None):
log = Mock()
pdf = pikepdf.open(infile)
if pdf.is_encrypted:
raise EncryptedPdfError() # Triggered by encryption with empty passwd
if detailed_analysis:
pages_xml = None
else:
@@ -676,13 +704,14 @@ class PageInfo:
if 'textboxes' not in self._pageinfo:
if visible is not None and corrupt is not None:
raise NotImplementedError(
'Ghostscript textboxes cannot be classified')
raise NotImplementedError('Ghostscript textboxes cannot be classified')
return self._pageinfo['bboxes']
return (obj.bbox for obj in self._pageinfo['textboxes']
if predicate(obj, visible, corrupt))
return (
obj.bbox
for obj in self._pageinfo['textboxes']
if predicate(obj, visible, corrupt)
)
@property
def xres(self):
@@ -705,11 +734,15 @@ class PageInfo:
def __repr__(self):
return (
'<PageInfo '
'pageno={} {}"x{}" rotation={} res={}x{} has_text={}>').format(
self.pageno, self.width_inches, self.height_inches,
'<PageInfo ' 'pageno={} {}"x{}" rotation={} res={}x{} has_text={}>'
).format(
self.pageno,
self.width_inches,
self.height_inches,
self.rotation,
self.xres, self.yres, self.has_text
self.xres,
self.yres,
self.has_text,
)
@@ -719,7 +752,8 @@ class PdfInfo:
def __init__(self, infile, detailed_page_analysis=False, log=None):
self._infile = infile
self._pages, pdf = _pdf_get_all_pageinfo(
infile, detailed_page_analysis, log=log)
infile, detailed_page_analysis, log=log
)
self._needs_rendering = pdf.root.get('/NeedsRendering', False)
self._has_acroform = '/AcroForm' in pdf.root
@@ -757,15 +791,18 @@ class PdfInfo:
return len(self._pages)
def __repr__(self):
return "<PdfInfo('...'), page count={}>".format(len(self))
return f"<PdfInfo('...'), page count={len(self)}>"
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)
+8 -6
View File
@@ -23,13 +23,16 @@ from ..exec import ghostscript
# Forgive me for I have sinned
# I am using regular expressions to parse XML. However the XML in this case,
# generated by Ghostscript, is self-consistent enough to be parseable.
regex_remove_char_tags = re.compile(br"""
regex_remove_char_tags = re.compile(
br"""
<char\b
(?: [^>] # anything single character but >
| \">\" # special case: trap ">"
)*
/> # terminate with '/>'
""", re.VERBOSE)
""",
re.VERBOSE,
)
def page_get_textblocks(infile, pageno, xmltext, height):
@@ -76,15 +79,14 @@ def extract_text_xml(infile, pdf, pageno=None, log=None):
existing_text = regex_remove_char_tags.sub(b' ', existing_text)
try:
root = ET.fromstringlist([
b'<document>\n', existing_text, b'</document>\n'
])
root = ET.fromstringlist([b'<document>\n', existing_text, b'</document>\n'])
page_xml = root.findall('page')
except ET.ParseError as e:
log.error(
"An error occurred while attempting to retrieve existing text in "
"the input file. Will attempt to continue assuming that there is "
"no existing text in the file. The error was:")
"no existing text in the file. The error was:"
)
log.error(e)
page_xml = [None] * len(pdf.pages)
+88 -26
View File
@@ -25,13 +25,11 @@ import pdfminer.pdfdevice
import pdfminer.pdfinterp
from pdfminer.converter import PDFLayoutAnalyzer
from pdfminer.glyphlist import glyphname2unicode
from pdfminer.layout import (LAParams, LTChar, LTContainer, LTLayoutContainer,
LTPage, LTTextBox, LTTextLine)
from pdfminer.layout import LAParams, LTChar, LTPage, LTTextBox
from pdfminer.pdfdocument import PDFTextExtractionNotAllowed
from pdfminer.pdffont import (PDFCIDFont, PDFFont, PDFType3Font,
PDFUnicodeNotDefined, PDFSimpleFont)
from pdfminer.pdffont import PDFFont, PDFSimpleFont, PDFUnicodeNotDefined
from pdfminer.pdfpage import PDFPage
from pdfminer.utils import bbox2str, fsplit, matrix2str
from pdfminer.utils import bbox2str, matrix2str
from ..exceptions import EncryptedPdfError
@@ -41,8 +39,9 @@ STRIP_NAME = re.compile(r'[0-9]+')
# Unconditional pdfminer patches
#
def name2unicode(name):
"""Fix pdfminer's regex in name2unicode function
"""Fix pdfminer's name2unicode function
Font cids that are mapped to names of the form /g123 seem to be, by convention
characters with no corresponding Unicode entry. These can be subsetted fonts
@@ -62,9 +61,13 @@ def name2unicode(name):
if not m:
raise KeyError(name)
return chr(int(m.group(0)))
pdfminer.encodingdb.name2unicode = name2unicode
original_PDFFont_init = PDFFont.__init__
def PDFFont__init__(self, descriptor, widths, default_width=None):
original_PDFFont_init(self, descriptor, widths, default_width)
# PDF spec says descent should be negative
@@ -74,9 +77,13 @@ def PDFFont__init__(self, descriptor, widths, default_width=None):
# to misposition text.
if self.descent > 0:
self.descent = -self.descent
PDFFont.__init__ = PDFFont__init__
original_PDFSimpleFont_init = PDFSimpleFont.__init__
def PDFSimpleFont__init__(self, descriptor, widths, spec):
# Font encoding is specified either by a name of
# built-in encoding or a dictionary that describes
@@ -87,20 +94,25 @@ def PDFSimpleFont__init__(self, descriptor, widths, spec):
if not self.unicode_map and 'Encoding' not in spec:
self.cid2unicode = {}
return
PDFSimpleFont.__init__ = PDFSimpleFont__init__
#
# pdfminer patches when creator is PScript5.dll
#
def PDFType3Font__PScript5_get_height(self):
h = self.bbox[3]-self.bbox[1]
h = self.bbox[3] - self.bbox[1]
if h == 0:
h = self.ascent - self.descent
return h * copysign(1.0, self.vscale)
def PDFType3Font__PScript5_get_descent(self):
return self.descent * copysign(1.0, self.vscale)
def PDFType3Font__PScript5_get_ascent(self):
return self.ascent * copysign(1.0, self.vscale)
@@ -109,14 +121,48 @@ class LTStateAwareChar(LTChar):
"""A subclass of LTChar that tracks text render mode at time of drawing"""
__slots__ = (
'rendermode', '_text', 'matrix', 'fontname', 'adv', 'upright', 'size',
'width', 'height', 'bbox', 'x0', 'x1', 'y0', 'y1'
'rendermode',
'_text',
'matrix',
'fontname',
'adv',
'upright',
'size',
'width',
'height',
'bbox',
'x0',
'x1',
'y0',
'y1',
)
def __init__(self, matrix, font, fontsize, scaling, rise, text, textwidth,
textdisp, textstate, *args):
super().__init__(matrix, font, fontsize, scaling, rise, text, textwidth,
textdisp, *args)
def __init__(
self,
matrix,
font,
fontsize,
scaling,
rise,
text,
textwidth,
textdisp,
ncs,
graphicstate,
textstate,
):
super().__init__(
matrix,
font,
fontsize,
scaling,
rise,
text,
textwidth,
textdisp,
ncs,
graphicstate,
)
self.rendermode = textstate.render
def is_compatible(self, obj):
@@ -126,8 +172,7 @@ class LTStateAwareChar(LTChar):
- the Unicode mapping is known, and both have the same render mode
- the Unicode mapping is unknown but both are part of the same font
"""
both_unicode_mapped = (isinstance(self._text, str) and
isinstance(obj._text, str))
both_unicode_mapped = isinstance(self._text, str) and isinstance(obj._text, str)
try:
if both_unicode_mapped:
return self.rendermode == obj.rendermode
@@ -143,10 +188,15 @@ class LTStateAwareChar(LTChar):
return self._text
def __repr__(self):
return ('<%s %s matrix=%s rendermode=%r font=%r adv=%s text=%r>' %
(self.__class__.__name__, bbox2str(self.bbox),
matrix2str(self.matrix), self.rendermode, self.fontname, self.adv,
self.get_text()))
return '<%s %s matrix=%s rendermode=%r font=%r adv=%s text=%r>' % (
self.__class__.__name__,
bbox2str(self.bbox),
matrix2str(self.matrix),
self.rendermode,
self.fontname,
self.adv,
self.get_text(),
)
class TextPositionTracker(PDFLayoutAnalyzer):
@@ -169,11 +219,13 @@ class TextPositionTracker(PDFLayoutAnalyzer):
self.pageno += 1
self.receive_layout(self.cur_item)
def render_string(self, textstate, seq, *args):
def render_string(self, textstate, seq, ncs, graphicstate):
self.textstate = textstate.copy()
super().render_string(self.textstate, seq, *args)
super().render_string(self.textstate, seq, ncs, graphicstate)
def render_char(self, matrix, font, fontsize, scaling, rise, cid, *args):
def render_char(
self, matrix, font, fontsize, scaling, rise, cid, ncs, graphicstate
):
try:
text = font.to_unichr(cid)
assert isinstance(text, str), str(type(text))
@@ -182,13 +234,23 @@ class TextPositionTracker(PDFLayoutAnalyzer):
textwidth = font.char_width(cid)
textdisp = font.char_disp(cid)
item = LTStateAwareChar(
matrix, font, fontsize, scaling, rise, text,
textwidth, textdisp, self.textstate, *args)
matrix,
font,
fontsize,
scaling,
rise,
text,
textwidth,
textdisp,
ncs,
graphicstate,
self.textstate,
)
self.cur_item.add(item)
return item.adv
def handle_undefined_char(self, font, cid):
#log.info('undefined: %r, %r', font, cid)
# log.info('undefined: %r, %r', font, cid)
return (font.fontname, cid)
def receive_layout(self, ltpage):
@@ -209,7 +271,7 @@ def get_page_analysis(infile, pageno, pscript5_mode):
spec=True,
get_ascent=PDFType3Font__PScript5_get_ascent,
get_descent=PDFType3Font__PScript5_get_descent,
get_height=PDFType3Font__PScript5_get_height
get_height=PDFType3Font__PScript5_get_height,
)
patcher.start()
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,25 +1,25 @@
é la Waterman
a la Waterman
h ens linzen
4 ons linzen
3 liter water
3 mien
3 uien
bloem} hater
bloem, boter
2 kopjes melk
laurier, kruidnagel, kerrie, gout
Be linzen wassen en in¢l liter kokend wa-
ter 1 dag laten weken. 2 liter water bi'
de linzen voegen, gender hat water wagrin
ze geweekt zijn af te gieten. De helft Van
de mien bakken met laurier en kruiénagal.
Alla mien, kerrie en zout bij de linzen
voegen. A1135 aan de Rook brengem. Van de
bloem mat boter-en melk een papje maken en
verder afmaken met de soap. A13 de linzen
gaar zijn is de seep klaar.
laurier, kruidnagel, kerrie, zout
De linzgen wassen en in -l liter kokend wa-
ter 1 dag laten weken, 2 liter water bij
de linzen voegen, zonder het water waarin
ze geweekt zijn af te gieten, De helft van
de uien bakken met laurier en Kruicdnagel.
Alle uien, kerrie en gout bij de linzen
voegen, Alles aan de kook brengen,. Van de
bloem met boter en melk een papje maken en
verder afmaken met de soep, Als de linzen
gaar Zijn is de soep klaar.
@@ -1,3 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Too few characters. Skipping this page
OSD: Weak margin (0.00) for 24 blob text block, but using orientation anyway: 0
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,4 +1,4 @@
This shnuld be a perfect circle-
This should be a perfect circle:
@@ -5,20 +5,27 @@
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name='ocr-system' content='tesseract 3.05.01' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
<meta name='ocr-system' content='tesseract 4.0.0' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/var/folders/37/78_114p552q16vv6vmgm5kr00000gn/T/com.github.ocrmypdf.7vjmeixk/000001.ocr.png"; bbox 0 0 1000 800; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/37/78_114p552q16vv6vmgm5kr00000gn/T/com.github.ocrmypdf.jz4b2s0s/000001.ocr.png"; bbox 0 0 1000 800; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 296 96 704 504">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 296 96 704 504">
<span class='ocr_line' id='line_1_1' title="bbox 296 96 704 504; baseline 0 296; x_size 169.33333; x_descenders 42.333332; x_ascenders 42.333336"><span class='ocrx_word' id='word_1_1' title='bbox 296 96 704 504; x_wconf 95'><strong> </strong></span>
<span class='ocr_line' id='line_1_1' title="bbox 296 96 704 504; baseline 0 296; x_size 169.33333; x_descenders 42.333332; x_ascenders 42.333336">
<span class='ocrx_word' id='word_1_1' title='bbox 296 96 704 504; x_wconf 95'><strong><em> </em></strong></span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_2' title="bbox 150 592 841 622">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 150 592 841 622">
<span class='ocr_line' id='line_1_2' title="bbox 150 592 841 622; baseline 0 -6; x_size 30; x_descenders 6; x_ascenders 8"><span class='ocrx_word' id='word_1_2' title='bbox 150 592 230 616; x_wconf 85'><strong>This</strong></span> <span class='ocrx_word' id='word_1_3' title='bbox 260 592 384 616; x_wconf 81'>shnuld</span> <span class='ocrx_word' id='word_1_4' title='bbox 413 592 449 616; x_wconf 81'><strong>be</strong></span> <span class='ocrx_word' id='word_1_5' title='bbox 479 600 493 616; x_wconf 85'><em>a</em></span> <span class='ocrx_word' id='word_1_6' title='bbox 523 592 668 622; x_wconf 76'>perfect</span> <span class='ocrx_word' id='word_1_7' title='bbox 698 592 841 616; x_wconf 76'><strong>circle-</strong></span>
<span class='ocr_line' id='line_1_2' title="bbox 150 592 841 622; baseline 0 -6; x_size 30; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_2' title='bbox 150 592 230 616; x_wconf 96'><strong><em>This</em></strong></span>
<span class='ocrx_word' id='word_1_3' title='bbox 260 592 384 616; x_wconf 95'><strong><em>should</em></strong></span>
<span class='ocrx_word' id='word_1_4' title='bbox 413 592 449 616; x_wconf 95'><strong><em>be</em></strong></span>
<span class='ocrx_word' id='word_1_5' title='bbox 479 600 493 616; x_wconf 95'><strong><em>a</em></strong></span>
<span class='ocrx_word' id='word_1_6' title='bbox 523 592 668 622; x_wconf 95'><strong><em>perfect</em></strong></span>
<span class='ocrx_word' id='word_1_7' title='bbox 698 592 841 616; x_wconf 55'><strong><em>circle:</em></strong></span>
</span>
</p>
</div>
@@ -1,3 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Too few characters. Skipping this page
OSD: Weak margin (0.00) for 24 blob text block, but using orientation anyway: 0
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,4 +1,4 @@
This shnuld be a perfect circle-
This should be a perfect circle:
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
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 multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ 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
e 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 !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” 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 realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* 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.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
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
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
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 spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile 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,
@@ -58,7 +58,7 @@ 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 specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ 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.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ 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
* 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 FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH 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.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* 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
¢ 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.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
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 multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ 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
e 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 !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” 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 realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* 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.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
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
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
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 spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile 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,
@@ -58,7 +58,7 @@ 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 specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ 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.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ 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
* 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 FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH 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.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* 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
¢ 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.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
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 multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ 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
e 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 !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” 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 realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* 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.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
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
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
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 spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile 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,
@@ -58,7 +58,7 @@ 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 specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ 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.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ 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
* 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 FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH 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.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* 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
¢ 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.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
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 multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ 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
e 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 !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” 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 realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* 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.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
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
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
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 spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile 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,
@@ -58,7 +58,7 @@ 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 specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ 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.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ 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
* 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 FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH 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.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* 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
¢ 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.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
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 multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ 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
e 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 !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” 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 realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* 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.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
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
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
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 spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile 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,
@@ -58,7 +58,7 @@ 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 specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ 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.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ 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
* 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 FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH 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.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* 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
¢ 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.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
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 multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ 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
e 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 !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” 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 realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* 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.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
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
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
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 spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile 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,
@@ -58,7 +58,7 @@ 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 specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ 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.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ 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
* 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 FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH 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.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* 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
¢ 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.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
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 multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ 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
e 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 !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” 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 realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* 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.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
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
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
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 spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile 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,
@@ -58,7 +58,7 @@ 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 specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ 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.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ 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
* 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 FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH 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.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* 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
¢ 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.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
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 multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ 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
e 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 !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” 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 realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* 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.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
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
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
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 spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile 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,
@@ -58,7 +58,7 @@ 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 specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ 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.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ 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
* 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 FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH 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.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* 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
¢ 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.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
@@ -1,6 +1,6 @@
Page number: 0
Orientation in degrees: 0
Rotate: 0
Orientation confidence: 18.91
Orientation confidence: 34.40
Script: Latin
Script confidence: 0.97
Script confidence: 2.90
@@ -1,6 +1,6 @@
Page number: 0
Orientation in degrees: 90
Rotate: 270
Orientation confidence: 17.21
Orientation confidence: 34.53
Script: Latin
Script confidence: 1.06
Script confidence: 3.83
@@ -1,6 +1,6 @@
Page number: 0
Orientation in degrees: 180
Rotate: 180
Orientation confidence: 14.79
Orientation confidence: 31.79
Script: Latin
Script confidence: 2.87
Script confidence: 3.18
@@ -1,6 +1,6 @@
Page number: 0
Orientation in degrees: 270
Rotate: 90
Orientation confidence: 19.19
Orientation confidence: 32.59
Script: Latin
Script confidence: 0.58
Script confidence: 2.85
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
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 multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ 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
e 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 !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” 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 realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* 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.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
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
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
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 spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rst track will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile 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,
@@ -58,7 +58,7 @@ 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 specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ 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.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ 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
* 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 FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH 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.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHZ 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* 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
¢ 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.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
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 multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ 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
e 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 !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” 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 realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* 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.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
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
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
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 spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rst track will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile 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,
@@ -58,7 +58,7 @@ 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 specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ 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.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ 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
* 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 FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH 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.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHZ 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* 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
¢ 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.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
@@ -1,2 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
OSD: Weak margin (6.83) for 47 blob text block, but using orientation anyway: 0
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,9 +1,9 @@
Multicolor Black
Pure Block (K = 100)
Pure Black (K = 100}
Pure Magenta
Pu re Cya n
Pure Cyan
Pure Yellow
@@ -5,34 +5,47 @@
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name='ocr-system' content='tesseract 3.05.01' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
<meta name='ocr-system' content='tesseract 4.0.0' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/var/folders/37/78_114p552q16vv6vmgm5kr00000gn/T/com.github.ocrmypdf.lfruqc8g/000001.ocr.png"; bbox 0 0 900 900; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/37/78_114p552q16vv6vmgm5kr00000gn/T/com.github.ocrmypdf.wy7q83pl/000001.ocr.png"; bbox 0 0 900 900; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 97 87 502 204">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 97 87 502 204">
<span class='ocr_line' id='line_1_1' title="bbox 97 87 399 122; baseline 0 0; x_size 40.714287; x_descenders 5.7142859; x_ascenders 15"><span class='ocrx_word' id='word_1_1' title='bbox 97 87 285 122; x_wconf 75'>Multicolor</span> <span class='ocrx_word' id='word_1_2' title='bbox 302 87 399 122; x_wconf 83'>Black</span>
<span class='ocr_line' id='line_1_1' title="bbox 97 87 399 122; baseline 0 0; x_size 40.714287; x_descenders 5.7142859; x_ascenders 15">
<span class='ocrx_word' id='word_1_1' title='bbox 97 87 285 122; x_wconf 95'><strong><em>Multicolor</em></strong></span>
<span class='ocrx_word' id='word_1_2' title='bbox 302 87 399 122; x_wconf 96'><strong><em>Black</em></strong></span>
</span>
<span class='ocr_line' id='line_1_2' title="bbox 114 160 502 204; baseline 0 -9; x_size 44; x_descenders 9; x_ascenders 15"><span class='ocrx_word' id='word_1_3' title='bbox 114 163 191 195; x_wconf 91'>Pure</span> <span class='ocrx_word' id='word_1_4' title='bbox 210 160 308 195; x_wconf 81'>Block</span> <span class='ocrx_word' id='word_1_5' title='bbox 325 160 363 204; x_wconf 88'>(K</span> <span class='ocrx_word' id='word_1_6' title='bbox 378 174 400 183; x_wconf 96'><strong>=</strong></span> <span class='ocrx_word' id='word_1_7' title='bbox 421 160 502 204; x_wconf 77'>100)</span>
<span class='ocr_line' id='line_1_2' title="bbox 114 160 502 204; baseline 0 -9; x_size 44; x_descenders 9; x_ascenders 15">
<span class='ocrx_word' id='word_1_3' title='bbox 114 163 191 195; x_wconf 96'><strong><em>Pure</em></strong></span>
<span class='ocrx_word' id='word_1_4' title='bbox 210 160 308 195; x_wconf 93'><strong><em>Black</em></strong></span>
<span class='ocrx_word' id='word_1_5' title='bbox 325 160 363 204; x_wconf 93'><strong><em>(K</em></strong></span>
<span class='ocrx_word' id='word_1_6' title='bbox 378 174 400 183; x_wconf 96'><strong><em>=</em></strong></span>
<span class='ocrx_word' id='word_1_7' title='bbox 421 160 502 204; x_wconf 70'><strong><em>100}</em></strong></span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_2' title="bbox 121 282 383 326">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 121 282 383 326">
<span class='ocr_line' id='line_1_3' title="bbox 121 282 383 326; baseline 0 -11; x_size 44; x_descenders 11; x_ascenders 13"><span class='ocrx_word' id='word_1_8' title='bbox 121 283 199 315; x_wconf 91'>Pure</span> <span class='ocrx_word' id='word_1_9' title='bbox 216 282 383 326; x_wconf 75'>Magenta</span>
<span class='ocr_line' id='line_1_3' title="bbox 121 282 383 326; baseline 0 -11; x_size 44; x_descenders 11; x_ascenders 13">
<span class='ocrx_word' id='word_1_8' title='bbox 121 283 199 315; x_wconf 96'><strong><em>Pure</em></strong></span>
<span class='ocrx_word' id='word_1_9' title='bbox 216 282 383 326; x_wconf 96'><strong><em>Magenta</em></strong></span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_3' title="bbox 140 375 329 418">
<p class='ocr_par' id='par_1_3' lang='eng' title="bbox 140 375 329 418">
<span class='ocr_line' id='line_1_4' title="bbox 140 375 329 418; baseline 0 -11; x_size 43; x_descenders 11; x_ascenders 12"><span class='ocrx_word' id='word_1_10' title='bbox 140 375 178 407; x_wconf 94'>Pu</span> <span class='ocrx_word' id='word_1_11' title='bbox 184 387 218 407; x_wconf 90'>re</span> <span class='ocrx_word' id='word_1_12' title='bbox 234 375 306 418; x_wconf 85'>Cya</span> <span class='ocrx_word' id='word_1_13' title='bbox 312 387 329 407; x_wconf 94'>n</span>
<span class='ocr_line' id='line_1_4' title="bbox 140 375 329 418; baseline 0 -11; x_size 43; x_descenders 11; x_ascenders 12">
<span class='ocrx_word' id='word_1_10' title='bbox 140 375 218 407; x_wconf 96'><strong><em>Pure</em></strong></span>
<span class='ocrx_word' id='word_1_11' title='bbox 234 375 329 418; x_wconf 96'><strong><em>Cyan</em></strong></span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_4' title="bbox 207 570 422 605">
<p class='ocr_par' id='par_1_4' lang='eng' title="bbox 207 570 422 605">
<span class='ocr_line' id='line_1_5' title="bbox 207 570 422 605; baseline 0 0; x_size 40.365852; x_descenders 5.3658538; x_ascenders 15"><span class='ocrx_word' id='word_1_14' title='bbox 207 573 285 605; x_wconf 91'><strong>Pure</strong></span> <span class='ocrx_word' id='word_1_15' title='bbox 299 570 422 605; x_wconf 88'>Yellow</span>
<span class='ocr_line' id='line_1_5' title="bbox 207 570 422 605; baseline 0 0; x_size 40.365852; x_descenders 5.3658538; x_ascenders 15">
<span class='ocrx_word' id='word_1_12' title='bbox 207 573 285 605; x_wconf 96'><strong><em>Pure</em></strong></span>
<span class='ocrx_word' id='word_1_13' title='bbox 299 570 422 605; x_wconf 95'><strong><em>Yellow</em></strong></span>
</span>
</p>
</div>
@@ -1,2 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
OSD: Weak margin (6.83) for 47 blob text block, but using orientation anyway: 0
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,9 +1,9 @@
Multicolor Black
Pure Block (K = 100)
Pure Black (K = 100}
Pure Magenta
Pu re Cya n
Pure Cyan
Pure Yellow
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,13 +1,13 @@
Portez ce vieux whisky au juge
blond qui fume sur son île
intérieure, à côté de l'alcôve
ovoïde, où les bûches se
consument dans l'être, ce qui
ovoide, où les bûches se
consument dans l'âtre, ce qui
lui permet de penser à la
cænogénèse de l'être dont il
est question dans la cause
ambiguë entendue à Moÿ, dans
un capharnaüm qui, pense—t—il,
diminue çà et là la qualité de son
oeuvre.
un capharnaüm qui, pense-t-il,
diminue cà et là la qualité de son
œuvre.
@@ -1,2 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
OSD: Weak margin (4.74) for 181 blob text block, but using orientation anyway: 0
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -4,8 +4,8 @@ Replacement of "creationism" with "intelligent design"
+ "Creation" and "creationist"
+ "Intelligent design"
—@— "Creation" and "creationist"
—®@-— "Intelligent design"
and "design proponent"
Word count
@@ -17,4 +17,4 @@ Word count
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica

Some files were not shown because too many files have changed in this diff Show More