1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 00:21:15 +03:00
WeasyPrint/weasyprint/images.py
2013-02-25 16:34:42 +01:00

352 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# coding: utf8
"""
weasyprint.images
-----------------
Fetch and decode images in various formats.
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from __future__ import division
# XXX No unicode_literals, cffi likes native strings
import sys
from io import BytesIO
from functools import partial
import cffi
import cairocffi as cairo
cairo.install_as_pycairo() # for CairoSVG
import cairosvg.parser
import cairosvg.surface
assert cairosvg.surface.cairo is cairo, (
'CairoSVG is using pycairo instead of cairocffi. '
'Make sure it is not imported before WeasyPrint.')
from .logger import LOGGER
from .compat import xrange
ffi = cffi.FFI()
ffi.cdef('''
typedef unsigned long gsize;
typedef unsigned int guint32;
typedef unsigned int guint;
typedef unsigned char guchar;
typedef char gchar;
typedef int gint;
typedef gint gboolean;
typedef guint32 GQuark;
typedef void* gpointer;
typedef struct {
GQuark domain;
gint code;
gchar *message;
} GError;
typedef struct {
gchar *name;
/* ... */
} GdkPixbufFormat;
typedef enum {
GDK_COLORSPACE_RGB
} GdkColorspace;
typedef ... GdkPixbufLoader;
typedef ... GdkPixbuf;
typedef ... cairo_t;
GdkPixbufLoader * gdk_pixbuf_loader_new (void);
GdkPixbufFormat * gdk_pixbuf_loader_get_format (GdkPixbufLoader *loader);
GdkPixbuf * gdk_pixbuf_loader_get_pixbuf (GdkPixbufLoader *loader);
gboolean gdk_pixbuf_loader_write (
GdkPixbufLoader *loader, const guchar *buf, gsize count,
GError **error);
gboolean gdk_pixbuf_loader_close (
GdkPixbufLoader *loader, GError **error);
GdkColorspace gdk_pixbuf_get_colorspace (const GdkPixbuf *pixbuf);
int gdk_pixbuf_get_n_channels (const GdkPixbuf *pixbuf);
gboolean gdk_pixbuf_get_has_alpha (const GdkPixbuf *pixbuf);
int gdk_pixbuf_get_bits_per_sample (const GdkPixbuf *pixbuf);
int gdk_pixbuf_get_width (const GdkPixbuf *pixbuf);
int gdk_pixbuf_get_height (const GdkPixbuf *pixbuf);
int gdk_pixbuf_get_rowstride (const GdkPixbuf *pixbuf);
guchar * gdk_pixbuf_get_pixels (const GdkPixbuf *pixbuf);
gsize gdk_pixbuf_get_byte_length (const GdkPixbuf *pixbuf);
gboolean gdk_pixbuf_save_to_buffer (
GdkPixbuf *pixbuf, gchar **buffer, gsize *buffer_size,
const char *type, GError **error, ...);
void gdk_cairo_set_source_pixbuf (
cairo_t *cr, const GdkPixbuf *pixbuf, double pixbuf_x, double pixbuf_y);
void g_object_ref (gpointer object);
void g_object_unref (gpointer object);
void g_error_free (GError *error);
void g_type_init (void);
''')
try:
gobject = ffi.dlopen('gobject-2.0')
gdk_pixbuf = ffi.dlopen('gdk_pixbuf-2.0')
except OSError:
gdk_pixbuf = gobject = gdk = None
else:
try:
gdk = ffi.dlopen('gdk-3')
except OSError:
try:
gdk = ffi.dlopen('gdk-x11-2.0')
except OSError:
gdk = None
gobject.g_type_init()
# TODO: currently CairoSVG only support images with an explicit
# width and height. When it supports images with only an intrinsic ratio
# this API will need to change.
def handle_g_error(error, raise_=False):
if error != ffi.NULL:
error_message = ffi.string(error.message).decode('utf8', 'replace')
gobject.g_error_free(error)
exception = ValueError('Pixbuf error: ' + error_message)
if raise_:
raise exception
else:
return exception
class Pixbuf(object):
def __init__(self, handle):
self._pointer = ffi.gc(handle, gobject.g_object_unref)
def __getattr__(self, name):
return partial(getattr(gdk_pixbuf, 'gdk_pixbuf_' + name), self._pointer)
def get_pixbuf(file_obj=None, string=None):
"""Create a Pixbuf object."""
if file_obj:
string = file_obj.read()
if not string:
raise ValueError('Could not load image: empty content')
loader = ffi.gc(
gdk_pixbuf.gdk_pixbuf_loader_new(), gobject.g_object_unref)
error = ffi.new('GError **')
gdk_pixbuf.gdk_pixbuf_loader_write(
loader, ffi.new('guchar[]', string), len(string), error)
write_exception = handle_g_error(error[0])
gdk_pixbuf.gdk_pixbuf_loader_close(loader, error)
close_exception = handle_g_error(error[0])
if write_exception is not None:
raise write_exception # Only after closing
if close_exception is not None:
raise close_exception
format_ = gdk_pixbuf.gdk_pixbuf_loader_get_format(loader)
is_jpeg = format_ != ffi.NULL and ffi.string(format_.name) == b'jpeg'
jpeg_data = string if is_jpeg else None
pixbuf = gdk_pixbuf.gdk_pixbuf_loader_get_pixbuf(loader)
assert pixbuf != ffi.NULL
gobject.g_object_ref(pixbuf)
return Pixbuf(pixbuf), jpeg_data
def gdkpixbuf_loader(file_obj, string):
"""Load raster images with gdk-pixbuf through introspection
and Gdk.
"""
if gdk_pixbuf is None:
raise OSError(
'Could not load GDK-Pixbuf. '
'PNG and SVG are the only image formats available.')
pixbuf, jpeg_data = get_pixbuf(file_obj, string)
surface = (
pixbuf_to_cairo_gdk(pixbuf) if gdk is not None
else pixbuf_to_cairo_slices(pixbuf) if not pixbuf.get_has_alpha()
else pixbuf_to_cairo_png(pixbuf))
if jpeg_data:
surface.set_mime_data('image/jpeg', jpeg_data)
get_pattern = lambda: cairo.SurfacePattern(surface)
return get_pattern, surface.get_width(), surface.get_height()
def pixbuf_to_cairo_gdk(pixbuf):
"""Convert with GDK.
This method is fastest but GDK is not always available.
"""
dummy_context = cairo.Context(cairo.PDFSurface(None, 1, 1))
gdk.gdk_cairo_set_source_pixbuf(
ffi.cast('cairo_t *', dummy_context._pointer), pixbuf._pointer, 0, 0)
return dummy_context.get_source().get_surface()
def pixbuf_to_cairo_slices(pixbuf):
"""Slice-based byte swapping.
This method is 2~5x slower than GDK but does not support an alpha channel.
(cairo uses pre-multiplied alpha, but not Pixbuf.)
"""
assert pixbuf.get_colorspace() == 'GDK_COLORSPACE_RGB'
assert pixbuf.get_n_channels() == 3
assert pixbuf.get_bits_per_sample() == 8
width = pixbuf.get_width()
height = pixbuf.get_height()
rowstride = pixbuf.get_rowstride()
pixels = ffi.buffer(pixbuf.get_pixels(), pixbuf.get_byte_length())
# TODO: remove this when cffi buffers support slicing with a stride.
pixels = pixels[:]
# Convert GdkPixbufs big-endian RGBA to cairos native-endian ARGB
cairo_stride = cairo.ImageSurface.format_stride_for_width('RGB24', width)
data = bytearray(cairo_stride * height)
big_endian = sys.byteorder == 'big'
pixbuf_row_length = width * 3 # stride == row_length + padding
cairo_row_length = width * 4 # stride == row_length + padding
for y in xrange(height):
offset = rowstride * y
end = offset + pixbuf_row_length
red = pixels[offset:end:3]
green = pixels[offset + 1:end:3]
blue = pixels[offset + 2:end:3]
offset = cairo_stride * y
end = offset + cairo_row_length
if big_endian:
# data[offset:end:4] is left un-initialized
data[offset + 1:end:4] = red
data[offset + 2:end:4] = green
data[offset + 3:end:4] = blue
else:
# data[offset + 3:end:4] is left un-initialized
data[offset + 2:end:4] = red
data[offset + 1:end:4] = green
data[offset:end:4] = blue
return cairo.ImageSurface('ARGB32', width, height, data, cairo_stride)
def pixbuf_to_cairo_png(pixbuf):
"""Going through PNG.
This method is 20~30x slower than GDK but always works.
"""
buffer_pointer = ffi.new('gchar **')
buffer_size = ffi.new('gsize *')
error = ffi.new('GError **')
pixbuf.save_to_buffer(
buffer_pointer, buffer_size, ffi.new('char[]', b'png'), error,
ffi.new('char[]', b'compression'), ffi.new('char[]', b'0'),
ffi.NULL)
handle_g_error(error[0], raise_=True)
png_bytes = ffi.buffer(buffer_pointer[0], buffer_size[0])
return cairo.ImageSurface.create_from_png(BytesIO(png_bytes))
def cairo_png_loader(file_obj, string):
"""Return a cairo Surface from a PNG byte stream."""
surface = cairo.ImageSurface.create_from_png(file_obj or BytesIO(string))
get_pattern = lambda: cairo.SurfacePattern(surface)
return get_pattern, surface.get_width(), surface.get_height()
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
def cairosvg_loader(file_obj, string, uri):
"""Return a cairo Surface from a SVG byte stream.
This loader uses CairoSVG: http://cairosvg.org/
"""
if uri.startswith('data:'):
# Dont pass data URIs to CairoSVG.
# They are useless for relative URIs anyway.
uri = None
if file_obj:
string = file_obj.read()
def get_surface():
tree = cairosvg.parser.Tree(bytestring=string, url=uri)
# Draw to a cairo surface but do not write to a file
surface = ScaledSVGSurface(tree, output=None, dpi=96)
return surface.cairo, surface.width, surface.height
def get_pattern():
# Do not re-use the Surface object, but regenerate it as needed.
# 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.
surface, _, _ = get_surface()
return cairo.SurfacePattern(surface)
# Render once to get the size and trigger any exception.
# If this does not raise, future calls to get_pattern() will hopefully
# not raise either.
_, width, height = get_surface()
if not (width > 0 and height > 0):
raise ValueError('Images without an intrinsic size are not supported.')
return get_pattern, width, height
def get_image_from_uri(cache, url_fetcher, uri, type_=None):
"""Get a :class:`cairo.Surface`` from an image URI."""
try:
missing = object()
image = cache.get(uri, missing)
if image is not missing:
return image
result = url_fetcher(uri)
try:
if not type_:
type_ = result['mime_type'] # Use eg. the HTTP header
#else: the type was forced by eg. a 'type' attribute on <embed>
if type_ == 'image/svg+xml':
image = cairosvg_loader(
result.get('file_obj'), result.get('string'), uri)
elif type_ == 'image/png':
image = cairo_png_loader(
result.get('file_obj'), result.get('string'))
else:
image = gdkpixbuf_loader(
result.get('file_obj'), result.get('string'))
finally:
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
except Exception as exc:
LOGGER.warn('Error for image at %s : %r', uri, exc)
image = None
cache[uri] = image
return image