From c56b96b3c1c467d0b75f6059682138ab1ace97f3 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Mon, 22 Jun 2020 16:05:14 +0200 Subject: [PATCH] Add an option to optimize embedded images size --- docs/install.rst | 1 + setup.cfg | 1 + weasyprint/__init__.py | 38 ++++++++++++++++++++++++-------------- weasyprint/__main__.py | 9 ++++++++- weasyprint/document.py | 11 ++++++----- weasyprint/images.py | 16 +++++++++++++++- 6 files changed, 55 insertions(+), 21 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index 1aed60fe9..198020808 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -14,6 +14,7 @@ WeasyPrint |version| depends on: * cssselect2_ ≥ 0.1 * CairoSVG_ ≥ 2.4.0 * Pyphen_ ≥ 0.9.1 +* Pillow ≥ 4.0.0 * GDK-PixBuf_ ≥ 2.25.0 [#]_ .. _CPython: http://www.python.org/ diff --git a/setup.cfg b/setup.cfg index df4d0f275..4ddc2cf2c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,6 +57,7 @@ install_requires = cssselect2>=0.1 CairoSVG>=2.4.0 Pyphen>=0.9.1 + Pillow>=4.0.0 tests_require = pytest-runner pytest-cov diff --git a/weasyprint/__init__.py b/weasyprint/__init__.py index ec27c5a0c..b6fcc5574 100644 --- a/weasyprint/__init__.py +++ b/weasyprint/__init__.py @@ -134,8 +134,8 @@ def _get_metadata(self): return get_html_metadata(self.wrapper_element, self.base_url) def render(self, stylesheets=None, enable_hinting=False, - presentational_hints=False, font_config=None, - counter_style=None): + presentational_hints=False, optimize_images=False, + font_config=None, counter_style=None): """Lay out and paginate the document, but do not (yet) export it to PDF or PNG. @@ -158,6 +158,8 @@ def render(self, stylesheets=None, enable_hinting=False, :type presentational_hints: bool :param presentational_hints: Whether HTML presentational hints are followed. + :type optimize_images: bool + :param optimize_images: Try to optimize the size of embedded images. :type font_config: :class:`~fonts.FontConfiguration` :param font_config: A font configuration handling ``@font-face`` rules. :type counter_style: :class:`~css.counters.CounterStyle` @@ -167,11 +169,11 @@ def render(self, stylesheets=None, enable_hinting=False, """ return Document._render( self, stylesheets, enable_hinting, presentational_hints, - font_config, counter_style) + optimize_images, font_config, counter_style) def write_pdf(self, target=None, stylesheets=None, zoom=1, attachments=None, presentational_hints=False, - font_config=None, counter_style=None): + optimize_images=False, font_config=None, counter_style=None): """Render the document to a PDF file. This is a shortcut for calling :meth:`render`, then @@ -199,6 +201,8 @@ def write_pdf(self, target=None, stylesheets=None, zoom=1, :type presentational_hints: bool :param presentational_hints: Whether HTML presentational hints are followed. + :type optimize_images: bool + :param optimize_images: Try to optimize the size of embedded images. :type font_config: :class:`~fonts.FontConfiguration` :param font_config: A font configuration handling ``@font-face`` rules. :type counter_style: :class:`~css.counters.CounterStyle` @@ -212,12 +216,12 @@ def write_pdf(self, target=None, stylesheets=None, zoom=1, return self.render( stylesheets, enable_hinting=False, presentational_hints=presentational_hints, - font_config=font_config, counter_style=counter_style).write_pdf( - target, zoom, attachments) + optimize_images=optimize_images, font_config=font_config, + counter_style=counter_style).write_pdf(target, zoom, attachments) def write_image_surface(self, stylesheets=None, resolution=96, - presentational_hints=False, font_config=None, - counter_style=None): + presentational_hints=False, optimize_images=False, + font_config=None, counter_style=None): """Render pages vertically on a cairo image surface. .. versionadded:: 0.17 @@ -242,6 +246,8 @@ def write_image_surface(self, stylesheets=None, resolution=96, :type presentational_hints: bool :param presentational_hints: Whether HTML presentational hints are followed. + :type optimize_images: bool + :param optimize_images: Try to optimize the size of embedded images. :type font_config: :class:`~fonts.FontConfiguration` :param font_config: A font configuration handling ``@font-face`` rules. :type counter_style: :class:`~css.counters.CounterStyle` @@ -250,15 +256,16 @@ def write_image_surface(self, stylesheets=None, resolution=96, """ surface, _width, _height = ( - self.render(stylesheets, enable_hinting=True, - presentational_hints=presentational_hints, - font_config=font_config) + self.render( + stylesheets, enable_hinting=True, + presentational_hints=presentational_hints, + font_config=font_config, optimize_images=optimize_images) .write_image_surface(resolution)) return surface def write_png(self, target=None, stylesheets=None, resolution=96, - presentational_hints=False, font_config=None, - counter_style=None): + presentational_hints=False, optimize_images=False, + font_config=None, counter_style=None): """Paint the pages vertically to a single PNG image. There is no decoration around pages other than those specified in CSS @@ -284,6 +291,8 @@ def write_png(self, target=None, stylesheets=None, resolution=96, :type presentational_hints: bool :param presentational_hints: Whether HTML presentational hints are followed. + :type optimize_images: bool + :param optimize_images: Try to optimize the size of embedded images. :type font_config: :class:`~fonts.FontConfiguration` :param font_config: A font configuration handling ``@font-face`` rules. :type counter_style: :class:`~css.counters.CounterStyle` @@ -298,7 +307,8 @@ def write_png(self, target=None, stylesheets=None, resolution=96, self.render( stylesheets, enable_hinting=True, presentational_hints=presentational_hints, - font_config=font_config, counter_style=counter_style) + optimize_images=optimize_images, font_config=font_config, + counter_style=counter_style) .write_png(target, resolution)) return png_bytes diff --git a/weasyprint/__main__.py b/weasyprint/__main__.py index 542cc7121..ddfb70157 100644 --- a/weasyprint/__main__.py +++ b/weasyprint/__main__.py @@ -87,6 +87,10 @@ def main(argv=None, stdout=None, stdin=None): `_. + .. option:: -o, --optimize-images + + Try to optimize the size of embedded images. + .. option:: -v, --verbose Show warnings and information messages. @@ -133,6 +137,8 @@ def main(argv=None, stdout=None, stdin=None): 'to attach to the PDF document') parser.add_argument('-p', '--presentational-hints', action='store_true', help='Follow HTML presentational hints.') + parser.add_argument('-o', '--optimize-images', action='store_true', + help='Try to optimize the size of embedded images.') parser.add_argument('-v', '--verbose', action='store_true', help='Show warnings and information messages.') parser.add_argument('-d', '--debug', action='store_true', @@ -175,7 +181,8 @@ def main(argv=None, stdout=None, stdin=None): kwargs = { 'stylesheets': args.stylesheet, - 'presentational_hints': args.presentational_hints} + 'presentational_hints': args.presentational_hints, + 'optimize_images': args.optimize_images} if args.resolution: if format_ == 'png': kwargs['resolution'] = args.resolution diff --git a/weasyprint/document.py b/weasyprint/document.py index 6947067ce..ba2db7eb4 100644 --- a/weasyprint/document.py +++ b/weasyprint/document.py @@ -349,7 +349,8 @@ class Document: @classmethod def _build_layout_context(cls, html, stylesheets, enable_hinting, - presentational_hints=False, font_config=None, + presentational_hints=False, + optimize_images=False, font_config=None, counter_style=None): if font_config is None: font_config = FontConfiguration() @@ -368,7 +369,7 @@ def _build_layout_context(cls, html, stylesheets, enable_hinting, html, user_stylesheets, presentational_hints, font_config, counter_style, page_rules, target_collector) get_image_from_uri = functools.partial( - original_get_image_from_uri, {}, html.url_fetcher) + original_get_image_from_uri, {}, html.url_fetcher, optimize_images) PROGRESS_LOGGER.info('Step 4 - Creating formatting structure') context = LayoutContext( enable_hinting, style_for, get_image_from_uri, font_config, @@ -377,8 +378,8 @@ def _build_layout_context(cls, html, stylesheets, enable_hinting, @classmethod def _render(cls, html, stylesheets, enable_hinting, - presentational_hints=False, font_config=None, - counter_style=None): + presentational_hints=False, optimize_images=False, + font_config=None, counter_style=None): if font_config is None: font_config = FontConfiguration() @@ -387,7 +388,7 @@ def _render(cls, html, stylesheets, enable_hinting, context = cls._build_layout_context( html, stylesheets, enable_hinting, presentational_hints, - font_config, counter_style) + optimize_images, font_config, counter_style) root_box = build_formatting_structure( html.etree_element, context.style_for, context.get_image_from_uri, diff --git a/weasyprint/images.py b/weasyprint/images.py index d9c24ddde..f5b6dfe2d 100644 --- a/weasyprint/images.py +++ b/weasyprint/images.py @@ -13,6 +13,7 @@ import cairocffi import cairosvg.parser import cairosvg.surface +from PIL import Image from .layout.percentages import percentage from .logger import LOGGER @@ -173,7 +174,8 @@ def draw(self, context, concrete_width, concrete_height, _image_rendering): 'Failed to draw an SVG image at %s : %s', self._base_url, e) -def get_image_from_uri(cache, url_fetcher, url, forced_mime_type=None): +def get_image_from_uri(cache, url_fetcher, optimize_images, url, + forced_mime_type=None): """Get a cairo Pattern from an image URI.""" missing = object() image = cache.get(url, missing) @@ -195,6 +197,7 @@ def get_image_from_uri(cache, url_fetcher, url, forced_mime_type=None): # Try to rely on given mimetype try: if mime_type == 'image/png': + # Cairo already optimizes PNG images, no PIL needed try: surface = cairocffi.ImageSurface.create_from_png( BytesIO(string)) @@ -216,6 +219,17 @@ def get_image_from_uri(cache, url_fetcher, url, forced_mime_type=None): try: image = SVGImage(string, url, url_fetcher) except BaseException: + if optimize_images: + try: + image = Image.open(BytesIO(string)) + output = BytesIO() + image.save( + output, format=image.format, optimize=True) + except BaseException: + # Optimization did not work, keep the original + pass + else: + string = output.getvalue() try: surface, format_name = ( pixbuf.decode_to_image_surface(string))