Refactor 'xyres' into Resolution

This commit is contained in:
James R. Barlow
2020-04-24 04:12:05 -07:00
parent 57771f06a3
commit 94c52a6fa3
10 changed files with 153 additions and 104 deletions
+37 -33
View File
@@ -39,7 +39,7 @@ from .exceptions import (
UnsupportedImageFormatError,
)
from .exec import ghostscript, tesseract
from .helpers import safe_symlink
from .helpers import Resolution, safe_symlink
from .hocrtransform import HocrTransform
from .optimize import optimize
from .pdfa import generate_pdfa_ps
@@ -99,7 +99,7 @@ def triage_image_file(input_file, output_file, options):
layout_fun = img2pdf.default_layout_fun
if options.image_dpi:
layout_fun = img2pdf.get_fixed_dpi_layout_fun(
(options.image_dpi, options.image_dpi)
Resolution(options.image_dpi, options.image_dpi)
)
with open(output_file, 'wb') as outf:
img2pdf.convert(
@@ -201,43 +201,45 @@ def validate_pdfinfo_options(context):
def get_page_dpi(pageinfo, options):
"Get the DPI when nonsquare DPI is tolerable"
xres = max(
pageinfo.xyres[0] or VECTOR_PAGE_DPI,
options.oversample or 0,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0,
pageinfo.dpi.x or VECTOR_PAGE_DPI,
options.oversample or 0.0,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0.0,
)
yres = max(
pageinfo.xyres[1] or VECTOR_PAGE_DPI,
pageinfo.dpi.y or VECTOR_PAGE_DPI,
options.oversample or 0,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0.0,
)
return (float(xres), float(yres))
return Resolution(float(xres), float(yres))
def get_page_square_dpi(pageinfo, options):
def get_page_square_dpi(pageinfo, options) -> Resolution:
"Get the DPI when we require xres == yres, scaled to physical units"
xres = pageinfo.xyres[0] or 0
yres = pageinfo.xyres[1] or 0
userunit = pageinfo.userunit or 1
return float(
xres = pageinfo.dpi.x or 0.0
yres = pageinfo.dpi.y or 0.0
userunit = float(pageinfo.userunit) or 1.0
units = float(
max(
(xres * userunit) or VECTOR_PAGE_DPI,
(yres * userunit) or VECTOR_PAGE_DPI,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0,
options.oversample or 0,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0.0,
options.oversample or 0.0,
)
)
return Resolution(units, units)
def get_canvas_square_dpi(pageinfo, options):
def get_canvas_square_dpi(pageinfo, options) -> Resolution:
"""Get the DPI when we require xres == yres, in Postscript units"""
return float(
units = float(
max(
(pageinfo.xyres[0]) or VECTOR_PAGE_DPI,
(pageinfo.xyres[1]) or VECTOR_PAGE_DPI,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0,
options.oversample or 0,
(pageinfo.dpi.x) or VECTOR_PAGE_DPI,
(pageinfo.dpi.y) or VECTOR_PAGE_DPI,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0.0,
options.oversample or 0.0,
)
)
return Resolution(units, units)
def is_ocr_required(page_context):
@@ -322,8 +324,8 @@ def rasterize_preview(input_file, page_context):
input_file,
output_file,
raster_device='jpeggray',
xyres=(canvas_dpi, canvas_dpi),
page_dpi=(page_dpi, page_dpi),
raster_dpi=canvas_dpi,
page_dpi=page_dpi,
pageno=page_context.pageinfo.pageno + 1,
)
return output_file
@@ -436,8 +438,8 @@ def rasterize(
input_file,
output_file,
raster_device=device,
xyres=(canvas_dpi, canvas_dpi),
page_dpi=(page_dpi, page_dpi),
raster_dpi=canvas_dpi,
page_dpi=page_dpi,
pageno=pageinfo.pageno + 1,
rotation=correction,
filter_vector=remove_vectors,
@@ -458,7 +460,7 @@ def preprocess_remove_background(input_file, page_context):
def preprocess_deskew(input_file, page_context):
output_file = page_context.get_path('pp_deskew.png')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
leptonica.deskew(input_file, output_file, dpi)
leptonica.deskew(input_file, output_file, dpi.x)
return output_file
@@ -467,7 +469,7 @@ def preprocess_clean(input_file, page_context):
output_file = page_context.get_path('pp_clean.png')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
unpaper.clean(input_file, output_file, dpi, page_context.options.unpaper_args)
unpaper.clean(input_file, output_file, dpi.x, page_context.options.unpaper_args)
return output_file
@@ -558,12 +560,14 @@ def create_visible_page_jpg(image, page_context):
# square DPI used to rasterize. When the preview image was
# rasterized, it was also converted to square resolution, which is
# what we want to give tesseract, so keep it square.
fallback_dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
dpi = im.info.get('dpi', (fallback_dpi, fallback_dpi))
if 'dpi' in im.info:
dpi = Resolution(*im.info['dpi'])
else:
# Fallback to page-implied DPI
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
# Pillow requires integer DPI
dpi = round(dpi[0]), round(dpi[1])
im.save(output_file, format='JPEG', dpi=dpi)
im.save(output_file, format='JPEG', dpi=dpi.to_int())
return output_file
@@ -576,7 +580,7 @@ def create_pdf_page_from_image(image, page_context):
# sandwich renderer would be fine.
output_file = page_context.get_path('visible.pdf')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
layout_fun = img2pdf.get_fixed_dpi_layout_fun((dpi, dpi))
layout_fun = img2pdf.get_fixed_dpi_layout_fun(dpi)
# This create a single page PDF
with open(image, 'rb') as imfile, open(output_file, 'wb') as pdf:
@@ -591,7 +595,7 @@ def create_pdf_page_from_image(image, page_context):
def render_hocr_page(hocr, page_context):
output_file = page_context.get_path('ocr_hocr.pdf')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
hocrtransform = HocrTransform(hocr, dpi)
hocrtransform = HocrTransform(hocr, dpi.x) # square
hocrtransform.to_pdf(
output_file,
image_filename=None,
+24 -24
View File
@@ -21,7 +21,6 @@ import logging
import os
import re
import warnings
from contextlib import suppress
from functools import lru_cache
from io import BytesIO
from os import fspath
@@ -31,8 +30,9 @@ from subprocess import PIPE, CalledProcessError
from PIL import Image
from ..exceptions import MissingDependencyError, SubprocessOutputError
from . import get_version, run
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
from ocrmypdf.exec import get_version, run
from ocrmypdf.helpers import Resolution
log = logging.getLogger(__name__)
@@ -62,7 +62,7 @@ def version():
return get_version(GS)
def jpeg_passthrough_available():
def jpeg_passthrough_available() -> bool:
"""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
@@ -79,7 +79,7 @@ def jpeg_passthrough_available():
return version() >= '9.24'
def _gs_error_reported(stream):
def _gs_error_reported(stream) -> bool:
return re.search(r'error', stream, flags=re.IGNORECASE)
@@ -133,35 +133,35 @@ def extract_text(input_file, pageno=1):
def rasterize_pdf(
input_file,
output_file,
input_file: os.PathLike,
output_file: os.PathLike,
*,
raster_device,
xyres,
pageno=1,
page_dpi=None,
rotation=None,
filter_vector=False,
raster_device: str,
raster_dpi: Resolution,
pageno: int = 1,
page_dpi: Resolution = None,
rotation: int = None,
filter_vector: bool = False,
):
"""Rasterize one page of a PDF at resolution xyres in canvas units.
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units.
The image is sized to match the integer pixels dimensions implied by
(xyres[0], xyres[1]) even if those numbers are noninteger. The image's DPI will
raster_dpi even if those numbers are noninteger. The image's DPI will
be overridden with the values in page_dpi.
:param input_file: pathlike
:param output_file: pathlike
:param raster_device:
:param xyres: resolution at which to rasterize page
:param raster_dpi: resolution at which to rasterize page
:param pageno: page number to rasterize (beginning at page 1)
:param page_dpi: resolution tuple (x, y) overriding output image DPI
:param rotation: 0, 90, 180, 270: clockwise angle to rotate page
:param filter_vector: if True, remove vector graphics objects
:return:
"""
res = round(xyres[0], 6), round(xyres[1], 6)
raster_dpi = raster_dpi.round(6)
if not page_dpi:
page_dpi = res
page_dpi = raster_dpi
args_gs = (
[
@@ -173,7 +173,7 @@ def rasterize_pdf(
f'-sDEVICE={raster_device}',
f'-dFirstPage={pageno}',
f'-dLastPage={pageno}',
f'-r{res[0]:f}x{res[1]:f}',
f'-r{raster_dpi.x:f}x{raster_dpi.y:f}',
]
+ (['-dFILTERVECTOR'] if filter_vector else [])
+ [
@@ -210,17 +210,17 @@ def rasterize_pdf(
elif rotation == 270:
im = im.transpose(Image.ROTATE_270)
if rotation % 180 == 90:
page_dpi = page_dpi[1], page_dpi[0]
page_dpi = page_dpi.flip_axis()
im.save(fspath(output_file), dpi=page_dpi)
def generate_pdfa(
pdf_pages,
output_file,
compression,
output_file: os.PathLike,
compression: str,
threads=None, # deprecated parameter
pdf_version='1.5',
pdfa_part='2',
pdf_version: str = '1.5',
pdfa_part: str = '2',
):
"""Generate a PDF/A.
+34
View File
@@ -20,14 +20,48 @@ import multiprocessing
import os
import shutil
import warnings
from collections import namedtuple
from collections.abc import Iterable
from contextlib import suppress
from functools import wraps
from math import inf, isclose
from pathlib import Path
log = logging.getLogger(__name__)
class Resolution(namedtuple('Resolution', ('x', 'y'))):
__slots__ = ()
def round(self, ndigits):
return Resolution(round(self.x, ndigits), round(self.y, ndigits))
def to_int(self):
return Resolution(int(round(self.x)), int(round(self.y)))
@property
def is_square(self):
return isclose(self.x, self.y, rel_tol=1e-3)
def take_max(self, vals, yvals=None):
if yvals is not None:
return Resolution(max(self.x, *vals), max(self.y, *yvals))
max_x, max_y = self.x, self.y
for x, y in vals:
max_x = max(x, max_x)
max_y = max(y, max_y)
return Resolution(max_x, max_y)
def flip_axis(self):
return Resolution(self.y, self.x)
def __str__(self):
return f"{self.x:f}x{self.y:f}"
def __repr__(self):
return f"Resolution({self.x}x{self.y} dpi)"
def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike, *args, **kwargs):
"""
Helper function: relinks soft symbolic link if necessary
+16 -24
View File
@@ -32,6 +32,7 @@ from tqdm import tqdm
from ocrmypdf.exceptions import EncryptedPdfError
from ocrmypdf.exec import ghostscript
from ocrmypdf.helpers import Resolution
from ocrmypdf.pdfinfo import ghosttext
from ocrmypdf.pdfinfo.layout import get_page_analysis, get_text_boxes
@@ -265,7 +266,7 @@ def _get_dpi(ctm_shorthand, image_size):
dpi_w = scale_w * 72.0
dpi_h = scale_h * 72.0
return dpi_w, dpi_h
return Resolution(dpi_w, dpi_h)
class ImageInfo:
@@ -356,11 +357,8 @@ class ImageInfo:
return self._enc
@property
def xyres(self):
return (
_get_dpi(self._shorthand, (self._width, self._height))[0],
_get_dpi(self._shorthand, (self._width, self._height))[1],
)
def dpi(self):
return _get_dpi(self._shorthand, (self._width, self._height))
def __repr__(self):
class_locals = {
@@ -370,7 +368,7 @@ class ImageInfo:
}
return (
"<ImageInfo '{name}' {type_} {width}x{height} {color} "
"{comp} {bpc} {enc} {xyres}>"
"{comp} {bpc} {enc} {dpi}>"
).format(**class_locals)
@@ -606,11 +604,10 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile: PathLike, xmltext: str):
pageinfo['images'] = [im for im in contentsinfo if isinstance(im, ImageInfo)]
if pageinfo['images']:
xres = Decimal(max(image.xyres[0] for image in pageinfo['images']))
yres = Decimal(max(image.xyres[1] for image in pageinfo['images']))
pageinfo['xyres'] = xres, yres
pageinfo['width_pixels'] = int(round(xres * pageinfo['width_inches']))
pageinfo['height_pixels'] = int(round(yres * pageinfo['height_inches']))
dpi = Resolution(0.0, 0.0).take_max(image.dpi for image in pageinfo['images'])
pageinfo['dpi'] = dpi
pageinfo['width_pixels'] = int(round(dpi.x * float(pageinfo['width_inches'])))
pageinfo['height_pixels'] = int(round(dpi.y * float(pageinfo['height_inches'])))
return pageinfo
@@ -678,11 +675,11 @@ class PageInfo:
@property
def width_pixels(self):
return int(round(self.width_inches * self.xyres[0]))
return int(round(float(self.width_inches) * self.dpi.x))
@property
def height_pixels(self):
return int(round(self.height_inches * self.xyres[1]))
return int(round(float(self.height_inches) * self.dpi.y))
@property
def rotation(self):
@@ -722,8 +719,8 @@ class PageInfo:
)
@property
def xyres(self):
return self._pageinfo.get('xyres', (0, 0))
def dpi(self):
return self._pageinfo.get('dpi', Resolution(0.0, 0.0))
@property
def userunit(self):
@@ -738,14 +735,9 @@ class PageInfo:
def __repr__(self):
return (
'<PageInfo ' 'pageno={} {}"x{}" rotation={} res={} has_text={}>'
).format(
self.pageno,
self.width_inches,
self.height_inches,
self.rotation,
self.xyres,
self.has_text,
f'<PageInfo '
f'pageno={self.pageno} {self.width_inches}"x{self.height_inches}" '
f'rotation={self.rotation} dpi={self.dpi} has_text={self.has_text}>'
)
+9 -4
View File
@@ -24,6 +24,7 @@ from PIL import Image
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.exec.ghostscript import rasterize_pdf
from ocrmypdf.helpers import Resolution
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
@@ -71,13 +72,15 @@ def test_rasterize_size(francais, outdir, caplog):
assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0
page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72))
target_size = Decimal('50.0'), Decimal('30.0')
forced_dpi = 42.0, 4242.0
forced_dpi = Resolution(42.0, 4242.0)
rasterize_pdf(
path,
outdir / 'out.png',
raster_device='pngmono',
xyres=(target_size[0] / page_size[0], target_size[1] / page_size[1]),
raster_dpi=Resolution(
target_size[0] / page_size[0], target_size[1] / page_size[1]
),
page_dpi=forced_dpi,
)
@@ -92,14 +95,16 @@ def test_rasterize_rotated(francais, outdir, caplog):
assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0
page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72))
target_size = Decimal('50.0'), Decimal('30.0')
forced_dpi = 42.0, 4242.0
forced_dpi = Resolution(42.0, 4242.0)
caplog.set_level(logging.DEBUG)
rasterize_pdf(
path,
outdir / 'out.png',
raster_device='pngmono',
xyres=(target_size[0] / page_size[0], target_size[1] / page_size[1]),
raster_dpi=Resolution(
target_size[0] / page_size[0], target_size[1] / page_size[1]
),
page_dpi=forced_dpi,
rotation=90,
)
+4 -4
View File
@@ -74,8 +74,8 @@ def test_oversample(spoof_tesseract_cache, renderer, resources, outpdf):
pdfinfo = PdfInfo(oversampled_pdf)
print(pdfinfo[0].xyres[0])
assert abs(pdfinfo[0].xyres[0] - 350) < 1
print(pdfinfo[0].dpi.x)
assert abs(pdfinfo[0].dpi.x - 350) < 1
def test_repeat_ocr(resources, no_outpdf):
@@ -393,8 +393,8 @@ def test_very_high_dpi(spoof_tesseract_cache, resources, outpdf):
pdfinfo = PdfInfo(outpdf)
image = pdfinfo[0].images[0]
assert isclose(image.xyres[0], image.xyres[1])
assert isclose(image.xyres[0], 2400)
assert isclose(image.dpi.x, image.dpi.y)
assert isclose(image.dpi.x, 2400)
def test_overlay(spoof_tesseract_noop, resources, outpdf):
+5 -1
View File
@@ -26,6 +26,7 @@ from PIL import Image
from ocrmypdf import optimize as opt
from ocrmypdf.exec import jbig2enc, pngquant
from ocrmypdf.exec.ghostscript import rasterize_pdf
from ocrmypdf.helpers import Resolution
check_ocrmypdf = pytest.helpers.check_ocrmypdf # pylint: disable=e1101
@@ -43,7 +44,10 @@ def test_mono_not_inverted(resources, outdir):
opt.main(infile, outdir / 'out.pdf', level=3)
rasterize_pdf(
outdir / 'out.pdf', outdir / 'im.png', raster_device='pnggray', xyres=(10, 10)
outdir / 'out.pdf',
outdir / 'im.png',
raster_device='pnggray',
raster_dpi=Resolution(10, 10),
)
with Image.open(fspath(outdir / 'im.png')) as im:
+5 -5
View File
@@ -85,8 +85,8 @@ def test_single_page_image(outdir):
assert pdfimage.color == Colorspace.gray
# DPI in a 1"x1" is the image width
assert isclose(pdfimage.xyres[0], 8)
assert isclose(pdfimage.xyres[1], 8)
assert isclose(pdfimage.dpi.x, 8)
assert isclose(pdfimage.dpi.y, 8)
def test_single_page_inline_image(outdir):
@@ -105,7 +105,7 @@ def test_single_page_inline_image(outdir):
info = pdfinfo.PdfInfo(filename)
print(info)
pdfimage = info[0].images[0]
assert isclose(pdfimage.xyres[0], 8)
assert isclose(pdfimage.dpi.x, 8)
assert pdfimage.color == Colorspace.gray
assert pdfimage.width == 8
@@ -117,7 +117,7 @@ def test_jpeg(resources, outdir):
pdfimage = pdf[0].images[0]
assert pdfimage.enc == Encoding.jpeg
assert isclose(pdfimage.xyres[0], 150)
assert isclose(pdfimage.dpi.x, 150)
def test_form_xobject(resources):
@@ -139,7 +139,7 @@ def test_no_contents(resources):
def test_oversized_page(resources):
pdf = pdfinfo.PdfInfo(resources / 'poster.pdf')
image = pdf[0].images[0]
assert image.width * image.xyres[0] > 200, "this is supposed to be oversized"
assert image.width * image.dpi.x > 200, "this is supposed to be oversized"
def test_pickle(resources):
+17 -8
View File
@@ -22,6 +22,7 @@ import pytest
from PIL import Image
from ocrmypdf.exec import ghostscript
from ocrmypdf.helpers import Resolution
from ocrmypdf.leptonica import Pix
from ocrmypdf.pdfinfo import PdfInfo
@@ -50,7 +51,11 @@ def test_deskew(spoof_tesseract_noop, resources, outdir):
deskewed_png = outdir / 'deskewed.png'
ghostscript.rasterize_pdf(
deskewed_pdf, deskewed_png, raster_device='pngmono', xyres=(150, 150), pageno=1
deskewed_pdf,
deskewed_png,
raster_device='pngmono',
raster_dpi=Resolution(150, 150),
pageno=1,
)
pix = Pix.open(deskewed_png)
@@ -77,7 +82,11 @@ def test_remove_background(spoof_tesseract_noop, resources, outdir):
output_png = outdir / 'remove_bg.png'
ghostscript.rasterize_pdf(
output_pdf, output_png, raster_device='png16m', xyres=(100, 100), pageno=1
output_pdf,
output_png,
raster_device='png16m',
raster_dpi=Resolution(100, 100),
pageno=1,
)
# The output image should contain pure white and black
@@ -117,7 +126,7 @@ def test_exotic_image(
def test_non_square_resolution(renderer, spoof_tesseract_cache, resources, outpdf):
# Confirm input image is non-square resolution
in_pageinfo = PdfInfo(resources / 'aspect.pdf')
assert in_pageinfo[0].xyres[0] != in_pageinfo[0].xyres[1]
assert in_pageinfo[0].dpi.x != in_pageinfo[0].dpi.y
check_ocrmypdf(
resources / 'aspect.pdf',
@@ -130,7 +139,7 @@ def test_non_square_resolution(renderer, spoof_tesseract_cache, resources, outpd
out_pageinfo = PdfInfo(outpdf)
# Confirm resolution was kept the same
assert in_pageinfo[0].xyres == out_pageinfo[0].xyres
assert in_pageinfo[0].dpi == out_pageinfo[0].dpi
@pytest.mark.parametrize('renderer', RENDERERS)
@@ -139,7 +148,7 @@ def test_convert_to_square_resolution(
):
# Confirm input image is non-square resolution
in_pageinfo = PdfInfo(resources / 'aspect.pdf')
assert in_pageinfo[0].xyres[0] != in_pageinfo[0].xyres[1]
assert in_pageinfo[0].dpi.x != in_pageinfo[0].dpi.y
# --force-ocr requires means forced conversion to square resolution
check_ocrmypdf(
@@ -156,7 +165,7 @@ def test_convert_to_square_resolution(
in_p0, out_p0 = in_pageinfo[0], out_pageinfo[0]
# Resolution show now be equal
assert out_p0.xyres[0] == out_p0.xyres[1]
assert out_p0.dpi.x == out_p0.dpi.y
# Page size should match input page size
assert isclose(in_p0.width_inches, out_p0.width_inches)
@@ -164,7 +173,7 @@ def test_convert_to_square_resolution(
# Because we rasterized the page to produce a new image, it should occupy
# the entire page
out_im_w = out_p0.images[0].width / out_p0.images[0].xyres[0]
out_im_h = out_p0.images[0].height / out_p0.images[0].xyres[1]
out_im_w = out_p0.images[0].width / out_p0.images[0].dpi.x
out_im_h = out_p0.images[0].height / out_p0.images[0].dpi.y
assert isclose(out_p0.width_inches, out_im_w)
assert isclose(out_p0.height_inches, out_im_h)
+2 -1
View File
@@ -27,6 +27,7 @@ from PIL import Image
from ocrmypdf import leptonica
from ocrmypdf.exec import ghostscript, tesseract
from ocrmypdf.helpers import Resolution
from ocrmypdf.pdfinfo import PdfInfo
# pytest.helpers is dynamic
@@ -59,7 +60,7 @@ def check_monochrome_correlation(
pdf,
png,
raster_device='pngmono',
xyres=(100, 100),
raster_dpi=Resolution(100, 100),
pageno=pageno,
rotation=0,
)