1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-04 16:07:57 +03:00

Have metadata account for CSS transforms.

This commit is contained in:
Simon Sapin 2012-10-06 22:09:17 +02:00
parent 28cb59fc8b
commit a0bb1f2752
5 changed files with 119 additions and 25 deletions

View File

@ -18,6 +18,10 @@ Not released yet.
- Layout of inline-blocks with `vertical-align: top` or `bottom`.
- Do not repeat a blocks margin-top or padding-top after a page break.
- Performance problem with large tables split across many pages.
- Anchors and hyperlinks areas now follow CSS transforms.
Since PDF links have to be axis-aligned rectangles, the bounding box
is used. This may be larger than expected with rotations that are
not a multiple of 90 degrees.
Version 0.14

View File

@ -74,6 +74,23 @@ def _get_matrix(box):
return matrix
def rectangle_aabb(matrix, pos_x, pos_y, width, height):
"""Apply a transformation matrix to an axis-aligned rectangle
and return its axis-aligned bounding box as ``(x, y, width, height)``
"""
transform_point = matrix.transform_point
x1, y1 = transform_point(pos_x, pos_y)
x2, y2 = transform_point(pos_x + width, pos_y)
x3, y3 = transform_point(pos_x, pos_y + height)
x4, y4 = transform_point(pos_x + width, pos_y + height)
box_x1 = min(x1, x2, x3, x4)
box_y1 = min(y1, y2, y3, y4)
box_x2 = max(x1, x2, x3, x4)
box_y2 = max(y1, y2, y3, y4)
return box_x1, box_y1, box_x2 - box_x1, box_y2 - box_y1
class _TaggedTuple(tuple):
"""A tuple with a :attr:`sourceline` attribute,
The line number in the HTML source for whatever the tuple represents.
@ -93,24 +110,28 @@ def _get_metadata(box, bookmarks, links, anchors, matrix):
if has_bookmark or has_link or has_anchor:
pos_x, pos_y, width, height = box.hit_area()
if matrix:
pos_x, pos_y = matrix.transform_point(pos_x, pos_y)
width, height = matrix.transform_distance(width, height)
if has_bookmark:
bookmarks.append((bookmark_level, bookmark_label, (pos_x, pos_y)))
if has_link:
link_type, target = link
link = _TaggedTuple(
(link_type, target, (pos_x, pos_y, width, height)))
if matrix:
link = _TaggedTuple(
(link_type, target, rectangle_aabb(
matrix, pos_x, pos_y, width, height)))
else:
link = _TaggedTuple(
(link_type, target, (pos_x, pos_y, width, height)))
link.sourceline = box.sourceline
links.append(link)
if matrix and (has_bookmark or has_anchor):
pos_x, pos_y = matrix.transform_point(pos_x, pos_y)
if has_bookmark:
bookmarks.append((bookmark_level, bookmark_label, (pos_x, pos_y)))
if has_anchor:
anchors[anchor_name] = pos_x, pos_y
def _prepare(box, bookmarks, links, anchors, matrix):
transform = _get_matrix(box)
# TODO: account for CSS transforms
# matrix *= …
if transform:
matrix = transform * matrix if matrix else transform
_get_metadata(box, bookmarks, links, anchors, matrix)
for child in box.all_children():
_prepare(child, bookmarks, links, anchors, matrix)

View File

@ -28,11 +28,12 @@ import pytest
from .testing_utils import (
resource_filename, assert_no_logs, capture_logs, TestHTML)
from .test_draw import png_to_pixels
from ..compat import urljoin, urlencode, urlparse_uses_relative
from ..compat import urljoin, urlencode, urlparse_uses_relative, iteritems
from ..urls import path2url
from .. import HTML, CSS, default_url_fetcher
from .. import __main__
from .. import navigator
from ..document import _TaggedTuple
CHDIR_LOCK = threading.Lock()
@ -526,10 +527,35 @@ def test_low_level_api():
assert png_size(document.copy([page_2]).write_png()) == (6, 4)
def round_meta(pages):
"""Eliminate errors of floating point arithmetic for metadata.
(eg. 49.99999999999994 instead of 50)
"""
for page in pages:
anchors = page.anchors
for anchor_name, (pos_x, pos_y) in iteritems(anchors):
anchors[anchor_name] = round(pos_x, 6), round(pos_y, 6)
links = page.links
for i, link in enumerate(links):
sourceline = link.sourceline
link_type, target, (pos_x, pos_y, width, height) = link
link = _TaggedTuple((
link_type, target, (round(pos_x, 6), round(pos_y, 6),
round(width, 6), round(height, 6))))
link.sourceline = sourceline
links[i] = link
bookmarks = page.bookmarks
for i, (level, label, (pos_x, pos_y)) in enumerate(bookmarks):
bookmarks[i] = level, label, (round(pos_x, 6), round(pos_y, 6))
@assert_no_logs
def test_bookmarks():
def assert_bookmarks(html, expected_by_page, expected_tree):
def assert_bookmarks(html, expected_by_page, expected_tree, round=False):
document = TestHTML(string=html).render()
if round:
round_meta(document.pages)
assert [p.bookmarks for p in document.pages] == expected_by_page
assert document.make_bookmark_tree() == expected_tree
assert_bookmarks('''
@ -645,16 +671,30 @@ def test_bookmarks():
('H', (0, 0, 130), [])])]),
('I', (0, 0, 150), []),
])
assert_bookmarks('<h1>é', [[(1, 'é', (0, 0))]], [('é', (0, 0, 0), [])])
assert_bookmarks('''
<h1 style="transform: translateX(50px)">!
''', [[(1, '!', (50, 0))]], [('!', (0, 50, 0), [])])
assert_bookmarks('''
<h1 style="transform-origin: 0 0;
transform: rotate(90deg) translateX(50px)">!
''', [[(1, '!', (0, 50))]], [('!', (0, 0, 50), [])], round=True)
assert_bookmarks('''
<body style="transform-origin: 0 0; transform: rotate(90deg)">
<h1 style="transform: translateX(50px)">!
''', [[(1, '!', (0, 50))]], [('!', (0, 0, 50), [])], round=True)
@assert_no_logs
def test_links():
def assert_links(html, expected_links_by_page, expected_anchors_by_page,
expected_resolved_links,
base_url=resource_filename('<inline HTML>'), warnings=()):
base_url=resource_filename('<inline HTML>'),
warnings=(), round=False):
with capture_logs() as logs:
document = TestHTML(string=html, base_url=base_url).render()
if round:
round_meta(document.pages)
resolved_links = list(document.resolve_links())
assert len(logs) == len(warnings)
for message, expected in zip(logs, warnings):
@ -770,6 +810,18 @@ def test_links():
]], base_url=None, warnings=[
'WARNING: No anchor #missing for internal URI reference'])
assert_links('''
<body style="width: 100px; transform: translateY(100px)">
<a href="#lipsum" id="lipsum" style="display: block; height: 20px;
transform: rotate(90deg) scale(2)">
''', [[
('internal', 'lipsum', (30, 10, 40, 200)),
]], [
{'lipsum': (70, 10)}
], [[
('internal', (0, 70, 10), (30, 10, 40, 200)),
]], round=True)
def wsgi_client(path_info, qs_args=None):
start_response_calls = []

View File

@ -1901,6 +1901,27 @@ def test_2d_transform():
</style>
<div><img src="pattern.png"></div>
''')
assert_pixels('nested_rotate90_translateX', 12, 12, [
_+_+_+_+_+_+_+_+_+_+_+_,
_+_+_+_+_+_+_+_+_+_+_+_,
_+_+_+_+_+_+_+_+_+_+_+_,
_+_+_+_+_+_+_+_+_+_+_+_,
_+_+_+_+_+_+_+_+_+_+_+_,
_+_+B+B+B+r+_+_+_+_+_+_,
_+_+B+B+B+B+_+_+_+_+_+_,
_+_+B+B+B+B+_+_+_+_+_+_,
_+_+B+B+B+B+_+_+_+_+_+_,
_+_+_+_+_+_+_+_+_+_+_+_,
_+_+_+_+_+_+_+_+_+_+_+_,
_+_+_+_+_+_+_+_+_+_+_+_,
], '''
<style>
@page { size: 12px; margin: 2px; background: #fff; }
div { transform: rotate(90deg); font-size: 0; width: 4px }
img { transform: translateX(3px) }
</style>
<div><img src="pattern.png"></div>
''')
assert_pixels('image_reflection', 8, 8, [
_+_+_+_+_+_+_+_,

View File

@ -63,18 +63,14 @@ def get_bookmarks(html, structure_only=False):
def get_links(html, **kwargs):
_root, _bookmarks, links = get_metadata(html, **kwargs)
return [
[
(
type_,
(target if type_ == 'external' else
(lambda page, x, y: (page, round(x, 6), round(y, 6)))
(*target)),
tuple(round(v, 6) for v in rect)
)
for type_, target, rect in page_links
]
for page_links in links]
for page_links in links:
for i, (link_type, target, rectangle) in enumerate(page_links):
if link_type == 'internal':
page, x, y = target
target = page, round(x, 6), round(y, 6)
rectangle = tuple(round(v, 6) for v in rectangle)
page_links[i] = link_type, target, rectangle
return links
@assert_no_logs