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

816 lines
31 KiB
Python
Raw Normal View History

2011-07-12 19:53:15 +04:00
# coding: utf8
"""
weasyprint.draw
---------------
2011-07-12 19:53:15 +04:00
Take an "after layout" box tree and draw it onto a cairo context.
2011-07-12 19:53:15 +04:00
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
2011-08-22 19:36:07 +04:00
"""
from __future__ import division, unicode_literals
2012-09-12 21:33:16 +04:00
import io
import sys
import contextlib
2012-02-08 18:44:03 +04:00
import math
2012-07-11 20:21:20 +04:00
import operator
2011-08-09 12:15:53 +04:00
2011-07-12 19:53:15 +04:00
import cairo
2012-09-12 21:33:16 +04:00
from .urls import FILESYSTEM_ENCODING
from .formatting_structure import boxes
2012-05-11 16:10:11 +04:00
from .stacking import StackingContext
from .text import show_first_line
2012-07-11 20:21:20 +04:00
from .compat import xrange
# Map values of the image-rendering property to cairo FILTER values:
# Values are normalized to lower case.
IMAGE_RENDERING_TO_FILTER = dict(
optimizespeed=cairo.FILTER_FAST,
auto=cairo.FILTER_GOOD,
optimizequality=cairo.FILTER_BEST,
)
@contextlib.contextmanager
def stacked(context):
"""Save and restore the context when used with the ``with`` keyword."""
context.save()
try:
yield
finally:
context.restore()
def lighten(color, offset):
"""Return a lighter color (or darker, for negative offsets)."""
return (
color.red + offset,
color.green + offset,
color.blue + offset,
color.alpha)
def draw_page(page, context, enable_hinting):
"""Draw the given PageBox."""
2012-05-11 16:10:11 +04:00
stacking_context = StackingContext.from_page(page)
draw_box_background(
context, stacking_context.page, stacking_context.box, enable_hinting)
draw_canvas_background(context, page, enable_hinting)
draw_border(context, page, enable_hinting)
draw_stacking_context(context, stacking_context, enable_hinting)
2012-05-11 16:10:11 +04:00
def draw_box_background_and_border(context, page, box, enable_hinting):
draw_box_background(context, page, box, enable_hinting)
2012-07-11 20:21:20 +04:00
if not isinstance(box, boxes.TableBox):
draw_border(context, box, enable_hinting)
2012-07-11 20:21:20 +04:00
else:
2012-05-11 16:10:11 +04:00
for column_group in box.column_groups:
draw_box_background(context, page, column_group, enable_hinting)
2012-05-11 16:10:11 +04:00
for column in column_group.children:
draw_box_background(context, page, column, enable_hinting)
2012-05-11 16:10:11 +04:00
for row_group in box.children:
draw_box_background(context, page, row_group, enable_hinting)
2012-05-11 16:10:11 +04:00
for row in row_group.children:
draw_box_background(context, page, row, enable_hinting)
2012-05-11 16:10:11 +04:00
for cell in row.children:
draw_box_background(context, page, cell, enable_hinting)
2012-07-11 20:21:20 +04:00
if box.style.border_collapse == 'separate':
draw_border(context, box, enable_hinting)
2012-07-11 20:21:20 +04:00
for row_group in box.children:
for row in row_group.children:
for cell in row.children:
draw_border(context, cell, enable_hinting)
2012-07-11 20:21:20 +04:00
else:
draw_collapsed_borders(context, box, enable_hinting)
2012-05-11 16:10:11 +04:00
def draw_stacking_context(context, stacking_context, enable_hinting):
2012-05-11 16:10:11 +04:00
"""Draw a ``stacking_context`` on ``context``."""
# See http://www.w3.org/TR/CSS2/zindex.html
with stacked(context):
2012-08-03 19:19:04 +04:00
box = stacking_context.box
if box.is_absolutely_positioned() and box.style.clip:
top, right, bottom, left = box.style.clip
2012-05-11 16:10:11 +04:00
if top == 'auto':
top = 0
if right == 'auto':
right = 0
if bottom == 'auto':
2012-08-03 19:19:04 +04:00
bottom = box.border_height()
2012-05-11 16:10:11 +04:00
if left == 'auto':
2012-08-03 19:19:04 +04:00
left = box.border_width()
2012-05-11 16:10:11 +04:00
context.rectangle(
2012-08-03 19:19:04 +04:00
box.border_box_x() + right,
box.border_box_y() + top,
2012-05-11 16:10:11 +04:00
left - right,
bottom - top)
context.clip()
2012-08-03 19:19:04 +04:00
if box.style.overflow != 'visible':
context.rectangle(*box_rectangle(box, 'padding-box'))
2012-05-11 16:10:11 +04:00
context.clip()
2012-08-03 19:19:04 +04:00
if box.style.opacity < 1:
2012-02-07 21:26:23 +04:00
context.push_group()
2012-08-03 19:19:04 +04:00
apply_2d_transforms(context, box)
2012-05-11 16:10:11 +04:00
# Point 1 is done in draw_page
2012-05-11 16:10:11 +04:00
# Point 2
2012-08-03 19:19:04 +04:00
if isinstance(box, (boxes.BlockBox, boxes.MarginBox,
boxes.InlineBlockBox)):
2012-05-11 16:10:11 +04:00
# The canvas background was removed by set_canvas_background
draw_box_background_and_border(
context, stacking_context.page, box, enable_hinting)
2011-11-29 15:43:42 +04:00
2012-05-11 16:10:11 +04:00
# Point 3
for child_context in stacking_context.negative_z_contexts:
draw_stacking_context(context, child_context, enable_hinting)
2012-02-07 21:06:59 +04:00
2012-05-11 16:10:11 +04:00
# Point 4
2012-08-03 19:19:04 +04:00
for block in stacking_context.block_level_boxes:
2012-05-11 16:10:11 +04:00
draw_box_background_and_border(
context, stacking_context.page, block, enable_hinting)
2012-05-11 16:10:11 +04:00
# Point 5
for child_context in stacking_context.float_contexts:
draw_stacking_context(context, child_context, enable_hinting)
2012-05-11 16:10:11 +04:00
# Point 6
2012-08-03 19:19:04 +04:00
if isinstance(box, boxes.InlineBox):
draw_inline_level(
context, stacking_context.page, box, enable_hinting)
2012-05-11 16:10:11 +04:00
# Point 7
2012-08-03 19:19:04 +04:00
for block in [box] + stacking_context.blocks_and_cells:
marker_box = getattr(block, 'outside_list_marker', None)
2012-05-11 16:10:11 +04:00
if marker_box:
draw_inline_level(
context, stacking_context.page, marker_box, enable_hinting)
2012-05-11 16:10:11 +04:00
2012-08-03 19:19:04 +04:00
if isinstance(block, boxes.ReplacedBox):
draw_replacedbox(context, block)
else:
2012-08-03 19:19:04 +04:00
for child in block.children:
2012-05-11 16:10:11 +04:00
if isinstance(child, boxes.LineBox):
# TODO: draw inline tables
draw_inline_level(
context, stacking_context.page, child,
enable_hinting)
2012-05-11 16:10:11 +04:00
# Point 8
for child_context in stacking_context.zero_z_contexts:
draw_stacking_context(context, child_context, enable_hinting)
2012-02-07 21:06:59 +04:00
2012-05-11 16:10:11 +04:00
# Point 9
for child_context in stacking_context.positive_z_contexts:
draw_stacking_context(context, child_context, enable_hinting)
2012-02-07 21:06:59 +04:00
2012-08-03 18:21:47 +04:00
# Point 10
draw_outlines(context, box, enable_hinting)
2012-08-03 18:21:47 +04:00
2012-08-03 19:19:04 +04:00
if box.style.opacity < 1:
2012-02-07 21:26:23 +04:00
context.pop_group_to_source()
2012-08-03 19:19:04 +04:00
context.paint_with_alpha(box.style.opacity)
2012-05-11 16:10:11 +04:00
def box_rectangle(box, which_rectangle):
if which_rectangle == 'border-box':
return (
box.border_box_x(),
box.border_box_y(),
box.border_width(),
box.border_height(),
)
elif which_rectangle == 'padding-box':
return (
box.padding_box_x(),
box.padding_box_y(),
box.padding_width(),
box.padding_height(),
)
2012-04-02 16:45:44 +04:00
else:
assert which_rectangle == 'content-box', which_rectangle
return (
box.content_box_x(),
box.content_box_y(),
box.width,
box.height,
)
def background_positioning_area(page, box, style):
if style.background_attachment == 'fixed' and box is not page:
# Initial containing block
return box_rectangle(page, 'content-box')
else:
return box_rectangle(box, box.style.background_origin)
def draw_canvas_background(context, page, enable_hinting):
assert not isinstance(page.children[0], boxes.MarginBox)
root_box = page.children[0]
style = page.canvas_background
draw_background(context, style, page.canvas_background_image,
painting_area=box_rectangle(page, 'padding-box'),
positioning_area=background_positioning_area(page, root_box, style),
enable_hinting=enable_hinting)
def draw_box_background(context, page, box, enable_hinting):
2011-08-22 19:36:07 +04:00
"""Draw the box background color and image to a ``cairo.Context``."""
2012-05-11 16:10:11 +04:00
if box.style.visibility == 'hidden':
return
if box is page:
painting_area = None
else:
2012-05-11 00:09:04 +04:00
painting_area = box_rectangle(box, box.style.background_clip)
2012-05-11 16:10:11 +04:00
draw_background(
context, box.style, box.background_image, painting_area,
positioning_area=background_positioning_area(page, box, box.style),
enable_hinting=enable_hinting)
2012-02-08 18:44:03 +04:00
def percentage(value, refer_to):
"""Return the evaluated percentage value, or the value unchanged."""
if value.unit == 'px':
return value.value
2012-02-08 18:44:03 +04:00
else:
assert value.unit == '%'
return refer_to * value.value / 100
2012-02-08 18:44:03 +04:00
def draw_background(context, style, image, painting_area, positioning_area,
enable_hinting):
"""Draw the background color and image to a ``cairo.Context``."""
bg_color = style.background_color
if bg_color.alpha == 0 and image is None:
# No background.
return
2011-08-10 12:01:56 +04:00
with stacked(context):
if enable_hinting:
# Prefer crisp edges on background rectangles.
context.set_antialias(cairo.ANTIALIAS_NONE)
if painting_area:
context.rectangle(*painting_area)
context.clip()
#else: unrestricted, whole page box
2011-08-10 12:01:56 +04:00
# Background color
if bg_color.alpha > 0:
context.set_source_rgba(*bg_color)
context.paint()
2011-08-10 12:01:56 +04:00
# Background image
2011-12-08 21:11:32 +04:00
if image is None:
2011-08-10 18:52:34 +04:00
return
(positioning_x, positioning_y,
positioning_width, positioning_height) = positioning_area
context.translate(positioning_x, positioning_y)
pattern, intrinsic_width, intrinsic_height = image
bg_size = style.background_size
if bg_size in ('cover', 'contain'):
scale_x = scale_y = {'cover': max, 'contain': min}[bg_size](
positioning_width / intrinsic_width,
positioning_height / intrinsic_height)
image_width = intrinsic_width * scale_x
image_height = intrinsic_height * scale_y
elif bg_size == ('auto', 'auto'):
scale_x = scale_y = 1
image_width = intrinsic_width
image_height = intrinsic_height
elif bg_size[0] == 'auto':
image_height = percentage(bg_size[1], positioning_height)
scale_x = scale_y = image_height / intrinsic_height
image_width = intrinsic_width * scale_x
elif bg_size[1] == 'auto':
image_width = percentage(bg_size[0], positioning_width)
scale_x = scale_y = image_width / intrinsic_width
image_height = intrinsic_height * scale_y
else:
image_width = percentage(bg_size[0], positioning_width)
image_height = percentage(bg_size[1], positioning_height)
scale_x = image_width / intrinsic_width
scale_y = image_height / intrinsic_height
bg_position_x, bg_position_y = style.background_position
context.translate(
percentage(bg_position_x, positioning_width - image_width),
percentage(bg_position_y, positioning_height - image_height),
)
2011-08-10 21:33:16 +04:00
bg_repeat = style.background_repeat
2012-01-13 21:16:27 +04:00
if bg_repeat in ('repeat-x', 'repeat-y'):
# Get the current clip rectangle. This is the same as
# painting_area, but in new coordinates after translate()
2011-08-10 18:52:34 +04:00
clip_x1, clip_y1, clip_x2, clip_y2 = context.clip_extents()
clip_width = clip_x2 - clip_x1
clip_height = clip_y2 - clip_y1
2012-01-13 21:16:27 +04:00
if bg_repeat == 'repeat-x':
2011-08-10 18:52:34 +04:00
# 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
2012-01-13 21:16:27 +04:00
else:
# repeat-y
2011-08-10 18:52:34 +04:00
# 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()
2012-01-13 21:16:27 +04:00
if bg_repeat == 'no-repeat':
# The same image/pattern may have been used
# in a repeating background.
pattern.set_extend(cairo.EXTEND_NONE)
else:
pattern.set_extend(cairo.EXTEND_REPEAT)
# TODO: de-duplicate this with draw_replacedbox()
pattern.set_filter(IMAGE_RENDERING_TO_FILTER[style.image_rendering])
context.scale(scale_x, scale_y)
2012-01-13 21:16:27 +04:00
context.set_source(pattern)
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
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-12-13 19:18:28 +04:00
def xy_offset(x, y, offset_x, offset_y, offset):
"""Increment X and Y coordinates by the given offsets."""
return x + offset_x * offset, y + offset_y * offset
def draw_border(context, box, enable_hinting):
2011-08-22 19:36:07 +04:00
"""Draw the box border to a ``cairo.Context``."""
2012-05-11 16:10:11 +04:00
if box.style.visibility == 'hidden':
return
if all(getattr(box, 'border_%s_width' % side) == 0
2011-08-22 19:36:07 +04:00
for side in ['top', 'right', 'bottom', 'left']):
# No border, return early.
return
2011-08-05 13:31:16 +04:00
2012-07-11 20:21:20 +04:00
for side, border_edge, padding_edge in zip(
['top', 'right', 'bottom', 'left'],
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(),
),
):
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]
draw_border_segment(context, enable_hinting, style, width, color,
2012-07-11 20:21:20 +04:00
side, border_edge, padding_edge)
2011-12-13 18:42:14 +04:00
def draw_border_segment(context, enable_hinting, style, width, color, side,
2012-07-11 20:21:20 +04:00
border_edge, padding_edge):
with stacked(context):
2012-07-11 20:21:20 +04:00
context.set_source_rgba(*color)
x_offset, y_offset = {
'top': (0, 1), 'bottom': (0, -1), 'left': (1, 0), 'right': (-1, 0),
}[side]
if enable_hinting and (
2012-07-11 20:21:20 +04:00
# Borders smaller than 1 device unit would disappear
# without anti-aliasing.
math.hypot(*context.user_to_device(width, 0)) >= 1 and
math.hypot(*context.user_to_device(0, width)) >= 1):
# Avoid an artefact in the corner joining two solid borders
# of the same color.
context.set_antialias(cairo.ANTIALIAS_NONE)
2011-12-13 18:42:14 +04:00
2012-07-11 20:21:20 +04:00
if style not in ('dotted', 'dashed'):
# Clip on the trapezoid shape
"""
Clip on the trapezoid formed by the border edge (longer)
and the padding edge (shorter).
This is on the top side:
+---------------+ <= border edge ^
\ / |
\ / | top border width
\ / |
+-------+ <= padding edge v
<--> <--> <= left and right border widths
"""
border_start, border_stop = border_edge
padding_start, padding_stop = padding_edge
context.move_to(*border_start)
for point in [border_stop, padding_stop,
padding_start, border_start]:
context.line_to(*point)
context.clip()
2011-12-13 18:42:14 +04:00
2012-07-11 20:21:20 +04:00
if style == 'solid':
# Fill the whole trapezoid
context.paint()
elif style in ('inset', 'outset'):
do_lighten = (side in ('top', 'left')) ^ (style == 'inset')
factor = 1 if do_lighten else -1
context.set_source_rgba(*lighten(color, 0.5 * factor))
context.paint()
elif style in ('groove', 'ridge'):
# TODO: these would look better with more color stops
"""
Divide the width in 2 and stroke lines in different colors
+-------------+
1\ /2
1'\ / 2'
+-------+
"""
do_lighten = (side in ('top', 'left')) ^ (style == 'groove')
factor = 1 if do_lighten else -1
context.set_line_width(width / 2)
(x1, y1), (x2, y2) = border_edge
# from the border edge to the center of the first line
x1, y1 = xy_offset(x1, y1, x_offset, y_offset, width / 4)
x2, y2 = xy_offset(x2, y2, x_offset, y_offset, width / 4)
context.move_to(x1, y1)
context.line_to(x2, y2)
context.set_source_rgba(*lighten(color, 0.5 * factor))
context.stroke()
# Between the centers of both lines. 1/4 + 1/4 = 1/2
x1, y1 = xy_offset(x1, y1, x_offset, y_offset, width / 2)
x2, y2 = xy_offset(x2, y2, x_offset, y_offset, width / 2)
context.move_to(x1, y1)
context.line_to(x2, y2)
context.set_source_rgba(*lighten(color, -0.5 * factor))
context.stroke()
elif style == 'double':
"""
Divide the width in 3 and stroke both outer lines
+---------------+
1\ /2
\ /
1' \ / 2'
+-------+
"""
context.set_line_width(width / 3)
(x1, y1), (x2, y2) = border_edge
# from the border edge to the center of the first line
x1, y1 = xy_offset(x1, y1, x_offset, y_offset, width / 6)
x2, y2 = xy_offset(x2, y2, x_offset, y_offset, width / 6)
context.move_to(x1, y1)
context.line_to(x2, y2)
context.stroke()
# Between the centers of both lines. 1/6 + 1/3 + 1/6 = 2/3
x1, y1 = xy_offset(x1, y1, x_offset, y_offset, 2 * width / 3)
x2, y2 = xy_offset(x2, y2, x_offset, y_offset, 2 * width / 3)
context.move_to(x1, y1)
context.line_to(x2, y2)
context.stroke()
else:
assert style in ('dotted', 'dashed')
(x1, y1), (x2, y2) = border_edge
if style == 'dotted':
# Half-way from the extremities of the border and padding
# edges.
(px1, py1), (px2, py2) = padding_edge
x1 = (x1 + px1) / 2
x2 = (x2 + px2) / 2
y1 = (y1 + py1) / 2
y2 = (y2 + py2) / 2
2011-12-13 18:42:14 +04:00
"""
2012-07-11 20:21:20 +04:00
+---------------+
\ /
1 2
\ /
+-------+
"""
2012-07-11 20:21:20 +04:00
else: # dashed
# From the border edge to the middle:
x1, y1 = xy_offset(x1, y1, x_offset, y_offset, width / 2)
x2, y2 = xy_offset(x2, y2, x_offset, y_offset, width / 2)
2011-12-13 19:18:28 +04:00
"""
+---------------+
2012-07-11 20:21:20 +04:00
\ /
1 \ / 2
\ /
2011-12-13 19:18:28 +04:00
+-------+
"""
2012-07-11 20:21:20 +04:00
length = ((x2 - x1)**2 + (y2 - y1)**2) ** 0.5
dash = 2 * width
if style == 'dotted':
if context.user_to_device_distance(width, 0)[0] > 3:
# Round so that dash is a divisor of length,
# but not in the dots are too small.
dash = length / round(length / dash)
context.set_line_cap(cairo.LINE_CAP_ROUND)
context.set_dash([0, dash])
else: # dashed
# Round so that 2*dash is a divisor of length
dash = length / (2 * round(length / (2 * dash)))
context.set_dash([dash])
# Stroke along the line in === above, as wide as the border
context.move_to(x1, y1)
context.line_to(x2, y2)
context.set_line_width(width)
context.stroke()
def draw_outlines(context, box, enable_hinting):
2012-08-03 18:21:47 +04:00
width = box.style.outline_width
color = box.style.outline_color
style = box.style.outline_style
if box.style.visibility != 'hidden' and width != 0 and color.alpha != 0:
border_box = (box.border_box_x(), box.border_box_y(),
box.border_width(), box.border_height())
outline_box = (border_box[0] - width, border_box[1] - width,
border_box[2] + 2 * width,border_box[3] + 2 * width)
for side, border_edge, padding_edge in zip(
['top', 'right', 'bottom', 'left'],
get_rectangle_edges(*outline_box),
get_rectangle_edges(*border_box),
):
draw_border_segment(context, enable_hinting, style, width, color,
2012-08-03 18:21:47 +04:00
side, border_edge, padding_edge)
if isinstance(box, boxes.ParentBox):
for child in box.children:
if isinstance(child, boxes.Box):
draw_outlines(context, child, enable_hinting)
2012-08-03 18:21:47 +04:00
def draw_collapsed_borders(context, table, enable_hinting):
2012-07-11 20:21:20 +04:00
row_heights = [row.height for row_group in table.children
for row in row_group.children]
column_widths = table.column_widths
if not (row_heights and column_widths):
# One of the list is empty: dont bother with empty tables
return
row_positions = [row.position_y for row_group in table.children
for row in row_group.children]
column_positions = table.column_positions
grid_height = len(row_heights)
grid_width = len(column_widths)
assert grid_width == len(column_positions)
# Add the end of the last column, but make a copy from the table attr.
column_positions += [column_positions[-1] + column_widths[-1]]
# Add the end of the last row. No copy here, we own this list
row_positions.append(row_positions[-1] + row_heights[-1])
vertical_borders, horizontal_borders = table.collapsed_border_grid
2012-07-11 21:19:13 +04:00
skipped_rows = table.skipped_rows
2012-07-11 20:21:20 +04:00
segments = []
def half_max_width(border_list, yx_pairs, vertical=True):
result = 0
for y, x in yx_pairs:
if (
(0 <= y < grid_height and 0 <= x <= grid_width)
if vertical else
(0 <= y <= grid_height and 0 <= x < grid_width)
):
_, (_, width, _) = border_list[skipped_rows + y][x]
result = max(result, width)
return result / 2
2012-07-11 20:21:20 +04:00
def add_vertical(x, y):
2012-07-11 21:19:13 +04:00
score, (style, width, color) = vertical_borders[skipped_rows + y][x]
2012-07-11 20:21:20 +04:00
if width == 0 or color.alpha == 0:
return
half_width = width / 2
pos_x = column_positions[x]
pos_y_1 = row_positions[y] - half_max_width(horizontal_borders, [
(y, x - 1), (y, x)], vertical=False)
pos_y_2 = row_positions[y + 1] + half_max_width(horizontal_borders, [
(y + 1, x - 1), (y + 1, x)], vertical=False)
2012-07-11 20:21:20 +04:00
edge_1 = (pos_x - half_width, pos_y_1), (pos_x - half_width, pos_y_2)
edge_2 = (pos_x + half_width, pos_y_1), (pos_x + half_width, pos_y_2)
segments.append((score, style, width, color, 'left', edge_1, edge_2))
def add_horizontal(x, y):
2012-07-11 21:19:13 +04:00
score, (style, width, color) = horizontal_borders[skipped_rows + y][x]
2012-07-11 20:21:20 +04:00
if width == 0 or color.alpha == 0:
return
half_width = width / 2
pos_y = row_positions[y]
# TODO: change signs for rtl when we support rtl tables?
pos_x_1 = column_positions[x] - half_max_width(vertical_borders, [
(y - 1, x), (y, x)])
pos_x_2 = column_positions[x + 1] + half_max_width(vertical_borders, [
(y - 1, x + 1), (y, x + 1)])
2012-07-11 20:21:20 +04:00
edge_1 = (pos_x_1, pos_y - half_width), (pos_x_2, pos_y - half_width)
edge_2 = (pos_x_1, pos_y + half_width), (pos_x_2, pos_y + half_width)
segments.append((score, style, width, color, 'top', edge_1, edge_2))
for x in xrange(grid_width):
add_horizontal(x, 0)
for y in xrange(grid_height):
add_vertical(0, y)
for x in xrange(grid_width):
add_vertical(x + 1, y)
add_horizontal(x, y + 1)
# Sort bigger scores last (painted later, on top)
# Since the number of different scores is expected to be small compared
# to the number of segments, there should be little changes and Timsort
# should be closer to O(n) than O(n * log(n))
2012-07-11 20:21:20 +04:00
segments.sort(key=operator.itemgetter(0))
for segment in segments:
draw_border_segment(context, enable_hinting, *segment[1:])
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``."""
2012-05-11 16:10:11 +04:00
if box.style.visibility == 'hidden':
return
x, y = box.content_box_x(), box.content_box_y()
2011-08-09 12:15:53 +04:00
width, height = box.width, box.height
2012-01-13 21:16:27 +04:00
pattern, intrinsic_width, intrinsic_height = box.replacement
2012-06-01 12:02:48 +04:00
scale_width = width / intrinsic_width
scale_height = height / intrinsic_height
# Draw nothing for width:0 or height:0
if scale_width != 0 and scale_height != 0:
with stacked(context):
2012-06-01 12:02:48 +04:00
context.translate(x, y)
context.rectangle(0, 0, width, height)
context.clip()
context.scale(scale_width, scale_height)
# The same image/pattern may have been used in a
# repeating background.
pattern.set_extend(cairo.EXTEND_NONE)
pattern.set_filter(IMAGE_RENDERING_TO_FILTER[
box.style.image_rendering])
context.set_source(pattern)
context.paint()
# Make sure `pattern` is garbage collected. 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.
# Use a unique string that can be traced back here.
# Replaced boxes are atomic, so they should only ever be drawn once.
# Use an object incompatible with the usual 3-tuple to cause an exception
# if this box is used more than that.
box.replacement = 'Removed to work around cairos behavior'
2012-05-11 00:09:04 +04:00
def draw_inline_level(context, page, box, enable_hinting):
if isinstance(box, StackingContext):
stacking_context = box
assert isinstance(stacking_context.box, boxes.InlineBlockBox)
draw_stacking_context(context, stacking_context, enable_hinting)
else:
draw_box_background(context, page, box, enable_hinting)
draw_border(context, box, enable_hinting)
2012-06-04 20:49:13 +04:00
if isinstance(box, (boxes.InlineBox, boxes.LineBox)):
for child in box.children:
if isinstance(child, boxes.TextBox):
draw_text(context, child, enable_hinting)
2012-06-04 20:49:13 +04:00
else:
draw_inline_level(context, page, child, enable_hinting)
2012-06-04 20:49:13 +04:00
elif isinstance(box, boxes.InlineReplacedBox):
draw_replacedbox(context, box)
else:
assert isinstance(box, boxes.TextBox)
# Should only happen for list markers
draw_text(context, box, enable_hinting)
2012-05-11 16:10:11 +04:00
def draw_text(context, textbox, enable_hinting):
"""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
2012-05-11 16:10:11 +04:00
if textbox.style.visibility == 'hidden':
return
context.move_to(textbox.position_x, textbox.position_y + textbox.baseline)
context.set_source_rgba(*textbox.style.color)
show_first_line(context, textbox.pango_layout, enable_hinting)
values = textbox.style.text_decoration
2012-04-04 15:57:20 +04:00
if 'overline' in values:
2012-07-12 14:43:23 +04:00
draw_text_decoration(context, textbox,
textbox.baseline - 0.15 * textbox.style.font_size,
enable_hinting)
2012-04-04 15:57:20 +04:00
elif 'underline' in values:
2012-07-12 14:43:23 +04:00
draw_text_decoration(context, textbox,
textbox.baseline + 0.15 * textbox.style.font_size,
enable_hinting)
2012-04-04 15:57:20 +04:00
elif 'line-through' in values:
draw_text_decoration(context, textbox, textbox.height * 0.5,
enable_hinting)
def draw_text_decoration(context, textbox, offset_y, enable_hinting):
"""Draw text-decoration of ``textbox`` to a ``cairo.Context``."""
with stacked(context):
if enable_hinting:
context.set_antialias(cairo.ANTIALIAS_NONE)
context.set_source_rgba(*textbox.style.color)
2011-10-11 18:04:00 +04:00
context.set_line_width(1) # TODO: make this proportional to font_size?
2012-04-04 15:57:20 +04:00
context.move_to(textbox.position_x, textbox.position_y + offset_y)
2011-10-11 18:04:00 +04:00
context.rel_line_to(textbox.width, 0)
context.stroke()
2012-02-08 18:44:03 +04:00
def apply_2d_transforms(context, box):
# "Transforms apply to block-level and atomic inline-level elements,
# but do not apply to elements which may be split into
# multiple inline-level boxes."
# http://www.w3.org/TR/css3-2d-transforms/#introduction
if box.style.transform and not isinstance(box, boxes.InlineBox):
border_width = box.border_width()
border_height = box.border_height()
origin_x, origin_y = box.style.transform_origin
origin_x = percentage(origin_x, border_width)
origin_y = percentage(origin_y, border_height)
origin_x += box.border_box_x()
origin_y += box.border_box_y()
context.translate(origin_x, origin_y)
for name, args in box.style.transform:
if name == 'scale':
context.scale(*args)
elif name == 'rotate':
context.rotate(args)
2012-02-08 18:44:03 +04:00
elif name == 'translate':
translate_x, translate_y = args
2012-02-08 18:44:03 +04:00
context.translate(
percentage(translate_x, border_width),
percentage(translate_y, border_height),
)
else:
if name == 'skewx':
2012-05-11 00:10:29 +04:00
args = (1, 0, math.tan(args), 1, 0, 0)
2012-02-08 18:44:03 +04:00
elif name == 'skewy':
2012-05-11 00:10:29 +04:00
args = (1, math.tan(args), 0, 1, 0, 0)
2012-02-08 18:44:03 +04:00
else:
assert name == 'matrix'
2012-03-07 17:02:07 +04:00
context.transform(cairo.Matrix(*args))
2012-02-08 18:44:03 +04:00
context.translate(-origin_x, -origin_y)
2012-09-12 21:33:16 +04:00
def write_png(image_surfaces, target=None):
"""Concatenate images vertically and write the result as PNG
:param image_surfaces: a list a cairo ImageSurface objects
"""
if len(image_surfaces) == 1:
surface = image_surfaces[0]
else:
total_height = sum(s.get_height() for s in image_surfaces)
max_width = max(s.get_width() for s in image_surfaces)
surface = cairo.ImageSurface(
cairo.FORMAT_ARGB32, max_width, total_height)
context = cairo.Context(surface)
pos_y = 0
for page_surface in image_surfaces:
pos_x = (max_width - page_surface.get_width()) // 2
context.set_source_surface(page_surface, pos_x, pos_y)
context.paint()
pos_y += page_surface.get_height()
if target is None:
target = io.BytesIO()
surface.write_to_png(target)
return target.getvalue()
else:
if sys.version_info[0] < 3 and isinstance(target, unicode):
# py2cairo 1.8 does not support unicode filenames.
target = target.encode(FILESYSTEM_ENCODING)
surface.write_to_png(target)