diff --git a/CHANGES b/CHANGES index 062c8bed..8611e17d 100644 --- a/CHANGES +++ b/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 diff --git a/weasyprint/document.py b/weasyprint/document.py index 40a87379..f8bcfb56 100644 --- a/weasyprint/document.py +++ b/weasyprint/document.py @@ -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) diff --git a/weasyprint/tests/test_api.py b/weasyprint/tests/test_api.py index 88ec722b..4ffc5cf7 100644 --- a/weasyprint/tests/test_api.py +++ b/weasyprint/tests/test_api.py @@ -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('

é', [[(1, 'é', (0, 0))]], [('é', (0, 0, 0), [])]) + assert_bookmarks(''' +

! + ''', [[(1, '!', (50, 0))]], [('!', (0, 50, 0), [])]) + assert_bookmarks(''' +

! + ''', [[(1, '!', (0, 50))]], [('!', (0, 0, 50), [])], round=True) + assert_bookmarks(''' + +

! + ''', [[(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(''), warnings=()): + base_url=resource_filename(''), + 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(''' + + + ''', [[ + ('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 = [] diff --git a/weasyprint/tests/test_draw.py b/weasyprint/tests/test_draw.py index df5ace6d..31960b72 100644 --- a/weasyprint/tests/test_draw.py +++ b/weasyprint/tests/test_draw.py @@ -1901,6 +1901,27 @@ def test_2d_transform():
''') + assert_pixels('nested_rotate90_translateX', 12, 12, [ + _+_+_+_+_+_+_+_+_+_+_+_, + _+_+_+_+_+_+_+_+_+_+_+_, + _+_+_+_+_+_+_+_+_+_+_+_, + _+_+_+_+_+_+_+_+_+_+_+_, + _+_+_+_+_+_+_+_+_+_+_+_, + _+_+B+B+B+r+_+_+_+_+_+_, + _+_+B+B+B+B+_+_+_+_+_+_, + _+_+B+B+B+B+_+_+_+_+_+_, + _+_+B+B+B+B+_+_+_+_+_+_, + _+_+_+_+_+_+_+_+_+_+_+_, + _+_+_+_+_+_+_+_+_+_+_+_, + _+_+_+_+_+_+_+_+_+_+_+_, + ], ''' + +
+ ''') assert_pixels('image_reflection', 8, 8, [ _+_+_+_+_+_+_+_, diff --git a/weasyprint/tests/test_pdf.py b/weasyprint/tests/test_pdf.py index 835cf41a..aa020e4e 100644 --- a/weasyprint/tests/test_pdf.py +++ b/weasyprint/tests/test_pdf.py @@ -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