# Copyright (c) 2015, Thomas Chiroux - Link Care Services
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of cairotft nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Load an display a svg image in cairo context.
This module is a modified version of the svg_image module from
WeasyPrint:
https://github.com/Kozea/WeasyPrint/blob/master/weasyprint/images.py#L99
"""
import cairosvg.parser
import cairosvg.surface
[docs]class ImageLoadingError(ValueError):
"""An error occured when loading an image.
The image data is probably corrupted or in an invalid format.
"""
@classmethod
[docs] def from_exception(cls, exception):
"""Instanciate the class with the given exception."""
name = type(exception).__name__
value = str(exception)
return cls('%s: %s' % (name, value) if value else name)
[docs]class ScaledSVGSurface(cairosvg.surface.SVGSurface):
"""Have the cairo Surface object have dimension in px instead of pt."""
@property
[docs] def device_units_per_user_units(self):
"""Force an eventual new scale.
In this case, does nothing new.
"""
scale = super().device_units_per_user_units
return scale / 1
[docs]class SVGImage():
"""SVGImage class."""
def __init__(self, base_url, svg_data=None):
"""Initialisation of the class.
:param str base_url: path to a SVG Image.
:param svg_data: bytestring containing the svg datas.
You can either provide a base_url, in this case, the file will be
loaded, or directly use svg_data with a svg content.
"""
# Don’t pass data URIs to CairoSVG.
# They are useless for relative URIs anyway.
self._base_url = (
base_url if not base_url.lower().startswith('data:') else None)
self._svg_data = svg_data
try:
# cache the rendering
self._svg = self._render()
except Exception as exc:
raise ImageLoadingError.from_exception(exc)
# TODO: support SVG images with none or only one of intrinsic
# width, height and ratio.
if not (self._svg.width > 0 and self._svg.height > 0):
raise ImageLoadingError(
'SVG images without an intrinsic size are not supported.')
self.intrinsic_width = self._svg.width
self.intrinsic_height = self._svg.height
self.intrinsic_ratio = self.intrinsic_width / self.intrinsic_height
[docs] def get_intrinsic_size(self, _image_resolution):
"""Return a tuple: (intrinsic_width, intrinsic_height)."""
# Vector images are affected by the 'image-resolution' property.
return self.intrinsic_width, self.intrinsic_height
[docs] 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)
@staticmethod
[docs] def _scale_to_fit(image, frame, enlarge=False):
"""scale an image always keeping aspect ratio inside the frame.
:param image: tuple of int (width, eight)
:param fram: tuple of int (width, eight)
:param bool enlarge: if True, do not only shrink, also enlarge.
:returns= a (widht, eight) tuple representing the target size of the
object.
(thanks to jon for this method)
"""
image_width, image_height = image
frame_width, frame_height = frame
image_aspect = float(image_width) / image_height
frame_aspect = float(frame_width) / frame_height
# Determine maximum width/height (prevent up-scaling).
if not enlarge:
max_width = min(frame_width, image_width)
max_height = min(frame_height, image_height)
else:
max_width = frame_width
max_height = frame_height
# Frame is wider than image.
if frame_aspect > image_aspect:
height = max_height
width = int(height * image_aspect)
# Frame is taller than image.
else:
width = max_width
height = int(width / image_aspect)
return (width, height)
[docs] def draw(self, context, pos_x, pos_y,
width, height, enlarge=True, center_y=False):
"""Draw the svg image inside a box of size (width, eight).
This draw methods keeps automatically the aspect ration of the image.
:param context: cairo context
:param int pos_x: x position (top left corner)
:param int pos_y: y position (top left corner)
:param int width: width of the box the image will fit inside
:param int height: height of the box the image will fit inside
:param bool enlarge: if true and if the base image is smaller than
the box, it will enlarge the image. If False, display the original
smaller image.
:param bool center_y: if True add an offset to y in order to center
the image into the box.
"""
# svg = self._render()
svg = self._svg # use cached version
image_width, image_height = self._scale_to_fit(
image=(svg.width, svg.height),
frame=(width, height),
enlarge=enlarge)
xratio = image_width / svg.width
yratio = image_height / svg.height
if center_y:
y_offset = int((height - image_height) / 2)
else:
y_offset = 0
context.scale(xratio, yratio)
context.set_source_surface(svg.cairo,
pos_x / xratio,
(pos_y + y_offset) / yratio)
context.paint()
# reset the scale
context.scale(1 / xratio, 1 / yratio)