2011-12-08 19:31:03 +04:00
|
|
|
|
# coding: utf8
|
2012-03-22 02:19:27 +04:00
|
|
|
|
"""
|
|
|
|
|
weasyprint.images
|
|
|
|
|
-----------------
|
2011-12-08 19:31:03 +04:00
|
|
|
|
|
2012-03-22 02:19:27 +04:00
|
|
|
|
Fetch and decode images in various formats.
|
|
|
|
|
|
|
|
|
|
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
|
|
|
|
|
:license: BSD, see LICENSE for details.
|
2011-12-08 19:31:03 +04:00
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2013-02-25 19:38:54 +04:00
|
|
|
|
from __future__ import division, unicode_literals
|
2012-02-17 21:49:58 +04:00
|
|
|
|
|
|
|
|
|
from io import BytesIO
|
2011-12-08 19:31:03 +04:00
|
|
|
|
|
2013-02-26 18:04:52 +04:00
|
|
|
|
import cairocffi
|
|
|
|
|
cairocffi.install_as_pycairo() # for CairoSVG
|
2013-02-25 19:26:39 +04:00
|
|
|
|
|
|
|
|
|
import cairosvg.parser
|
|
|
|
|
import cairosvg.surface
|
2013-02-26 18:04:52 +04:00
|
|
|
|
assert cairosvg.surface.cairo is cairocffi, (
|
2013-02-25 19:26:39 +04:00
|
|
|
|
'CairoSVG is using pycairo instead of cairocffi. '
|
|
|
|
|
'Make sure it is not imported before WeasyPrint.')
|
|
|
|
|
|
2012-12-30 01:59:13 +04:00
|
|
|
|
try:
|
2013-02-26 18:04:52 +04:00
|
|
|
|
from cairocffi import pixbuf
|
2012-12-30 01:59:13 +04:00
|
|
|
|
except OSError:
|
2013-02-26 18:04:52 +04:00
|
|
|
|
pixbuf = None
|
2012-12-29 17:20:38 +04:00
|
|
|
|
|
2013-02-26 18:04:52 +04:00
|
|
|
|
from .logger import LOGGER
|
2012-12-29 14:50:11 +04:00
|
|
|
|
|
|
|
|
|
|
2013-04-03 18:00:31 +04:00
|
|
|
|
# Map values of the image-rendering property to cairo FILTER values:
|
|
|
|
|
# Values are normalized to lower case.
|
|
|
|
|
IMAGE_RENDERING_TO_FILTER = dict(
|
|
|
|
|
optimizespeed=cairocffi.FILTER_FAST,
|
|
|
|
|
auto=cairocffi.FILTER_GOOD,
|
|
|
|
|
optimizequality=cairocffi.FILTER_BEST,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2013-04-03 15:34:14 +04:00
|
|
|
|
class RasterImage(object):
|
|
|
|
|
def __init__(self, image_surface):
|
|
|
|
|
self.image_surface = image_surface
|
|
|
|
|
self.intrinsic_width = image_surface.get_width()
|
|
|
|
|
self.intrinsic_height = image_surface.get_height()
|
|
|
|
|
self.intrinsic_ratio = self.intrinsic_width / self.intrinsic_height
|
|
|
|
|
|
2013-04-03 18:00:31 +04:00
|
|
|
|
def draw(self, context, concrete_width, concrete_height, image_rendering):
|
|
|
|
|
if self.intrinsic_width > 0 and self.intrinsic_height > 0:
|
|
|
|
|
context.scale(concrete_width / self.intrinsic_width,
|
|
|
|
|
concrete_height / self.intrinsic_height)
|
|
|
|
|
context.set_source_surface(self.image_surface)
|
|
|
|
|
context.get_source().set_filter(
|
|
|
|
|
IMAGE_RENDERING_TO_FILTER[image_rendering])
|
|
|
|
|
context.paint()
|
2011-12-08 19:31:03 +04:00
|
|
|
|
|
|
|
|
|
|
2013-02-25 19:26:39 +04:00
|
|
|
|
class ScaledSVGSurface(cairosvg.surface.SVGSurface):
|
|
|
|
|
"""
|
|
|
|
|
Have the cairo Surface object have intrinsic dimension
|
|
|
|
|
in pixels instead of points.
|
|
|
|
|
"""
|
|
|
|
|
@property
|
|
|
|
|
def device_units_per_user_units(self):
|
|
|
|
|
scale = super(ScaledSVGSurface, self).device_units_per_user_units
|
|
|
|
|
return scale / 0.75
|
|
|
|
|
|
|
|
|
|
|
2013-04-03 15:34:14 +04:00
|
|
|
|
class SVGImage(object):
|
|
|
|
|
def __init__(self, svg_data, base_url):
|
2012-02-21 15:59:06 +04:00
|
|
|
|
# Don’t pass data URIs to CairoSVG.
|
|
|
|
|
# They are useless for relative URIs anyway.
|
2013-04-03 15:34:14 +04:00
|
|
|
|
self._base_url = (
|
|
|
|
|
base_url if not base_url.lower().startswith('data:') else None)
|
|
|
|
|
self._svg_data = svg_data
|
|
|
|
|
|
|
|
|
|
# TODO: find a way of not doing twice the whole rendering.
|
2013-04-03 18:00:31 +04:00
|
|
|
|
svg = self._render()
|
2013-04-03 15:34:14 +04:00
|
|
|
|
# TODO: support SVG images with none or only one of intrinsic
|
|
|
|
|
# width, height and ratio.
|
2013-04-03 18:00:31 +04:00
|
|
|
|
if not (svg.width > 0 and svg.height > 0):
|
2013-04-03 15:34:14 +04:00
|
|
|
|
raise ValueError(
|
|
|
|
|
'SVG Images without an intrinsic size are not supported.')
|
2013-04-03 18:00:31 +04:00
|
|
|
|
self.intrinsic_width = svg.width
|
|
|
|
|
self.intrinsic_height = svg.height
|
2013-04-03 15:34:14 +04:00
|
|
|
|
self.intrinsic_ratio = self.intrinsic_width / self.intrinsic_height
|
|
|
|
|
|
|
|
|
|
def _render(self):
|
|
|
|
|
# Draw to a cairo surface but do not write to a file.
|
|
|
|
|
# This is a CairoSVG surface, not a cairo surface.
|
|
|
|
|
return ScaledSVGSurface(
|
|
|
|
|
cairosvg.parser.Tree(bytestring=self._svg_data, url=self._base_url),
|
|
|
|
|
output=None, dpi=96)
|
|
|
|
|
|
2013-04-03 18:00:31 +04:00
|
|
|
|
def draw(self, context, concrete_width, concrete_height, image_rendering):
|
|
|
|
|
# Do not re-use the rendered Surface object, but regenerate it as needed.
|
2012-09-26 18:59:40 +04:00
|
|
|
|
# If a surface for a SVG image is still alive by the time we call
|
|
|
|
|
# show_page(), cairo will rasterize the image instead writing vectors.
|
2013-04-03 18:00:31 +04:00
|
|
|
|
svg = self._render()
|
|
|
|
|
context.scale(concrete_width / svg.width, concrete_height / svg.height)
|
|
|
|
|
context.set_source_surface(svg.cairo)
|
|
|
|
|
context.paint()
|
2012-01-12 22:26:27 +04:00
|
|
|
|
|
|
|
|
|
|
2013-04-03 15:34:14 +04:00
|
|
|
|
def get_image_from_uri(cache, url_fetcher, uri, forced_mime_type=None):
|
2013-02-26 18:04:52 +04:00
|
|
|
|
"""Get a cairo Pattern from an image URI."""
|
2011-12-08 19:31:03 +04:00
|
|
|
|
try:
|
2012-05-29 18:24:14 +04:00
|
|
|
|
missing = object()
|
2012-09-26 18:59:40 +04:00
|
|
|
|
image = cache.get(uri, missing)
|
|
|
|
|
if image is not missing:
|
|
|
|
|
return image
|
2012-07-13 14:24:55 +04:00
|
|
|
|
result = url_fetcher(uri)
|
2013-04-03 15:34:14 +04:00
|
|
|
|
mime_type = forced_mime_type or result['mime_type']
|
2012-05-29 18:24:14 +04:00
|
|
|
|
try:
|
2013-04-03 15:34:14 +04:00
|
|
|
|
if mime_type == 'image/svg+xml':
|
|
|
|
|
image = SVGImage(
|
|
|
|
|
result.get('string') or result['file_obj'].read(), uri)
|
|
|
|
|
elif mime_type == 'image/png':
|
|
|
|
|
image = RasterImage(cairocffi.ImageSurface.create_from_png(
|
|
|
|
|
result.get('file_obj') or BytesIO(result.get('string'))))
|
2012-07-29 00:11:28 +04:00
|
|
|
|
else:
|
2013-04-03 15:34:14 +04:00
|
|
|
|
if pixbuf is None:
|
|
|
|
|
raise OSError(
|
|
|
|
|
'Could not load GDK-Pixbuf. '
|
|
|
|
|
'PNG and SVG are the only image formats available.')
|
|
|
|
|
string = result.get('string') or result['file_obj'].read()
|
|
|
|
|
surface, format_name = pixbuf.decode_to_image_surface(string)
|
|
|
|
|
if format_name == 'jpeg':
|
|
|
|
|
surface.set_mime_data('image/jpeg', string)
|
|
|
|
|
image = RasterImage(surface)
|
2012-05-29 18:24:14 +04:00
|
|
|
|
finally:
|
2012-08-03 15:32:42 +04:00
|
|
|
|
if 'file_obj' in result:
|
|
|
|
|
try:
|
|
|
|
|
result['file_obj'].close()
|
|
|
|
|
except Exception: # pragma: no cover
|
|
|
|
|
# May already be closed or something.
|
|
|
|
|
# This is just cleanup anyway.
|
|
|
|
|
pass
|
2012-03-21 17:34:27 +04:00
|
|
|
|
except Exception as exc:
|
|
|
|
|
LOGGER.warn('Error for image at %s : %r', uri, exc)
|
2012-09-26 18:59:40 +04:00
|
|
|
|
image = None
|
|
|
|
|
cache[uri] = image
|
|
|
|
|
return image
|