1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 00:21:15 +03:00
WeasyPrint/weasy/draw/helpers.py

385 lines
13 KiB
Python
Raw Normal View History

2011-07-12 19:53:15 +04:00
# coding: utf8
# WeasyPrint converts web documents (HTML, CSS, ...) to PDF.
# Copyright (C) 2011 Simon Sapin
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2011-08-22 19:36:07 +04:00
"""
Various drawing helpers.
"""
2011-08-09 12:15:53 +04:00
from __future__ import division
2011-08-10 12:01:56 +04:00
import urllib
2011-08-09 12:15:53 +04:00
2011-07-12 19:53:15 +04:00
import cairo
from StringIO import StringIO
2011-07-12 19:53:15 +04:00
2011-08-22 19:36:07 +04:00
from ..formatting_structure import boxes
from ..css.values import get_percentage_value
2011-08-09 12:15:53 +04:00
SUPPORTED_IMAGES = ['image/png', 'image/gif', 'image/jpeg', 'image/bmp']
2011-08-23 13:42:40 +04:00
2011-08-10 16:51:18 +04:00
def get_image_surface_from_uri(uri):
2011-08-22 19:36:07 +04:00
"""Get a :class:`cairo.ImageSurface`` from an image URI."""
2011-10-10 18:39:41 +04:00
fileimage = urllib.FancyURLopener().open(uri)
info = fileimage.info()
if hasattr(info, 'get_content_type'):
# Python 3
mime_type = info.get_content_type()
else:
# Python 2
mime_type = info.gettype()
2011-08-25 19:29:16 +04:00
# TODO: implement image type sniffing?
# http://www.w3.org/TR/html5/fetching-resources.html#content-type-sniffing:-image
2011-08-22 12:17:37 +04:00
if mime_type in SUPPORTED_IMAGES:
if mime_type == "image/png":
image = fileimage
else:
2011-10-11 14:08:22 +04:00
from PIL import Image
2011-08-23 13:42:40 +04:00
content = fileimage.read()
pil_image = Image.open(StringIO(content))
2011-08-22 12:17:37 +04:00
image = StringIO()
2011-08-23 13:42:40 +04:00
pil_image = pil_image.convert('RGBA')
pil_image.save(image, "PNG")
2011-08-22 12:17:37 +04:00
image.seek(0)
return cairo.ImageSurface.create_from_png(image)
def draw_box(context, box):
2011-08-22 19:36:07 +04:00
"""Draw a ``box`` on ``context``."""
if has_background(box):
draw_background(context, box)
marker_box = getattr(box, 'outside_list_marker', None)
if marker_box:
draw_box(context, marker_box)
if isinstance(box, boxes.TextBox):
draw_text(context, box)
return
2011-08-05 13:31:16 +04:00
if isinstance(box, boxes.ReplacedBox):
draw_replacedbox(context, box)
if isinstance(box, boxes.ParentBox):
for child in box.children:
draw_box(context, child)
draw_border(context, box)
2011-08-05 13:31:16 +04:00
def has_background(box):
2011-08-22 19:36:07 +04:00
"""Return whether the given box has any background."""
return box.style.background_color.alpha > 0 or \
box.style.background_image != 'none'
2011-08-24 17:40:13 +04:00
def draw_page_background(context, page):
"""Draw the backgrounds for the page box (from @page style) and for the
page area (from the root element).
2011-08-24 17:40:13 +04:00
If the root element is "html" and has no background, the page areas
background is taken from its "body" child.
In both cases the background position is the same as if it was drawn on
the element.
See http://www.w3.org/TR/CSS21/colors.html#background
2011-08-22 19:36:07 +04:00
"""
2011-08-24 17:40:13 +04:00
# TODO: this one should have its origin at (0, 0), not the border box
# of the page.
# TODO: more tests for this, see
# http://www.w3.org/TR/css3-page/#page-properties
draw_background(context, page, clip=False)
if has_background(page.root_box):
2011-08-24 17:40:13 +04:00
draw_background(context, page.root_box, clip=False)
elif page.root_box.element.tag.lower() == 'html':
for child in page.root_box.children:
if child.element.tag.lower() == 'body':
# This must be drawn now, before anything on the root element.
2011-08-24 17:40:13 +04:00
draw_background(context, child, clip=False)
2011-08-24 17:40:13 +04:00
def draw_background(context, box, clip=True):
2011-08-22 19:36:07 +04:00
"""Draw the box background color and image to a ``cairo.Context``."""
if getattr(box, 'background_drawn', False):
return
box.background_drawn = True
if not has_background(box):
return
2011-08-10 12:01:56 +04:00
with context.stacked():
bg_x = box.border_box_x()
bg_y = box.border_box_y()
2011-08-10 18:52:34 +04:00
bg_width = box.border_width()
bg_height = box.border_height()
bg_attachement = box.style.background_attachment
2011-10-06 17:36:19 +04:00
if bg_attachement == 'fixed':
# There should not be any clip yet
x1, y1, x2, y2 = context.clip_extents()
page_width = x2 - x1
page_height = y2 - y1
2011-08-24 17:40:13 +04:00
if clip:
context.rectangle(bg_x, bg_y, bg_width, bg_height)
context.clip()
2011-08-10 12:01:56 +04:00
# Background color
bg_color = box.style.background_color
if bg_color.alpha > 0:
context.set_source_colorvalue(bg_color)
context.paint()
if bg_attachement == 'scroll':
# Change coordinates to make the rest easier.
context.translate(bg_x, bg_y)
else:
assert bg_attachement == 'fixed'
2011-10-06 17:36:19 +04:00
bg_width = page_width
2011-10-07 13:36:08 +04:00
bg_height = page_height
2011-08-10 12:01:56 +04:00
# Background image
bg_image = box.style.background_image
if bg_image == 'none':
2011-08-10 18:52:34 +04:00
return
surface = box.document.get_image_surface_from_uri(bg_image)
if surface is None:
2011-08-10 18:52:34 +04:00
return
2011-08-10 21:33:16 +04:00
image_width = surface.get_width()
image_height = surface.get_height()
bg_position = box.style.background_position
2011-08-11 13:15:41 +04:00
bg_position_x, bg_position_y = absolute_background_position(
bg_position, (bg_width, bg_height), (image_width, image_height))
context.translate(bg_position_x, bg_position_y)
2011-08-10 21:33:16 +04:00
bg_repeat = box.style.background_repeat
2011-08-10 18:52:34 +04:00
if bg_repeat != 'repeat':
# Get the current clip rectangle
clip_x1, clip_y1, clip_x2, clip_y2 = context.clip_extents()
clip_width = clip_x2 - clip_x1
clip_height = clip_y2 - clip_y1
if bg_repeat in ('no-repeat', 'repeat-x'):
# Limit the drawn area vertically
clip_y1 = 0 # because of the last context.translate()
2011-08-10 21:33:16 +04:00
clip_height = image_height
2011-08-10 18:52:34 +04:00
if bg_repeat in ('no-repeat', 'repeat-y'):
# Limit the drawn area horizontally
clip_x1 = 0 # because of the last context.translate()
2011-08-10 21:33:16 +04:00
clip_width = image_width
2011-08-10 18:52:34 +04:00
# Second clip for the background image
context.rectangle(clip_x1, clip_y1, clip_width, clip_height)
context.clip()
2011-10-11 13:26:20 +04:00
context.set_source_surface(surface)
context.get_source().set_extend(cairo.EXTEND_REPEAT)
2011-08-10 18:52:34 +04:00
context.paint()
2011-07-12 19:53:15 +04:00
2011-08-05 13:31:16 +04:00
2011-08-11 13:15:41 +04:00
def absolute_background_position(css_values, bg_dimensions, image_dimensions):
2011-08-22 19:36:07 +04:00
"""Return the background's ``position_x, position_y`` in pixels.
2011-08-10 21:33:16 +04:00
http://www.w3.org/TR/CSS21/colors.html#propdef-background-position
2011-08-11 13:15:41 +04:00
:param css_values: a list of one or two cssutils Value objects.
2011-08-22 19:36:07 +04:00
:param bg_dimensions: ``width, height`` of the background positionning area
:param image_dimensions: ``width, height`` of the background image
2011-08-10 21:33:16 +04:00
"""
2011-08-11 13:15:41 +04:00
values = list(css_values)
2011-08-10 21:33:16 +04:00
if len(css_values) == 1:
values.append('center')
2011-08-10 21:33:16 +04:00
else:
assert len(css_values) == 2
if values[1] in ('left', 'right') or values[0] in ('top', 'bottom'):
2011-08-11 13:15:41 +04:00
values.reverse()
# Order is now [horizontal, vertical]
kw_to_percentage = dict(top=0, left=0, center=50, bottom=100, right=100)
for value, bg_dimension, image_dimension in zip(
values, bg_dimensions, image_dimensions):
percentage = kw_to_percentage.get(value, get_percentage_value(value))
2011-08-11 13:15:41 +04:00
if percentage is not None:
yield (bg_dimension - image_dimension) * percentage / 100.
else:
yield value
2011-08-10 21:33:16 +04:00
def get_rectangle_edges(x, y, width, height):
"""Return the 4 edges of a rectangle as a list.
Edges are in clock-wise order, starting from the top.
Each edge is returned as ``(start_point, end_point)`` and each point
as ``(x, y)`` coordinates.
"""
# In clock-wise order, starting on top left
corners = [
(x, y),
(x + width, y),
(x + width, y + height),
(x, y + height)]
# clock-wise order, starting on top right
shifted_corners = corners[1:] + corners[:1]
return zip(corners, shifted_corners)
2011-08-05 13:31:16 +04:00
def draw_border(context, box):
2011-08-22 19:36:07 +04:00
"""Draw the box border to a ``cairo.Context``."""
if all(getattr(box, 'border_%s_width' % side) == 0
for side in ['top', 'right', 'bottom', 'left']):
# No border, return early.
return
2011-08-05 13:31:16 +04:00
for side, x_offset, y_offset, border_edge, padding_edge in zip(
['top', 'right', 'bottom', 'left'],
[0, -1, 0, 1],
[1, 0, -1, 0],
get_rectangle_edges(
box.border_box_x(), box.border_box_y(),
box.border_width(), box.border_height(),
),
get_rectangle_edges(
box.padding_box_x(), box.padding_box_y(),
box.padding_width(), box.padding_height(),
),
):
2011-08-22 19:36:07 +04:00
width = getattr(box, 'border_%s_width' % side)
if width == 0:
continue
color = box.style['border_%s_color' % side]
if color.alpha == 0:
continue
style = box.style['border_%s_style' % side]
with context.stacked():
"""
Both edges form a trapezoid. This is the top one:
+---------------+
\ /
=================
\ /
+-------+
We clip on its outline on draw on the big line on the middle.
"""
# TODO: implement other styles.
if not style in ['dotted', 'dashed']:
border_start, border_stop = border_edge
padding_start, padding_stop = padding_edge
# Move to one of the Trapezoids corner
context.move_to(*border_start)
for point in [border_stop, padding_stop,
padding_start, border_start]:
context.line_to(*point)
context.clip()
elif style == 'dotted':
# TODO: find a way to make a real dotted border
context.set_dash([width], 0)
elif style == 'dashed':
# TODO: find a way to make a real dashed border
context.set_dash([4 * width], 0)
(x1, y1), (x2, y2) = border_edge
offset = width / 2
x_offset *= offset
y_offset *= offset
x1 += x_offset
x2 += x_offset
y1 += y_offset
y2 += y_offset
context.move_to(x1, y1)
context.line_to(x2, y2)
context.set_source_colorvalue(color)
context.set_line_width(width)
context.stroke()
2011-08-05 13:31:16 +04:00
2011-08-09 12:15:53 +04:00
def draw_replacedbox(context, box):
2011-08-22 19:36:07 +04:00
"""Draw the given :class:`boxes.ReplacedBox` to a ``cairo.context``."""
x, y = box.padding_box_x(), box.padding_box_y()
2011-08-09 12:15:53 +04:00
width, height = box.width, box.height
with context.stacked():
context.translate(x, y)
context.rectangle(0, 0, width, height)
context.clip()
2011-08-22 19:36:07 +04:00
scale_width = width / box.replacement.intrinsic_width()
scale_height = height / box.replacement.intrinsic_height()
context.scale(scale_width, scale_height)
box.replacement.draw(context)
def draw_text(context, textbox):
"""Draw ``textbox`` to a ``cairo.Context`` from ``PangoCairo.Context``."""
2011-09-02 19:42:00 +04:00
# Pango crashes with font-size: 0
2011-10-11 18:04:00 +04:00
assert textbox.style.font_size
2011-09-02 19:42:00 +04:00
context.move_to(textbox.position_x, textbox.position_y + textbox.baseline)
context.set_source_colorvalue(textbox.style.color)
textbox.show_line(context)
values = textbox.style.text_decoration
for value in values:
if value == 'overline':
draw_overline(context, textbox)
elif value == 'underline':
draw_underline(context, textbox)
elif value == 'line-through':
draw_line_through(context, textbox)
def draw_overline(context, textbox):
"""Draw overline of ``textbox`` to a ``cairo.Context``."""
font_size = textbox.style.font_size
position_y = textbox.baseline + textbox.position_y - (font_size * 0.15)
draw_text_decoration(context, position_y, textbox)
def draw_underline(context, textbox):
"""Draw underline of ``textbox`` to a ``cairo.Context``."""
font_size = textbox.style.font_size
position_y = textbox.baseline + textbox.position_y + (font_size * 0.15)
draw_text_decoration(context, position_y, textbox)
def draw_line_through(context, textbox):
"""Draw line-through of ``textbox`` to a ``cairo.Context``."""
position_y = textbox.position_y + (textbox.height * 0.5)
draw_text_decoration(context, position_y, textbox)
def draw_text_decoration(context, position_y, textbox):
"""Draw text-decoration of ``textbox`` to a ``cairo.Context``."""
with context.stacked():
2011-10-11 18:04:00 +04:00
context.set_source_colorvalue(textbox.style.color)
context.set_line_width(1) # TODO: make this proportional to font_size?
context.move_to(textbox.position_x, position_y)
context.rel_line_to(textbox.width, 0)
context.stroke()