Compare commits

...
17 Commits
Author SHA1 Message Date
James R. Barlow efa7ea4fde Fix log.error where log is None 2018-09-19 23:01:27 -07:00
James R. Barlow 137a6e45f5 ghostscript: fix missing fspath for py3.5 2018-09-19 22:57:20 -07:00
James R. Barlow 29116e1dec Change to README.md 2018-09-19 21:01:24 -07:00
James R. Barlow 87193335b9 v7.1.0 notes 2018-09-19 20:57:18 -07:00
James R. Barlow cfd4f8a850 Improve error handling for improvements to Ghostscript text extraction 2018-09-19 20:29:18 -07:00
James R. Barlow eaa324939f Upgrade to pikepdf 0.3.3
Closes #231
2018-09-19 15:30:54 -07:00
James R. Barlow ef70e538f7 Improve error message on handling KeyboardInterrupt
Closes #301
2018-09-19 01:40:26 -07:00
James R. Barlow b7b912e56a Fix test suite and blank pages 2018-09-17 01:12:58 -07:00
James R. Barlow 4615cf2f1e First cut at improving text extraction speed 2018-09-16 23:34:18 -07:00
James R. Barlow eaf772f80a Merge v6.2.4 release notes 2018-09-16 15:45:38 -07:00
James R. Barlow 96ba75eabd Ghostscript: fix issues in strict ASCII implementation 2018-09-16 15:41:54 -07:00
James R. Barlow fdfe52c1ad main: add debug option to force threads 2018-09-15 00:01:45 -07:00
James R. Barlow 932b2e2a29 main: print Ghostscript version too 2018-09-14 23:58:06 -07:00
James R. Barlow 57e489c957 main: Cleanup; support overriding sys.args in run_pipeline 2018-09-14 23:57:35 -07:00
James R. Barlow 17a3fa671c ghostscript: API docs update 2018-09-14 23:51:52 -07:00
James R. Barlow 2659afb4f6 Cleanup gitignore 2018-09-14 21:02:22 -07:00
James R. Barlow 7392115507 Blacklist Ghostscript 9.24 due to regressions
As per issue #291. Forced push to remove a copyrighted test file that was
accidentally included.
2018-09-14 20:41:13 -07:00
12 changed files with 299 additions and 230 deletions
+14 -21
View File
@@ -1,47 +1,40 @@
# Development environment
.bash_history
.pylintrc
.pytest_cache/
.ruffus_history.sqlite
.venv/
*.pyc
*.sublime-*
venv*/
.venv/
pyvenv.cfg
tasks.py
.bash_history
.ruffus_history.sqlite
.idea/
.pytest_cache/
.pylintrc
# Package building
*.egg-info/
.cache/
.eggs/
*.egg-info/
build/
dist/
wheelhouse/
# Automatically generated files
ocrmypdf/lib/_*.py
ocrmypdf/version.py
docs/_build/
docs/_static/
docs/_templates/
docs/Makefile
ocrmypdf/lib/_*.py
# Code coverage
.coverage
htmlcov/
# Testing
log/
.ipynb_checkpoints/
.vscode/
*.ipynb
*.profile
/*.pdf
/*.qdf
*.ipynb
.ipynb_checkpoints/
/scratch.py
IDEAS
log/
tests/output/
tests/resources/private/
tmp/
pdfbox-app*.jar
.vscode/
IDEAS
_Dockerfile.local
/scratch.py
+132
View File
@@ -0,0 +1,132 @@
OCRmyPDF
========
![image](https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master%0A%20:target:%20https://travis-ci.org/jbarlow83/OCRmyPDF)
![image](https://img.shields.io/pypi/v/ocrmypdf.svg%0A%20:target:%20https://pypi.org/project/ocrmypdf/)
![image](https://img.shields.io/homebrew/v/ocrmypdf.svg%0A%20:alt:%20homebrew%0A%20:target:%20http://brewformulas.org/Ocrmypdf)
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched or copy-pasted.
```bash
ocrmypdf # it's a scriptable command line program
-l eng+fra # it supports multiple languages
--rotate-pages # it can fix pages that are misrotated
--deskew # it can deskew crooked PDFs!
--title "My PDF" # it can change output metadata
--jobs 4 # it uses multiple cores by default
--output-type pdfa # it produces PDF/A by default
input_scanned.pdf # takes PDF input (or images)
output_searchable.pdf # produces validated PDF output
```
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
For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/en/latest/).
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)
...so I decided to develop my own tool.
Installation
------------
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
```bash
apt-get install ocrmypdf
```
and macOS users with Homebrew may simply
```bash
brew install ocrmypdf
```
For everyone else, [see our documentation](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for installation steps.
Languages
---------
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users, you can often find packages that provide language packs:
```bash
# Display a list of all Tesseract language packs
apt-cache search tesseract-ocr
# Debian/Ubuntu users
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
```
You can then pass the `-l LANG` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple languages can be requested.
Documentation and support
-------------------------
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
```bash
ocrmypdf --help
```
Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/en/latest/index.html).
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)
Requirements
------------
Runs on CPython 3.5, 3.6 and 3.7. Requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
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)
License
-------
The OCRmyPDF software is licensed under the GNU GPLv3. Certain files are covered by other licenses, as noted in their source files.
The license for each test file varies, and is noted in tests/resources/README.rst. The documentation is licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA 4.0).
OCRmyPDF versions prior to 6.0 were licensed under the MIT License.
Disclaimer
----------
The software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-150
View File
@@ -1,150 +0,0 @@
OCRmyPDF
========
.. image:: https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master
:target: https://travis-ci.org/jbarlow83/OCRmyPDF
.. image:: https://img.shields.io/pypi/v/ocrmypdf.svg
:target: https://pypi.org/project/ocrmypdf/
.. image:: https://img.shields.io/homebrew/v/ocrmypdf.svg
:alt: homebrew
:target: http://brewformulas.org/Ocrmypdf
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
be searched or copy-pasted.
.. code-block:: bash
ocrmypdf # it's a scriptable command line program
-l eng+fra # it supports multiple languages
--rotate-pages # it can fix pages that are misrotated
--deskew # it can deskew crooked PDFs!
--title "My PDF" # it can change output metadata
--jobs 4 # it uses multiple cores by default
--output-type pdfa # it produces PDF/A by default
input_scanned.pdf # takes PDF input (or images)
output_searchable.pdf # produces validated PDF output
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
For details: please consult the `documentation <https://ocrmypdf.readthedocs.io/en/latest/>`_.
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)
...so I decided to develop my own tool.
Installation
------------
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
.. code-block:: bash
apt-get install ocrmypdf
and macOS users with Homebrew may simply
.. code-block:: bash
brew install ocrmypdf
For everyone else, `see our documentation <https://ocrmypdf.readthedocs.io/en/latest/installation.html>`_ for installation steps.
Languages
---------
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users,
you can often find packages that provide language packs:
.. code-block:: bash
# Display a list of all Tesseract language packs
apt-cache search tesseract-ocr
# Debian/Ubuntu users
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
languages can be requested.
Documentation and support
-------------------------
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
.. code-block:: bash
ocrmypdf --help
Our `documentation is served on Read the Docs <https://ocrmypdf.readthedocs.io/en/latest/index.html>`_.
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)
Requirements
------------
Runs on CPython 3.5, 3.6 and 3.7. Requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
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>`_
License
-------
The OCRmyPDF software is licensed under the GNU GPLv3. Certain files are covered by other licenses, as noted in their source files.
The license for each test file varies, and is noted in tests/resources/README.rst. The documentation is licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA 4.0).
OCRmyPDF versions prior to 6.0 were licensed under the MIT License.
Disclaimer
----------
The software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
+28
View File
@@ -13,6 +13,27 @@ Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and ar
find: [^`]\#([0-9]{1,3})[^0-9]
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
v7.1.0
------
- Improve the performance of initial text extraction, which is done to determine if a file contains existing text of some kind or not. On large files, this initial processing is now about 20x times faster. (`#299 <https://github.com/jbarlow83/OCRmyPDF/issues/299>`_)
- pikepdf 0.3.3 is now required.
- Fixed issue `#231 <https://github.com/jbarlow83/OCRmyPDF/issues/231>`_, a problem with JPEG2000 images where image metadata was only available inside the JPEG2000 file.
- Fixed some additional Ghostscript 9.25 compatibility issues.
- Improved handling of KeyboardInterrupt error messages. (`#301 <https://github.com/jbarlow83/OCRmyPDF/issues/301>`_)
- README.md is now served in GitHub markdown instead of reStructuredText.
v7.0.6
------
- Blacklist Ghostscript 9.24, now that 9.25 is available and fixes many regressions in 9.24.
v7.0.5
------
@@ -99,6 +120,13 @@ v7.0.0
+ It may be necessary to separately ``pip install pycparser`` to avoid `another Python 3.7 issue <https://github.com/eliben/pycparser/pull/135>`_.
v6.2.4
------
- Backport Ghostscript 9.25 compatibility fixes, which removes support for setting Unicode metadata
- Backport blacklisting Ghostscript 9.24
- Older versions of Ghostscript are still supported
v6.2.3
------
+1 -1
View File
@@ -3,7 +3,7 @@
# installation
cffi == 1.11.5
img2pdf == 0.3.0
pikepdf == 0.3.2
pikepdf == 0.3.3
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
pycparser == 2.18
python-xmp-toolkit == 2.0.1
+10 -3
View File
@@ -205,13 +205,14 @@ tests_require = open('test_requirements.txt').read().splitlines()
def readme():
with open('README.rst') as f:
with open('README.md') as f:
return f.read()
setup(
name='ocrmypdf',
description='OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched',
long_description=readme(),
long_description_content_type='text/markdown',
url='https://github.com/jbarlow83/OCRmyPDF',
author='James R. Barlow',
author_email='jim@purplerock.ca',
@@ -250,7 +251,7 @@ setup(
install_requires=[
'cffi >= 1.9.1', # must be a setup and install requirement
'img2pdf >= 0.2.4, < 0.4', # pure Python, so track HEAD closely
'pikepdf >= 0.3.2, < 0.4',
'pikepdf >= 0.3.3, < 0.4',
'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
@@ -269,4 +270,10 @@ setup(
},
package_data={'ocrmypdf': ['data/sRGB.icc']},
include_package_data=True,
zip_safe=False)
zip_safe=False,
project_urls={
'Documentation': 'https://ocrmypdf.readthedocs.io/',
'Source': 'https://github.com/jbarlow83/ocrmypdf',
'Tracker': 'https://github.com/jbarlow83/ocrmypdf/issues'
}
)
+26 -10
View File
@@ -17,7 +17,6 @@
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from tempfile import mkdtemp
from collections.abc import Sequence
from pathlib import Path
import sys
import os
@@ -36,7 +35,7 @@ import ruffus.proxy_logger as proxy_logger
from ._jobcontext import JobContext, JobContextManager, cleanup_working_files
from ._pipeline import build_pipeline
from .pdfa import file_claims_pdfa
from .helpers import is_iterable_notstr, re_symlink, is_file_writable, \
from .helpers import re_symlink, is_file_writable, \
available_cpu_count
from .exec import tesseract, qpdf, ghostscript
from . import PROGRAM_NAME, VERSION
@@ -598,18 +597,24 @@ def do_ruffus_exception(ruffus_five_tuple, options, log):
description of the error message that occurred."""
exit_code = None
task_name, job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
task_name = task_name # unused
job_name = job_name # unused
if exc_name == 'builtins.SystemExit':
_task_name, _job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
if isinstance(exc_name, type):
# ruffus is full of mystery... sometimes (probably when the process
# group leader is killed) exc_name is the class object of the exception,
# rather than a str. So reach into the object and get its name.
exc_name = exc_name.__name__
if exc_name in ('builtins.SystemExit', 'SystemExit'):
match = re.search(r"\.(.+?)\)", exc_value)
exit_code_name = match.groups()[0]
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
log.error(cleanup_ruffus_error_message(exc_value))
exit_code = ExitCode.input_file
elif exc_name == 'builtins.KeyboardInterrupt':
log.error("Interrupted by user")
elif exc_name in ('builtins.KeyboardInterrupt', 'KeyboardInterrupt'):
# We have to print in this case because the log daemon might be toast
print("Interrupted by user", file=sys.stderr)
exit_code = ExitCode.ctrl_c
elif exc_name == 'subprocess.CalledProcessError':
# It's up to the subprocess handler to report something useful
@@ -751,6 +756,7 @@ def preamble(_log):
_log.debug('ocrmypdf ' + VERSION)
_log.debug('tesseract ' + tesseract.version())
_log.debug('qpdf ' + qpdf.version())
_log.debug('gs ' + ghostscript.version())
def check_environ(options, _log):
@@ -835,9 +841,11 @@ def report_output_file_size(options, _log, input_file, output_file):
""".format(ratio, explanation)))
def run_pipeline():
options = parser.parse_args()
def run_pipeline(args=None):
options = parser.parse_args(args=args)
options.verbose_abbreviated_path = 1
if os.environ.get('_OCRMYPDF_THREADS'):
options.use_threads = True
if not check_closed_streams(options):
return ExitCode.bad_args
@@ -858,6 +866,14 @@ def run_pipeline():
"security vulnerabilities with certain malformed PDFs. Consider "
"upgrading to version 7.0.0 or newer.".format(qpdf.version()))
if ghostscript.version() == '9.24':
complain(
"Ghostscript 9.24 contains serious regressions and is not "
"supported. Please upgrade to Ghostscript 9.25 or use an older "
"version."
)
return ExitCode.missing_dependency
# Any changes to options will not take effect for options that are already
# bound to function parameters in the pipeline. (For example
# options.input_file, options.pdf_renderer are already bound.)
+3 -3
View File
@@ -163,7 +163,7 @@ def repair_and_parse_pdf(
copyfile(input_file, output_file)
try:
pdfinfo = PdfInfo(output_file)
pdfinfo = PdfInfo(output_file, log=log)
except pikepdf.PasswordError as e:
raise EncryptedPdfError()
except pikepdf.PdfError as e:
@@ -789,8 +789,8 @@ def generate_postscript_stub(
v.encode('ascii', errors='strict')
except UnicodeEncodeError:
log.warning(
"Ghostscript 9.24 does not support Unicode strings in metadata."
" These will be converted to ASCII if possible."
"Ghostscript 9.24+ does not support Unicode strings in "
" metadata. These will be converted to ASCII if possible."
)
generate_pdfa_ps(output_file, pdfmark, ascii_docinfo=ascii_docinfo)
+33 -11
View File
@@ -32,8 +32,7 @@ def version():
def jpeg_passthrough_available():
"""
Returns True if the installed version of Ghostscript supports JPEG passthru
"""Returns True if the installed version of Ghostscript supports JPEG passthru
Prior to 9.23, Ghostscript decode and re-encoded JPEGs internally. In 9.23
it gained the ability to keep JPEGs unmodified. However, the 9.23
@@ -42,7 +41,8 @@ def jpeg_passthrough_available():
https://bugs.ghostscript.com/show_bug.cgi?id=699216
The issue was fixed for 9.24, hence that is the first version we consider
the feature available.
the feature available. (However, we don't use 9.24 at all, so the first
version that allows JPEG passthrough is 9.25.
"""
return version() >= '9.24'
@@ -53,8 +53,7 @@ def _gs_error_reported(stream):
def extract_text(input_file, pageno=1):
"""
Use the txtwrite device to get text layout information out
"""Use the txtwrite device to get text layout information out
For details on options of -dTextFormat see
https://www.ghostscript.com/doc/current/VectorDevices.htm#TXT
@@ -65,10 +64,18 @@ def extract_text(input_file, pageno=1):
<span bbox="left top right bottom" font="..." size="...">
<char bbox="...." c="X"/>
:param pageno: number of page to extract, or all pages if None
:return: XML-ish text representation in bytes
"""
if pageno is not None:
pages = [
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno
]
else:
pages = []
args_gs = [
'gs',
'-dQUIET',
@@ -77,10 +84,9 @@ def extract_text(input_file, pageno=1):
'-dNOPAUSE',
'-sDEVICE=txtwrite',
'-dTextFormat=0',
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno,
] + pages + [
'-o', '-',
input_file
fspath(input_file)
]
p = run(args_gs, stdout=PIPE, stderr=PIPE)
@@ -96,8 +102,7 @@ def extract_text(input_file, pageno=1):
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
pageno=1, page_dpi=None, rotation=None):
"""
Rasterize one page of a PDF at resolution (xres, yres) in canvas units.
"""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
(xres, yres) even if those numbers are noninteger. The image's DPI will
@@ -179,6 +184,23 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
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
PDF files may be merged. One of the files in this list must be a pdfmark
file that provides Ghostscript with details on how to perform the PDF/A
conversion. By default with we pick PDF/A-2b, but this works for 1 or 3.
compression can be 'jpeg', 'lossless', or an empty string. In 'jpeg',
Ghostscript is instructed to convert color and grayscale images to DCT
(JPEG encoding). In 'lossless' Ghostscript is told to convert images to
Flate (lossless/PNG). If the parameter is omitted Ghostscript is left to
make its own decisions about how to encode images; it appears to use a
heuristic to decide how to encode images. As of Ghostscript 9.25, we
support passthrough JPEG which allows Ghostscript to avoid transcoding
images entirely. (The feature was added in 9.23 but broken, and the 9.24
release of Ghostscript had regressions, so we don't support it until 9.25.)
"""
compression_args = []
if compression == 'jpeg':
compression_args = [
+2 -1
View File
@@ -131,6 +131,7 @@ def _encode_ascii(s: str) -> str:
'(': '',
')': '',
'\\': '',
'\0': ''
})
return s.translate(trans).encode('ascii', errors='replace').decode()
@@ -284,7 +285,7 @@ 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)
ps = _get_pdfa_def(icc_profile, icc, pdfmark, ascii_docinfo=ascii_docinfo)
# We should have encoded everything to pure ASCII by this point, and
# to be safe, only allow ASCII in PostScript
+49 -29
View File
@@ -16,12 +16,14 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from decimal import Decimal
from math import hypot, isclose
import re
from collections import namedtuple
from pathlib import Path
from decimal import Decimal
from enum import Enum
from math import hypot, isclose
from pathlib import Path
from unittest.mock import Mock
import re
import xml.etree.ElementTree as ET
from .exec import ghostscript
from .helpers import fspath
@@ -486,27 +488,13 @@ def _find_images(*, pdf, container, shorthand=None):
yield from _find_form_xobject_images(pdf, container, contentsinfo)
def _page_get_textblocks(infile, pageno):
def _page_get_textblocks(infile, pageno, xmltext):
"""Smarter text detection"""
import xml.etree.ElementTree as ET
gstext = ghostscript.extract_text(infile, pageno+1)
# Remove all <char /> tags, because they might contain invalid XML entities
# like <char bbox="348 596 348 596" c="&#x1;"/> which chokes on the
# inclusion of U+0001. Understandably.
# Just remove the whole <char /> tag since we don't use it at all, and they
# are only generated as innermost self-closing tags.
gstext = regex_remove_char_tags.sub(b' ', gstext)
if gstext.strip() == '':
root = xmltext
if not hasattr(xmltext, 'findall'):
return []
try:
root = ET.fromstring(gstext)
except ET.ParseError as e:
return [] # If we can't parse, assume none...
def blocks():
for span in root.findall('.//span'):
bbox_str = span.attrib['bbox']
@@ -565,14 +553,15 @@ def _page_has_text(text_blocks, page_width, page_height):
return has_text
def _pdf_get_pageinfo(pdf, pageno: int, infile):
def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
pageinfo = {}
pageinfo['pageno'] = pageno
pageinfo['images'] = []
page = pdf.pages[pageno]
pageinfo['textinfo'] = _page_get_textblocks(fspath(infile), pageno)
pageinfo['textinfo'] = _page_get_textblocks(
fspath(infile), pageno, xmltext=xmltext)
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
width_pt = mediabox[2] - mediabox[0]
@@ -609,16 +598,47 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile):
return pageinfo
def _pdf_get_all_pageinfo(infile):
def _pdf_get_all_pageinfo(infile, log=None):
if not log:
log = Mock()
pdf = pikepdf.open(infile)
return [PageInfo(pdf, n, infile) for n in range(len(pdf.pages))], pdf
existing_text = ghostscript.extract_text(infile, pageno=None)
existing_text = regex_remove_char_tags.sub(b' ', existing_text)
try:
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:")
log.error(e)
page_xml = [None] * len(pdf.pages)
page_count_difference = len(pdf.pages) - len(page_xml)
if page_count_difference != 0:
log.error("The number of pages in the input file is inconsistent.")
if page_count_difference > 0:
page_xml.extend([None] * page_count_difference)
pages = []
for n in range(len(pdf.pages)):
page = PageInfo(pdf, n, infile, page_xml[n])
pages.append(page)
return pages, pdf
class PageInfo:
def __init__(self, pdf, pageno, infile):
def __init__(self, pdf, pageno, infile, xmltext):
self._pageno = pageno
self._infile = infile
self._pageinfo = _pdf_get_pageinfo(pdf, pageno, infile)
self._pageinfo = _pdf_get_pageinfo(pdf, pageno, infile, xmltext)
@property
def pageno(self):
@@ -695,9 +715,9 @@ class PdfInfo:
"""Get summary information about a PDF
"""
def __init__(self, infile):
def __init__(self, infile, log=None):
self._infile = infile
self._pages, pdf = _pdf_get_all_pageinfo(infile)
self._pages, pdf = _pdf_get_all_pageinfo(infile, log=log)
self._needs_rendering = pdf.root.get('/NeedsRendering', False)
@property
+1 -1
View File
@@ -20,11 +20,11 @@
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Replicate Ghostscript render failure while allowing rasterizing"""
import sys
import os
"""Replicate Ghostscript render failure while allowing rasterizing"""
def real_ghostscript(argv):