1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-09-11 20:47:56 +03:00

Add API controlling JPEG quality

This commit is contained in:
Guillaume Ayoub 2023-03-26 13:37:15 +02:00
parent 51971f3293
commit eb6491f895
8 changed files with 55 additions and 31 deletions

View File

@ -370,7 +370,9 @@ def test_command_line_render(tmpdir):
_run('not_optimized.html out22.pdf -O all -O none')
_run('not_optimized.html out23.pdf -O pdf')
_run('not_optimized.html out24.pdf -O none -O fonts -O pdf')
_run('not_optimized.html out25.pdf -O all -j 10')
assert (
len(tmpdir.join('out25.pdf').read_binary()) <
len(tmpdir.join('out16.pdf').read_binary()) <
len(tmpdir.join('out15.pdf').read_binary()) <
len(tmpdir.join('out20.pdf').read_binary()))

View File

@ -55,15 +55,16 @@ class FakeHTML(HTML):
def write_pdf(self, target=None, stylesheets=None, zoom=1,
attachments=None, finisher=None, presentational_hints=False,
optimize_size=('fonts',), font_config=None,
counter_style=None, image_cache=None, identifier=None,
variant=None, version=None, forms=False,
optimize_size=('fonts',), jpeg_quality=None,
font_config=None, counter_style=None, image_cache=None,
identifier=None, variant=None, version=None, forms=False,
custom_metadata=False):
# Override function to set PDF size optimization to False by default
return super().write_pdf(
target, stylesheets, zoom, attachments, finisher,
presentational_hints, optimize_size, font_config, counter_style,
image_cache, identifier, variant, version, forms, custom_metadata)
presentational_hints, optimize_size, jpeg_quality, font_config,
counter_style, image_cache, identifier, variant, version, forms,
custom_metadata)
def resource_filename(basename):
@ -194,7 +195,7 @@ def _parse_base(html_content, base_url=BASE_URL):
style_for = get_all_computed_styles(document, counter_style=counter_style)
get_image_from_uri = functools.partial(
images.get_image_from_uri, cache={}, url_fetcher=document.url_fetcher,
optimize_size=())
optimize_size=(), jpeg_quality=None)
target_collector = TargetCollector()
footnotes = []
return (

View File

@ -118,8 +118,9 @@ class HTML:
return [HTML5_PH_STYLESHEET]
def render(self, stylesheets=None, presentational_hints=False,
optimize_size=('fonts', 'pdf'), font_config=None,
counter_style=None, image_cache=None, forms=False):
optimize_size=('fonts', 'pdf'), jpeg_quality=None,
font_config=None, counter_style=None, image_cache=None,
forms=False):
"""Lay out and paginate the document, but do not (yet) export it.
This returns a :class:`document.Document` object which provides
@ -135,6 +136,7 @@ class HTML:
:param tuple optimize_size:
Optimize size of generated PDF. Can contain "images", "fonts" and
"pdf".
:param int jpeg_quality: JPEG quality between 0 (worst) to 95 (best).
:type font_config: :class:`text.fonts.FontConfiguration`
:param font_config: A font configuration handling ``@font-face`` rules.
:type counter_style: :class:`css.counters.CounterStyle`
@ -150,13 +152,13 @@ class HTML:
"""
return Document._render(
self, stylesheets, presentational_hints, optimize_size,
font_config, counter_style, image_cache, forms)
jpeg_quality, font_config, counter_style, image_cache, forms)
def write_pdf(self, target=None, stylesheets=None, zoom=1,
attachments=None, finisher=None, presentational_hints=False,
optimize_size=('fonts', 'pdf'), font_config=None,
counter_style=None, image_cache=None, identifier=None,
variant=None, version=None, forms=False,
optimize_size=('fonts', 'pdf'), jpeg_quality=None,
font_config=None, counter_style=None, image_cache=None,
identifier=None, variant=None, version=None, forms=False,
custom_metadata=False):
"""Render the document to a PDF file.
@ -188,6 +190,7 @@ class HTML:
:param tuple optimize_size:
Optimize size of generated PDF. Can contain "images", "fonts" and
"pdf".
:param int jpeg_quality: JPEG quality between 0 (worst) to 95 (best).
:type font_config: :class:`text.fonts.FontConfiguration`
:param font_config: A font configuration handling ``@font-face`` rules.
:type counter_style: :class:`css.counters.CounterStyle`
@ -211,8 +214,8 @@ class HTML:
"""
return (
self.render(
stylesheets, presentational_hints, optimize_size, font_config,
counter_style, image_cache, forms)
stylesheets, presentational_hints, optimize_size, jpeg_quality,
font_config, counter_style, image_cache, forms)
.write_pdf(
target, zoom, attachments, finisher, identifier, variant,
version, custom_metadata))

View File

@ -100,6 +100,10 @@ def main(argv=None, stdout=None, stdin=None):
Store cache on disk instead of memory. The ``folder`` is created if
needed and cleaned after the PDF is generated.
.. option:: -j <quality>, --jpeg-quality <quality>
JPEG quality between 0 (worst) to 95 (best).
.. option:: -v, --verbose
Show warnings and information messages.
@ -167,6 +171,9 @@ def main(argv=None, stdout=None, stdin=None):
'-c', '--cache-folder',
help='Store cache on disk instead of memory. The ``folder`` is '
'created if needed and cleaned after the PDF is generated.')
parser.add_argument(
'-j', '--jpeg-quality', type=int,
help='JPEG quality between 0 (worst) to 95 (best)')
parser.add_argument(
'-v', '--verbose', action='store_true',
help='show warnings and information messages')
@ -208,6 +215,7 @@ def main(argv=None, stdout=None, stdin=None):
'stylesheets': args.stylesheet,
'presentational_hints': args.presentational_hints,
'optimize_size': tuple(optimize_size),
'jpeg_quality': args.jpeg_quality,
'attachments': args.attachment,
'identifier': args.pdf_identifier,
'variant': args.pdf_variant,

View File

@ -219,8 +219,8 @@ class Document:
@classmethod
def _build_layout_context(cls, html, stylesheets, presentational_hints,
optimize_size, font_config, counter_style,
image_cache, forms):
optimize_size, jpeg_quality, font_config,
counter_style, image_cache, forms):
if font_config is None:
font_config = FontConfiguration()
if counter_style is None:
@ -243,7 +243,8 @@ class Document:
counter_style, page_rules, target_collector, forms)
get_image_from_uri = functools.partial(
original_get_image_from_uri, cache=image_cache,
url_fetcher=html.url_fetcher, optimize_size=optimize_size)
url_fetcher=html.url_fetcher, optimize_size=optimize_size,
jpeg_quality=jpeg_quality)
PROGRESS_LOGGER.info('Step 4 - Creating formatting structure')
context = LayoutContext(
style_for, get_image_from_uri, font_config, counter_style,
@ -252,7 +253,7 @@ class Document:
@classmethod
def _render(cls, html, stylesheets, presentational_hints, optimize_size,
font_config, counter_style, image_cache, forms):
jpeg_quality, font_config, counter_style, image_cache, forms):
if font_config is None:
font_config = FontConfiguration()
@ -261,7 +262,7 @@ class Document:
context = cls._build_layout_context(
html, stylesheets, presentational_hints, optimize_size,
font_config, counter_style, image_cache, forms)
jpeg_quality, font_config, counter_style, image_cache, forms)
root_box = build_formatting_structure(
html.etree_element, context.style_for, context.get_image_from_uri,

View File

@ -1202,8 +1202,7 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y,
png_data = ffi.unpack(hb_data, int(stream.length[0]))
pillow_image = Image.open(BytesIO(png_data))
image_id = f'{font.hash}{glyph}'
image = RasterImage(
pillow_image, image_id, optimize_size=(), cache={})
image = RasterImage(pillow_image, image_id)
d = font.widths[glyph] / 1000
a = pillow_image.width / pillow_image.height * d
pango.pango_font_get_glyph_extents(

View File

@ -36,9 +36,17 @@ class ImageLoadingError(ValueError):
class RasterImage:
def __init__(self, pillow_image, image_id, optimize_size, cache):
def __init__(self, pillow_image, image_id, cache=None, optimize_size=(),
jpeg_quality=None):
self.id = image_id
self._cache = cache
self._cache = {} if cache is None else cache
self._optimize_size = optimize_size
self._jpeg_quality = jpeg_quality
self._intrinsic_width = pillow_image.width
self._intrinsic_height = pillow_image.height
self._intrinsic_ratio = (
self._intrinsic_width / self._intrinsic_height
if self._intrinsic_height != 0 else inf)
if 'transparency' in pillow_image.info:
pillow_image = pillow_image.convert('RGBA')
@ -71,7 +79,10 @@ class RasterImage:
if pillow_image.format in ('JPEG', 'MPO'):
self.extra['Filter'] = '/DCTDecode'
image_file = io.BytesIO()
pillow_image.save(image_file, format='JPEG', optimize=optimize)
options = {'format': 'JPEG', 'optimize': optimize}
if jpeg_quality is not None:
options['quality'] = jpeg_quality
pillow_image.save(image_file, **options)
self.stream = self.get_stream(image_file.getvalue())
else:
self.extra['Filter'] = '/FlateDecode'
@ -116,7 +127,6 @@ class RasterImage:
def draw(self, stream, concrete_width, concrete_height, image_rendering):
if self.width <= 0 or self.height <= 0:
return
image_name = stream.add_image(self, image_rendering)
stream.transform(
concrete_width, 0, 0, -concrete_height, 0, concrete_height)
@ -138,12 +148,12 @@ class RasterImage:
# Each chunk begins with its data length (four bytes, may be zero),
# then its type (four ASCII characters), then the data, then four
# bytes of a CRC.
chunk_len, = struct.unpack('!I', raw_chunk_length)
chunk_length, = struct.unpack('!I', raw_chunk_length)
chunk_type = image_file.read(4)
if chunk_type == b'IDAT':
png_data.append(image_file.read(chunk_len))
png_data.append(image_file.read(chunk_length))
else:
image_file.seek(chunk_len, io.SEEK_CUR)
image_file.seek(chunk_length, io.SEEK_CUR)
# We aren't checking the CRC, we assume this is a valid PNG.
image_file.seek(4, io.SEEK_CUR)
raw_chunk_length = image_file.read(4)
@ -198,7 +208,7 @@ class SVGImage:
self._url_fetcher, self._context)
def get_image_from_uri(cache, url_fetcher, optimize_size, url,
def get_image_from_uri(cache, url_fetcher, optimize_size, jpeg_quality, url,
forced_mime_type=None, context=None,
orientation='from-image'):
"""Get an Image instance from an image URI."""
@ -242,7 +252,7 @@ def get_image_from_uri(cache, url_fetcher, optimize_size, url,
image_id = md5(url.encode()).hexdigest()
pillow_image = rotate_pillow_image(pillow_image, orientation)
image = RasterImage(
pillow_image, image_id, optimize_size, cache)
pillow_image, image_id, cache, optimize_size, jpeg_quality)
except (URLFetchingError, ImageLoadingError) as exception:
LOGGER.error('Failed to load image at %r: %s', url, exception)

View File

@ -377,7 +377,7 @@ class Stream(pydyf.Stream):
extra['SMask'].compress)
extra['SMask'].extra['Interpolate'] = interpolate
xobject = pydyf.Stream(image.stream, extra=extra)
xobject = pydyf.Stream(image.stream, extra)
self._images[image_name] = xobject
return image_name