Repository: Kozea/WeasyPrint
Pull request #352 (2016-08-29 19:30:25)

Support presentational hints
See #253.

This feature has been implemented in a clean way.

Random thoughts:
- I've added this feature because it's useful for many W3C tests.
- The diff is big but really simple.
- Many hints are given in CSS in the spec, they could (should?) be added in another stylesheet in WP with priority set to 0 (instead of code).
- Merging `find_style_attributes` and `find_presetational_hints` is possible, it would avoid going through the whole tree twice.
- The code is not tested, but the test suites available on `test.weasyprint.org` pass without a crash.
- Calling `check_style_attribute` with constant strings is probably useless.
- This features makes the tests last ~5% more for me, and it's probably much worst on real cases. Should we allow the users to enable/disable it with an option?
- Some rules have not been implemented, mainly because they would be much easier to handle with CSS than with code (one more reason to use an extra stylesheet).
- Some parsing errors are logged, some aren't (who cares?).

Comments are welcome, as usual. I won't merge it right now, I'd like (at least):
- [x] a new option to activate this feature (and keep it deactivated by default),
- [x] a stylesheet for rules described in CSS in the spec,
- [x] tests.


Patch:
diff --git a/docs/features.rst b/docs/features.rst
index 5c3e8493..c5c97004 100644
--- a/docs/features.rst
+++ b/docs/features.rst
@@ -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
diff --git a/weasyprint/__init__.py b/weasyprint/__init__.py
index e475e6e3..824f6eaa 100644
--- a/weasyprint/__init__.py
+++ b/weasyprint/__init__.py
@@ -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
diff --git a/weasyprint/__main__.py b/weasyprint/__main__.py
index 326ab7db..0f45b94d 100644
--- a/weasyprint/__main__.py
+++ b/weasyprint/__main__.py
@@ -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
diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py
index 76e3def0..43d0fdcc 100644
--- a/weasyprint/css/__init__.py
+++ b/weasyprint/css/__init__.py
@@ -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)
diff --git a/weasyprint/css/html5_ph.css b/weasyprint/css/html5_ph.css
new file mode 100644
index 00000000..6b41f39b
--- /dev/null
+++ b/weasyprint/css/html5_ph.css
@@ -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;
+}
diff --git a/weasyprint/document.py b/weasyprint/document.py
index a337b193..12df8598 100644
--- a/weasyprint/document.py
+++ b/weasyprint/document.py
@@ -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(
diff --git a/weasyprint/html.py b/weasyprint/html.py
index 589f0c09..b285a7cf 100644
--- a/weasyprint/html.py
+++ b/weasyprint/html.py
@@ -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)
 


Test patch:
diff --git a/weasyprint/tests/test_presentational_hints.py b/weasyprint/tests/test_presentational_hints.py
new file mode 100644
index 00000000..b72faf62
--- /dev/null
+++ b/weasyprint/tests/test_presentational_hints.py
@@ -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