Repository: Kozea/WeasyPrint
Pull request #1146 (2020-06-18 21:44:07)

Bookmark label
Incited by #1145 I started a research about how to implement bookmark-labels for the `:before` and `:after` pseudo-elements and stumbled upon page counters and target counters not being updated in bookmark-labels at all.

Of course, nobody ever will use page or target counters in her bookmark-labels...


Patch:
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..1463e0ea
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,58 @@
+language: python
+env: PYTHON=python
+
+git:
+  submodules: false
+
+jobs:
+  include:
+    - os: linux
+      python: 3.5
+    - os: linux
+      python: 3.6
+    - dist: bionic
+      python: 3.7
+    - dist: bionic
+      python: 3.8
+    - os: osx
+      language: generic
+      env:
+        - HOMEBREW_NO_AUTO_UPDATE=1
+        - PYTHON=python3
+    - os: windows
+      # Windows doesn't support python or even generic language
+      language: cpp
+      env: PYTHON=/c/Python38/python
+  allow_failures:
+    - os: windows
+
+addons:
+  apt:
+    packages:
+    - ttf-dejavu
+
+before_install:
+  - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then export "PATH=$PATH;C:\msys64\mingw64\bin"; fi
+  - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then choco install -y python dejavufonts; fi
+  - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then wget "http://repo.msys2.org/distrib/x86_64/msys2-base-x86_64-20180531.tar.xz"; fi
+  - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then 7z e msys2-base-x86_64-20180531.tar.xz; fi
+  - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then 7z x -y msys2-base-x86_64-20180531.tar -oc:\\; fi
+  # We need powershell because Travis' bash and MSYS2's bash rely on conflicting libraries
+  - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then powershell "C:\msys64\usr\bin\bash -lc 'pacman -S mingw-w64-x86_64-gtk3 --noconfirm'"; fi
+
+  # Pango 1.38+ is needed to make @font-face work, remove these two lines when it's available on Travis
+  - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then mkdir -p ~/.fonts; fi
+  - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then wget "https://github.com/Kozea/Ahem/blob/master/Ahem.ttf?raw=true" -O ~/.fonts/Ahem.ttf; fi
+
+  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew tap homebrew/cask-fonts; fi
+  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew cask install font-dejavu; fi
+  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install cairo pango gdk-pixbuf libffi; fi
+
+  # As setup.py doesn't use pip, setuptools is not updated before installing dependencies
+  - $PYTHON -m pip install --upgrade pip setuptools
+
+  # Upgrade cffi to avoid errors while installing cairocffi
+  - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then $PYTHON -m pip install --upgrade cffi; fi
+
+script:
+  - $PYTHON setup.py test
diff --git a/README.rst b/README.rst
index 5c3bee77..e661b9fc 100644
--- a/README.rst
+++ b/README.rst
@@ -17,7 +17,7 @@ WebKit or Gecko. The CSS layout engine is written in Python, designed for
 pagination, and meant to be easy to hack on.
 
 * Free software: BSD licensed
-* Python 3.6+
+* Python 3.5+
 * Website: https://weasyprint.org/
 * Documentation: https://weasyprint.readthedocs.io/
 * Source code and issue tracker: https://github.com/Kozea/WeasyPrint
diff --git a/docs/install.rst b/docs/install.rst
index e3bbf83e..1aed60fe 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -3,7 +3,7 @@ Installing
 
 WeasyPrint |version| depends on:
 
-* CPython_ ≥ 3.6.0
+* CPython_ ≥ 3.5.0
 * cairo_ ≥ 1.15.4 [#]_
 * Pango_ ≥ 1.38.0 [#]_
 * setuptools_ ≥ 30.3.0 [#]_
@@ -14,7 +14,6 @@ WeasyPrint |version| depends on:
 * cssselect2_ ≥ 0.1
 * CairoSVG_ ≥ 2.4.0
 * Pyphen_ ≥ 0.9.1
-* Pillow ≥ 4.0.0
 * GDK-PixBuf_ ≥ 2.25.0 [#]_
 
 .. _CPython: http://www.python.org/
diff --git a/setup.cfg b/setup.cfg
index d0a05c27..df4d0f27 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -23,6 +23,7 @@ classifiers =
   Intended Audience :: Developers
   License :: OSI Approved :: BSD License
   Programming Language :: Python :: 3
+  Programming Language :: Python :: 3.5
   Programming Language :: Python :: 3.6
   Programming Language :: Python :: 3.7
   Programming Language :: Python :: 3.8
@@ -56,13 +57,12 @@ install_requires =
   cssselect2>=0.1
   CairoSVG>=2.4.0
   Pyphen>=0.9.1
-  Pillow>=4.0.0
 tests_require =
   pytest-runner
   pytest-cov
   pytest-flake8
   pytest-isort
-python_requires = >= 3.6
+python_requires = >= 3.5
 
 [options.entry_points]
 console-scripts = weasyprint = weasyprint.__main__:main
diff --git a/weasyprint/__init__.py b/weasyprint/__init__.py
index acaa7d9a..ec27c5a0 100644
--- a/weasyprint/__init__.py
+++ b/weasyprint/__init__.py
@@ -134,8 +134,8 @@ class HTML:
         return get_html_metadata(self.wrapper_element, self.base_url)
 
     def render(self, stylesheets=None, enable_hinting=False,
-               presentational_hints=False, optimize_images=False,
-               font_config=None, counter_style=None, image_cache=None):
+               presentational_hints=False, font_config=None,
+               counter_style=None):
         """Lay out and paginate the document, but do not (yet) export it
         to PDF or PNG.
 
@@ -158,25 +158,20 @@ class HTML:
         :type presentational_hints: bool
         :param presentational_hints: Whether HTML presentational hints are
             followed.
-        :type optimize_images: bool
-        :param optimize_images: Try to optimize the size of embedded images.
         :type font_config: :class:`~fonts.FontConfiguration`
         :param font_config: A font configuration handling ``@font-face`` rules.
         :type counter_style: :class:`~css.counters.CounterStyle`
         :param counter_style: A dictionary storing ``@counter-style`` rules.
-        :type image_cache: dict
-        :param image_cache: A dictionary used to cache images.
         :returns: A :class:`~document.Document` object.
 
         """
         return Document._render(
             self, stylesheets, enable_hinting, presentational_hints,
-            optimize_images, font_config, counter_style, image_cache)
+            font_config, counter_style)
 
     def write_pdf(self, target=None, stylesheets=None, zoom=1,
                   attachments=None, presentational_hints=False,
-                  optimize_images=False, font_config=None, counter_style=None,
-                  image_cache=None):
+                  font_config=None, counter_style=None):
         """Render the document to a PDF file.
 
         This is a shortcut for calling :meth:`render`, then
@@ -204,32 +199,25 @@ class HTML:
         :type presentational_hints: bool
         :param presentational_hints: Whether HTML presentational hints are
             followed.
-        :type optimize_images: bool
-        :param optimize_images: Try to optimize the size of embedded images.
         :type font_config: :class:`~fonts.FontConfiguration`
         :param font_config: A font configuration handling ``@font-face`` rules.
         :type counter_style: :class:`~css.counters.CounterStyle`
         :param counter_style: A dictionary storing ``@counter-style`` rules.
-        :type image_cache: dict
-        :param image_cache: A dictionary used to cache images.
         :returns:
             The PDF as :obj:`bytes` if ``target`` is not provided or
             :obj:`None`, otherwise :obj:`None` (the PDF is written to
             ``target``).
 
         """
-        return (
-            self.render(
-                stylesheets, enable_hinting=False,
-                presentational_hints=presentational_hints,
-                optimize_images=optimize_images, font_config=font_config,
-                counter_style=counter_style, image_cache=image_cache)
-            .write_pdf(target, zoom, attachments))
+        return self.render(
+            stylesheets, enable_hinting=False,
+            presentational_hints=presentational_hints,
+            font_config=font_config, counter_style=counter_style).write_pdf(
+                target, zoom, attachments)
 
     def write_image_surface(self, stylesheets=None, resolution=96,
-                            presentational_hints=False, optimize_images=False,
-                            font_config=None, counter_style=None,
-                            image_cache=None):
+                            presentational_hints=False, font_config=None,
+                            counter_style=None):
         """Render pages vertically on a cairo image surface.
 
         .. versionadded:: 0.17
@@ -254,29 +242,23 @@ class HTML:
         :type presentational_hints: bool
         :param presentational_hints: Whether HTML presentational hints are
             followed.
-        :type optimize_images: bool
-        :param optimize_images: Try to optimize the size of embedded images.
         :type font_config: :class:`~fonts.FontConfiguration`
         :param font_config: A font configuration handling ``@font-face`` rules.
         :type counter_style: :class:`~css.counters.CounterStyle`
         :param counter_style: A dictionary storing ``@counter-style`` rules.
-        :type image_cache: dict
-        :param image_cache: A dictionary used to cache images.
         :returns: A cairo :class:`ImageSurface <cairocffi.ImageSurface>`.
 
         """
         surface, _width, _height = (
-            self.render(
-                stylesheets, enable_hinting=True,
-                presentational_hints=presentational_hints,
-                font_config=font_config, optimize_images=optimize_images,
-                image_cache=image_cache)
+            self.render(stylesheets, enable_hinting=True,
+                        presentational_hints=presentational_hints,
+                        font_config=font_config)
             .write_image_surface(resolution))
         return surface
 
     def write_png(self, target=None, stylesheets=None, resolution=96,
-                  presentational_hints=False, optimize_images=False,
-                  font_config=None, counter_style=None, image_cache=None):
+                  presentational_hints=False, font_config=None,
+                  counter_style=None):
         """Paint the pages vertically to a single PNG image.
 
         There is no decoration around pages other than those specified in CSS
@@ -302,14 +284,10 @@ class HTML:
         :type presentational_hints: bool
         :param presentational_hints: Whether HTML presentational hints are
             followed.
-        :type optimize_images: bool
-        :param optimize_images: Try to optimize the size of embedded images.
         :type font_config: :class:`~fonts.FontConfiguration`
         :param font_config: A font configuration handling ``@font-face`` rules.
         :type counter_style: :class:`~css.counters.CounterStyle`
         :param counter_style: A dictionary storing ``@counter-style`` rules.
-        :type image_cache: dict
-        :param image_cache: A dictionary used to cache images.
         :returns:
             The image as :obj:`bytes` if ``target`` is not provided or
             :obj:`None`, otherwise :obj:`None` (the image is written to
@@ -320,8 +298,7 @@ class HTML:
             self.render(
                 stylesheets, enable_hinting=True,
                 presentational_hints=presentational_hints,
-                optimize_images=optimize_images, font_config=font_config,
-                counter_style=counter_style, image_cache=image_cache)
+                font_config=font_config, counter_style=counter_style)
             .write_png(target, resolution))
         return png_bytes
 
diff --git a/weasyprint/__main__.py b/weasyprint/__main__.py
index 295051ce..542cc712 100644
--- a/weasyprint/__main__.py
+++ b/weasyprint/__main__.py
@@ -87,10 +87,6 @@ def main(argv=None, stdout=None, stdin=None):
         <https://www.w3.org/TR/html/rendering.html\
         #the-css-user-agent-style-sheet-and-presentational-hints>`_.
 
-    .. option:: -o, --optimize-images
-
-        Try to optimize the size of embedded images.
-
     .. option:: -v, --verbose
 
         Show warnings and information messages.
@@ -99,10 +95,6 @@ def main(argv=None, stdout=None, stdin=None):
 
         Show debugging messages.
 
-    .. option:: -q, --quiet
-
-        Hide logging messages.
-
     .. option:: --version
 
         Show the version number. Other options and arguments are ignored.
@@ -141,8 +133,6 @@ def main(argv=None, stdout=None, stdin=None):
                              'to attach to the PDF document')
     parser.add_argument('-p', '--presentational-hints', action='store_true',
                         help='Follow HTML presentational hints.')
-    parser.add_argument('-o', '--optimize-images', action='store_true',
-                        help='Try to optimize the size of embedded images.')
     parser.add_argument('-v', '--verbose', action='store_true',
                         help='Show warnings and information messages.')
     parser.add_argument('-d', '--debug', action='store_true',
@@ -185,8 +175,7 @@ def main(argv=None, stdout=None, stdin=None):
 
     kwargs = {
         'stylesheets': args.stylesheet,
-        'presentational_hints': args.presentational_hints,
-        'optimize_images': args.optimize_images}
+        'presentational_hints': args.presentational_hints}
     if args.resolution:
         if format_ == 'png':
             kwargs['resolution'] = args.resolution
diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py
index f7a0be49..33de77e6 100644
--- a/weasyprint/css/computed_values.py
+++ b/weasyprint/css/computed_values.py
@@ -176,46 +176,6 @@ def register_computer(name):
     return decorator
 
 
-def compute_variable(value, name, computed, base_url, parent_style):
-    from .validation.properties import PROPERTIES
-    already_computed_value = False
-
-    if value and isinstance(value, tuple) and value[0] == 'var()':
-        variable_name, default = value[1]
-        computed_value = _resolve_var(computed, variable_name, default)
-        if computed_value is None:
-            new_value = None
-        else:
-            prop = PROPERTIES[name.replace('_', '-')]
-            if prop.wants_base_url:
-                new_value = prop(computed_value, base_url)
-            else:
-                new_value = prop(computed_value)
-
-        # See https://drafts.csswg.org/css-variables/#invalid-variables
-        if new_value is None:
-            try:
-                computed_value = ''.join(
-                    token.serialize() for token in computed_value)
-            except BaseException:
-                pass
-            LOGGER.warning(
-                'Unsupported computed value `%s` set in variable `%s` '
-                'for property `%s`.', computed_value,
-                variable_name.replace('_', '-'), name.replace('_', '-'))
-            if name in INHERITED and parent_style:
-                already_computed_value = True
-                value = parent_style[name]
-            else:
-                already_computed_value = name not in INITIAL_NOT_COMPUTED
-                value = INITIAL_VALUES[name]
-        elif isinstance(new_value, list):
-            value, = new_value
-        else:
-            value = new_value
-    return value, already_computed_value
-
-
 def compute(element, pseudo_type, specified, computed, parent_style,
             root_style, base_url, target_collector):
     """Create a dict of computed values.
@@ -233,6 +193,7 @@ def compute(element, pseudo_type, specified, computed, parent_style,
     :param target_collector: A target collector used to get computed targets.
 
     """
+    from .validation.properties import PROPERTIES
 
     computer = {
         'is_root_element': parent_style is None,
@@ -257,19 +218,37 @@ def compute(element, pseudo_type, specified, computed, parent_style,
         function = getter(name)
         already_computed_value = False
 
-        if value:
-            converted_to_list = False
-
-            if not isinstance(value, list):
-                converted_to_list = True
-                value = [value]
-
-            for i, v in enumerate(value):
-                value[i], already_computed_value = compute_variable(
-                    v, name, computed, base_url, parent_style)
-
-            if converted_to_list:
-                value, = value
+        if value and isinstance(value, tuple) and value[0] == 'var()':
+            variable_name, default = value[1]
+            computed_value = _resolve_var(computed, variable_name, default)
+            if computed_value is None:
+                new_value = None
+            else:
+                prop = PROPERTIES[name.replace('_', '-')]
+                if prop.wants_base_url:
+                    new_value = prop(computed_value, base_url)
+                else:
+                    new_value = prop(computed_value)
+
+            # See https://drafts.csswg.org/css-variables/#invalid-variables
+            if new_value is None:
+                try:
+                    computed_value = ''.join(
+                        token.serialize() for token in computed_value)
+                except BaseException:
+                    pass
+                LOGGER.warning(
+                    'Unsupported computed value `%s` set in variable `%s` '
+                    'for property `%s`.', computed_value,
+                    variable_name.replace('_', '-'), name.replace('_', '-'))
+                if name in INHERITED and parent_style:
+                    already_computed_value = True
+                    value = parent_style[name]
+                else:
+                    already_computed_value = name not in INITIAL_NOT_COMPUTED
+                    value = INITIAL_VALUES[name]
+            else:
+                value = new_value
 
         if function is not None and not already_computed_value:
             value = function(computer, name, value)
diff --git a/weasyprint/css/html5_ua.css b/weasyprint/css/html5_ua.css
index fbbbcfe2..bb2997aa 100644
--- a/weasyprint/css/html5_ua.css
+++ b/weasyprint/css/html5_ua.css
@@ -557,7 +557,7 @@ xmp { display: block; font-family: monospace; margin-bottom: 1em; margin-top: 1e
 
 @counter-style square {
   system: cyclic;
-  symbols: ▪;
+  symbols: ◾;
   suffix: " ";
 }
 
diff --git a/weasyprint/css/utils.py b/weasyprint/css/utils.py
index 983754bd..53665762 100644
--- a/weasyprint/css/utils.py
+++ b/weasyprint/css/utils.py
@@ -699,11 +699,6 @@ def get_content_list_token(token, base_url):
     if string is not None:
         return string
 
-    # <var>
-    var = check_var_function(token)
-    if var is not None:
-        return var
-
     # contents
     if get_keyword(token) == 'contents':
         return ('content', 'text')
diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py
index 3575cf97..f4fbc1c1 100644
--- a/weasyprint/css/validation/properties.py
+++ b/weasyprint/css/validation/properties.py
@@ -21,7 +21,6 @@ from ..utils import (
 PREFIX = '-weasy-'
 PROPRIETARY = set()
 UNSTABLE = set()
-MULTIVAL_PROPERTIES = set()
 
 # Yes/no validators for non-shorthand properties
 # Maps property names to functions taking a property name and a value list,
@@ -34,7 +33,7 @@ PROPERTIES = {}
 # Validators
 
 def property(property_name=None, proprietary=False, unstable=False,
-             multiple_values=False, wants_base_url=False):
+             wants_base_url=False):
     """Decorator adding a function to the ``PROPERTIES``.
 
     The name of the property covered by the decorated function is set to
@@ -70,8 +69,6 @@ def property(property_name=None, proprietary=False, unstable=False,
             PROPRIETARY.add(name)
         if unstable:
             UNSTABLE.add(name)
-        if multiple_values:
-            MULTIVAL_PROPERTIES.add(name)
         return function
     return decorator
 
@@ -92,11 +89,10 @@ def validate_non_shorthand(base_url, name, tokens, required=False):
     if not required and name not in PROPERTIES:
         raise InvalidValues('property not supported yet')
 
-    if name not in MULTIVAL_PROPERTIES:
-        for token in tokens:
-            var_function = check_var_function(token)
-            if var_function:
-                return ((name, var_function),)
+    for token in tokens:
+        var_function = check_var_function(token)
+        if var_function:
+            return ((name, var_function),)
 
     keyword = get_single_keyword(tokens)
     if keyword in ('initial', 'inherit'):
@@ -458,7 +454,7 @@ def clip(token):
         return ()
 
 
-@property(multiple_values=True, wants_base_url=True)
+@property(wants_base_url=True)
 def content(tokens, base_url):
     """``content`` property validation."""
     # See https://www.w3.org/TR/css-content-3/#content-property
diff --git a/weasyprint/document.py b/weasyprint/document.py
index fbaa1b6d..6947067c 100644
--- a/weasyprint/document.py
+++ b/weasyprint/document.py
@@ -10,10 +10,8 @@ import io
 import math
 import shutil
 import warnings
-from urllib.parse import quote
 
 import cairocffi as cairo
-
 from weasyprint.layout import LayoutContext
 
 from . import CSS
@@ -120,8 +118,7 @@ def _gather_links_and_bookmarks(box, bookmarks, links, anchors, matrix):
     has_link = link and not isinstance(box, boxes.TextBox)
     # In case of duplicate IDs, only the first is an anchor.
     has_anchor = anchor_name and anchor_name not in anchors
-    is_attachment = getattr(box, 'is_attachment', False)
-    download_name = getattr(box, 'attachment_download', None)
+    is_attachment = hasattr(box, 'is_attachment') and box.is_attachment
 
     if has_bookmark or has_link or has_anchor:
         pos_x, pos_y, width, height = box.hit_area()
@@ -135,11 +132,9 @@ def _gather_links_and_bookmarks(box, bookmarks, links, anchors, matrix):
             if matrix:
                 link = (
                     link_type, target, rectangle_aabb(
-                        matrix, pos_x, pos_y, width, height), download_name)
+                        matrix, pos_x, pos_y, width, height))
             else:
-                link = (
-                    link_type, target, (pos_x, pos_y, width, height),
-                    download_name)
+                link = (link_type, target, (pos_x, pos_y, width, height))
             links.append(link)
         if matrix and (has_bookmark or has_anchor):
             pos_x, pos_y = matrix.transform_point(pos_x, pos_y)
@@ -354,9 +349,8 @@ class Document:
 
     @classmethod
     def _build_layout_context(cls, html, stylesheets, enable_hinting,
-                              presentational_hints=False,
-                              optimize_images=False, font_config=None,
-                              counter_style=None, image_cache=None):
+                              presentational_hints=False, font_config=None,
+                              counter_style=None):
         if font_config is None:
             font_config = FontConfiguration()
         if counter_style is None:
@@ -364,7 +358,6 @@ class Document:
         target_collector = TargetCollector()
         page_rules = []
         user_stylesheets = []
-        image_cache = {} if image_cache is None else image_cache
         for css in stylesheets or []:
             if not hasattr(css, 'matcher'):
                 css = CSS(
@@ -375,8 +368,7 @@ class Document:
             html, user_stylesheets, presentational_hints, font_config,
             counter_style, page_rules, target_collector)
         get_image_from_uri = functools.partial(
-            original_get_image_from_uri, image_cache, html.url_fetcher,
-            optimize_images)
+            original_get_image_from_uri, {}, html.url_fetcher)
         PROGRESS_LOGGER.info('Step 4 - Creating formatting structure')
         context = LayoutContext(
             enable_hinting, style_for, get_image_from_uri, font_config,
@@ -385,8 +377,8 @@ class Document:
 
     @classmethod
     def _render(cls, html, stylesheets, enable_hinting,
-                presentational_hints=False, optimize_images=False,
-                font_config=None, counter_style=None, image_cache=None):
+                presentational_hints=False, font_config=None,
+                counter_style=None):
         if font_config is None:
             font_config = FontConfiguration()
 
@@ -395,7 +387,7 @@ class Document:
 
         context = cls._build_layout_context(
             html, stylesheets, enable_hinting, presentational_hints,
-            optimize_images, font_config, counter_style, image_cache)
+            font_config, counter_style)
 
         root_box = build_formatting_structure(
             html.etree_element, context.style_for, context.get_image_from_uri,
@@ -491,15 +483,14 @@ class Document:
         for page in self.pages:
             page_links = []
             for link in page.links:
-                link_type, anchor_name, rectangle, _ = link
+                link_type, anchor_name, rectangle = link
                 if link_type == 'internal':
                     if anchor_name not in anchors:
                         LOGGER.error(
                             'No anchor #%s for internal URI reference',
                             anchor_name)
                     else:
-                        page_links.append(
-                            (link_type, anchor_name, rectangle, None))
+                        page_links.append((link_type, anchor_name, rectangle))
                 else:
                     # External link
                     page_links.append(link)
@@ -570,17 +561,15 @@ class Document:
         # defined by cairo when drawing targets. This would give a feeling
         # similiar to what browsers do with links that span multiple lines.
         for link in links:
-            link_type, link_target, rectangle, _ = link
+            link_type, link_target, rectangle = link
             if link_type == 'external':
                 attributes = "rect=[{} {} {} {}] uri='{}'".format(*(
                     [int(round(i * scale)) for i in rectangle] +
                     [link_target.replace("'", '%27')]))
             elif link_type == 'internal':
-                # Use urllib.parse.quote to make link ids the same with
-                # TAG_DEST and TAG_LINK with Cairo
                 attributes = "rect=[{} {} {} {}] dest='{}'".format(*(
                     [int(round(i * scale)) for i in rectangle] +
-                    [quote(link_target)]))
+                    [link_target.replace("'", '%27')]))
             elif link_type == 'attachment':
                 # Attachments are handled in write_pdf_metadata
                 continue
@@ -589,10 +578,8 @@ class Document:
 
         for anchor in anchors:
             anchor_name, x, y = anchor
-            # Use urllib.parse.quote to make link ids the same with TAG_DEST
-            # and TAG_LINK with Cairo
             attributes = "name='{}' x={} y={}".format(
-                quote(anchor_name), int(round(x * scale)),
+                anchor_name.replace("'", '%27'), int(round(x * scale)),
                 int(round(y * scale)))
             context.tag_begin(cairo.TAG_DEST, attributes)
             context.tag_end(cairo.TAG_DEST)
diff --git a/weasyprint/draw.py b/weasyprint/draw.py
index ed3a5300..94484143 100644
--- a/weasyprint/draw.py
+++ b/weasyprint/draw.py
@@ -184,15 +184,6 @@ def draw_stacking_context(context, stacking_context, enable_hinting):
     # See http://www.w3.org/TR/CSS2/zindex.html
     with stacked(context):
         box = stacking_context.box
-
-        # apply the viewport_overflow to the html box, see #35
-        if box.is_for_root_element and (
-                stacking_context.page.style['overflow'] != 'visible'):
-            rounded_box_path(
-                context,
-                stacking_context.page.rounded_padding_box())
-            context.clip()
-
         if box.is_absolutely_positioned() and box.style['clip']:
             top, right, bottom, left = box.style['clip']
             if top == 'auto':
@@ -232,9 +223,7 @@ def draw_stacking_context(context, stacking_context, enable_hinting):
                 context, stacking_context.page, box, enable_hinting)
 
         with stacked(context):
-            # dont clip the PageBox, see #35
-            if box.style['overflow'] != 'visible' and not isinstance(
-                    box, boxes.PageBox):
+            if box.style['overflow'] != 'visible':
                 # Only clip the content and the children:
                 # - the background is already clipped
                 # - the border must *not* be clipped
diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py
index d46a4ff3..75ca503b 100644
--- a/weasyprint/formatting_structure/build.py
+++ b/weasyprint/formatting_structure/build.py
@@ -244,6 +244,15 @@ def before_after_to_box(element, pseudo_type, state, style_for,
         target_collector, counter_style))
 
     box.children = children
+
+    # calculate the bookmark-label
+    if style['bookmark_label'] == 'none':
+        box.bookmark_label = ''
+    else:
+        _quote_depth, counter_values, _counter_scopes = state
+        compute_bookmark_label(
+            element, box, style['bookmark_label'], counter_values,
+            target_collector, counter_style)
     return [box]
 
 
diff --git a/weasyprint/html.py b/weasyprint/html.py
index f1c5169f..5959a98f 100644
--- a/weasyprint/html.py
+++ b/weasyprint/html.py
@@ -249,7 +249,6 @@ def handle_td(element, box, _get_image_from_uri, _base_url):
 def handle_a(element, box, _get_image_from_uri, base_url):
     """Handle the ``rel`` attribute."""
     box.is_attachment = element_has_link_type(element, 'attachment')
-    box.attachment_download = element.get('download')
     return [box]
 
 
diff --git a/weasyprint/images.py b/weasyprint/images.py
index f5b6dfe2..d9c24ddd 100644
--- a/weasyprint/images.py
+++ b/weasyprint/images.py
@@ -13,7 +13,6 @@ from xml.etree import ElementTree
 import cairocffi
 import cairosvg.parser
 import cairosvg.surface
-from PIL import Image
 
 from .layout.percentages import percentage
 from .logger import LOGGER
@@ -174,8 +173,7 @@ class SVGImage:
                 'Failed to draw an SVG image at %s : %s', self._base_url, e)
 
 
-def get_image_from_uri(cache, url_fetcher, optimize_images, url,
-                       forced_mime_type=None):
+def get_image_from_uri(cache, url_fetcher, url, forced_mime_type=None):
     """Get a cairo Pattern from an image URI."""
     missing = object()
     image = cache.get(url, missing)
@@ -197,7 +195,6 @@ def get_image_from_uri(cache, url_fetcher, optimize_images, url,
                 # Try to rely on given mimetype
                 try:
                     if mime_type == 'image/png':
-                        # Cairo already optimizes PNG images, no PIL needed
                         try:
                             surface = cairocffi.ImageSurface.create_from_png(
                                 BytesIO(string))
@@ -219,17 +216,6 @@ def get_image_from_uri(cache, url_fetcher, optimize_images, url,
                     try:
                         image = SVGImage(string, url, url_fetcher)
                     except BaseException:
-                        if optimize_images:
-                            try:
-                                image = Image.open(BytesIO(string))
-                                output = BytesIO()
-                                image.save(
-                                    output, format=image.format, optimize=True)
-                            except BaseException:
-                                # Optimization did not work, keep the original
-                                pass
-                            else:
-                                string = output.getvalue()
                         try:
                             surface, format_name = (
                                 pixbuf.decode_to_image_surface(string))
diff --git a/weasyprint/layout/__init__.py b/weasyprint/layout/__init__.py
index 45457fd9..e2bfe83d 100644
--- a/weasyprint/layout/__init__.py
+++ b/weasyprint/layout/__init__.py
@@ -140,10 +140,15 @@ def layout_document(html, root_box, context, max_loops=8):
         if not reloop_content and not reloop_pages:
             break
 
-    # Calculate string-sets and bookmark-label containing page based counters
+    # Calculate string-sets and bookmark-labels containing page based counters
     # when pagination is finished. No need to do that (maybe multiple times) in
     # make_page because they dont create boxes, only appear in MarginBoxes and
     # in the final PDF.
+    # Prevent repetition of bookmarks (see #1145).
+
+    watch_elements = []
+    watch_elements_before = []
+    watch_elements_after = []
     for i, page in enumerate(pages):
         # We need the updated page_counter_values
         resume_at, next_page, right_page, page_state, remake_state = (
@@ -151,12 +156,33 @@ def layout_document(html, root_box, context, max_loops=8):
         page_counter_values = page_state[1]
 
         for child in page.descendants():
+            # Only one bookmark per original box
+            if child.bookmark_label:
+                if child.element_tag.endswith('::before'):
+                    checklist = watch_elements_before
+                elif child.element_tag.endswith('::after'):
+                    checklist = watch_elements_after
+                else:
+                    checklist = watch_elements
+                if child.element in checklist:
+                    child.bookmark_label = ''
+                else:
+                    checklist.append(child.element)
+
             # TODO: remove attribute or set a default value in Box class
             if hasattr(child, 'missing_link'):
                 for (box, css_token), item in (
                         context.target_collector.counter_lookup_items.items()):
                     if child.missing_link == box and css_token != 'content':
+                        if (css_token == 'bookmark-label' and
+                                not child.bookmark_label):
+                            # don't refill it!
+                            continue
                         item.parse_again(page_counter_values)
+                        # string_set is a pointer, but the bookmark_label is
+                        # just a string: copy it
+                        if css_token == 'bookmark-label':
+                            child.bookmark_label = box.bookmark_label
             # Collect the string_sets in the LayoutContext
             string_sets = child.string_set
             if string_sets and string_sets != 'none':
diff --git a/weasyprint/layout/flex.py b/weasyprint/layout/flex.py
index 3d610a50..4e30a481 100644
--- a/weasyprint/layout/flex.py
+++ b/weasyprint/layout/flex.py
@@ -784,7 +784,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
         position_cross += line.cross_size
 
     # Step 15
-    if getattr(box, cross) == 'auto':
+    if box.style[cross] == 'auto':
         # TODO: handle min-max
         setattr(box, cross, sum(line.cross_size for line in flex_lines))
 
diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py
index 17b4827e..024d4773 100644
--- a/weasyprint/layout/preferred.py
+++ b/weasyprint/layout/preferred.py
@@ -718,7 +718,7 @@ def flex_max_content_width(context, box, outer=True):
 
 def trailing_whitespace_size(context, box):
     """Return the size of the trailing whitespace of ``box``."""
-    from .inlines import split_first_line, split_text_box
+    from .inlines import split_text_box, split_first_line
 
     while isinstance(box, (boxes.InlineBox, boxes.LineBox)):
         if not box.children:
diff --git a/weasyprint/pdf.py b/weasyprint/pdf.py
index ccd26e7e..336aa522 100644
--- a/weasyprint/pdf.py
+++ b/weasyprint/pdf.py
@@ -26,6 +26,7 @@
 
 import hashlib
 import io
+import mimetypes
 import os
 import re
 import string
@@ -201,13 +202,10 @@ class PDFFile:
         assert int(line[:-7]) == object_number  # len(b' 0 obj\n') == 7
         object_lines = []
         for line in fileobj:
-            if line == b'endobj\n':
+            if line == b'>>\n':
+                assert next(fileobj) == b'endobj\n'
                 # No newline, we’ll add it when writing.
-                if object_lines[-1].endswith(b'>>\n'):
-                    # Some objects have ">>" on the same line as their content
-                    object_lines[-1] = object_lines[-1][:-1]
-                else:
-                    object_lines.append(b'>>')
+                object_lines.append(b'>>')
                 return b''.join(object_lines)
             object_lines.append(line)
 
@@ -363,7 +361,7 @@ def _write_compressed_file_object(pdf, file):
     return object_number
 
 
-def _get_filename_from_url(url):
+def _get_filename_from_result(url, result):
     """Derive a filename from a fetched resource.
 
     This is either the filename returned by the URL fetcher, the last URL path
@@ -373,6 +371,12 @@ def _get_filename_from_url(url):
 
     filename = None
 
+    # A given filename will always take precedence
+    if result:
+        filename = result.get('filename')
+        if filename:
+            return filename
+
     # The URL path likely contains a filename, which is a good second guess
     if url:
         split = urlsplit(url)
@@ -381,7 +385,32 @@ def _get_filename_from_url(url):
             if filename == '':
                 filename = None
 
-    return 'attachment.bin' if filename is None else unquote(filename)
+    if filename is None:
+        # The URL lacks a path altogether. Use a synthetic name.
+
+        # Using guess_extension is a great idea, but sadly the extension is
+        # probably random, depending on the alignment of the stars, which car
+        # you're driving and which software has been installed on your machine.
+        #
+        # Unfortuneatly this isn't even imdepodent on one machine, because the
+        # extension can depend on PYTHONHASHSEED if mimetypes has multiple
+        # extensions to offer
+        extension = None
+        if result:
+            mime_type = result.get('mime_type')
+            if mime_type == 'text/plain':
+                # text/plain has a phletora of extensions - all garbage
+                extension = '.txt'
+            else:
+                extension = mimetypes.guess_extension(mime_type) or '.bin'
+        else:
+            extension = '.bin'
+
+        filename = 'attachment' + extension
+    else:
+        filename = unquote(filename)
+
+    return filename
 
 
 def _write_pdf_embedded_files(pdf, attachments, url_fetcher):
@@ -409,7 +438,7 @@ def _write_pdf_embedded_files(pdf, attachments, url_fetcher):
     return pdf.write_new_object(b''.join(content))
 
 
-def _write_pdf_attachment(pdf, attachment, url_fetcher, download_name=None):
+def _write_pdf_attachment(pdf, attachment, url_fetcher):
     """Write an attachment to the PDF stream.
 
     :return:
@@ -437,7 +466,7 @@ def _write_pdf_attachment(pdf, attachment, url_fetcher, download_name=None):
 
     # TODO: Use the result object from a URL fetch operation to provide more
     # details on the possible filename
-    filename = download_name or _get_filename_from_url(url)
+    filename = _get_filename_from_result(url, None)
 
     return pdf.write_new_object(pdf_format(
         '<< /Type /Filespec /F () /UF {0!P} /EF << /F {1} 0 R >> '
@@ -465,18 +494,11 @@ def write_pdf_metadata(fileobj, scale, url_fetcher, attachments,
     embedded_files_id = _write_pdf_embedded_files(
         pdf, attachments, url_fetcher)
     if embedded_files_id is not None:
-        # The Catalog dictionary is extended when it hasn’t a Names entry,
-        # else it’s the Names dictionary which is extended as the Catalog
-        # dictionary shall have only one Name entry
-        try:
-            names_dict = pdf.catalog.get_indirect_dict('Names', pdf)
-        except AttributeError:
-            params = pdf_format(
-                ' /Names << /EmbeddedFiles {0} 0 R >>', embedded_files_id)
-            pdf.extend_dict(pdf.catalog, params)
-        else:
-            params = pdf_format('  /EmbeddedFiles {0} 0 R', embedded_files_id)
-            pdf.extend_dict(names_dict, params)
+        params = b''
+        if embedded_files_id is not None:
+            params += pdf_format(' /Names << /EmbeddedFiles {0} 0 R >>',
+                                 embedded_files_id)
+        pdf.extend_dict(pdf.catalog, params)
 
     # Add attachments
 
@@ -487,11 +509,11 @@ def write_pdf_metadata(fileobj, scale, url_fetcher, attachments,
     # because two links might have the same href, but different titles.
     annot_files = {}
     for page_links in attachment_links:
-        for link_type, target, rectangle, download_name in page_links:
+        for link_type, target, rectangle in page_links:
             if link_type == 'attachment' and target not in annot_files:
                 # TODO: use the title attribute as description
                 annot_files[target] = _write_pdf_attachment(
-                    pdf, (target, None), url_fetcher, download_name)
+                    pdf, (target, None), url_fetcher)
 
     for pdf_page, document_page, page_links in zip(
             pdf.pages, pages, attachment_links):
@@ -533,7 +555,7 @@ def write_pdf_metadata(fileobj, scale, url_fetcher, attachments,
         # would give a feeling similiar to what browsers do with links that
         # span multiple lines.
         annotations = []
-        for link_type, target, rectangle, _ in page_links:
+        for link_type, target, rectangle in page_links:
             if link_type == 'attachment' and annot_files[target] is not None:
                 matrix = cairo.Matrix(
                     xx=scale, yy=-scale, y0=document_page.height * scale)
diff --git a/weasyprint/tools/navigator.py b/weasyprint/tools/navigator.py
index f870864f..3f5e339a 100644
--- a/weasyprint/tools/navigator.py
+++ b/weasyprint/tools/navigator.py
@@ -65,7 +65,7 @@ def render_template(url):
         for width, height, data_url, links, anchors in get_pages(html):
             write('<section style="width: {0}px; height: {1}px">\n'
                   '  <img src="{2}">\n'.format(width, height, data_url))
-            for link_type, target, (pos_x, pos_y, width, height), _ in links:
+            for link_type, target, (pos_x, pos_y, width, height) in links:
                 href = ('#' + target if link_type == 'internal'
                         else '/view/' + target)
                 write('  <a style="left: {0}px; top: {1}px; '


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index ce457645..00000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: WeasyPrint's tests
-on: [push]
-
-jobs:
-  tests:
-    name: ${{ matrix.os }} - ${{ matrix.python-version }}
-    runs-on: ${{ matrix.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: [3.6, 3.7, 3.8, pypy3]
-        exclude:
-          # Pillow wheel missing for this configuration
-          - os: macos-latest
-            python-version: pypy3
-          # Cannot find installed libraries
-          - os: windows-latest
-            python-version: pypy3
-    steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v1
-        with:
-          python-version: ${{ matrix.python-version }}
-      - name: Install DejaVu (Ubuntu)
-        if: matrix.os == 'ubuntu-latest'
-        run: sudo apt-get update -y && sudo apt-get install ttf-dejavu -y
-      - name: Install DejaVu, cairo, pango, gdk-pixbuf and libffi (MacOS)
-        if: matrix.os == 'macos-latest'
-        run: |
-          brew tap homebrew/cask-fonts
-          brew cask install font-dejavu
-          brew install cairo pango gdk-pixbuf libffi
-      - name: Install msys2 and DejaVu (Windows)
-        if: matrix.os == 'windows-latest'
-        run: |
-          choco install -y --no-progress msys2
-          powershell "C:\tools\msys64\usr\bin\bash -lc 'pacman -S mingw-w64-x86_64-ttf-dejavu mingw-w64-x86_64-gtk3 --noconfirm'"
-          xcopy "C:\tools\msys64\mingw64\share\fonts\TTF" "C:\Users\runneradmin\.fonts" /e /i
-          $env:PATH += ";C:\tools\msys64\mingw64\bin"
-          echo "::set-env name=PATH::$env:PATH"
-      - name: Upgrade pip and setuptools
-        run: python -m pip install --upgrade pip setuptools
-      - name: Upgrade cffi (Windows)
-        if: matrix.os == 'windows-latest'
-        run: python -m pip install --upgrade cffi
-      - name: Launch tests
-        run: python setup.py test --addopts="-v"
diff --git a/weasyprint/tests/test_api.py b/weasyprint/tests/test_api.py
index 2801e7aa..4669802d 100644
--- a/weasyprint/tests/test_api.py
+++ b/weasyprint/tests/test_api.py
@@ -121,12 +121,10 @@ def _round_meta(pages):
             anchors[anchor_name] = round(pos_x, 6), round(pos_y, 6)
         links = page.links
         for i, link in enumerate(links):
-            link_type, target, (pos_x, pos_y, width, height), download_name = (
-                link)
+            link_type, target, (pos_x, pos_y, width, height) = link
             link = (
                 link_type, target, (round(pos_x, 6), round(pos_y, 6),
-                                    round(width, 6), round(height, 6)),
-                download_name)
+                                    round(width, 6), round(height, 6)))
             links[i] = link
         bookmarks = page.bookmarks
         for i, (level, label, (pos_x, pos_y), state) in enumerate(bookmarks):
@@ -722,28 +720,28 @@ def test_links():
         </p>
     ''', [
         [
-            ('external', 'http://weasyprint.org', (0, 0, 30, 20), None),
-            ('external', 'http://weasyprint.org', (0, 0, 30, 30), None),
-            ('internal', 'lipsum', (10, 100, 32, 20), None),
-            ('internal', 'lipsum', (10, 100, 32, 32), None)
+            ('external', 'http://weasyprint.org', (0, 0, 30, 20)),
+            ('external', 'http://weasyprint.org', (0, 0, 30, 30)),
+            ('internal', 'lipsum', (10, 100, 32, 20)),
+            ('internal', 'lipsum', (10, 100, 32, 32))
         ],
-        [('internal', 'hello', (0, 0, 200, 30), None)],
+        [('internal', 'hello', (0, 0, 200, 30))],
     ], [
         {'hello': (0, 200)},
         {'lipsum': (0, 0)}
     ], [
         (
             [
-                ('external', 'http://weasyprint.org', (0, 0, 30, 20), None),
-                ('external', 'http://weasyprint.org', (0, 0, 30, 30), None),
-                ('internal', 'lipsum', (10, 100, 32, 20), None),
-                ('internal', 'lipsum', (10, 100, 32, 32), None)
+                ('external', 'http://weasyprint.org', (0, 0, 30, 20)),
+                ('external', 'http://weasyprint.org', (0, 0, 30, 30)),
+                ('internal', 'lipsum', (10, 100, 32, 20)),
+                ('internal', 'lipsum', (10, 100, 32, 32))
             ],
             [('hello', 0, 200)],
         ),
         (
             [
-                ('internal', 'hello', (0, 0, 200, 30), None)
+                ('internal', 'hello', (0, 0, 200, 30))
             ],
             [('lipsum', 0, 0)]),
     ])
@@ -753,20 +751,19 @@ def test_links():
             <body style="width: 200px">
             <a href="../lipsum/é_%E9" style="display: block; margin: 10px 5px">
         ''', [[('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9',
-                (5, 10, 190, 0), None)]],
+                (5, 10, 190, 0))]],
         [{}], [([('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9',
-                  (5, 10, 190, 0), None)], [])],
+                  (5, 10, 190, 0))], [])],
         base_url='http://weasyprint.org/foo/bar/')
-
     assert_links(
         '''
             <body style="width: 200px">
             <div style="display: block; margin: 10px 5px;
                         -weasy-link: url(../lipsum/é_%E9)">
         ''', [[('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9',
-                (5, 10, 190, 0), None)]],
+                (5, 10, 190, 0))]],
         [{}], [([('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9',
-                  (5, 10, 190, 0), None)], [])],
+                  (5, 10, 190, 0))], [])],
         base_url='http://weasyprint.org/foo/bar/')
 
     # Relative URI reference without a base URI: allowed for links
@@ -774,9 +771,8 @@ def test_links():
         '''
             <body style="width: 200px">
             <a href="../lipsum" style="display: block; margin: 10px 5px">
-        ''', [[('external', '../lipsum', (5, 10, 190, 0), None)]], [{}],
-        [([('external', '../lipsum', (5, 10, 190, 0), None)], [])],
-        base_url=None)
+        ''', [[('external', '../lipsum', (5, 10, 190, 0))]], [{}],
+        [([('external', '../lipsum', (5, 10, 190, 0))], [])], base_url=None)
 
     # Relative URI reference without a base URI: not supported for -weasy-link
     assert_links(
@@ -795,11 +791,11 @@ def test_links():
             <a href="#lipsum" id="lipsum"
                 style="display: block; margin: 10px 5px"></a>
             <a href="http://weasyprint.org/" style="display: block"></a>
-        ''', [[('internal', 'lipsum', (5, 10, 190, 0), None),
-               ('external', 'http://weasyprint.org/', (0, 10, 200, 0), None)]],
+        ''', [[('internal', 'lipsum', (5, 10, 190, 0)),
+               ('external', 'http://weasyprint.org/', (0, 10, 200, 0))]],
         [{'lipsum': (5, 10)}],
-        [([('internal', 'lipsum', (5, 10, 190, 0), None),
-           ('external', 'http://weasyprint.org/', (0, 10, 200, 0), None)],
+        [([('internal', 'lipsum', (5, 10, 190, 0)),
+           ('external', 'http://weasyprint.org/', (0, 10, 200, 0))],
           [('lipsum', 5, 10)])],
         base_url=None)
 
@@ -809,10 +805,9 @@ def test_links():
             <div style="-weasy-link: url(#lipsum);
                         margin: 10px 5px" id="lipsum">
         ''',
-        [[('internal', 'lipsum', (5, 10, 190, 0), None)]],
+        [[('internal', 'lipsum', (5, 10, 190, 0))]],
         [{'lipsum': (5, 10)}],
-        [([('internal', 'lipsum', (5, 10, 190, 0), None)],
-          [('lipsum', 5, 10)])],
+        [([('internal', 'lipsum', (5, 10, 190, 0))], [('lipsum', 5, 10)])],
         base_url=None)
 
     assert_links(
@@ -822,11 +817,10 @@ def test_links():
                 <a href="#lipsum"></a>
                 <a href="#missing" id="lipsum"></a>
         ''',
-        [[('internal', 'lipsum', (0, 0, 200, 15), None),
-          ('internal', 'missing', (0, 15, 200, 15), None)]],
+        [[('internal', 'lipsum', (0, 0, 200, 15)),
+          ('internal', 'missing', (0, 15, 200, 15))]],
         [{'lipsum': (0, 15)}],
-        [([('internal', 'lipsum', (0, 0, 200, 15), None)],
-          [('lipsum', 0, 15)])],
+        [([('internal', 'lipsum', (0, 0, 200, 15))], [('lipsum', 0, 15)])],
         base_url=None,
         warnings=[
             'ERROR: No anchor #missing for internal URI reference'])
@@ -837,24 +831,11 @@ def test_links():
             <a href="#lipsum" id="lipsum" style="display: block; height: 20px;
                 transform: rotate(90deg) scale(2)">
         ''',
-        [[('internal', 'lipsum', (30, 10, 40, 200), None)]],
+        [[('internal', 'lipsum', (30, 10, 40, 200))]],
         [{'lipsum': (70, 10)}],
-        [([('internal', 'lipsum', (30, 10, 40, 200), None)],
-          [('lipsum', 70, 10)])],
+        [([('internal', 'lipsum', (30, 10, 40, 200))], [('lipsum', 70, 10)])],
         round=True)
 
-    # Download for attachment
-    assert_links(
-        '''
-            <body style="width: 200px">
-            <a rel=attachment href="pattern.png" download="wow.png"
-                style="display: block; margin: 10px 5px">
-        ''', [[('attachment', 'pattern.png',
-                (5, 10, 190, 0), 'wow.png')]],
-        [{}], [([('attachment', 'pattern.png',
-                  (5, 10, 190, 0), 'wow.png')], [])],
-        base_url=None)
-
 
 # Make relative URL references work with our custom URL scheme.
 uses_relative.append('weasyprint-custom')
diff --git a/weasyprint/tests/test_boxes.py b/weasyprint/tests/test_boxes.py
index 4307355b..12c8431c 100644
--- a/weasyprint/tests/test_boxes.py
+++ b/weasyprint/tests/test_boxes.py
@@ -53,7 +53,7 @@ def _parse_base(html_content, base_url=BASE_URL):
     counter_style = CounterStyle()
     style_for = get_all_computed_styles(document, counter_style=counter_style)
     get_image_from_uri = functools.partial(
-        images.get_image_from_uri, {}, document.url_fetcher, False)
+        images.get_image_from_uri, {}, document.url_fetcher)
     target_collector = TargetCollector()
     return (
         document.etree_element, style_for, get_image_from_uri, base_url,
diff --git a/weasyprint/tests/test_counters.py b/weasyprint/tests/test_counters.py
index a5035b84..ff7f59f5 100644
--- a/weasyprint/tests/test_counters.py
+++ b/weasyprint/tests/test_counters.py
@@ -245,29 +245,25 @@ def test_counters_8():
 def test_counter_styles_1():
     assert_tree(parse_all('''
       <style>
-        body { --var: 'Counter'; counter-reset: p -12 }
+        body { counter-reset: p -12 }
         p { counter-increment: p }
         p:nth-child(1):before { content: '-' counter(p, none) '-'; }
         p:nth-child(2):before { content: counter(p, disc); }
         p:nth-child(3):before { content: counter(p, circle); }
         p:nth-child(4):before { content: counter(p, square); }
         p:nth-child(5):before { content: counter(p); }
-        p:nth-child(6):before { content: var(--var) ':' counter(p); }
-        p:nth-child(7):before { content: counter(p) ':' var(--var); }
       </style>
       <p></p>
       <p></p>
       <p></p>
       <p></p>
       <p></p>
-      <p></p>
-      <p></p>
     '''), [
         ('p', 'Block', [
             ('p', 'Line', [
                 ('p::before', 'Inline', [
                     ('p::before', 'Text', counter)])])])
-        for counter in '--  •  ◦  ▪  -7 Counter:-6 -5:Counter'.split()])
+        for counter in '--  •  ◦  ◾  -7'.split()])
 
 
 @assert_no_logs
diff --git a/weasyprint/tests/test_draw/test_overflow.py b/weasyprint/tests/test_draw/test_overflow.py
index ae2ca055..814ef644 100644
--- a/weasyprint/tests/test_draw/test_overflow.py
+++ b/weasyprint/tests/test_draw/test_overflow.py
@@ -48,7 +48,10 @@ def test_overflow_2():
         ________
     ''', '''
       <style>
-        @page { size: 8px; background: #fff; margin: 2px 2px 3px 2px }
+        @page { size: 8px; background: #fff;
+                margin: 2px;
+                padding-bottom: 2px;
+                border-bottom: 1px transparent solid; }
         body { height: 1px; overflow: hidden; font-size: 0 }
       </style>
       <div><img src="pattern.png"></div>''')
@@ -75,30 +78,6 @@ def test_overflow_3():
       <div></div>''')
 
 
-@assert_no_logs
-def test_overflow_4():
-    # Assert that the page margins aren't clipped by body's overflow
-    assert_pixels('border_box_overflow', 8, 8, '''
-        rr______
-        rr______
-        __BBBB__
-        __BBBB__
-        __BBBB__
-        __BBBB__
-        ________
-        ________
-    ''', '''
-      <style>
-        @page {
-          size: 8px;
-          margin: 2px;
-          background:#fff;
-          @top-left-corner { content: ''; background:#f00; } }
-        body { overflow: auto; background:#00f; }
-      </style>
-      ''')
-
-
 @assert_no_logs
 @requires('cairo', (1, 12, 0))
 @pytest.mark.parametrize('number, css, pixels', (
diff --git a/weasyprint/tests/test_layout/test_flex.py b/weasyprint/tests/test_layout/test_flex.py
index e0c3be1c..17a96fb0 100644
--- a/weasyprint/tests/test_layout/test_flex.py
+++ b/weasyprint/tests/test_layout/test_flex.py
@@ -426,13 +426,3 @@ def test_flex_item_percentage():
     flex, = body.children
     flex_item, = flex.children
     assert flex_item.height == 15
-
-
-@assert_no_logs
-def test_flex_undefined_percentage_height_multiple_lines():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/1204
-    page, = render_pages('''
-      <div style="display: flex; flex-wrap: wrap; height: 100%">
-        <div style="width: 100%">a</div>
-        <div style="width: 100%">b</div>
-      </div>''')
diff --git a/weasyprint/tests/test_layout/test_list.py b/weasyprint/tests/test_layout/test_list.py
index c1a4c28f..c311e3e6 100644
--- a/weasyprint/tests/test_layout/test_list.py
+++ b/weasyprint/tests/test_layout/test_list.py
@@ -17,7 +17,7 @@ from ..testing_utils import assert_no_logs
 @pytest.mark.parametrize('style, character', (
     ('circle', '◦ '),
     ('disc', '• '),
-    ('square', '▪ '),
+    ('square', '◾ '),
 ))
 def test_lists_style(inside, style, character):
     page, = parse('''
diff --git a/weasyprint/tests/test_pdf.py b/weasyprint/tests/test_pdf.py
index 4305b372..9b9b11eb 100644
--- a/weasyprint/tests/test_pdf.py
+++ b/weasyprint/tests/test_pdf.py
@@ -331,6 +331,76 @@ def test_bookmarks_8():
     assert o2.get_value('Title', '(.*)') == b'(g)'
 
 
+@assert_no_logs
+@requires('cairo', (1, 15, 4))
+def test_bookmarks_9():
+    fileobj = io.BytesIO()
+    FakeHTML(string='''
+      <h1 style="bookmark-label: 'h1 on page ' counter(page)">a</h1>
+    ''').write_pdf(target=fileobj)
+    # h1 on page 1
+    pdf_file = pdf.PDFFile(fileobj)
+    outlines = pdf_file.catalog.get_indirect_dict('Outlines', pdf_file)
+    assert outlines.get_type() == 'Outlines'
+    assert outlines.get_value('Count', '(.*)') == b'-1'
+    o1 = outlines.get_indirect_dict('First', pdf_file)
+    assert o1.get_value('Title', '(.*)') == b'(h1 on page 1)'
+
+
+@assert_no_logs
+@requires('cairo', (1, 15, 4))
+def test_bookmarks_10():
+    fileobj = io.BytesIO()
+    FakeHTML(string='''
+      <style>
+      div:before, div:after {
+         content: '';
+         bookmark-level: 1;
+         bookmark-label: 'x';
+      }
+      </style>
+      <div>a</div>
+    ''').write_pdf(target=fileobj)
+    # x
+    # x
+    pdf_file = pdf.PDFFile(fileobj)
+    outlines = pdf_file.catalog.get_indirect_dict('Outlines', pdf_file)
+    assert outlines.get_type() == 'Outlines'
+    assert outlines.get_value('Count', '(.*)') == b'-2'
+    o1 = outlines.get_indirect_dict('First', pdf_file)
+    assert o1.get_value('Title', '(.*)') == b'(x)'
+    o2 = o1.get_indirect_dict('Next', pdf_file)
+    assert o2.get_value('Title', '(.*)') == b'(x)'
+
+
+@assert_no_logs
+@requires('cairo', (1, 15, 4))
+def test_bookmarks_11():
+    fileobj = io.BytesIO()
+    FakeHTML(string='''
+      <div style="display:inline; white-space:pre;
+       bookmark-level:1; bookmark-label:'a'">
+      a
+      a
+      a
+      </div>
+      <div style="bookmark-level:1; bookmark-label:'b'">
+        <div>b</div>
+        <div style="break-before:always">c</div>
+      </div>
+    ''').write_pdf(target=fileobj)
+    # a
+    # b
+    pdf_file = pdf.PDFFile(fileobj)
+    outlines = pdf_file.catalog.get_indirect_dict('Outlines', pdf_file)
+    assert outlines.get_type() == 'Outlines'
+    assert outlines.get_value('Count', '(.*)') == b'-2'
+    o1 = outlines.get_indirect_dict('First', pdf_file)
+    assert o1.get_value('Title', '(.*)') == b'(a)'
+    o2 = o1.get_indirect_dict('Next', pdf_file)
+    assert o2.get_value('Title', '(.*)') == b'(b)'
+
+
 @assert_no_logs
 def test_links_none():
     fileobj = io.BytesIO()
diff --git a/weasyprint/tests/test_variables.py b/weasyprint/tests/test_variables.py
index c36cb849..2477c495 100644
--- a/weasyprint/tests/test_variables.py
+++ b/weasyprint/tests/test_variables.py
@@ -11,8 +11,6 @@ import pytest
 from ..css.properties import KNOWN_PROPERTIES
 from .test_boxes import render_pages as parse
 
-SIDES = ('top', 'right', 'bottom', 'left')
-
 
 def test_variable_simple():
     page, = parse('''
@@ -85,23 +83,6 @@ def test_variable_chain():
     assert paragraph.width == 10
 
 
-def test_variable_partial_1():
-    page, = parse('''
-      <style>
-        html { --var: 10px }
-        div { margin: 0 0 0 var(--var) }
-      </style>
-      <div></div>
-    ''')
-    html, = page.children
-    body, = html.children
-    div, = body.children
-    assert div.margin_top == 0
-    assert div.margin_right == 0
-    assert div.margin_bottom == 0
-    assert div.margin_left == 10
-
-
 def test_variable_initial():
     page, = parse('''
       <style>