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('