mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-10-05 00:21:15 +03:00
Merge branch 'master' into float
This commit is contained in:
commit
037e4b955c
@ -17,7 +17,7 @@ import math
|
||||
|
||||
from .properties import INITIAL_VALUES, Dimension
|
||||
from ..urls import get_url_attribute
|
||||
from ..compat import urlsplit
|
||||
from ..compat import urlsplit, unquote
|
||||
|
||||
|
||||
ZERO_PIXELS = Dimension(0, 'px')
|
||||
@ -435,11 +435,9 @@ def line_height(computer, name, value):
|
||||
@register_computer('anchor')
|
||||
def anchor(computer, name, values):
|
||||
"""Compute the ``anchor`` property."""
|
||||
if values == 'none':
|
||||
return None
|
||||
else:
|
||||
if values != 'none':
|
||||
_, key = values
|
||||
return computer.element.get(key)
|
||||
return computer.element.get(key) or None
|
||||
|
||||
|
||||
@register_computer('link')
|
||||
@ -459,7 +457,7 @@ def link(computer, name, values):
|
||||
parsed = urlsplit(url)
|
||||
# Compare with fragments removed
|
||||
if parsed[:-1] == document_uri[:-1]:
|
||||
return 'internal', parsed.fragment
|
||||
return 'internal', unquote(parsed.fragment)
|
||||
else:
|
||||
return 'external', url
|
||||
|
||||
|
@ -8,8 +8,10 @@ web browsers use.
|
||||
|
||||
*/
|
||||
|
||||
/* http://www.w3.org/TR/html5/Overview#scroll-to-the-fragment-identifier */
|
||||
*[id] { -weasy-anchor: attr(id); }
|
||||
*[name] { -weasy-anchor: attr(name); }
|
||||
a[name] { -weasy-anchor: attr(name); }
|
||||
|
||||
*[dir] { unicode-bidi: embed; }
|
||||
*[hidden] { display: none; }
|
||||
*[dir=ltr] { direction: ltr; }
|
||||
|
@ -20,3 +20,13 @@ col { display: table-column }
|
||||
colgroup { display: table-column-group }
|
||||
td, th { display: table-cell }
|
||||
caption { display: table-caption }
|
||||
|
||||
a[href] { -weasy-link: attr(href); }
|
||||
a[name] { -weasy-anchor: attr(name); }
|
||||
*[id] { -weasy-anchor: attr(id); }
|
||||
h1 { -weasy-bookmark-level: 1; -weasy-bookmark-label: contents; }
|
||||
h2 { -weasy-bookmark-level: 2; -weasy-bookmark-label: contents; }
|
||||
h3 { -weasy-bookmark-level: 3; -weasy-bookmark-label: contents; }
|
||||
h4 { -weasy-bookmark-level: 4; -weasy-bookmark-label: contents; }
|
||||
h5 { -weasy-bookmark-level: 5; -weasy-bookmark-label: contents; }
|
||||
h6 { -weasy-bookmark-level: 6; -weasy-bookmark-label: contents; }
|
||||
|
@ -17,6 +17,7 @@ from .formatting_structure.build import build_formatting_structure
|
||||
from . import layout
|
||||
from . import draw
|
||||
from . import images
|
||||
from .logger import LOGGER
|
||||
|
||||
|
||||
class Document(object):
|
||||
@ -81,19 +82,12 @@ class Document(object):
|
||||
return self._pages
|
||||
|
||||
def get_image_from_uri(self, uri, type_=None):
|
||||
"""
|
||||
Same as ``weasy.images.get_image_from_uri`` but cache results
|
||||
"""
|
||||
missing = object()
|
||||
surface = self._image_cache.get(uri, missing)
|
||||
if surface is missing:
|
||||
surface = images.get_image_from_uri(uri, type_)
|
||||
self._image_cache[uri] = surface
|
||||
return surface
|
||||
return images.get_image_from_uri(self._image_cache, uri, type_)
|
||||
|
||||
def write_to(self, target):
|
||||
backend = self.backend(target)
|
||||
for page in self.pages:
|
||||
context = backend.start_page(page.outer_width, page.outer_height)
|
||||
draw.draw_page(self, page, context)
|
||||
|
||||
backend.finish(self)
|
||||
|
@ -62,6 +62,7 @@ def draw_page(document, page, context):
|
||||
draw_box_background(
|
||||
document, context, stacking_context.page, stacking_context.box)
|
||||
draw_canvas_background(document, context, page)
|
||||
draw_border(context, page)
|
||||
draw_stacking_context(document, context, stacking_context)
|
||||
|
||||
|
||||
@ -556,6 +557,14 @@ def draw_replacedbox(context, box):
|
||||
box.style.image_rendering])
|
||||
context.set_source(pattern)
|
||||
context.paint()
|
||||
# Make sure `pattern` is garbage collected. If a surface for a SVG image
|
||||
# is still alive by the time we call show_page(), cairo will rasterize
|
||||
# the image instead writing vectors.
|
||||
# Use a unique string that can be traced back here.
|
||||
# Replaced boxes are atomic, so they should only ever be drawn once.
|
||||
# Use an object incompatible with the usual 3-tuple to cause an exception
|
||||
# if this box is used more than that.
|
||||
box.replacement = 'Removed to work around cairo’s behavior'
|
||||
|
||||
|
||||
def draw_inline_level(document, context, page, box):
|
||||
|
@ -21,9 +21,13 @@ from .urls import urlopen
|
||||
from .css.computed_values import LENGTHS_TO_PIXELS
|
||||
from .logger import LOGGER
|
||||
|
||||
#import faulthandler
|
||||
#faulthandler.enable()
|
||||
|
||||
# Map MIME types to functions that take a byte stream and return
|
||||
# ``(pattern, width, height)`` a cairo Pattern and its dimension in pixels.
|
||||
|
||||
# Map MIME types to functions that take a byte stream and return a callable
|
||||
# that returns ``(pattern, width, height)`` a cairo Pattern and
|
||||
# its dimension in pixels.
|
||||
FORMAT_HANDLERS = {}
|
||||
|
||||
# TODO: currently CairoSVG only support images with an explicit
|
||||
@ -44,7 +48,8 @@ def png_handler(file_like, _uri):
|
||||
"""Return a cairo Surface from a PNG byte stream."""
|
||||
surface = cairo.ImageSurface.create_from_png(file_like)
|
||||
pattern = cairo.SurfacePattern(surface)
|
||||
return pattern, surface.get_width(), surface.get_height()
|
||||
result = pattern, surface.get_width(), surface.get_height()
|
||||
return lambda: result
|
||||
|
||||
|
||||
@register_format('image/svg+xml')
|
||||
@ -70,11 +75,19 @@ def cairosvg_handler(file_like, uri):
|
||||
# Don’t pass data URIs to CairoSVG.
|
||||
# They are useless for relative URIs anyway.
|
||||
uri = None
|
||||
# Draw to a cairo surface but do not write to a file
|
||||
tree = Tree(file_obj=file_like, url=uri)
|
||||
surface = ScaledSVGSurface(tree, output=None, dpi=96)
|
||||
pattern = cairo.SurfacePattern(surface.cairo)
|
||||
return pattern, surface.width, surface.height
|
||||
bytestring = file_like.read()
|
||||
|
||||
# Do not keep a Surface object alive, but regenerate it as needed.
|
||||
# If a surface for a SVG image is still alive by the time we call
|
||||
# show_page(), cairo will rasterize the image instead writing vectors.
|
||||
def draw_svg():
|
||||
# Draw to a cairo surface but do not write to a file
|
||||
tree = Tree(bytestring=bytestring, url=uri)
|
||||
surface = ScaledSVGSurface(tree, output=None, dpi=96)
|
||||
pattern = cairo.SurfacePattern(surface.cairo)
|
||||
return pattern, surface.width, surface.height
|
||||
|
||||
return draw_svg
|
||||
|
||||
|
||||
def fallback_handler(file_like, uri):
|
||||
@ -90,20 +103,29 @@ def fallback_handler(file_like, uri):
|
||||
return png_handler(BytesIO(png_bytes), uri)
|
||||
|
||||
|
||||
def get_image_from_uri(uri, type_=None):
|
||||
def get_image_from_uri(cache, uri, type_=None):
|
||||
"""Get a :class:`cairo.Surface`` from an image URI."""
|
||||
try:
|
||||
missing = object()
|
||||
function = cache.get(uri, missing)
|
||||
if function is not missing:
|
||||
return function()
|
||||
file_like, mime_type, _charset = urlopen(uri)
|
||||
if not type_:
|
||||
type_ = mime_type # Use eg. the HTTP header
|
||||
#else: the type was forced by eg. a 'type' attribute on <embed>
|
||||
handler = FORMAT_HANDLERS.get(type_, fallback_handler)
|
||||
return handler(file_like, uri)
|
||||
try:
|
||||
if not type_:
|
||||
type_ = mime_type # Use eg. the HTTP header
|
||||
#else: the type was forced by eg. a 'type' attribute on <embed>
|
||||
handler = FORMAT_HANDLERS.get(type_, fallback_handler)
|
||||
function = handler(file_like, uri)
|
||||
finally:
|
||||
try:
|
||||
file_like.close()
|
||||
except Exception:
|
||||
# May already be closed or something.
|
||||
# This is just cleanup anyway.
|
||||
pass
|
||||
|
||||
cache[uri] = function
|
||||
return function()
|
||||
except Exception as exc:
|
||||
LOGGER.warn('Error for image at %s : %r', uri, exc)
|
||||
finally:
|
||||
try:
|
||||
file_like.close()
|
||||
except Exception:
|
||||
# May already be closed or something. This is just cleanup anyway.
|
||||
pass
|
||||
|
@ -117,7 +117,8 @@ def absolute_block(document, box, containing_block):
|
||||
box.margin_left = 0
|
||||
if margin_r == 'auto':
|
||||
box.margin_right = 0
|
||||
available_width = cb_width
|
||||
available_width = cb_width - (
|
||||
padding_plus_borders_x + box.margin_left + box.margin_right)
|
||||
box.width = shrink_to_fit(box, available_width)
|
||||
elif left != 'auto' and right != 'auto' and width != 'auto':
|
||||
width_for_margins = cb_width - (
|
||||
|
@ -21,6 +21,9 @@ from ..text import TextFragment
|
||||
def shrink_to_fit(box, available_width):
|
||||
"""Return the shrink-to-fit width of ``box``.
|
||||
|
||||
*Warning:* both available_outer_width and the return value are
|
||||
for width of the *content area*, not margin area.
|
||||
|
||||
http://www.w3.org/TR/CSS21/visudet.html#float-width
|
||||
|
||||
"""
|
||||
|
@ -3521,18 +3521,19 @@ def test_absolute_positioning():
|
||||
<img src=pattern.png>
|
||||
<span style="position: relative">
|
||||
<span style="position: absolute">2</span>
|
||||
<span>3</span>
|
||||
<span style="position: absolute">3</span>
|
||||
<span>4</span>
|
||||
</span>
|
||||
''')
|
||||
html, = page.children
|
||||
body, = html.children
|
||||
line, = body.children
|
||||
img, span1 = line.children
|
||||
span2, span3 = span1.children
|
||||
span2, span3, span4 = span1.children
|
||||
assert span1.position_x == 4
|
||||
assert span2.position_x == 4
|
||||
assert span3.position_x == 4
|
||||
assert span2.position_y == span3.position_y
|
||||
assert (span2.position_x, span2.position_y) == (4, 0)
|
||||
assert (span3.position_x, span3.position_y) == (4, 0)
|
||||
assert span4.position_x == 4
|
||||
|
||||
page, = parse('''
|
||||
<style> img { width: 5px; height: 20px} </style>
|
||||
|
@ -16,10 +16,9 @@ import io
|
||||
|
||||
import cairo
|
||||
|
||||
from .. import HTML
|
||||
from ..backends import MetadataPDFBackend
|
||||
from .. import HTML, CSS
|
||||
from .. import pdf
|
||||
from .testing_utils import assert_no_logs, resource_filename
|
||||
from .testing_utils import assert_no_logs, resource_filename, TestPDFDocument
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
@ -40,10 +39,10 @@ def test_pdf_parser():
|
||||
assert sizes == [b'0 0 100 100', b'0 0 200 10', b'0 0 3.14 987654321']
|
||||
|
||||
|
||||
def get_metadata(html):
|
||||
document = HTML(
|
||||
string=html, base_url=resource_filename('<inline HTML>')
|
||||
)._get_document(MetadataPDFBackend, [])
|
||||
def get_metadata(html, base_url=resource_filename('<inline HTML>')):
|
||||
document = TestPDFDocument(html, base_url=base_url,
|
||||
user_stylesheets=[CSS(
|
||||
string='@page { -weasy-size: 500pt 1000pt; margin: 50pt }')])
|
||||
return pdf.gather_metadata(document)
|
||||
|
||||
|
||||
@ -61,8 +60,8 @@ def get_bookmarks(html, structure_only=False):
|
||||
return root, bookmarks
|
||||
|
||||
|
||||
def get_links(html):
|
||||
_bookmarks, links, anchors = get_metadata(html)
|
||||
def get_links(html, **kwargs):
|
||||
_bookmarks, links, anchors = get_metadata(html, **kwargs)
|
||||
links = [
|
||||
[(uri, tuple(round(v, 6) for v in rect)) for uri, rect in page_links]
|
||||
for page_links in links]
|
||||
@ -84,12 +83,11 @@ def test_bookmarks():
|
||||
|
||||
root, bookmarks = get_bookmarks('''
|
||||
<style>
|
||||
@page { -weasy-size: 1000pt; margin: 0 }
|
||||
* { height: 90pt; margin: 0 0 10pt 0; page-break-inside: auto }
|
||||
</style>
|
||||
<h1>Title 1</h1>
|
||||
<h1>Title 2</h1>
|
||||
<div style="margin-left: 50pt"><h2>Title 3</h2></div>
|
||||
<div style="margin-left: 20pt"><h2>Title 3</h2></div>
|
||||
<h2>Title 4</h2>
|
||||
<h3>
|
||||
Title 5
|
||||
@ -105,27 +103,27 @@ def test_bookmarks():
|
||||
assert root == dict(Count=11, First=1, Last=10)
|
||||
assert bookmarks == [
|
||||
dict(Count=0, First=None, Last=None, Next=2, Parent=0, Prev=None,
|
||||
label='Title 1', destination=(0, 0, 1000)),
|
||||
label='Title 1', destination=(0, 50, 950)),
|
||||
dict(Count=4, First=3, Last=6, Next=7, Parent=0, Prev=1,
|
||||
label='Title 2', destination=(0, 0, 900)),
|
||||
label='Title 2', destination=(0, 50, 850)),
|
||||
dict(Count=0, First=None, Last=None, Next=4, Parent=2, Prev=None,
|
||||
label='Title 3', destination=(0, 50, 800)),
|
||||
label='Title 3', destination=(0, 70, 750)),
|
||||
dict(Count=1, First=5, Last=5, Next=6, Parent=2, Prev=3,
|
||||
label='Title 4', destination=(0, 0, 700)),
|
||||
label='Title 4', destination=(0, 50, 650)),
|
||||
dict(Count=0, First=None, Last=None, Next=None, Parent=4, Prev=None,
|
||||
label='Title 5', destination=(0, 0, 600)),
|
||||
label='Title 5', destination=(0, 50, 550)),
|
||||
dict(Count=0, First=None, Last=None, Next=None, Parent=2, Prev=4,
|
||||
label='Title 6', destination=(1, 0, 900)),
|
||||
label='Title 6', destination=(1, 50, 850)),
|
||||
dict(Count=2, First=8, Last=8, Next=10, Parent=0, Prev=2,
|
||||
label='Title 7', destination=(1, 0, 800)),
|
||||
label='Title 7', destination=(1, 50, 750)),
|
||||
dict(Count=1, First=9, Last=9, Next=None, Parent=7, Prev=None,
|
||||
label='Title 8', destination=(1, 0, 700)),
|
||||
label='Title 8', destination=(1, 50, 650)),
|
||||
dict(Count=0, First=None, Last=None, Next=None, Parent=8, Prev=None,
|
||||
label='Title 9', destination=(1, 0, 600)),
|
||||
label='Title 9', destination=(1, 50, 550)),
|
||||
dict(Count=1, First=11, Last=11, Next=None, Parent=0, Prev=7,
|
||||
label='Title 10', destination=(1, 0, 500)),
|
||||
label='Title 10', destination=(1, 50, 450)),
|
||||
dict(Count=0, First=None, Last=None, Next=None, Parent=10, Prev=None,
|
||||
label='Title 11', destination=(1, 0, 400))]
|
||||
label='Title 11', destination=(1, 50, 350))]
|
||||
|
||||
root, bookmarks = get_bookmarks('''
|
||||
<h2>1</h2> level 1
|
||||
@ -174,7 +172,6 @@ def test_links():
|
||||
|
||||
links, anchors = get_links('''
|
||||
<style>
|
||||
@page { -weasy-size: 1000pt; margin: 50pt }
|
||||
body { margin: 0; font-size: 10pt; line-height: 2 }
|
||||
p { display: block; height: 90pt; margin: 0 0 10pt 0 }
|
||||
</style>
|
||||
@ -184,7 +181,7 @@ def test_links():
|
||||
src=pattern.png></a></p>
|
||||
<p id=hello>Hello, World</p>
|
||||
<p id=lipsum><a style="display: block; page-break-before: always"
|
||||
href="#hello">Lorem ipsum ...</a></p>
|
||||
href="#hel%6Co">Lorem ipsum ...</a></p>
|
||||
''')
|
||||
assert links == [
|
||||
[
|
||||
@ -193,8 +190,20 @@ def test_links():
|
||||
# 5pt wide (image + 2 * 1pt of border), 20pt high
|
||||
(('internal', 'lipsum'), (60, 850, 65, 830)),
|
||||
], [
|
||||
# 900pt wide (block), 20pt high
|
||||
(('internal', 'hello'), (50, 950, 950, 930)),
|
||||
# 400pt wide (block), 20pt high
|
||||
(('internal', 'hello'), (50, 950, 450, 930)),
|
||||
]
|
||||
]
|
||||
assert anchors == {'hello': (0, 50, 750), 'lipsum': (1, 50, 950)}
|
||||
|
||||
links, anchors = get_links(
|
||||
'<div style="-weasy-link: url(../lipsum)">',
|
||||
base_url='http://weasyprint.org/foo/bar/')
|
||||
assert links == [[(('external', 'http://weasyprint.org/foo/lipsum'),
|
||||
(50, 950, 450, 950))]]
|
||||
|
||||
# Relative URI reference without a base URI.
|
||||
# links, anchors = get_links(
|
||||
# '<div style="-weasy-link: url(../lipsum)">',
|
||||
# base_url=None)
|
||||
# assert links == [[]]
|
||||
|
@ -20,7 +20,7 @@ import functools
|
||||
|
||||
from .. import HTML, CSS
|
||||
from ..document import Document
|
||||
from ..backends import PNGBackend
|
||||
from ..backends import PNGBackend, MetadataPDFBackend
|
||||
from ..logger import LOGGER
|
||||
|
||||
|
||||
@ -33,20 +33,31 @@ TEST_UA_STYLESHEET = CSS(os.path.join(
|
||||
|
||||
|
||||
class TestPNGDocument(Document):
|
||||
"""Like PNGDocument, but with a different user-agent stylesheet.
|
||||
"""A Document with a PNG backend and a different user-agent stylesheet.
|
||||
|
||||
This stylesheet is shorter, which makes tests faster.
|
||||
|
||||
"""
|
||||
backend_class = PNGBackend
|
||||
|
||||
def __init__(self, html_source, base_url=None, user_stylesheets=(),
|
||||
user_agent_stylesheets=(TEST_UA_STYLESHEET,)):
|
||||
super(TestPNGDocument, self).__init__(
|
||||
PNGBackend,
|
||||
self.backend_class,
|
||||
HTML(string=html_source, base_url=base_url).root_element,
|
||||
user_stylesheets=user_stylesheets,
|
||||
user_agent_stylesheets=user_agent_stylesheets)
|
||||
|
||||
|
||||
class TestPDFDocument(TestPNGDocument):
|
||||
"""A Document with a PDF backend and a different user-agent stylesheet.
|
||||
|
||||
This stylesheet is shorter, which makes tests faster.
|
||||
|
||||
"""
|
||||
backend_class = MetadataPDFBackend
|
||||
|
||||
|
||||
def resource_filename(basename):
|
||||
"""Return the absolute path of the resource called ``basename``."""
|
||||
return os.path.join(os.path.dirname(__file__), 'resources', basename)
|
||||
|
@ -130,8 +130,9 @@ def run(suite_directory):
|
||||
from pygments.lexers import HtmlLexer
|
||||
from pygments.formatters import HtmlFormatter
|
||||
|
||||
with open(safe_join(suite_directory, test['test_id'] + '.htm')) as fd:
|
||||
source = fd.read()
|
||||
filename = safe_join(suite_directory, test['test_id'] + '.htm')
|
||||
with open(filename, 'rb') as fd:
|
||||
source = fd.read().decode('utf8')
|
||||
|
||||
formatter = HtmlFormatter()
|
||||
source = highlight(source, HtmlLexer(), formatter)
|
||||
|
Loading…
Reference in New Issue
Block a user