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:
parent
28cb59fc8b
commit
a0bb1f2752
4
CHANGES
4
CHANGES
@ -18,6 +18,10 @@ Not released yet.
|
||||
- Layout of inline-blocks with `vertical-align: top` or `bottom`.
|
||||
- Do not repeat a block’s 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
|
||||
|
@ -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)
|
||||
|
@ -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 = []
|
||||
|
@ -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, [
|
||||
_+_+_+_+_+_+_+_,
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user