Skip to content

Commit

Permalink
Add an option to optimize embedded images size
Browse files Browse the repository at this point in the history
  • Loading branch information
liZe committed Jun 22, 2020
1 parent bea6cef commit c56b96b
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 21 deletions.
1 change: 1 addition & 0 deletions docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 24 additions & 14 deletions weasyprint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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`
Expand All @@ -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
Expand Down Expand Up @@ -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`
Expand All @@ -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
Expand All @@ -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`
Expand All @@ -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
Expand All @@ -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`
Expand All @@ -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

Expand Down
9 changes: 8 additions & 1 deletion weasyprint/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ def main(argv=None, stdout=None, stdin=None):
<https://www.w3.org/TR/html/rendering.html\
#the-css-user-agent-style-sheet-and-presentational-hints>`_.
.. option:: -o, --optimize-images
Try to optimize the size of embedded images.
.. option:: -v, --verbose
Show warnings and information messages.
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions weasyprint/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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,
Expand All @@ -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()

Expand All @@ -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,
Expand Down
16 changes: 15 additions & 1 deletion weasyprint/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand All @@ -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))
Expand Down

0 comments on commit c56b96b

Please sign in to comment.