mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-10-05 16:37:47 +03:00
Merge pull request #352 from Kozea/presentationalhints
Support presentational hints
This commit is contained in:
commit
3fc6e3334a
@ -33,8 +33,11 @@ Some elements need special treatment:
|
||||
or in SVG with CairoSVG_. SVG images are not rasterized but rendered
|
||||
as vectors in the PDF output.
|
||||
|
||||
HTML `presentational hints`_ (like the ``width`` attribute on an ``img``
|
||||
element) are **not** supported. Use CSS in the ``style`` attribute instead.
|
||||
HTML `presentational hints`_ are not supported by default, but can be supported:
|
||||
|
||||
* by using the ``--presentational-hints`` CLI parameter, or
|
||||
* by setting the ``presentational_hints`` parameter of the ``HTML.render`` or
|
||||
``HTML.write_*`` methods to ``True``.
|
||||
|
||||
.. _CairoSVG: http://cairosvg.org/
|
||||
.. _GdkPixbuf: https://live.gnome.org/GdkPixbuf
|
||||
|
@ -107,10 +107,14 @@ class HTML(object):
|
||||
def _ua_stylesheets(self):
|
||||
return [HTML5_UA_STYLESHEET]
|
||||
|
||||
def _ph_stylesheets(self):
|
||||
return [HTML5_PH_STYLESHEET]
|
||||
|
||||
def _get_metadata(self):
|
||||
return get_html_metadata(self.root_element)
|
||||
|
||||
def render(self, stylesheets=None, enable_hinting=False):
|
||||
def render(self, stylesheets=None, enable_hinting=False,
|
||||
presentational_hints=False):
|
||||
"""Lay out and paginate the document, but do not (yet) export it
|
||||
to PDF or another format.
|
||||
|
||||
@ -129,13 +133,17 @@ class HTML(object):
|
||||
Whether text, borders and background should be *hinted* to fall
|
||||
at device pixel boundaries. Should be enabled for pixel-based
|
||||
output (like PNG) but not vector based output (like PDF).
|
||||
:type presentational_hints: bool
|
||||
:param presentational_hints: Whether HTML presentational hints are
|
||||
followed.
|
||||
:returns: A :class:`~document.Document` object.
|
||||
|
||||
"""
|
||||
return Document._render(self, stylesheets, enable_hinting)
|
||||
return Document._render(
|
||||
self, stylesheets, enable_hinting, presentational_hints)
|
||||
|
||||
def write_pdf(self, target=None, stylesheets=None, zoom=1,
|
||||
attachments=None):
|
||||
attachments=None, presentational_hints=False):
|
||||
"""Render the document to a PDF file.
|
||||
|
||||
This is a shortcut for calling :meth:`render`, then
|
||||
@ -158,21 +166,28 @@ class HTML(object):
|
||||
:param attachments: A list of additional file attachments for the
|
||||
generated PDF document or :obj:`None`. The list's elements are
|
||||
:class:`Attachment` objects, filenames, URLs or file-like objects.
|
||||
:type presentational_hints: bool
|
||||
:param presentational_hints: Whether HTML presentational hints are
|
||||
followed.
|
||||
:returns:
|
||||
The PDF as byte string if :obj:`target` is not provided or
|
||||
:obj:`None`, otherwise :obj:`None` (the PDF is written to
|
||||
:obj:`target`.)
|
||||
|
||||
"""
|
||||
return self.render(stylesheets).write_pdf(target, zoom, attachments)
|
||||
return self.render(stylesheets, presentational_hints).write_pdf(
|
||||
target, zoom, attachments)
|
||||
|
||||
def write_image_surface(self, stylesheets=None, resolution=96):
|
||||
def write_image_surface(self, stylesheets=None, resolution=96,
|
||||
presentational_hints=False):
|
||||
surface, _width, _height = (
|
||||
self.render(stylesheets, enable_hinting=True)
|
||||
self.render(stylesheets, enable_hinting=True,
|
||||
presentational_hints=presentational_hints)
|
||||
.write_image_surface(resolution))
|
||||
return surface
|
||||
|
||||
def write_png(self, target=None, stylesheets=None, resolution=96):
|
||||
def write_png(self, target=None, stylesheets=None, resolution=96,
|
||||
presentational_hints=False):
|
||||
"""Paint the pages vertically to a single PNG image.
|
||||
|
||||
There is no decoration around pages other than those specified in CSS
|
||||
@ -192,6 +207,9 @@ class HTML(object):
|
||||
:param resolution:
|
||||
The output resolution in PNG pixels per CSS inch. At 96 dpi
|
||||
(the default), PNG pixels match the CSS ``px`` unit.
|
||||
:type presentational_hints: bool
|
||||
:param presentational_hints: Whether HTML presentational hints are
|
||||
followed.
|
||||
:returns:
|
||||
The image as byte string if :obj:`target` is not provided or
|
||||
:obj:`None`, otherwise :obj:`None` (the image is written to
|
||||
@ -199,7 +217,8 @@ class HTML(object):
|
||||
|
||||
"""
|
||||
png_bytes, _width, _height = (
|
||||
self.render(stylesheets, enable_hinting=True)
|
||||
self.render(stylesheets, enable_hinting=True,
|
||||
presentational_hints=presentational_hints)
|
||||
.write_png(target, resolution))
|
||||
return png_bytes
|
||||
|
||||
@ -336,5 +355,7 @@ def _select_source(guess=None, filename=None, url=None, file_obj=None,
|
||||
|
||||
# Work around circular imports.
|
||||
from .css import PARSER, preprocess_stylesheet # noqa
|
||||
from .html import find_base_url, HTML5_UA_STYLESHEET, get_html_metadata # noqa
|
||||
from .html import (
|
||||
find_base_url, HTML5_UA_STYLESHEET, HTML5_PH_STYLESHEET,
|
||||
get_html_metadata) # noqa
|
||||
from .document import Document, Page # noqa
|
||||
|
@ -66,6 +66,10 @@ def main(argv=None, stdout=None, stdin=None):
|
||||
Adds an attachment to the document which is included in the PDF output.
|
||||
This option can be added multiple times to attach more files.
|
||||
|
||||
.. option:: -p, --presentational-hints
|
||||
|
||||
Follow HTML presentational hints.
|
||||
|
||||
.. option:: --version
|
||||
|
||||
Show the version number. Other options and arguments are ignored.
|
||||
@ -100,6 +104,8 @@ def main(argv=None, stdout=None, stdin=None):
|
||||
parser.add_argument('-a', '--attachment', action='append',
|
||||
help='URL or filename of a file '
|
||||
'to attach to the PDF document')
|
||||
parser.add_argument('-p', '--presentational-hints', action='store_true',
|
||||
help='Follow HTML presentational hints.')
|
||||
parser.add_argument(
|
||||
'input', help='URL or filename of the HTML input, or - for stdin')
|
||||
parser.add_argument(
|
||||
@ -138,7 +144,9 @@ def main(argv=None, stdout=None, stdin=None):
|
||||
else:
|
||||
output = args.output
|
||||
|
||||
kwargs = {'stylesheets': args.stylesheet}
|
||||
kwargs = {
|
||||
'stylesheets': args.stylesheet,
|
||||
'presentational_hints': args.presentational_hints}
|
||||
if args.resolution:
|
||||
if format_ == 'png':
|
||||
kwargs['resolution'] = args.resolution
|
||||
|
@ -202,19 +202,271 @@ def find_stylesheets(element_tree, device_media_type, url_fetcher):
|
||||
href, exc)
|
||||
|
||||
|
||||
def find_style_attributes(element_tree):
|
||||
"""
|
||||
Yield ``element, declaration, base_url`` for elements with
|
||||
a "style" attribute.
|
||||
def check_style_attribute(parser, element, style_attribute):
|
||||
declarations, errors = parser.parse_style_attr(style_attribute)
|
||||
for error in errors:
|
||||
LOGGER.warning(error)
|
||||
return element, declarations, element_base_url(element)
|
||||
|
||||
|
||||
def find_style_attributes(element_tree, presentational_hints=False):
|
||||
"""Yield ``specificity, (element, declaration, base_url)`` rules.
|
||||
|
||||
Rules from "style" attribute are returned with specificity
|
||||
``(1, 0, 0, 0)``.
|
||||
|
||||
If ``presentational_hints`` is ``True``, rules from presentational hints
|
||||
are returned with specificity ``(0, 0, 0, 0)``.
|
||||
|
||||
"""
|
||||
parser = PARSER
|
||||
for element in element_tree.iter():
|
||||
specificity = (1, 0, 0, 0)
|
||||
style_attribute = element.get('style')
|
||||
if style_attribute:
|
||||
declarations, errors = parser.parse_style_attr(style_attribute)
|
||||
for error in errors:
|
||||
LOGGER.warning(error)
|
||||
yield element, declarations, element_base_url(element)
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if not presentational_hints:
|
||||
continue
|
||||
specificity = (0, 0, 0, 0)
|
||||
if element.tag == 'body':
|
||||
# TODO: we should check the container frame element
|
||||
for part, position in (
|
||||
('height', 'top'), ('height', 'bottom'),
|
||||
('width', 'left'), ('width', 'right')):
|
||||
style_attribute = None
|
||||
for prop in ('margin%s' % part, '%smargin' % position):
|
||||
if element.get(prop):
|
||||
style_attribute = 'margin-%s:%spx' % (
|
||||
position, element.get(prop))
|
||||
break
|
||||
if style_attribute:
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('background'):
|
||||
style_attribute = 'background-image:url(%s)' % (
|
||||
element.get('background'))
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('bgcolor'):
|
||||
style_attribute = 'background-color:%s' % (
|
||||
element.get('bgcolor'))
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('text'):
|
||||
style_attribute = 'color:%s' % element.get('text')
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
# TODO: we should support link, vlink, alink
|
||||
elif element.tag == 'center':
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'text-align:center')
|
||||
elif element.tag == 'div':
|
||||
align = element.get('align', '').lower()
|
||||
if align == 'middle':
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'text-align:center')
|
||||
elif align in ('center', 'left', 'right', 'justify'):
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'text-align:%s' % align)
|
||||
elif element.tag == 'font':
|
||||
if element.get('color'):
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'color:%s' % element.get('color'))
|
||||
if element.get('face'):
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'font-family:%s' % element.get('face'))
|
||||
if element.get('size'):
|
||||
size = element.get('size').strip()
|
||||
relative_plus = size.startswith('+')
|
||||
relative_minus = size.startswith('-')
|
||||
if relative_plus or relative_minus:
|
||||
size = size[1:].strip()
|
||||
try:
|
||||
size = int(size)
|
||||
except ValueError:
|
||||
LOGGER.warning('Invalid value for size: %s' % size)
|
||||
else:
|
||||
font_sizes = {
|
||||
1: 'x-small',
|
||||
2: 'small',
|
||||
3: 'medium',
|
||||
4: 'large',
|
||||
5: 'x-large',
|
||||
6: 'xx-large',
|
||||
7: '48px', # 1.5 * xx-large
|
||||
}
|
||||
if relative_plus:
|
||||
size += 3
|
||||
elif relative_minus:
|
||||
size -= 3
|
||||
size = max(1, min(7, size))
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'font-size:%s' % font_sizes[size])
|
||||
elif element.tag == 'table':
|
||||
# TODO: we should support cellpadding
|
||||
if element.get('cellspacing'):
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element,
|
||||
'border-spacing:%spx' % element.get('cellspacing'))
|
||||
if element.get('hspace'):
|
||||
hspace = element.get('hspace')
|
||||
if hspace.isdigit():
|
||||
hspace += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element,
|
||||
'margin-left:%s;margin-right:%s' % (hspace, hspace))
|
||||
if element.get('vspace'):
|
||||
vspace = element.get('vspace')
|
||||
if vspace.isdigit():
|
||||
vspace += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element,
|
||||
'margin-top:%s;margin-bottom:%s' % (vspace, vspace))
|
||||
if element.get('width'):
|
||||
style_attribute = 'width:%s' % element.get('width')
|
||||
if element.get('width').isdigit():
|
||||
style_attribute += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('height'):
|
||||
style_attribute = 'height:%s' % element.get('height')
|
||||
if element.get('height').isdigit():
|
||||
style_attribute += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('background'):
|
||||
style_attribute = 'background-image:url(%s)' % (
|
||||
element.get('background'))
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('bgcolor'):
|
||||
style_attribute = 'background-color:%s' % (
|
||||
element.get('bgcolor'))
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('bordercolor'):
|
||||
style_attribute = 'border-color:%s' % (
|
||||
element.get('bordercolor'))
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('border'):
|
||||
style_attribute = 'border-width:%spx' % (
|
||||
element.get('border'))
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
elif element.tag in ('tr', 'td', 'th', 'thead', 'tbody', 'tfoot'):
|
||||
align = element.get('align', '').lower()
|
||||
if align in ('left', 'right', 'justify'):
|
||||
# TODO: we should align descendants too
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'text-align:%s' % align)
|
||||
if element.get('background'):
|
||||
style_attribute = 'background-image:url(%s)' % (
|
||||
element.get('background'))
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('bgcolor'):
|
||||
style_attribute = 'background-color:%s' % (
|
||||
element.get('bgcolor'))
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.tag in ('tr', 'td', 'th'):
|
||||
if element.get('height'):
|
||||
style_attribute = 'height:%s' % element.get('height')
|
||||
if element.get('height').isdigit():
|
||||
style_attribute += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.tag in ('td', 'th'):
|
||||
if element.get('width'):
|
||||
style_attribute = 'width:%s' % element.get('width')
|
||||
if element.get('width').isdigit():
|
||||
style_attribute += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
elif element.tag == 'caption':
|
||||
align = element.get('align', '').lower()
|
||||
# TODO: we should align descendants too
|
||||
if align in ('left', 'right', 'justify'):
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'text-align:%s' % align)
|
||||
elif element.tag == 'col':
|
||||
if element.get('width'):
|
||||
style_attribute = 'width:%s' % element.get('width')
|
||||
if element.get('width').isdigit():
|
||||
style_attribute += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
elif element.tag == 'hr':
|
||||
size = 0
|
||||
if element.get('size'):
|
||||
try:
|
||||
size = int(element.get('size'))
|
||||
except ValueError:
|
||||
LOGGER.warning('Invalid value for size: %s' % size)
|
||||
if (element.get('color'), element.get('noshade')) != (None, None):
|
||||
if size >= 1:
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'border-width:%spx' % (size / 2))
|
||||
elif size == 1:
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'border-bottom-width:0')
|
||||
elif size > 1:
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'height:%spx' % (size - 2))
|
||||
if element.get('width'):
|
||||
style_attribute = 'width:%s' % element.get('width')
|
||||
if element.get('width').isdigit():
|
||||
style_attribute += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('color'):
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'color:%s' % element.get('color'))
|
||||
elif element.tag in (
|
||||
'iframe', 'applet', 'embed', 'img', 'input', 'object'):
|
||||
if (element.tag != 'input' or
|
||||
element.get('type', '').lower() == 'image'):
|
||||
align = element.get('align', '').lower()
|
||||
if align in ('middle', 'center'):
|
||||
# TODO: middle and center values are wrong
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, 'vertical-align:middle')
|
||||
if element.get('hspace'):
|
||||
hspace = element.get('hspace')
|
||||
if hspace.isdigit():
|
||||
hspace += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element,
|
||||
'margin-left:%s;margin-right:%s' % (hspace, hspace))
|
||||
if element.get('vspace'):
|
||||
vspace = element.get('vspace')
|
||||
if vspace.isdigit():
|
||||
vspace += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element,
|
||||
'margin-top:%s;margin-bottom:%s' % (vspace, vspace))
|
||||
# TODO: img seems to be excluded for width and height, but a
|
||||
# lot of W3C tests rely on this attribute being applied to img
|
||||
if element.get('width'):
|
||||
style_attribute = 'width:%s' % element.get('width')
|
||||
if element.get('width').isdigit():
|
||||
style_attribute += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.get('height'):
|
||||
style_attribute = 'height:%s' % element.get('height')
|
||||
if element.get('height').isdigit():
|
||||
style_attribute += 'px'
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element, style_attribute)
|
||||
if element.tag in ('img', 'object', 'input'):
|
||||
if element.get('border'):
|
||||
yield specificity, check_style_attribute(
|
||||
parser, element,
|
||||
'border-width:%spx;border-style:solid' %
|
||||
element.get('border'))
|
||||
|
||||
|
||||
def evaluate_media_query(query_list, device_media_type):
|
||||
@ -435,9 +687,9 @@ def preprocess_stylesheet(device_media_type, base_url, rules, url_fetcher):
|
||||
yield margin_rule, selector_list, declarations
|
||||
|
||||
|
||||
def get_all_computed_styles(html, user_stylesheets=None):
|
||||
"""Compute all the computed styles of all elements
|
||||
in the given ``html`` document.
|
||||
def get_all_computed_styles(html, user_stylesheets=None,
|
||||
presentational_hints=False):
|
||||
"""Compute all the computed styles of all elements in ``html`` document.
|
||||
|
||||
Do everything from finding author stylesheets to parsing and applying them.
|
||||
|
||||
@ -451,6 +703,10 @@ def get_all_computed_styles(html, user_stylesheets=None):
|
||||
ua_stylesheets = html._ua_stylesheets()
|
||||
author_stylesheets = list(find_stylesheets(
|
||||
element_tree, device_media_type, url_fetcher))
|
||||
if presentational_hints:
|
||||
ph_stylesheets = html._ph_stylesheets()
|
||||
else:
|
||||
ph_stylesheets = []
|
||||
|
||||
# keys: (element, pseudo_element_type)
|
||||
# element: a lxml element object or the '@page' string for @page styles
|
||||
@ -464,17 +720,18 @@ def get_all_computed_styles(html, user_stylesheets=None):
|
||||
# http://www.w3.org/TR/CSS21/cascade.html#cascading-order
|
||||
cascaded_styles = {}
|
||||
|
||||
for sheets, origin in (
|
||||
for sheets, origin, sheet_specificity in (
|
||||
# Order here is not important ('origin' is).
|
||||
# Use this order for a regression test
|
||||
(ua_stylesheets or [], 'user agent'),
|
||||
(author_stylesheets, 'author'),
|
||||
(user_stylesheets or [], 'user'),
|
||||
(ua_stylesheets or [], 'user agent', None),
|
||||
(ph_stylesheets, 'author', (0, 0, 0, 0)),
|
||||
(author_stylesheets, 'author', None),
|
||||
(user_stylesheets or [], 'user', None),
|
||||
):
|
||||
for sheet in sheets:
|
||||
for _rule, selector_list, declarations in sheet.rules:
|
||||
for selector in selector_list:
|
||||
specificity = selector.specificity
|
||||
specificity = sheet_specificity or selector.specificity
|
||||
pseudo_type = selector.pseudo_element
|
||||
for element in selector.match(element_tree):
|
||||
for name, values, importance in declarations:
|
||||
@ -485,8 +742,9 @@ def get_all_computed_styles(html, user_stylesheets=None):
|
||||
cascaded_styles, name, values, weight,
|
||||
element, pseudo_type)
|
||||
|
||||
specificity = (1, 0, 0, 0)
|
||||
for element, declarations, base_url in find_style_attributes(element_tree):
|
||||
for specificity, attributes in find_style_attributes(
|
||||
element_tree, presentational_hints):
|
||||
element, declarations, base_url = attributes
|
||||
for name, values, importance in preprocess_declarations(
|
||||
base_url, declarations):
|
||||
precedence = declaration_precedence('author', importance)
|
||||
|
193
weasyprint/css/html5_ph.css
Normal file
193
weasyprint/css/html5_ph.css
Normal file
@ -0,0 +1,193 @@
|
||||
/*
|
||||
|
||||
Presentational hints stylsheet for HTML.
|
||||
|
||||
This stylesheet contains all the presentational hints rules that can be
|
||||
expressed as CSS.
|
||||
|
||||
See https://www.w3.org/TR/html5/rendering.html#rendering
|
||||
|
||||
TODO: Attribute values are not case-insensitive, but they should be. We can add
|
||||
a "i" flag when CSS Selectors Level 4 is supported.
|
||||
|
||||
*/
|
||||
|
||||
pre[wrap] { white-space: pre-wrap; }
|
||||
|
||||
br[clear=left] { clear: left; }
|
||||
br[clear=right] { clear: right; }
|
||||
br[clear=all], br[clear=both] { clear: both; }
|
||||
|
||||
ol[type=1], li[type=1] { list-style-type: decimal; }
|
||||
ol[type=a], li[type=a] { list-style-type: lower-alpha; }
|
||||
ol[type=A], li[type=A] { list-style-type: upper-alpha; }
|
||||
ol[type=i], li[type=i] { list-style-type: lower-roman; }
|
||||
ol[type=I], li[type=I] { list-style-type: upper-roman; }
|
||||
ul[type=disc], li[type=disc] { list-style-type: disc; }
|
||||
ul[type=circle], li[type=circle] { list-style-type: circle; }
|
||||
ul[type=square], li[type=square] { list-style-type: square; }
|
||||
|
||||
table[align=left] { float: left; }
|
||||
table[align=right] { float: right; }
|
||||
table[align=center] { margin-left: auto; margin-right: auto; }
|
||||
thead[align=absmiddle], tbody[align=absmiddle], tfoot[align=absmiddle],
|
||||
tr[align=absmiddle], td[align=absmiddle], th[align=absmiddle] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
caption[align=bottom] { caption-side: bottom; }
|
||||
p[align=left], h1[align=left], h2[align=left], h3[align=left],
|
||||
h4[align=left], h5[align=left], h6[align=left] {
|
||||
text-align: left;
|
||||
}
|
||||
p[align=right], h1[align=right], h2[align=right], h3[align=right],
|
||||
h4[align=right], h5[align=right], h6[align=right] {
|
||||
text-align: right;
|
||||
}
|
||||
p[align=center], h1[align=center], h2[align=center], h3[align=center],
|
||||
h4[align=center], h5[align=center], h6[align=center] {
|
||||
text-align: center;
|
||||
}
|
||||
p[align=justify], h1[align=justify], h2[align=justify], h3[align=justify],
|
||||
h4[align=justify], h5[align=justify], h6[align=justify] {
|
||||
text-align: justify;
|
||||
}
|
||||
thead[valign=top], tbody[valign=top], tfoot[valign=top],
|
||||
tr[valign=top], td[valign=top], th[valign=top] {
|
||||
vertical-align: top;
|
||||
}
|
||||
thead[valign=middle], tbody[valign=middle], tfoot[valign=middle],
|
||||
tr[valign=middle], td[valign=middle], th[valign=middle] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
thead[valign=bottom], tbody[valign=bottom], tfoot[valign=bottom],
|
||||
tr[valign=bottom], td[valign=bottom], th[valign=bottom] {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
thead[valign=baseline], tbody[valign=baseline], tfoot[valign=baseline],
|
||||
tr[valign=baseline], td[valign=baseline], th[valign=baseline] {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
td[nowrap], th[nowrap] { white-space: nowrap; }
|
||||
|
||||
table[rules=none], table[rules=groups], table[rules=rows],
|
||||
table[rules=cols], table[rules=all] {
|
||||
border-style: hidden;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table[border] { border-style: outset; } /* only if border is not equivalent to zero */
|
||||
table[frame=void] { border-style: hidden; }
|
||||
table[frame=above] { border-style: outset hidden hidden hidden; }
|
||||
table[frame=below] { border-style: hidden hidden outset hidden; }
|
||||
table[frame=hsides] { border-style: outset hidden outset hidden; }
|
||||
table[frame=lhs] { border-style: hidden hidden hidden outset; }
|
||||
table[frame=rhs] { border-style: hidden outset hidden hidden; }
|
||||
table[frame=vsides] { border-style: hidden outset; }
|
||||
table[frame=box], table[frame=border] { border-style: outset; }
|
||||
|
||||
table[border] > tr > td, table[border] > tr > th,
|
||||
table[border] > thead > tr > td, table[border] > thead > tr > th,
|
||||
table[border] > tbody > tr > td, table[border] > tbody > tr > th,
|
||||
table[border] > tfoot > tr > td, table[border] > tfoot > tr > th {
|
||||
/* only if border is not equivalent to zero */
|
||||
border-width: 1px;
|
||||
border-style: inset;
|
||||
}
|
||||
table[rules=none] > tr > td, table[rules=none] > tr > th,
|
||||
table[rules=none] > thead > tr > td, table[rules=none] > thead > tr > th,
|
||||
table[rules=none] > tbody > tr > td, table[rules=none] > tbody > tr > th,
|
||||
table[rules=none] > tfoot > tr > td, table[rules=none] > tfoot > tr > th,
|
||||
table[rules=groups] > tr > td, table[rules=groups] > tr > th,
|
||||
table[rules=groups] > thead > tr > td, table[rules=groups] > thead > tr > th,
|
||||
table[rules=groups] > tbody > tr > td, table[rules=groups] > tbody > tr > th,
|
||||
table[rules=groups] > tfoot > tr > td, table[rules=groups] > tfoot > tr > th,
|
||||
table[rules=rows] > tr > td, table[rules=rows] > tr > th,
|
||||
table[rules=rows] > thead > tr > td, table[rules=rows] > thead > tr > th,
|
||||
table[rules=rows] > tbody > tr > td, table[rules=rows] > tbody > tr > th,
|
||||
table[rules=rows] > tfoot > tr > td, table[rules=rows] > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: none;
|
||||
}
|
||||
table[rules=cols] > tr > td, table[rules=cols] > tr > th,
|
||||
table[rules=cols] > thead > tr > td, table[rules=cols] > thead > tr > th,
|
||||
table[rules=cols] > tbody > tr > td, table[rules=cols] > tbody > tr > th,
|
||||
table[rules=cols] > tfoot > tr > td, table[rules=cols] > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: none solid;
|
||||
}
|
||||
table[rules=all] > tr > td, table[rules=all] > tr > th,
|
||||
table[rules=all] > thead > tr > td, table[rules=all] > thead > tr > th,
|
||||
table[rules=all] > tbody > tr > td, table[rules=all] > tbody > tr > th,
|
||||
table[rules=all] > tfoot > tr > td, table[rules=all] > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
table[rules=groups] > colgroup {
|
||||
border-left-width: 1px;
|
||||
border-left-style: solid;
|
||||
border-right-width: 1px;
|
||||
border-right-style: solid;
|
||||
}
|
||||
table[rules=groups] > thead,
|
||||
table[rules=groups] > tbody,
|
||||
table[rules=groups] > tfoot {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
table[rules=rows] > tr, table[rules=rows] > thead > tr,
|
||||
table[rules=rows] > tbody > tr, table[rules=rows] > tfoot > tr {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
hr[align=left] { margin-left: 0; margin-right: auto; }
|
||||
hr[align=right] { margin-left: auto; margin-right: 0; }
|
||||
hr[align=center] { margin-left: auto; margin-right: auto; }
|
||||
hr[color], hr[noshade] { border-style: solid; }
|
||||
|
||||
iframe[frameborder=0], iframe[frameborder=no] { border: none; }
|
||||
|
||||
applet[align=left], embed[align=left], iframe[align=left],
|
||||
img[align=left], input[type=image][align=left], object[align=left] {
|
||||
float: left;
|
||||
}
|
||||
|
||||
applet[align=right], embed[align=right], iframe[align=right],
|
||||
img[align=right], input[type=image][align=right], object[align=right] {
|
||||
float: right;
|
||||
}
|
||||
|
||||
applet[align=top], embed[align=top], iframe[align=top],
|
||||
img[align=top], input[type=image][align=top], object[align=top] {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
applet[align=baseline], embed[align=baseline], iframe[align=baseline],
|
||||
img[align=baseline], input[type=image][align=baseline], object[align=baseline] {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
applet[align=texttop], embed[align=texttop], iframe[align=texttop],
|
||||
img[align=texttop], input[type=image][align=texttop], object[align=texttop] {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
applet[align=absmiddle], embed[align=absmiddle], iframe[align=absmiddle],
|
||||
img[align=absmiddle], input[type=image][align=absmiddle], object[align=absmiddle],
|
||||
applet[align=abscenter], embed[align=abscenter], iframe[align=abscenter],
|
||||
img[align=abscenter], input[type=image][align=abscenter], object[align=abscenter] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
applet[align=bottom], embed[align=bottom], iframe[align=bottom],
|
||||
img[align=bottom], input[type=image][align=bottom],
|
||||
object[align=bottom] {
|
||||
vertical-align: bottom;
|
||||
}
|
@ -313,11 +313,13 @@ class Document(object):
|
||||
|
||||
"""
|
||||
@classmethod
|
||||
def _render(cls, html, stylesheets, enable_hinting):
|
||||
style_for = get_all_computed_styles(html, user_stylesheets=[
|
||||
css if hasattr(css, 'rules')
|
||||
else CSS(guess=css, media_type=html.media_type)
|
||||
for css in stylesheets or []])
|
||||
def _render(cls, html, stylesheets, enable_hinting,
|
||||
presentational_hints=False):
|
||||
style_for = get_all_computed_styles(
|
||||
html, presentational_hints=presentational_hints, user_stylesheets=[
|
||||
css if hasattr(css, 'rules')
|
||||
else CSS(guess=css, media_type=html.media_type)
|
||||
for css in stylesheets or []])
|
||||
get_image_from_uri = functools.partial(
|
||||
images.get_image_from_uri, {}, html.url_fetcher)
|
||||
page_boxes = layout_document(
|
||||
|
@ -39,6 +39,7 @@ if hasattr(sys, "frozen"):
|
||||
else:
|
||||
root = os.path.dirname(__file__)
|
||||
HTML5_UA_STYLESHEET = CSS(filename=os.path.join(root, 'css', 'html5_ua.css'))
|
||||
HTML5_PH_STYLESHEET = CSS(filename=os.path.join(root, 'css', 'html5_ph.css'))
|
||||
|
||||
LOGGER.setLevel(level)
|
||||
|
||||
|
288
weasyprint/tests/test_presentational_hints.py
Normal file
288
weasyprint/tests/test_presentational_hints.py
Normal file
@ -0,0 +1,288 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
weasyprint.tests.test_presentational_hints
|
||||
------------------------------------------
|
||||
|
||||
Test the HTML presentational hints.
|
||||
|
||||
:copyright: Copyright 2016 Simon Sapin and contributors, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import division, unicode_literals
|
||||
|
||||
from .testing_utils import assert_no_logs
|
||||
from .. import HTML, CSS
|
||||
|
||||
PH_TESTING_CSS = CSS(string='''
|
||||
@page {margin: 0; size: 1000px 1000px}
|
||||
body {margin: 0}
|
||||
''')
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_no_ph():
|
||||
"""Check that presentational hints are not applied when not requested."""
|
||||
# Test both CSS and non-CSS rules
|
||||
document = HTML(string='''
|
||||
<hr size=100 />
|
||||
<table align=right width=100><td>0</td></table>
|
||||
''').render(stylesheets=[PH_TESTING_CSS])
|
||||
page, = document.pages
|
||||
html, = page._page_box.children
|
||||
body, = html.children
|
||||
hr, table = body.children
|
||||
assert hr.border_height() != 100
|
||||
assert table.position_x == 0
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_ph_page():
|
||||
"""Check presentational hints on the page."""
|
||||
document = HTML(string='''
|
||||
<body marginheight=2 topmargin=3 leftmargin=5
|
||||
bgcolor=red text=blue />
|
||||
''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)
|
||||
page, = document.pages
|
||||
html, = page._page_box.children
|
||||
body, = html.children
|
||||
assert body.margin_top == 2
|
||||
assert body.margin_bottom == 2
|
||||
assert body.margin_left == 5
|
||||
assert body.margin_right == 0
|
||||
assert body.style.background_color == (1, 0, 0, 1)
|
||||
assert body.style.color == (0, 0, 1, 1)
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_ph_flow():
|
||||
"""Check presentational hints on the flow content."""
|
||||
document = HTML(string='''
|
||||
<pre wrap></pre>
|
||||
<center></center>
|
||||
<div align=center></div>
|
||||
<div align=middle></div>
|
||||
<div align=left></div>
|
||||
<div align=right></div>
|
||||
<div align=justify></div>
|
||||
''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)
|
||||
page, = document.pages
|
||||
html, = page._page_box.children
|
||||
body, = html.children
|
||||
pre, center, div1, div2, div3, div4, div5 = body.children
|
||||
assert pre.style.white_space == 'pre-wrap'
|
||||
assert center.style.text_align == 'center'
|
||||
assert div1.style.text_align == 'center'
|
||||
assert div2.style.text_align == 'center'
|
||||
assert div3.style.text_align == 'left'
|
||||
assert div4.style.text_align == 'right'
|
||||
assert div5.style.text_align == 'justify'
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_ph_phrasing():
|
||||
"""Check presentational hints on the phrasing content."""
|
||||
document = HTML(string='''
|
||||
<br clear=left>
|
||||
<br clear=right />
|
||||
<br clear=both />
|
||||
<br clear=all />
|
||||
<font color=red face=ahem size=7></font>
|
||||
<Font size=4></Font>
|
||||
<font size=+5 ></font>
|
||||
<font size=-5 ></font>
|
||||
''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)
|
||||
page, = document.pages
|
||||
html, = page._page_box.children
|
||||
body, = html.children
|
||||
line1, line2, line3, line4, line5 = body.children
|
||||
br1, = line1.children
|
||||
br2, = line2.children
|
||||
br3, = line3.children
|
||||
br4, = line4.children
|
||||
font1, font2, font3, font4 = line5.children
|
||||
assert br1.style.clear == 'left'
|
||||
assert br2.style.clear == 'right'
|
||||
assert br3.style.clear == 'both'
|
||||
assert br4.style.clear == 'both'
|
||||
assert font1.style.color == (1, 0, 0, 1)
|
||||
assert font1.style.font_family == ['ahem']
|
||||
assert font1.style.font_size == 1.5 * 2 * 16
|
||||
assert font2.style.font_size == 6 / 5 * 16
|
||||
assert font3.style.font_size == 1.5 * 2 * 16
|
||||
assert font4.style.font_size == 8 / 9 * 16
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_ph_lists():
|
||||
"""Check presentational hints on lists."""
|
||||
document = HTML(string='''
|
||||
<ol>
|
||||
<li type=A></li>
|
||||
<li type=1></li>
|
||||
<li type=a></li>
|
||||
<li type=i></li>
|
||||
<li type=I></li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li type=circle></li>
|
||||
<li type=disc></li>
|
||||
<li type=square></li>
|
||||
</ul>
|
||||
''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)
|
||||
page, = document.pages
|
||||
html, = page._page_box.children
|
||||
body, = html.children
|
||||
ol, ul = body.children
|
||||
oli1, oli2, oli3, oli4, oli5 = ol.children
|
||||
uli1, uli2, uli3 = ul.children
|
||||
assert oli1.style.list_style_type == 'upper-alpha'
|
||||
assert oli2.style.list_style_type == 'decimal'
|
||||
assert oli3.style.list_style_type == 'lower-alpha'
|
||||
assert oli4.style.list_style_type == 'lower-roman'
|
||||
assert oli5.style.list_style_type == 'upper-roman'
|
||||
assert uli1.style.list_style_type == 'circle'
|
||||
assert uli2.style.list_style_type == 'disc'
|
||||
assert uli3.style.list_style_type == 'square'
|
||||
|
||||
document = HTML(string='''
|
||||
<ol type=A></ol>
|
||||
<ol type=1></ol>
|
||||
<ol type=a></ol>
|
||||
<ol type=i></ol>
|
||||
<ol type=I></ol>
|
||||
<ul type=circle></ul>
|
||||
<ul type=disc></ul>
|
||||
<ul type=square></ul>
|
||||
''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)
|
||||
page, = document.pages
|
||||
html, = page._page_box.children
|
||||
body, = html.children
|
||||
ol1, ol2, ol3, ol4, ol5, ul1, ul2, ul3 = body.children
|
||||
assert ol1.style.list_style_type == 'upper-alpha'
|
||||
assert ol2.style.list_style_type == 'decimal'
|
||||
assert ol3.style.list_style_type == 'lower-alpha'
|
||||
assert ol4.style.list_style_type == 'lower-roman'
|
||||
assert ol5.style.list_style_type == 'upper-roman'
|
||||
assert ul1.style.list_style_type == 'circle'
|
||||
assert ul2.style.list_style_type == 'disc'
|
||||
assert ul3.style.list_style_type == 'square'
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_ph_tables():
|
||||
"""Check presentational hints on tables."""
|
||||
document = HTML(string='''
|
||||
<table align=left rules=none></table>
|
||||
<table align=right rules=groups></table>
|
||||
<table align=center rules=rows></table>
|
||||
<table border=10 cellspacing=3 bordercolor=green>
|
||||
<thead>
|
||||
<tr>
|
||||
<th valign=top></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td nowrap><h1 align=right></h1><p align=center></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
</tr>
|
||||
<tfoot align=justify>
|
||||
<tr>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)
|
||||
page, = document.pages
|
||||
html, = page._page_box.children
|
||||
body, = html.children
|
||||
wrapper1, wrapper2, wrapper3, wrapper4, = body.children
|
||||
assert wrapper1.style.float == 'left'
|
||||
assert wrapper2.style.float == 'right'
|
||||
assert wrapper3.style.margin_left == 'auto'
|
||||
assert wrapper3.style.margin_right == 'auto'
|
||||
assert wrapper1.children[0].style.border_left_style == 'hidden'
|
||||
assert wrapper1.style.border_collapse == 'collapse'
|
||||
assert wrapper2.children[0].style.border_left_style == 'hidden'
|
||||
assert wrapper2.style.border_collapse == 'collapse'
|
||||
assert wrapper3.children[0].style.border_left_style == 'hidden'
|
||||
assert wrapper3.style.border_collapse == 'collapse'
|
||||
|
||||
table4, = wrapper4.children
|
||||
assert table4.style.border_top_style == 'outset'
|
||||
assert table4.style.border_top_width == 10
|
||||
assert table4.style.border_spacing == (3, 3)
|
||||
r, g, b, a = table4.style.border_left_color
|
||||
assert g > r and g > b
|
||||
head_group, rows_group, foot_group = table4.children
|
||||
head, = head_group.children
|
||||
th, = head.children
|
||||
assert th.style.vertical_align == 'top'
|
||||
line1, line2 = rows_group.children
|
||||
td, = line1.children
|
||||
assert td.style.white_space == 'nowrap'
|
||||
assert td.style.border_top_width == 1
|
||||
assert td.style.border_top_style == 'inset'
|
||||
h1, p = td.children
|
||||
assert h1.style.text_align == 'right'
|
||||
assert p.style.text_align == 'center'
|
||||
foot, = foot_group.children
|
||||
tr, = foot.children
|
||||
assert tr.style.text_align == 'justify'
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_ph_hr():
|
||||
"""Check presentational hints on horizontal rules."""
|
||||
document = HTML(string='''
|
||||
<hr align=left>
|
||||
<hr align=right />
|
||||
<hr align=both color=red />
|
||||
<hr align=center noshade size=10 />
|
||||
<hr align=all size=8 width=100 />
|
||||
''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)
|
||||
page, = document.pages
|
||||
html, = page._page_box.children
|
||||
body, = html.children
|
||||
hr1, hr2, hr3, hr4, hr5 = body.children
|
||||
assert hr1.margin_left == 0
|
||||
assert hr1.style.margin_right == 'auto'
|
||||
assert hr2.style.margin_left == 'auto'
|
||||
assert hr2.margin_right == 0
|
||||
assert hr3.style.margin_left == 'auto'
|
||||
assert hr3.style.margin_right == 'auto'
|
||||
assert hr3.style.color == (1, 0, 0, 1)
|
||||
assert hr4.style.margin_left == 'auto'
|
||||
assert hr4.style.margin_right == 'auto'
|
||||
assert hr4.border_height() == 10
|
||||
assert hr4.style.border_top_width == 5
|
||||
assert hr5.border_height() == 8
|
||||
assert hr5.height == 6
|
||||
assert hr5.width == 100
|
||||
assert hr5.style.border_top_width == 1
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_ph_embedded():
|
||||
"""Check presentational hints on embedded contents."""
|
||||
document = HTML(string='''
|
||||
<object data="data:image/svg+xml,<svg></svg>"
|
||||
align=top hspace=10 vspace=20></object>
|
||||
<img src="data:image/svg+xml,<svg></svg>" alt=text
|
||||
align=right width=10 height=20 />
|
||||
<embed src="data:image/svg+xml,<svg></svg>" align=texttop />
|
||||
''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)
|
||||
page, = document.pages
|
||||
html, = page._page_box.children
|
||||
body, = html.children
|
||||
line, = body.children
|
||||
object_, text1, img, embed, text2 = line.children
|
||||
assert embed.style.vertical_align == 'text-top'
|
||||
assert object_.style.vertical_align == 'top'
|
||||
assert object_.margin_top == 20
|
||||
assert object_.margin_left == 10
|
||||
assert img.style.float == 'right'
|
||||
assert img.width == 10
|
||||
assert img.height == 20
|
Loading…
Reference in New Issue
Block a user