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

327 lines
11 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 PIL import Image
from StringIO import StringIO
2011-07-12 19:53:15 +04:00
from .figures import Point, Line, Trapezoid
2011-08-22 19:36:07 +04:00
from ..text import TextLineFragment
from ..formatting_structure import boxes
from ..css.values import (
get_single_keyword, get_keyword, get_pixel_value, 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."""
try:
fileimage = urllib.urlopen(uri)
except IOError:
2011-08-10 16:51:18 +04:00
return None
2011-08-22 12:17:37 +04:00
mime_type = fileimage.info().gettype()
if mime_type in SUPPORTED_IMAGES:
if mime_type == "image/png":
image = fileimage
else:
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'][0].alpha > 0 or \
get_single_keyword(box.style['background-image']) != 'none'
def draw_canvas_background(context, page):
2011-08-22 19:36:07 +04:00
"""Draw the canvas background, taken from the root element.
2011-08-22 19:36:07 +04:00
If the root element is "html" and has no background, the canvas
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
"""
if has_background(page.root_box):
draw_background(context, page.root_box, on_entire_canvas=True)
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.
draw_background(context, child, on_entire_canvas=True)
def get_page_size(box):
2011-08-22 19:36:07 +04:00
"""Return the outer ``width, height`` of the ``box``'s root ``PageBox``."""
while not isinstance(box, boxes.PageBox):
box = box.parent
return box.outer_width, box.outer_height
def draw_background(context, box, on_entire_canvas=False):
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()
if not on_entire_canvas:
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'][0]
if bg_color.alpha > 0:
context.set_source_colorvalue(bg_color)
context.paint()
bg_attachement = get_single_keyword(box.style['background-attachment'])
if bg_attachement == 'scroll':
# Change coordinates to make the rest easier.
context.translate(bg_x, bg_y)
else:
assert bg_attachement == 'fixed'
bg_width, bg_height = get_page_size(box)
2011-08-10 12:01:56 +04:00
# Background image
2011-08-10 16:51:18 +04:00
bg_image = box.style['background-image'][0]
2011-08-10 18:52:34 +04:00
if bg_image.type != 'URI':
return
surface = get_image_surface_from_uri(bg_image.absoluteUri)
if not surface:
return
2011-08-10 21:33:16 +04:00
image_width = surface.get_width()
image_height = surface.get_height()
2011-08-11 13:15:41 +04:00
bg_position = box.style['background-position']
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
2011-08-10 18:52:34 +04:00
bg_repeat = get_single_keyword(box.style['background-repeat'])
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()
pattern = cairo.SurfacePattern(surface)
pattern.set_extend(cairo.EXTEND_REPEAT)
context.set_source(pattern)
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)
keywords = [get_keyword(value) for value in values]
2011-08-10 21:33:16 +04:00
if len(css_values) == 1:
2011-08-11 13:15:41 +04:00
values.append(None) # dummy value for zip()
keywords.append('center')
2011-08-10 21:33:16 +04:00
else:
assert len(css_values) == 2
2011-08-22 19:36:07 +04:00
if not (None in keywords or
keywords[0] in ('left', 'right') or
keywords[1] in ('top', 'bottom')):
2011-08-11 13:15:41 +04:00
values.reverse()
keywords.reverse()
# Order is now [horizontal, vertical]
kw_to_percentage = dict(top=0, left=0, center=50, bottom=100, right=100)
for value, keyword, bg_dimension, image_dimension in zip(
values, keywords, bg_dimensions, image_dimensions):
if keyword is not None:
percentage = kw_to_percentage[keyword]
2011-08-10 21:33:16 +04:00
else:
2011-08-11 13:15:41 +04:00
percentage = get_percentage_value(value)
if percentage is not None:
yield (bg_dimension - image_dimension) * percentage / 100.
else:
yield get_pixel_value(value)
2011-08-10 21:33:16 +04:00
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
def get_edge(x, y, width, height):
2011-08-22 19:36:07 +04:00
"""Get the 4 points corresponding to the given parameters."""
return (Point(x, y), Point(x + width, y),
Point(x + width, y + height), Point(x, y + height))
2011-08-05 13:31:16 +04:00
def get_border_area():
2011-08-22 19:36:07 +04:00
"""Get the border area of ``box``."""
# Border area
2011-08-05 13:31:16 +04:00
x = box.position_x + box.margin_left
y = box.position_y + box.margin_top
border_edge = get_edge(x, y, box.border_width(), box.border_height())
2011-08-22 19:36:07 +04:00
# Padding area
2011-08-05 13:31:16 +04:00
x = x + box.border_left_width
y = y + box.border_top_width
2011-08-22 19:36:07 +04:00
padding_edge = get_edge(
x, y, box.padding_width(), box.padding_height())
2011-08-05 13:31:16 +04:00
return border_edge, padding_edge
def get_lines(rectangle):
2011-08-22 19:36:07 +04:00
"""Get the 4 lines of ``rectangle``."""
lines_number = len(rectangle)
for i in range(lines_number):
yield Line(rectangle[i], rectangle[(i + 1) % lines_number])
2011-08-05 13:31:16 +04:00
def get_trapezoids():
2011-08-22 19:36:07 +04:00
"""Get the 4 trapezoids of ``context``."""
border_lines, padding_lines = [
get_lines(area) for area in get_border_area()]
for line1, line2 in zip(border_lines, padding_lines):
2011-08-05 13:31:16 +04:00
yield Trapezoid(line1, line2)
def draw_border_side(side, trapezoid):
2011-08-22 19:36:07 +04:00
"""Draw ``trapezoid`` at the box's ``side``."""
width = getattr(box, 'border_%s_width' % side)
if width == 0:
2011-08-05 13:31:16 +04:00
return
2011-08-22 19:36:07 +04:00
color = box.style['border-%s-color' % side][0]
style = box.style['border-%s-style' % side][0].value
2011-08-05 13:31:16 +04:00
if color.alpha > 0:
with context.stacked():
# TODO: implement other styles.
2011-08-22 19:36:07 +04:00
if not style in ['dotted', 'dashed']:
trapezoid.draw_path(context)
context.clip()
2011-08-22 19:36:07 +04:00
elif style == 'dotted':
# TODO: find a way to make a real dotted border
context.set_dash([width], 0)
2011-08-22 19:36:07 +04:00
elif style == 'dashed':
# TODO: find a way to make a real dashed border
context.set_dash([4 * width], 0)
line = trapezoid.get_middle_line()
line.draw_path(context)
context.set_source_colorvalue(color)
context.set_line_width(width)
context.stroke()
2011-08-05 13:31:16 +04:00
2011-08-22 19:36:07 +04:00
trapezoids_side = zip(['top', 'right', 'bottom', 'left'], get_trapezoids())
2011-08-05 13:31:16 +04:00
for side, trapezoid in trapezoids_side:
draw_border_side(side, trapezoid)
2011-07-28 20:39:35 +04:00
def draw_text(context, textbox):
2011-08-22 19:36:07 +04:00
"""Draw ``textbox`` to a ``cairo.Context`` from ``pangocairo.Context``."""
fragment = TextLineFragment.from_textbox(textbox)
2011-07-28 20:39:35 +04:00
context.move_to(textbox.position_x, textbox.position_y)
context.show_layout(fragment.get_layout())
2011-07-13 13:31:44 +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)