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`.
|
- 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.
|
- Do not repeat a block’s margin-top or padding-top after a page break.
|
||||||
- Performance problem with large tables split across many pages.
|
- 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
|
Version 0.14
|
||||||
|
@ -74,6 +74,23 @@ def _get_matrix(box):
|
|||||||
return matrix
|
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):
|
class _TaggedTuple(tuple):
|
||||||
"""A tuple with a :attr:`sourceline` attribute,
|
"""A tuple with a :attr:`sourceline` attribute,
|
||||||
The line number in the HTML source for whatever the tuple represents.
|
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:
|
if has_bookmark or has_link or has_anchor:
|
||||||
pos_x, pos_y, width, height = box.hit_area()
|
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:
|
if has_link:
|
||||||
link_type, target = link
|
link_type, target = link
|
||||||
link = _TaggedTuple(
|
if matrix:
|
||||||
(link_type, target, (pos_x, pos_y, width, height)))
|
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
|
link.sourceline = box.sourceline
|
||||||
links.append(link)
|
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:
|
if has_anchor:
|
||||||
anchors[anchor_name] = pos_x, pos_y
|
anchors[anchor_name] = pos_x, pos_y
|
||||||
|
|
||||||
def _prepare(box, bookmarks, links, anchors, matrix):
|
def _prepare(box, bookmarks, links, anchors, matrix):
|
||||||
transform = _get_matrix(box)
|
transform = _get_matrix(box)
|
||||||
# TODO: account for CSS transforms
|
if transform:
|
||||||
# matrix *= …
|
matrix = transform * matrix if matrix else transform
|
||||||
_get_metadata(box, bookmarks, links, anchors, matrix)
|
_get_metadata(box, bookmarks, links, anchors, matrix)
|
||||||
for child in box.all_children():
|
for child in box.all_children():
|
||||||
_prepare(child, bookmarks, links, anchors, matrix)
|
_prepare(child, bookmarks, links, anchors, matrix)
|
||||||
|
@ -28,11 +28,12 @@ import pytest
|
|||||||
from .testing_utils import (
|
from .testing_utils import (
|
||||||
resource_filename, assert_no_logs, capture_logs, TestHTML)
|
resource_filename, assert_no_logs, capture_logs, TestHTML)
|
||||||
from .test_draw import png_to_pixels
|
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 ..urls import path2url
|
||||||
from .. import HTML, CSS, default_url_fetcher
|
from .. import HTML, CSS, default_url_fetcher
|
||||||
from .. import __main__
|
from .. import __main__
|
||||||
from .. import navigator
|
from .. import navigator
|
||||||
|
from ..document import _TaggedTuple
|
||||||
|
|
||||||
|
|
||||||
CHDIR_LOCK = threading.Lock()
|
CHDIR_LOCK = threading.Lock()
|
||||||
@ -526,10 +527,35 @@ def test_low_level_api():
|
|||||||
assert png_size(document.copy([page_2]).write_png()) == (6, 4)
|
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
|
@assert_no_logs
|
||||||
def test_bookmarks():
|
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()
|
document = TestHTML(string=html).render()
|
||||||
|
if round:
|
||||||
|
round_meta(document.pages)
|
||||||
assert [p.bookmarks for p in document.pages] == expected_by_page
|
assert [p.bookmarks for p in document.pages] == expected_by_page
|
||||||
assert document.make_bookmark_tree() == expected_tree
|
assert document.make_bookmark_tree() == expected_tree
|
||||||
assert_bookmarks('''
|
assert_bookmarks('''
|
||||||
@ -645,16 +671,30 @@ def test_bookmarks():
|
|||||||
('H', (0, 0, 130), [])])]),
|
('H', (0, 0, 130), [])])]),
|
||||||
('I', (0, 0, 150), []),
|
('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
|
@assert_no_logs
|
||||||
def test_links():
|
def test_links():
|
||||||
def assert_links(html, expected_links_by_page, expected_anchors_by_page,
|
def assert_links(html, expected_links_by_page, expected_anchors_by_page,
|
||||||
expected_resolved_links,
|
expected_resolved_links,
|
||||||
base_url=resource_filename('<inline HTML>'), warnings=()):
|
base_url=resource_filename('<inline HTML>'),
|
||||||
|
warnings=(), round=False):
|
||||||
with capture_logs() as logs:
|
with capture_logs() as logs:
|
||||||
document = TestHTML(string=html, base_url=base_url).render()
|
document = TestHTML(string=html, base_url=base_url).render()
|
||||||
|
if round:
|
||||||
|
round_meta(document.pages)
|
||||||
resolved_links = list(document.resolve_links())
|
resolved_links = list(document.resolve_links())
|
||||||
assert len(logs) == len(warnings)
|
assert len(logs) == len(warnings)
|
||||||
for message, expected in zip(logs, warnings):
|
for message, expected in zip(logs, warnings):
|
||||||
@ -770,6 +810,18 @@ def test_links():
|
|||||||
]], base_url=None, warnings=[
|
]], base_url=None, warnings=[
|
||||||
'WARNING: No anchor #missing for internal URI reference'])
|
'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):
|
def wsgi_client(path_info, qs_args=None):
|
||||||
start_response_calls = []
|
start_response_calls = []
|
||||||
|
@ -1901,6 +1901,27 @@ def test_2d_transform():
|
|||||||
</style>
|
</style>
|
||||||
<div><img src="pattern.png"></div>
|
<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, [
|
assert_pixels('image_reflection', 8, 8, [
|
||||||
_+_+_+_+_+_+_+_,
|
_+_+_+_+_+_+_+_,
|
||||||
|
@ -63,18 +63,14 @@ def get_bookmarks(html, structure_only=False):
|
|||||||
|
|
||||||
def get_links(html, **kwargs):
|
def get_links(html, **kwargs):
|
||||||
_root, _bookmarks, links = get_metadata(html, **kwargs)
|
_root, _bookmarks, links = get_metadata(html, **kwargs)
|
||||||
return [
|
for page_links in links:
|
||||||
[
|
for i, (link_type, target, rectangle) in enumerate(page_links):
|
||||||
(
|
if link_type == 'internal':
|
||||||
type_,
|
page, x, y = target
|
||||||
(target if type_ == 'external' else
|
target = page, round(x, 6), round(y, 6)
|
||||||
(lambda page, x, y: (page, round(x, 6), round(y, 6)))
|
rectangle = tuple(round(v, 6) for v in rectangle)
|
||||||
(*target)),
|
page_links[i] = link_type, target, rectangle
|
||||||
tuple(round(v, 6) for v in rect)
|
return links
|
||||||
)
|
|
||||||
for type_, target, rect in page_links
|
|
||||||
]
|
|
||||||
for page_links in links]
|
|
||||||
|
|
||||||
|
|
||||||
@assert_no_logs
|
@assert_no_logs
|
||||||
|
Loading…
Reference in New Issue
Block a user