Skip to content

Commit c56b96b

Browse files
committed
Add an option to optimize embedded images size
1 parent bea6cef commit c56b96b

File tree

6 files changed

+55
-21
lines changed

6 files changed

+55
-21
lines changed

docs/install.rst

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ WeasyPrint |version| depends on:
1414
* cssselect2_ ≥ 0.1
1515
* CairoSVG_ ≥ 2.4.0
1616
* Pyphen_ ≥ 0.9.1
17+
* Pillow ≥ 4.0.0
1718
* GDK-PixBuf_ ≥ 2.25.0 [#]_
1819

1920
.. _CPython: http://www.python.org/

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ install_requires =
5757
cssselect2>=0.1
5858
CairoSVG>=2.4.0
5959
Pyphen>=0.9.1
60+
Pillow>=4.0.0
6061
tests_require =
6162
pytest-runner
6263
pytest-cov

weasyprint/__init__.py

+24-14
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ def _get_metadata(self):
134134
return get_html_metadata(self.wrapper_element, self.base_url)
135135

136136
def render(self, stylesheets=None, enable_hinting=False,
137-
presentational_hints=False, font_config=None,
138-
counter_style=None):
137+
presentational_hints=False, optimize_images=False,
138+
font_config=None, counter_style=None):
139139
"""Lay out and paginate the document, but do not (yet) export it
140140
to PDF or PNG.
141141
@@ -158,6 +158,8 @@ def render(self, stylesheets=None, enable_hinting=False,
158158
:type presentational_hints: bool
159159
:param presentational_hints: Whether HTML presentational hints are
160160
followed.
161+
:type optimize_images: bool
162+
:param optimize_images: Try to optimize the size of embedded images.
161163
:type font_config: :class:`~fonts.FontConfiguration`
162164
:param font_config: A font configuration handling ``@font-face`` rules.
163165
:type counter_style: :class:`~css.counters.CounterStyle`
@@ -167,11 +169,11 @@ def render(self, stylesheets=None, enable_hinting=False,
167169
"""
168170
return Document._render(
169171
self, stylesheets, enable_hinting, presentational_hints,
170-
font_config, counter_style)
172+
optimize_images, font_config, counter_style)
171173

172174
def write_pdf(self, target=None, stylesheets=None, zoom=1,
173175
attachments=None, presentational_hints=False,
174-
font_config=None, counter_style=None):
176+
optimize_images=False, font_config=None, counter_style=None):
175177
"""Render the document to a PDF file.
176178
177179
This is a shortcut for calling :meth:`render`, then
@@ -199,6 +201,8 @@ def write_pdf(self, target=None, stylesheets=None, zoom=1,
199201
:type presentational_hints: bool
200202
:param presentational_hints: Whether HTML presentational hints are
201203
followed.
204+
:type optimize_images: bool
205+
:param optimize_images: Try to optimize the size of embedded images.
202206
:type font_config: :class:`~fonts.FontConfiguration`
203207
:param font_config: A font configuration handling ``@font-face`` rules.
204208
:type counter_style: :class:`~css.counters.CounterStyle`
@@ -212,12 +216,12 @@ def write_pdf(self, target=None, stylesheets=None, zoom=1,
212216
return self.render(
213217
stylesheets, enable_hinting=False,
214218
presentational_hints=presentational_hints,
215-
font_config=font_config, counter_style=counter_style).write_pdf(
216-
target, zoom, attachments)
219+
optimize_images=optimize_images, font_config=font_config,
220+
counter_style=counter_style).write_pdf(target, zoom, attachments)
217221

218222
def write_image_surface(self, stylesheets=None, resolution=96,
219-
presentational_hints=False, font_config=None,
220-
counter_style=None):
223+
presentational_hints=False, optimize_images=False,
224+
font_config=None, counter_style=None):
221225
"""Render pages vertically on a cairo image surface.
222226
223227
.. versionadded:: 0.17
@@ -242,6 +246,8 @@ def write_image_surface(self, stylesheets=None, resolution=96,
242246
:type presentational_hints: bool
243247
:param presentational_hints: Whether HTML presentational hints are
244248
followed.
249+
:type optimize_images: bool
250+
:param optimize_images: Try to optimize the size of embedded images.
245251
:type font_config: :class:`~fonts.FontConfiguration`
246252
:param font_config: A font configuration handling ``@font-face`` rules.
247253
:type counter_style: :class:`~css.counters.CounterStyle`
@@ -250,15 +256,16 @@ def write_image_surface(self, stylesheets=None, resolution=96,
250256
251257
"""
252258
surface, _width, _height = (
253-
self.render(stylesheets, enable_hinting=True,
254-
presentational_hints=presentational_hints,
255-
font_config=font_config)
259+
self.render(
260+
stylesheets, enable_hinting=True,
261+
presentational_hints=presentational_hints,
262+
font_config=font_config, optimize_images=optimize_images)
256263
.write_image_surface(resolution))
257264
return surface
258265

259266
def write_png(self, target=None, stylesheets=None, resolution=96,
260-
presentational_hints=False, font_config=None,
261-
counter_style=None):
267+
presentational_hints=False, optimize_images=False,
268+
font_config=None, counter_style=None):
262269
"""Paint the pages vertically to a single PNG image.
263270
264271
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,
284291
:type presentational_hints: bool
285292
:param presentational_hints: Whether HTML presentational hints are
286293
followed.
294+
:type optimize_images: bool
295+
:param optimize_images: Try to optimize the size of embedded images.
287296
:type font_config: :class:`~fonts.FontConfiguration`
288297
:param font_config: A font configuration handling ``@font-face`` rules.
289298
:type counter_style: :class:`~css.counters.CounterStyle`
@@ -298,7 +307,8 @@ def write_png(self, target=None, stylesheets=None, resolution=96,
298307
self.render(
299308
stylesheets, enable_hinting=True,
300309
presentational_hints=presentational_hints,
301-
font_config=font_config, counter_style=counter_style)
310+
optimize_images=optimize_images, font_config=font_config,
311+
counter_style=counter_style)
302312
.write_png(target, resolution))
303313
return png_bytes
304314

weasyprint/__main__.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ def main(argv=None, stdout=None, stdin=None):
8787
<https://www.w3.org/TR/html/rendering.html\
8888
#the-css-user-agent-style-sheet-and-presentational-hints>`_.
8989
90+
.. option:: -o, --optimize-images
91+
92+
Try to optimize the size of embedded images.
93+
9094
.. option:: -v, --verbose
9195
9296
Show warnings and information messages.
@@ -133,6 +137,8 @@ def main(argv=None, stdout=None, stdin=None):
133137
'to attach to the PDF document')
134138
parser.add_argument('-p', '--presentational-hints', action='store_true',
135139
help='Follow HTML presentational hints.')
140+
parser.add_argument('-o', '--optimize-images', action='store_true',
141+
help='Try to optimize the size of embedded images.')
136142
parser.add_argument('-v', '--verbose', action='store_true',
137143
help='Show warnings and information messages.')
138144
parser.add_argument('-d', '--debug', action='store_true',
@@ -175,7 +181,8 @@ def main(argv=None, stdout=None, stdin=None):
175181

176182
kwargs = {
177183
'stylesheets': args.stylesheet,
178-
'presentational_hints': args.presentational_hints}
184+
'presentational_hints': args.presentational_hints,
185+
'optimize_images': args.optimize_images}
179186
if args.resolution:
180187
if format_ == 'png':
181188
kwargs['resolution'] = args.resolution

weasyprint/document.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,8 @@ class Document:
349349

350350
@classmethod
351351
def _build_layout_context(cls, html, stylesheets, enable_hinting,
352-
presentational_hints=False, font_config=None,
352+
presentational_hints=False,
353+
optimize_images=False, font_config=None,
353354
counter_style=None):
354355
if font_config is None:
355356
font_config = FontConfiguration()
@@ -368,7 +369,7 @@ def _build_layout_context(cls, html, stylesheets, enable_hinting,
368369
html, user_stylesheets, presentational_hints, font_config,
369370
counter_style, page_rules, target_collector)
370371
get_image_from_uri = functools.partial(
371-
original_get_image_from_uri, {}, html.url_fetcher)
372+
original_get_image_from_uri, {}, html.url_fetcher, optimize_images)
372373
PROGRESS_LOGGER.info('Step 4 - Creating formatting structure')
373374
context = LayoutContext(
374375
enable_hinting, style_for, get_image_from_uri, font_config,
@@ -377,8 +378,8 @@ def _build_layout_context(cls, html, stylesheets, enable_hinting,
377378

378379
@classmethod
379380
def _render(cls, html, stylesheets, enable_hinting,
380-
presentational_hints=False, font_config=None,
381-
counter_style=None):
381+
presentational_hints=False, optimize_images=False,
382+
font_config=None, counter_style=None):
382383
if font_config is None:
383384
font_config = FontConfiguration()
384385

@@ -387,7 +388,7 @@ def _render(cls, html, stylesheets, enable_hinting,
387388

388389
context = cls._build_layout_context(
389390
html, stylesheets, enable_hinting, presentational_hints,
390-
font_config, counter_style)
391+
optimize_images, font_config, counter_style)
391392

392393
root_box = build_formatting_structure(
393394
html.etree_element, context.style_for, context.get_image_from_uri,

weasyprint/images.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import cairocffi
1414
import cairosvg.parser
1515
import cairosvg.surface
16+
from PIL import Image
1617

1718
from .layout.percentages import percentage
1819
from .logger import LOGGER
@@ -173,7 +174,8 @@ def draw(self, context, concrete_width, concrete_height, _image_rendering):
173174
'Failed to draw an SVG image at %s : %s', self._base_url, e)
174175

175176

176-
def get_image_from_uri(cache, url_fetcher, url, forced_mime_type=None):
177+
def get_image_from_uri(cache, url_fetcher, optimize_images, url,
178+
forced_mime_type=None):
177179
"""Get a cairo Pattern from an image URI."""
178180
missing = object()
179181
image = cache.get(url, missing)
@@ -195,6 +197,7 @@ def get_image_from_uri(cache, url_fetcher, url, forced_mime_type=None):
195197
# Try to rely on given mimetype
196198
try:
197199
if mime_type == 'image/png':
200+
# Cairo already optimizes PNG images, no PIL needed
198201
try:
199202
surface = cairocffi.ImageSurface.create_from_png(
200203
BytesIO(string))
@@ -216,6 +219,17 @@ def get_image_from_uri(cache, url_fetcher, url, forced_mime_type=None):
216219
try:
217220
image = SVGImage(string, url, url_fetcher)
218221
except BaseException:
222+
if optimize_images:
223+
try:
224+
image = Image.open(BytesIO(string))
225+
output = BytesIO()
226+
image.save(
227+
output, format=image.format, optimize=True)
228+
except BaseException:
229+
# Optimization did not work, keep the original
230+
pass
231+
else:
232+
string = output.getvalue()
219233
try:
220234
surface, format_name = (
221235
pixbuf.decode_to_image_surface(string))

0 commit comments

Comments
 (0)