Compare commits

...
14 Commits
10 changed files with 108 additions and 37 deletions
+2 -1
View File
@@ -190,7 +190,8 @@ of ocrmypdf, and install the following dependencies:
python3-reportlab \ python3-reportlab \
qpdf \ qpdf \
tesseract-ocr \ tesseract-ocr \
zlib1g zlib1g \
unpaper
We will need a newer version of ``pip`` then was available for Ubuntu 18.04: We will need a newer version of ``pip`` then was available for Ubuntu 18.04:
+25 -1
View File
@@ -12,12 +12,36 @@ may be unreliable. Use the API to depend on precise behavior.
The public API may be useful in scripts that launch OCRmyPDF processes or that The public API may be useful in scripts that launch OCRmyPDF processes or that
wish to use some of its features for working with PDFs. wish to use some of its features for working with PDFs.
v11.2.0
=======
- Fixed an issue with optimizing PNG-type images that had soft masks or image masks.
This is a regression introduced in (or about) v11.1.0.
- Improved type checking of the ``plugins`` parameter for the ``ocrmypdf.ocr``
API call.
v11.1.2
=======
- Fixed hOCR renderer writing the text in roughly reverse order. This should not
affect reasonably smart PDF readers that properly locate the position of all
text, but may confuse those that rely on the order of objects in the content
stream. (#642)
v11.1.1
=======
- We now avoid using named temporary files when using pngquant allowing containerized
pngquant installs to be used.
- Clarified an error message.
- Highest number of 1's in a release ever!
v11.1.0 v11.1.0
======= =======
- Fixed page rotation issues: #634, #589. - Fixed page rotation issues: #634, #589.
- Fixed some cases where optimization created an invalid image such as a - Fixed some cases where optimization created an invalid image such as a
1-bit "RGB" iamge: #629, #620. 1-bit "RGB" image: #629, #620.
- Page numbers are now displayed in debug logs when pages are being grafted. - Page numbers are now displayed in debug logs when pages are being grafted.
- ocrmypdf.optimize.rewrite_png and ocrmypdf.optimize.rewrite_png_as_g4 were - ocrmypdf.optimize.rewrite_png and ocrmypdf.optimize.rewrite_png_as_g4 were
marked deprecated. Strictly speaking these should have been internal APIs, marked deprecated. Strictly speaking these should have been internal APIs,
+19
View File
@@ -18,6 +18,25 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
"""
An example of an OCRmyPDF plugin.
This plugin adds two new command line arguments
--grayscale-ocr: converts the image to grayscale before performing OCR on it
(This is occasionally useful for images whose color confounds OCR. It only
affects the image shown to OCR. The image is not saved.)
--mono-page: converts pages all pages in the output file to black and white
To use this from the command line:
ocrmypdf --plugin path/to/example_plugin.py --mono-page input.pdf output.pdf
To use this as an API:
import ocrmypdf
ocrmypdf.ocr('input.pdf', 'output.pdf',
plugins=['path/to/example_plugin.py'], mono_page=True
)
"""
import logging import logging
from PIL import Image from PIL import Image
+25 -23
View File
@@ -7,7 +7,11 @@
"""Interface to pngquant executable""" """Interface to pngquant executable"""
from contextlib import contextmanager
from io import BytesIO
from os import fspath from os import fspath
from pathlib import Path
from subprocess import PIPE
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from PIL import Image from PIL import Image
@@ -28,34 +32,32 @@ def available():
return True return True
def quantize(input_file, output_file, quality_min, quality_max): @contextmanager
input_file = fspath(input_file) def input_as_png(input_file: Path):
output_file = fspath(output_file) if not input_file.name.endswith('.png'):
if input_file.endswith('.jpg'): with Image.open(input_file) as im:
with Image.open(input_file) as im, NamedTemporaryFile(suffix='.png') as tmp: bio = BytesIO()
im.save(tmp) im.save(bio, format='png')
args = [ bio.seek(0)
'pngquant', yield bio
'--force',
'--skip-if-larger',
'--output',
output_file,
'--quality',
f'{quality_min}-{quality_max}',
'--',
tmp.name,
]
run(args)
else: else:
with open(input_file, 'rb') as f:
yield f
def quantize(input_file: Path, output_file: Path, quality_min: int, quality_max: int):
with input_as_png(input_file) as input_stream:
args = [ args = [
'pngquant', 'pngquant',
'--force', '--force',
'--skip-if-larger', '--skip-if-larger',
'--output',
output_file,
'--quality', '--quality',
f'{quality_min}-{quality_max}', f'{quality_min}-{quality_max}',
'--', '--', # pngquant: stop processing arguments
input_file, '-', # pngquant: stream input and output
] ]
run(args) result = run(args, stdin=input_stream, stdout=PIPE, stderr=PIPE, check=False)
if result.returncode == 0:
# input_file could be the same as output_file, so we defer the write
output_file.write_bytes(result.stdout)
+1 -1
View File
@@ -107,7 +107,7 @@ def run(input_file, output_file, dpi, mode_args):
def validate_custom_args(args: str): def validate_custom_args(args: str):
unpaper_args = shlex.split(args) unpaper_args = shlex.split(args)
if any('/' in arg for arg in unpaper_args): if any(('/' in arg or arg == '.' or arg == '..') for arg in unpaper_args):
raise ValueError('No filenames allowed in --unpaper-args') raise ValueError('No filenames allowed in --unpaper-args')
return unpaper_args return unpaper_args
+3 -1
View File
@@ -226,7 +226,7 @@ def ocr( # pylint: disable=unused-argument
user_words: os.PathLike = None, user_words: os.PathLike = None,
user_patterns: os.PathLike = None, user_patterns: os.PathLike = None,
fast_web_view: float = None, fast_web_view: float = None,
plugins: Iterable[str] = None, plugins: Iterable[Union[str, Path]] = None,
keep_temporary_files: bool = None, keep_temporary_files: bool = None,
progress_bar: bool = None, progress_bar: bool = None,
**kwargs, **kwargs,
@@ -280,6 +280,8 @@ def ocr( # pylint: disable=unused-argument
""" """
if not plugins: if not plugins:
plugins = [] plugins = []
elif isinstance(plugins, (str, Path)):
plugins = [plugins]
else: else:
plugins = list(plugins) plugins = list(plugins)
+3 -5
View File
@@ -39,13 +39,11 @@ def check_options(options):
if gs_version < '9.20' and options.output_type != 'pdf' and not is_latin: if gs_version < '9.20' and options.output_type != 'pdf' and not is_latin:
# https://bugs.ghostscript.com/show_bug.cgi?id=696874 # https://bugs.ghostscript.com/show_bug.cgi?id=696874
# Ghostscript < 9.20 fails to encode multibyte characters properly # Ghostscript < 9.20 fails to encode multibyte characters properly
msg = ( log.warning(
"The installed version of Ghostscript does not work correctly " f"The installed version of Ghostscript ({gs_version}) does not work "
"with the OCR languages you specified. Use --output-type pdf or " "correctly with the OCR languages you specified. Use --output-type pdf or "
"upgrade to Ghostscript 9.20 or later to avoid this issue." "upgrade to Ghostscript 9.20 or later to avoid this issue."
) )
msg += f"Found Ghostscript {gs_version}"
log.warning(msg)
if options.output_type == 'pdfa': if options.output_type == 'pdfa':
options.output_type = 'pdfa-2' options.output_type = 'pdfa-2'
+4 -1
View File
@@ -167,7 +167,10 @@ class HocrTransform:
def topdown_position(self, element): def topdown_position(self, element):
pxl_line_coords = self.element_coordinates(element) pxl_line_coords = self.element_coordinates(element)
line_box = self.pt_from_pixel(pxl_line_coords) line_box = self.pt_from_pixel(pxl_line_coords)
return -line_box.y2 # Coordinates here are still in the hocr coordinate system, so 0 on the y axis
# is the top of the page and increasing values of y will move towards the
# bottom of the page.
return line_box.y2
def to_pdf( def to_pdf(
self, self,
+25 -3
View File
@@ -77,23 +77,26 @@ def extract_image_filter(
if image.Subtype != Name.Image: if image.Subtype != Name.Image:
return None return None
if image.Length < 100: if image.Length < 100:
log.debug("Skipping small image, xref %s", xref) log.debug(f"Skipping small image, xref {xref}")
return None return None
pim = PdfImage(image) pim = PdfImage(image)
if len(pim.filter_decodeparms) > 1: if len(pim.filter_decodeparms) > 1:
log.debug("Skipping multiply filtered, xref %s", xref) log.debug(f"Skipping multiply filtered image, xref {xref}")
return None return None
filtdp = pim.filter_decodeparms[0] filtdp = pim.filter_decodeparms[0]
if pim.bits_per_component > 8: if pim.bits_per_component > 8:
log.debug(f"Skipping wide gamut image, xref {xref}")
return None # Don't mess with wide gamut images return None # Don't mess with wide gamut images
if filtdp[0] == Name.JPXDecode: if filtdp[0] == Name.JPXDecode:
log.debug(f"Skipping JPEG2000 iamge, xref {xref}")
return None # Don't do JPEG2000 return None # Don't do JPEG2000
if Name.Decode in image: if Name.Decode in image:
log.debug(f"Skipping image with Decode table, xref {xref}")
return None # Don't mess with custom Decode tables return None # Don't mess with custom Decode tables
return pim, filtdp return pim, filtdp
@@ -229,7 +232,9 @@ def extract_images(
# Ignore soft masks # Ignore soft masks
smask_xref = Xref(image.SMask.objgen[0]) smask_xref = Xref(image.SMask.objgen[0])
exclude_xrefs.add(smask_xref) exclude_xrefs.add(smask_xref)
log.debug(f"Skipping image {smask_xref} because it is an SMask")
include_xrefs.add(xref) include_xrefs.add(xref)
log.debug(f"Treating {xref} as an optimization candidate")
if xref not in pageno_for_xref: if xref not in pageno_for_xref:
pageno_for_xref[xref] = pageno pageno_for_xref[xref] = pageno
@@ -411,12 +416,29 @@ def _transcode_png(pike: Pdf, filename: Path, xref: Xref) -> bool:
decode_parms=local_image.DecodeParms, decode_parms=local_image.DecodeParms,
) )
# Don't copy keys from the new image...
del_keys = set(im_obj.keys()) - set(local_image.keys()) del_keys = set(im_obj.keys()) - set(local_image.keys())
# ...except for the keep_fields, which are essential to displaying
# the image correctly and preserving its metadata. (/Decode arrays
# and /SMaskInData are implicitly discarded prior to this point.)
keep_fields = {
'/ID',
'/Intent',
'/Interpolate',
'/Mask',
'/Metadata',
'/OC',
'/OPI',
'/SMask',
'/StructParent',
}
del_keys -= keep_fields
for key in local_image.keys(): for key in local_image.keys():
if key != Name.Length: if key != Name.Length and str(key) not in keep_fields:
im_obj[key] = local_image[key] im_obj[key] = local_image[key]
for key in del_keys: for key in del_keys:
del im_obj[key] del im_obj[key]
return True
def transcode_pngs( def transcode_pngs(
+1 -1
View File
@@ -55,7 +55,7 @@ def test_old_ghostscript(caplog):
vd._check_options( vd._check_options(
*make_opts_pm(language='chi_sim', output_type='pdfa'), {'chi_sim'} *make_opts_pm(language='chi_sim', output_type='pdfa'), {'chi_sim'}
) )
assert 'Ghostscript does not work correctly' in caplog.text assert 'does not work correctly' in caplog.text
with patch('ocrmypdf._exec.ghostscript.version', return_value='9.18'), patch( with patch('ocrmypdf._exec.ghostscript.version', return_value='9.18'), patch(
'ocrmypdf._exec.tesseract.has_textonly_pdf', return_value=True 'ocrmypdf._exec.tesseract.has_textonly_pdf', return_value=True