[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\nopen_collective: courtbouillon\n"
  },
  {
    "path": ".github/workflows/doconfly.yml",
    "content": "name: doconfly\non:\n  push:\n    branches:\n      - main\n    tags:\n      - \"*\"\n\njobs:\n  doconfly:\n    name: doconfly job\n    runs-on: ubuntu-latest\n    env:\n      PORT: ${{ secrets.PORT }}\n      SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}\n      TAKOYAKI: ${{ secrets.TAKOYAKI }}\n      USER: ${{ secrets.USER }}\n      DOCUMENTATION_PATH: ${{ secrets.DOCUMENTATION_PATH }}\n      DOCUMENTATION_URL: ${{ secrets.DOCUMENTATION_URL }}\n    steps:\n      - run: |\n          which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )\n          eval $(ssh-agent -s)\n          echo \"$SSH_PRIVATE_KEY\" | tr -d '\\r' | ssh-add -\n          mkdir -p ~/.ssh\n          chmod 700 ~/.ssh\n          ssh-keyscan -p $PORT $TAKOYAKI >> ~/.ssh/known_hosts\n          chmod 644 ~/.ssh/known_hosts\n          ssh $USER@$TAKOYAKI -p $PORT \"doconfly/doconfly.sh $GITHUB_REPOSITORY $GITHUB_REF $DOCUMENTATION_PATH $DOCUMENTATION_URL\"\n"
  },
  {
    "path": ".github/workflows/exe.yml",
    "content": "name: WeasyPrint’s exe generation\non: [push]\n\njobs:\n  generate:\n    name: ${{ matrix.os }}\n    runs-on: 'windows-latest'\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.13'\n      - name: Use absolute imports and install Pango (Windows)\n        run: |\n          C:\\msys64\\usr\\bin\\bash -lc 'pacman -S mingw-w64-x86_64-pango mingw-w64-x86_64-sed --noconfirm'\n          C:\\msys64\\mingw64\\bin\\sed -i 's/^from \\. /from weasyprint /' weasyprint/__main__.py\n          C:\\msys64\\mingw64\\bin\\sed  -i 's/^from \\./from weasyprint\\./' weasyprint/__main__.py\n          echo \"C:\\msys64\\mingw64\\bin\" | Out-File -FilePath $env:GITHUB_PATH\n          rm C:\\msys64\\mingw64\\bin\\python.exe\n      - name: Install requirements\n        run: python -m pip install . pyinstaller\n      - name: Generate executable\n        run: python -m PyInstaller weasyprint/__main__.py -n weasyprint -F\n      - name: Test executable\n        run: dist/weasyprint --info\n      - name: Store executable\n        uses: actions/upload-artifact@v4\n        with:\n          name: weasyprint-windows\n          path: |\n            dist/weasyprint\n            dist/weasyprint.exe\n            README.rst\n            LICENSE\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release new version\non:\n  push:\n    tags:\n      - v*\n\njobs:\n  pypi-publish:\n    name: Upload release to PyPI\n    runs-on: ubuntu-latest\n    environment:\n      name: pypi\n      url: https://pypi.org/p/weasyprint\n    permissions:\n      id-token: write\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n      - name: Install requirements\n        run: python -m pip install flit\n      - name: Build packages\n        run: flit build\n      - name: Publish package distributions to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n  add-version:\n    name: Add version to GitHub\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n      - name: Install requirements\n        run: sudo apt-get install pandoc\n      - name: Generate content\n        run: |\n          pandoc docs/changelog.rst -f rst -t gfm | csplit - /##/ \"{1}\" -f .part\n          sed -r \"s/^([A-Z].*)\\:\\$/## \\1/\" .part01 | sed -r \"s/^ *//\" | sed -rz \"s/([^\\n])\\n([^\\n^-])/\\1 \\2/g\" | tail -n +5 > .body\n      - name: Create Release\n        uses: softprops/action-gh-release@v2\n        with:\n          body_path: .body\n"
  },
  {
    "path": ".github/workflows/test_pdfa.yml",
    "content": "name: WeasyPrint's PDF/A tests\non: [push, pull_request]\n\nenv:\n  PDF_FOLDER: 'pdfs'\n  VERA_FOLDER: 'vera'\n\njobs:\n  pdf-ua:\n    name: Test PDF/A samples\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install requirements\n        run: python -m pip install .\n      - name: Create output folders\n        run: |\n          mkdir ${{env.PDF_FOLDER}}\n          mkdir ${{env.VERA_FOLDER}}\n      - name: Clone samples repository\n        run: git clone https://github.com/CourtBouillon/weasyprint-samples.git\n      - name: Generate samples\n        run: |\n          python -m weasyprint --pdf-variant pdf/a-1a weasyprint-samples/book/book.html -s weasyprint-samples/book/book-classical.css ${{env.PDF_FOLDER}}/book-classical.pdf\n          python -m weasyprint --pdf-variant pdf/a-2u weasyprint-samples/book/book.html -s weasyprint-samples/book/book.css -s <(echo '* { image-rendering: crisp-edges }') ${{env.PDF_FOLDER}}/book-fancy.pdf\n          python -m weasyprint --pdf-variant pdf/a-2b weasyprint-samples/letter/letter.html -s <(echo '* { image-rendering: crisp-edges }') ${{env.PDF_FOLDER}}/letter.pdf\n          python -m weasyprint --pdf-variant pdf/a-3a -s <(echo '* { image-rendering: crisp-edges }') weasyprint-samples/report/report.html ${{env.PDF_FOLDER}}/report.pdf\n      - name: Install VeraPDF\n        run: |\n          wget https://software.verapdf.org/releases/verapdf-installer.zip\n          unzip verapdf-installer.zip\n          java -DINSTALL_PATH=/tmp -jar verapdf-greenfield-*/verapdf-izpack-installer-*.jar -options-system\n      - name: Generate VeraPDF reports\n        run: |\n          /tmp/verapdf -f 1a --format html ${{env.PDF_FOLDER}}/book-classical.pdf > ${{env.VERA_FOLDER}}/book-classical-verapdf.html\n          /tmp/verapdf -f 2u --format html ${{env.PDF_FOLDER}}/book-fancy.pdf > ${{env.VERA_FOLDER}}/book-fancy-verapdf.html\n          /tmp/verapdf -f 2b --format html ${{env.PDF_FOLDER}}/letter.pdf > ${{env.VERA_FOLDER}}/letter-verapdf.html\n          /tmp/verapdf -f 3a --format html ${{env.PDF_FOLDER}}/report.pdf > ${{env.VERA_FOLDER}}/report-verapdf.html\n          /tmp/verapdf --format html ${{env.PDF_FOLDER}}/ > ${{env.VERA_FOLDER}}/summary.html\n      - name: Archive generated PDFs\n        if: ${{ always() }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: vera-results\n          path: |\n            ${{env.VERA_FOLDER}}\n            ${{env.PDF_FOLDER}}\n          retention-days: 1\n"
  },
  {
    "path": ".github/workflows/test_pdfua.yml",
    "content": "name: WeasyPrint's PDF/UA tests\non: [push, pull_request]\n\nenv:\n  PDF_FOLDER: 'pdfs'\n  VERA_FOLDER: 'vera'\n\njobs:\n  pdf-ua:\n    name: Test PDF/UA samples\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install requirements\n        run: python -m pip install .\n      - name: Create output folders\n        run: |\n          mkdir ${{env.PDF_FOLDER}}\n          mkdir ${{env.VERA_FOLDER}}\n      - name: Clone samples repository\n        run: git clone https://github.com/CourtBouillon/weasyprint-samples.git\n      - name: Generate samples\n        run: |\n          python -m weasyprint --pdf-variant pdf/ua-1 weasyprint-samples/book/book.html -s weasyprint-samples/book/book-classical.css ${{env.PDF_FOLDER}}/book-classical.pdf\n          python -m weasyprint --pdf-variant pdf/ua-1 weasyprint-samples/book/book.html -s weasyprint-samples/book/book.css ${{env.PDF_FOLDER}}/book-fancy.pdf\n          python -m weasyprint --pdf-variant pdf/ua-1 weasyprint-samples/letter/letter.html ${{env.PDF_FOLDER}}/letter.pdf\n          python -m weasyprint --pdf-variant pdf/ua-1 weasyprint-samples/report/report.html ${{env.PDF_FOLDER}}/report.pdf\n      - name: Install VeraPDF\n        run: |\n          wget https://software.verapdf.org/releases/verapdf-installer.zip\n          unzip verapdf-installer.zip\n          java -DINSTALL_PATH=/tmp -jar verapdf-greenfield-*/verapdf-izpack-installer-*.jar -options-system\n      - name: Generate VeraPDF reports\n        run: |\n          /tmp/verapdf -f ua1 --format html ${{env.PDF_FOLDER}}/book-classical.pdf > ${{env.VERA_FOLDER}}/book-classical-verapdf.html\n          /tmp/verapdf -f ua1 --format html ${{env.PDF_FOLDER}}/book-fancy.pdf > ${{env.VERA_FOLDER}}/book-fancy-verapdf.html\n          /tmp/verapdf -f ua1 --format html ${{env.PDF_FOLDER}}/letter.pdf > ${{env.VERA_FOLDER}}/letter-verapdf.html\n          /tmp/verapdf -f ua1 --format html ${{env.PDF_FOLDER}}/report.pdf > ${{env.VERA_FOLDER}}/report-verapdf.html\n          /tmp/verapdf -f ua1 --format html ${{env.PDF_FOLDER}}/ > ${{env.VERA_FOLDER}}/summary.html\n      - name: Archive generated PDFs\n        if: ${{ always() }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: vera-results\n          path: |\n            ${{env.VERA_FOLDER}}\n            ${{env.PDF_FOLDER}}\n          retention-days: 1\n"
  },
  {
    "path": ".github/workflows/test_samples.yml",
    "content": "name: WeasyPrint's samples tests\non: [push, pull_request]\n\nenv:\n  REPORTS_FOLDER: 'report'\n\njobs:\n  samples:\n    name: Generate samples\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Upgrade pip and setuptools\n        run: python -m pip install --upgrade pip setuptools\n      - name: Install requirements\n        run: python -m pip install .\n      - name: Clone samples repository\n        run: git clone https://github.com/CourtBouillon/weasyprint-samples.git\n      - name: Create output folder\n        run: mkdir ${{env.REPORTS_FOLDER}}\n      - name: Book classical\n        run: python -m weasyprint weasyprint-samples/book/book.html -s weasyprint-samples/book/book-classical.css ${{env.REPORTS_FOLDER}}/book-classical.pdf\n      - name: Book fancy\n        run: python -m weasyprint weasyprint-samples/book/book.html -s weasyprint-samples/book/book.css ${{env.REPORTS_FOLDER}}/book-fancy.pdf\n      - name: Invoice\n        run: python -m weasyprint weasyprint-samples/invoice/invoice.html ${{env.REPORTS_FOLDER}}/invoice.pdf\n      - name: Letter\n        run: python -m weasyprint weasyprint-samples/letter/letter.html ${{env.REPORTS_FOLDER}}/letter.pdf\n      - name: Poster\n        run: python -m weasyprint weasyprint-samples/poster/poster.html -s weasyprint-samples/poster/poster.css ${{env.REPORTS_FOLDER}}/poster.pdf\n      - name: Flyer\n        run: python -m weasyprint weasyprint-samples/poster/poster.html -s weasyprint-samples/poster/flyer.css ${{env.REPORTS_FOLDER}}/flyer.pdf\n      - name: Report\n        run: python -m weasyprint weasyprint-samples/report/report.html ${{env.REPORTS_FOLDER}}/report.pdf\n      - name: Ticket\n        run: python -m weasyprint weasyprint-samples/ticket/ticket.html ${{env.REPORTS_FOLDER}}/ticket.pdf\n      - name: Archive generated PDFs\n        uses: actions/upload-artifact@v4\n        with:\n          name: generated-documents\n          path: ${{env.REPORTS_FOLDER}}\n          retention-days: 1\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: WeasyPrint's tests\non: [push, pull_request]\n\njobs:\n  tests:\n    name: ${{ matrix.os }} - ${{ matrix.python-version }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n        python-version: ['3.14']\n        include:\n          - os: ubuntu-latest\n            python-version: '3.10'\n          - os: ubuntu-latest\n            python-version: 'pypy-3.11'\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install Ghostscript (Ubuntu)\n        if: matrix.os == 'ubuntu-latest'\n        run: sudo apt-get update -y && sudo apt-get install ghostscript -y\n      - name: Install DejaVu, Pango and Ghostscript (MacOS)\n        if: matrix.os == 'macos-latest'\n        run: |\n          brew update\n          brew install --cask font-dejavu\n          brew install pango ghostscript\n      - name: Install DejaVu, Pango and Ghostscript (Windows)\n        if: matrix.os == 'windows-latest'\n        run: |\n          C:\\msys64\\usr\\bin\\bash -lc 'pacman -S mingw-w64-x86_64-ttf-dejavu mingw-w64-x86_64-pango mingw-w64-x86_64-ghostscript --noconfirm'\n          xcopy \"C:\\msys64\\mingw64\\share\\fonts\\TTF\" \"C:\\Users\\runneradmin\\.fonts\" /e /i\n          echo \"C:\\msys64\\mingw64\\bin\" | Out-File -FilePath $env:GITHUB_PATH\n          rm C:\\msys64\\mingw64\\bin\\python.exe\n      - name: Upgrade pip and setuptools\n        run: python -m pip install --upgrade pip setuptools\n      - name: Install tests’ requirements\n        run: python -m pip install .[test] pytest-xdist\n      - name: Launch tests\n        run: python -m pytest -n auto\n        env:\n          DYLD_FALLBACK_LIBRARY_PATH: /opt/homebrew/lib\n      - name: Check coding style\n        run: python -m ruff check\n"
  },
  {
    "path": ".gitignore",
    "content": "*.pyc\n.cache\n/.coverage\n/build\n/dist\n/docs/_build\n/pytest_cache\n/tests/draw/results\n/venv\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2011-2021, Simon Sapin and contributors.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.rst",
    "content": "**The Awesome Document Factory**\n\nWeasyPrint is a smart solution helping web developers to create PDF\ndocuments. It turns simple HTML pages into gorgeous statistical reports,\ninvoices, tickets…\n\nFrom a technical point of view, WeasyPrint is a visual rendering engine for\nHTML and CSS that can export to PDF. It aims to support web standards for\nprinting. WeasyPrint is free software made available under a BSD license.\n\nIt is based on various libraries but *not* on a full rendering engine like\nWebKit or Gecko. The CSS layout engine is written in Python, designed for\npagination, and meant to be easy to hack on.\n\n* Free software: BSD license\n* For Python 3.10+, tested on CPython and PyPy\n* Documentation: https://doc.courtbouillon.org/weasyprint\n* Examples: https://weasyprint.org/#samples\n* Changelog: https://github.com/Kozea/WeasyPrint/releases\n* Code, issues, tests: https://github.com/Kozea/WeasyPrint\n* Code of conduct: https://www.courtbouillon.org/code-of-conduct\n* Professional support: https://www.courtbouillon.org\n* Donation: https://opencollective.com/courtbouillon\n\nWeasyPrint has been created and developed by Kozea (https://kozea.fr/).\nProfessional support, maintenance and community management is provided by\nCourtBouillon (https://www.courtbouillon.org/).\n\nCopyrights are retained by their contributors, no copyright assignment is\nrequired to contribute to WeasyPrint. Unless explicitly stated otherwise, any\ncontribution intentionally submitted for inclusion is licensed under the BSD\n3-clause license, without any additional terms or conditions. For full\nauthorship information, see the version control history.\n"
  },
  {
    "path": "docs/api_reference.rst",
    "content": "API Reference\n=============\n\n.. currentmodule:: weasyprint\n\n\nThis page is for WeasyPrint |version|. See :doc:`changelog </changelog>` for\nolder versions.\n\n\nAPI Stability\n-------------\n\nEverything described here is considered “public”: this is what you can rely\non. We will try to maintain backward-compatibility, and we really often do, but\nthere is no hard promise.\n\nAnything else should not be used outside of WeasyPrint itself. We reserve\nthe right to change it or remove it at any point. Use it at your own risk,\nor have dependency to a specific WeasyPrint version.\n\n\nVersioning\n----------\n\nWeasyPrint provides frequent major releases, and minor releases with only bug\nfixes. Versioning is close to what many browsers do, including Firefox and\nChrome: big major numbers, small minor numbers.\n\nEven if each version does not break the API, each version does break the way\ndocuments are rendered, which is what really matters at the end. Providing\nminor versions would give the illusion that developers can just update\nWeasyPrint without checking that everything works.\n\nUnfortunately, we have the same problem as the other browsers: when a new\nversion is released, most of the user's websites are rendered exactly the same,\nbut a small part is not. And the only ways to know that, for web developers,\nare to read the changelog and to check that their pages are correctly rendered.\n\nMore about this choice can be found in\nissue `#900`_.\n\n.. _#900: https://github.com/Kozea/WeasyPrint/issues/900\n\n\nCommand-line API\n----------------\n\n.. autofunction:: weasyprint.__main__.main(argv=sys.argv)\n\n\nPython API\n----------\n\n.. autoclass:: HTML(input, **kwargs)\n    :members:\n.. autoclass:: CSS(input, **kwargs)\n.. autoclass:: Attachment(input, **kwargs)\n.. autodata:: DEFAULT_OPTIONS\n\n.. module:: weasyprint.document\n.. autoclass:: Document\n    :members:\n.. autoclass:: DocumentMetadata()\n    :members:\n.. autoclass:: Page()\n    :members:\n    :exclude-members: paint\n\n.. module:: weasyprint.urls\n.. autoclass:: URLFetcher\n    :members:\n    :member-order: bysource\n.. autoclass:: URLFetcherResponse\n.. autoclass:: FatalURLFetchingError\n\n.. module:: weasyprint.text.fonts\n.. autoclass:: FontConfiguration()\n\n.. module:: weasyprint.css.counters\n.. autoclass:: CounterStyle()\n\n\nSupported Features\n------------------\n\n\nURLs\n~~~~\n\nWeasyPrint can read normal files, HTTP, FTP and `data URIs`_. It will follow\nHTTP redirects but more advanced features like cookies and authentication\nare currently not supported, although a custom :ref:`URL fetcher\n<URL Fetchers>` can help.\n\n.. _data URIs: https://en.wikipedia.org/wiki/Data_URI_scheme\n\n\nHTML\n~~~~\n\nSupported HTML Tags\n+++++++++++++++++++\n\nMany HTML elements are implemented in CSS through the HTML5\n`User-Agent stylesheet`_.\n\nSome elements need special treatment:\n\n* The ``<base>`` element, if present, determines the base for relative URLs.\n* CSS stylesheets can be embedded in ``<style>`` elements or linked by\n  ``<link rel=stylesheet>`` elements.\n* ``<img>``, ``<embed>`` or ``<object>`` elements accept images either\n  in raster formats supported by Pillow_ (including PNG, JPEG, GIF, ...)\n  or in SVG. SVG images are not rasterized but rendered\n  as vectors in the PDF output.\n\nHTML `presentational hints`_ are not supported by default, but most of them can\nbe supported:\n\n* by using the ``--presentational-hints`` CLI parameter, or\n* by setting the ``presentational_hints`` parameter of the ``HTML.render`` or\n  ``HTML.write_*`` methods to ``True``.\n\nPresentational hints include a wide array of attributes that direct styling in\nHTML, including font ``color`` and ``size``, list attributes like ``type`` and\n``start``, various table alignment attributes, and others. If the document\ngenerated by WeasyPrint is missing some of the features you expect from the\nHTML, try to enable this option.\n\n.. _User-Agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/main/weasyprint/css/html5_ua.css\n.. _presentational hints: https://www.w3.org/TR/html5/rendering.html#presentational-hints\n.. _Pillow: https://python-pillow.org/\n\nStylesheet Origins\n++++++++++++++++++\n\nHTML documents are rendered with stylesheets from three *origins*:\n\n* The HTML5 `user agent stylesheet`_ (defines the default appearance\n  of HTML elements);\n* Author stylesheets embedded in the document in ``<style>`` elements\n  or linked by ``<link rel=stylesheet>`` elements;\n* User stylesheets provided in the API.\n\nKeep in mind that *user* stylesheets have a lower priority than *author*\nstylesheets in the cascade_, unless you use `!important`_ in declarations\nto raise their priority.\n\n.. _user agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/main/weasyprint/css/html5_ua.css\n.. _cascade: https://www.w3.org/TR/CSS21/cascade.html#cascading-order\n.. _!important: https://www.w3.org/TR/CSS21/cascade.html#important-rules\n\n\nPDF\n~~~\n\nIn addition to text, raster and vector graphics, WeasyPrint’s PDF files\ncan contain hyperlinks, bookmarks, attachments and forms.\n\nHyperlinks will be clickable in PDF viewers that support them. They can\nbe either internal, to another part of the same document (eg.\n``<a href=\"#pdf\">``) or external, to an URL. External links are resolved\nto absolute URLs: ``<a href=\"/blog-articles/\">`` on the CourtBouillon website\nwould always point to https://www.courtbouillon.org/blog-articles/ in PDF\nfiles.\n\nPDF bookmarks are also called outlines and are generally shown in a\nsidebar. Clicking on an entry scrolls the matching part of the document\ninto view. By default all ``<h1>`` to ``<h6>`` titles generate bookmarks,\nbut this can be controlled with `PDF bookmarks`_.)\n\nAttachments are related files, embedded in the PDF itself. They can be\nspecified through ``<link rel=attachment>`` elements to add resources globally\nor through regular links with ``<a rel=attachment>`` to attach a resource that\ncan be saved by clicking on said link. The ``title`` attribute can be used as\ndescription of the attachment.\n\nGenerated documents can also include PDF forms, using the ``appearance: auto``\nCSS property or the ``--pdf-forms`` CLI option.\n\nThe generation of PDF/A and PDF/UA documents is supported. However, the\ngenerated documents are not guaranteed to be valid, and users have the\nresponsibility to check that they follow the rules listed by the related\nspecifications.\n\n\nFonts\n~~~~~\n\nWeasyPrint can use any font that Pango_ can find installed on the system. Fonts are\nautomatically embedded in PDF files and are subset by default to only include the glyphs\nused in the PDF. Subsetting is done with hb-subset_ when available on the system,\nor by the slower fontTools_ library as a fallback.\n\nPango_ always uses Fontconfig_ to access fonts, even on Windows and macOS. You\ncan list the available fonts thanks to the ``fc-list`` command, and know which\nfont is matched by a given pattern thanks to ``fc-match``. Copying a font file\ninto the ``~/.local/share/fonts`` directory is generally enough to install a\nnew font. WeasyPrint should support the major font formats handled by HarfBuzz_.\n\nWeasyPrint follows the Fontconfig_ configuration of the system. This default\nconfiguration is often useful for many cases: font fallbacks for missing glyphs, aliases\nfor standard font families like \"serif\" or \"monospace\", colored variants for emojis,\netc. But some of these default rules can sometimes interfere with CSS rules, and it may\nbe interesting to disable them if you need to tweak details about font management.\n\nWhen a Unicode code point is not supported by a font and its fallbacks, the font’s `.notdef glyph`_ is\ndisplayed instead, and a warning is displayed in logs. Because of the way Pango handles\nthis case, the .notdef glyph may be rendered with an incorrect width (advance), but the layout of\nthe other glyphs is correct. Text search and selection work as if the glyph was\navailable.\n\n.. _hb-subset: https://harfbuzz.github.io/harfbuzz-hb-subset.html\n.. _fontTools: https://fonttools.readthedocs.io/en/latest/\n.. _Pango: https://www.pango.org/\n.. _Fontconfig: https://www.freedesktop.org/wiki/Software/fontconfig/\n.. _HarfBuzz: https://harfbuzz.github.io/\n.. _.notdef glyph: https://en.wikipedia.org/wiki/Notdef_glyph\n\n\nCSS\n~~~\n\nWeasyPrint supports many of the `CSS specifications`_ written by the W3C. You\nwill find in this chapter a comprehensive list of the specifications or drafts\nwith at least one feature implemented in WeasyPrint.\n\n.. _CSS specifications: https://www.w3.org/Style/CSS/current-work\n\nCSS Level 2 Revision 1\n++++++++++++++++++++++\n\nThe `CSS Level 2 Revision 1`_ specification, best known as CSS 2.1, is pretty\nwell supported by WeasyPrint. Since version 0.11, it passes the famous `Acid2\nTest`_.\n\nThe CSS 2.1 features listed here are **not** supported:\n\n* On tables: `visibility: collapse`_.\n* Minimum and maximum height_ on table-related boxes.\n* Minimum and maximum width_ and height_ on page-margin boxes.\n* Conforming `font matching algorithm`_. Currently ``font-family``\n  is passed as-is to Pango.\n* Right-to-left or `bi-directional text`_.\n* `System colors`_ and `system fonts`_. The former are deprecated in `CSS Color\n  Module Level 3`_.\n\n.. _CSS Level 2 Revision 1: https://www.w3.org/TR/CSS21/\n.. _Acid2 Test: https://www.webstandards.org/files/acid2/test.html\n.. _empty-cells: https://www.w3.org/TR/CSS21/tables.html#empty-cells\n.. _visibility\\: collapse: https://www.w3.org/TR/CSS21/tables.html#dynamic-effects\n.. _width: https://www.w3.org/TR/CSS21/visudet.html#min-max-widths\n.. _height: https://www.w3.org/TR/CSS21/visudet.html#min-max-heights\n.. _font matching algorithm: https://www.w3.org/TR/CSS21/fonts.html#algorithm\n.. _Bi-directional text: https://www.w3.org/TR/CSS21/visuren.html#direction\n.. _System colors: https://www.w3.org/TR/CSS21/ui.html#system-colors\n.. _system fonts: https://www.w3.org/TR/CSS21/fonts.html#propdef-font\n.. _CSS Color Module Level 3: https://www.w3.org/TR/css-color-3/\n\nTo the best of our knowledge, everything else that applies to the\nprint media **is** supported. Please report a bug if you find this list\nincomplete.\n\nSelectors Level 3 / 4\n+++++++++++++++++++++\n\nWith the exceptions noted here, all `Selectors Level 3`_ are supported.\n\nPDF is generally not interactive. The ``:hover``, ``:active``, ``:focus``,\n``:target`` and ``:visited`` pseudo-classes are accepted as valid but\nnever match anything.\n\nEverything in `Selectors Level 4`_ is supported, except:\n\n- ``:dir``,\n- input pseudo-classes (``:valid``, ``:invalid``…),\n- column selector (``||``, ``:nth-col()``, ``:nth-last-col()``).\n\n.. _Selectors Level 3: https://www.w3.org/TR/selectors-3/\n.. _Selectors Level 4: https://www.w3.org/TR/selectors-4/\n\nCSS Text Module Level 3 / 4\n+++++++++++++++++++++++++++\n\nThe `CSS Text Module Level 3`_ and `CSS Text Module Level 4`_ are working\ndrafts defining \"properties for text manipulation\" and covering \"line breaking,\njustification and alignment, white space handling, and text transformation\".\n\nAmong their features, some are already included in CSS 2.1, sometimes with\nmissing or different values (``text-indent``, ``text-align``,\n``letter-spacing``, ``word-spacing``, ``text-transform``, ``white-space``).\n\nNew properties defined in Level 3 are supported:\n\n- the ``overflow-wrap`` property replacing ``word-wrap``;\n- the ``break-all`` value of the ``word-break`` property (see `#1153`_);\n- the ``full-width`` value of the ``text-transform`` property; and\n- the ``start``, ``end`` and ``justify-all`` values of the ``text-align`` property;\n- the ``text-align-last`` and ``text-justify`` properties; and\n- the ``tab-size`` property.\n\nProperties controlling hyphenation_ are supported by WeasyPrint:\n\n- ``hyphens``,\n- ``hyphenate-character``,\n- ``hyphenate-limit-chars``, and\n- ``hyphenate-limit-zone``.\n\nTo get automatic hyphenation, you to set it to ``auto``\n*and* have the ``lang`` HTML attribute set to one of the languages\n`supported by Pyphen`_.\n\n.. code-block:: html\n\n    <!doctype html>\n    <html lang=en>\n    <style>\n      html { hyphens: auto }\n    </style>\n    …\n\nAutomatic hyphenation can be disabled again with the ``manual`` value:\n\n.. code-block:: css\n\n    html { hyphens: auto }\n    a[href]::after { content: ' [' attr(href) ']'; hyphens: manual }\n\nThe other features provided by `CSS Text Module Level 3`_ are **not**\nsupported:\n\n- the ``line-break`` property;\n- the ``match-parent`` value of the ``text-align`` property;\n- the ``text-indent`` and ``hanging-punctuation`` properties.\n\nThe other features provided by `CSS Text Module Level 4`_ are **not**\nsupported:\n\n- the ``text-space-collapse`` and ``text-space-trim`` properties;\n- the ``text-wrap``, ``wrap-before``, ``wrap-after`` and ``wrap-inside``\n  properties;\n- the ``text-align`` property with an alignment character;\n- the ``pre-wrap-auto`` value of the ``white-space`` property; and\n- the ``text-spacing`` property.\n\n.. _#1153: https://github.com/Kozea/WeasyPrint/issues/1153\n.. _supported by Pyphen: https://github.com/Kozea/Pyphen/tree/main/pyphen/dictionaries\n.. _hyphenation: https://www.w3.org/TR/css-text-3/#hyphenation\n.. _CSS Text Module Level 3: https://www.w3.org/TR/css-text-3/\n.. _CSS Text Module Level 4: https://www.w3.org/TR/css-text-4/\n\nCSS Fonts Module Level 3 / 4\n++++++++++++++++++++++++++++\n\nThe `CSS Fonts Module Level 3`_ is a candidate recommendation describing \"how\nfont properties are specified and how font resources are loaded dynamically\".\n\nWeasyPrint supports the ``font-size``, ``font-stretch``, ``font-style`` and\n``font-weight`` properties, coming from CSS 2.1.\n\nWeasyPrint also supports the following font features added in Level 3:\n- ``font-kerning``,\n- ``font-variant-ligatures``,\n- ``font-variant-position``,\n- ``font-variant-caps``,\n- ``font-variant-numeric``,\n- ``font-variant-east-asian``,\n- ``font-feature-settings``, and\n- ``font-language-override``.\n\n``font-family`` is supported. The string is given to Pango that tries to find a\nmatching font in a way different from what is defined in the recommendation,\nbut that should not be a problem for common use.\n\nThe shorthand ``font`` and ``font-variant`` properties are supported.\n\nWeasyPrint supports the ``@font-face`` rule.\n\nWeasyPrint does **not** support the ``@font-feature-values`` rule and the\nvalues of ``font-variant-alternates`` other than ``normal`` and\n``historical-forms``.\n\nFrom `CSS Fonts Module Level 4`_ we only support the\n``font-variation-settings`` property enabling specific font variations.\n\n.. _CSS Fonts Module Level 3: https://www.w3.org/TR/css-fonts-3/\n.. _CSS Fonts Module Level 4: https://www.w3.org/TR/css-fonts-4/\n\n\nCSS Paged Media Module Level 3\n++++++++++++++++++++++++++++++\n\nThe `CSS Paged Media Module Level 3`_ is a working draft including features for\npaged media \"describing how:\n\n- page breaks are created and avoided;\n- the page properties such as size, orientation, margins, border, and padding\n  are specified;\n- headers and footers are established within the page margins;\n- content such as page counters are placed in the headers and footers; and\n- orphans and widows can be controlled.\"\n\nAll the features of this draft are available, including:\n\n- the ``@page`` rule and the ``:left``, ``:right``, ``:first`` and ``:blank``\n  selectors;\n- the page margin boxes;\n- the page-based counters (with known limitations  `#93`_);\n- the page ``size``, ``bleed`` and ``marks`` properties;\n- the named pages.\n\n.. _CSS Paged Media Module Level 3: https://drafts.csswg.org/css-page-3/\n.. _#93: https://github.com/Kozea/WeasyPrint/issues/93\n\nCSS Generated Content for Paged Media Module\n++++++++++++++++++++++++++++++++++++++++++++\n\nThe `CSS Generated Content for Paged Media Module`_ (GCPM) is a working draft\ndefining \"new properties and values, so that authors may bring new techniques\n(running headers and footers, footnotes, page selection) to paged media\".\n\n`Page selectors`_ are supported by WeasyPrint. You can select pages according\nto their position in the document:\n\n.. code-block:: css\n\n    @page :nth(3) { background: red } /* Third page */\n    @page :nth(2n+1) { background: green } /* Odd pages */\n    @page :nth(1 of chapter) { background: blue } /* First pages of chapters */\n\nYou can also use `running elements`_ to put HTML boxes into the page margins\n(but the ``start`` parameter of ``element()`` is not supported).\n\nFootnotes_ are supported. You can put a box in the footnote area using the\n``float: footnote`` property. Footnote markers and footnote calls can be\ndefined using the ``::footnote-marker`` and ``::footnote-call``\npseudo-elements. You can also change the way footnotes are displayed using the\n``footnote-display`` property (``compact`` is not supported), and influence\nover the rendering of difficult pages with ``footnote-policy``.\n\n.. _CSS Generated Content for Paged Media Module: https://www.w3.org/TR/css-gcpm-3/\n.. _Page selectors: https://www.w3.org/TR/css-gcpm-3/#document-page-selectors\n.. _running elements: https://www.w3.org/TR/css-gcpm-3/#running-elements\n.. _Footnotes: https://www.w3.org/TR/css-gcpm-3/#footnotes\n\nCSS Generated Content Module Level 3\n++++++++++++++++++++++++++++++++++++\n\nThe `CSS Generated Content Module Level 3`_ is a working draft helping \"authors\n[who] sometimes want user agents to render content that does not come from the\ndocument tree. One familiar example of this is numbered headings\n[…]. Similarly, authors may want the user agent to insert the word \"Figure\"\nbefore the caption of a figure […], or replacing elements with images or other\nmultimedia content.\"\n\n`Named strings`_ are supported by WeasyPrint. You can define strings related to\nthe first or last element of a type present on a page, and display these\nstrings in page borders. This feature is really useful to add the title of the\ncurrent chapter at the top of the pages of a book for example.\n\nThe named strings can embed static strings, counters, cross-references, tag\ncontents and tag attributes.\n\n.. code-block:: css\n\n    @top-center { content: string(chapter) }\n    h2 { string-set: chapter \"Current chapter: \" content() }\n\n`Cross-references`_ retrieve counter or content values from targets (anchors or\nidentifiers) in the current document:\n\n.. code-block:: css\n\n    a::after { content: \", on page \" target-counter(attr(href), page) }\n    a::after { content: \", see \" target-text(attr(href)) }\n\nIn particular, ``target-counter()`` and ``target-text()`` are useful when it\ncomes to tables of contents (see `an example`_).\n\nYou can also control `PDF bookmarks`_ with WeasyPrint. Using the\n``bookmark-level``, ``bookmark-label`` and ``bookmark-state`` properties, you\ncan add bookmarks that will be available in your PDF reader.\n\nBookmarks have already been added in the WeasyPrint's `user agent stylesheet`_,\nso your generated documents will automatically have bookmarks on headers (from\n``<h1>`` to ``<h6>``). But for example, if you have only one top-level ``<h1>``\nand do not wish to include it in the bookmarks, add this in your stylesheet:\n\n.. code-block:: css\n\n    h1 { bookmark-level: none }\n\n`Leaders`_ are also supported:\n\n.. code-block:: css\n\n    li a::after {\n        content: ' ' leader(dotted) ' ' target-counter(attr(href), page);\n    }\n\nThe other features of this module are **not** implemented:\n\n- quotes (``content: *-quote``);\n\n.. _CSS Generated Content Module Level 3: https://www.w3.org/TR/css-content-3/\n.. _Quotes: https://www.w3.org/TR/css-content-3/#quotes\n.. _Named strings: https://www.w3.org/TR/css-content-3/#named-strings\n.. _Cross-references: https://www.w3.org/TR/css-content-3/#cross-references\n.. _an example: https://github.com/Kozea/WeasyPrint/pull/652#issuecomment-403276559\n.. _PDF bookmarks: https://www.w3.org/TR/css-content-3/#bookmark-generation\n.. _user agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/main/weasyprint/css/html5_ua.css\n.. _Leaders: https://www.w3.org/TR/css-content-3/#leaders\n\nCSS Color Module Level 4 / 5\n++++++++++++++++++++++++++++\n\nThe `CSS Color Module Level 4`_ is a recommendation defining \"CSS properties which allow\nauthors to specify the foreground color and opacity of the text content of an element\".\nIts main goal is to specify how colors are defined, including color keywords and many\ncolor notations including ``#rgba``, ``rgb()``, ``hsl()``, ``hwb()``, ``lab()``, etc.\nThe standard ``color()`` function gives a common way to define colors giving their color\nspace. Opacity and alpha compositing are also defined in this document.\n\nThis recommendation is fully implemented in WeasyPrint, except the deprecated\nSystem Colors.\n\nThe `CSS Color Module Level 5`_ is a working draft adding \"color modification functions,\ncustom color spaces (ICC profiles), ``contrast-color()``, ``light-dark()`` and\n``device-cmyk()``\" to level 4.\n\nWeasyPrint supports the ``light-dark()`` and ``device-cmyk()`` properties, and the\n``@color-profile`` at-rule.\n\nWeasyPrint does **not** support the ``color-mix()`` and ``contrast-color()`` properties.\n\n.. _CSS Color Module Level 4: https://www.w3.org/TR/css-color-4/\n.. _CSS Color Module Level 5: https://www.w3.org/TR/css-color-5/\n\nCSS Transforms Module Level 1\n+++++++++++++++++++++++++++++\n\nThe `CSS Transforms Module Level 1`_ working draft \"describes a coordinate\nsystem within each element is positioned. This coordinate space can be modified\nwith the transform property. Using transform, elements can be translated,\nrotated and scaled in two or three dimensional space.\"\n\nWeasyPrint supports the ``transform`` and ``transform-origin`` properties, and\nall the 2D transformations (``matrix``, ``rotate``, ``translate``,\n``translateX``, ``translateY``, ``scale``, ``scaleX``, ``scaleY``, ``skew``,\n``skewX``, ``skewY``).\n\nWeasyPrint does **not** support the ``transform-style``, ``perspective``,\n``perspective-origin`` and ``backface-visibility`` properties, and all the 3D\ntransformations (``matrix3d``, ``rotate3d``, ``rotateX``, ``rotateY``,\n``rotateZ``, ``translate3d``, ``translateZ``, ``scale3d``, ``scaleZ``).\n\n.. _CSS Transforms Module Level 1: https://drafts.csswg.org/css-transforms-1/\n\nCSS Backgrounds and Borders Module Level 3\n++++++++++++++++++++++++++++++++++++++++++\n\nThe `CSS Backgrounds and Borders Module Level 3`_ is a candidate recommendation\ndefining properties dealing \"with the decoration of the border area and with\nthe background of the content, padding and border areas\".\n\nThe `border part`_ of this module is supported, as it is already included in\nthe the CSS 2.1 specification.\n\nWeasyPrint supports the `background part`_ of this module (allowing multiple\nbackground layers per box), including the ``background``, ``background-color``,\n``background-image``, ``background-repeat``, ``background-attachment``,\n``background-position``, ``background-clip``, ``background-origin`` and\n``background-size`` properties.\n\nWeasyPrint also supports the `rounded corners part`_ of this module, including\nthe ``border-radius`` property.\n\nWeasyPrint also supports the `border images part`_ of this module, including the\n``border-image``, ``border-image-source``, ``border-image-slice``,\n``border-image-width``, ``border-image-outset`` and ``border-image-repeat``\nproperties.\n\nWeasyPrint does **not** support the `box shadow part`_ of this module,\nincluding the ``box-shadow`` property. This feature has been implemented in a\n`git branch`_ that is not released, as it relies on raster implementation of\nshadows.\n\n.. _CSS Backgrounds and Borders Level 3: https://www.w3.org/TR/css-backgrounds-3/\n.. _border part: https://www.w3.org/TR/css-backgrounds-3/#borders\n.. _background part: https://www.w3.org/TR/css-backgrounds-3/#backgrounds\n.. _rounded corners part: https://www.w3.org/TR/css-backgrounds-3/#corners\n.. _border images part: https://www.w3.org/TR/css-backgrounds-3/#border-images\n.. _box shadow part: https://www.w3.org/TR/css-backgrounds-3/#misc\n.. _git branch: https://github.com/Kozea/WeasyPrint/pull/149\n\nCSS Image Values and Replaced Content Module Level 3 / 4\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n\nThe `Image Values and Replaced Content Module Level 3`_ is a candidate\nrecommendation introducing \"additional ways of representing 2D images, for\nexample as a list of URIs denoting fallbacks, or as a gradient\", defining\n\"several properties for manipulating raster images and for sizing or\npositioning replaced elements\" and \"generic sizing algorithm for replaced\nelements\".\n\nThe `Image Values and Replaced Content Module Level 4`_ is a working draft on\nthe same subject.\n\nThe ``linear-gradient()``, ``radial-gradient()`` and\n``repeating-radial-gradient()`` properties are supported as background images.\n\nThe the ``url()`` notation is supported, but the ``image()`` notation is\n**not** supported for background images.\n\nThe ``object-fit`` and ``object-position`` properties are supported.\n\nThe ``from-image`` and ``snap`` values of the ``image-resolution`` property are\n**not** supported, but the ``resolution`` value is supported.\n\nThe ``image-rendering`` and ``image-orientation`` properties are supported.\n\n.. _Image Values and Replaced Content Module Level 3: https://www.w3.org/TR/css-images-3/\n.. _Image Values and Replaced Content Module Level 4: https://www.w3.org/TR/css-images-4/\n\nCSS Box Sizing Module Level 3\n+++++++++++++++++++++++++++++\n\nThe `CSS Box Sizing Module Level 3`_ is a candidate recommendation extending\n\"the CSS sizing properties with keywords that represent content-based\n'intrinsic' sizes and context-based 'extrinsic' sizes.\"\n\nThe new property defined in this document is implemented in WeasyPrint:\n``box-sizing``.\n\nThe ``min-content``, ``max-content`` and ``fit-content()`` sizing values are\n**not** supported.\n\n.. _CSS Box Sizing Module Level 3: https://www.w3.org/TR/css-sizing-3/\n\nCSS Overflow Module Level 3\n+++++++++++++++++++++++++++\n\nThe `CSS Overflow Module Level 3`_ is a working draft containing \"the features\nof CSS relating to scrollable overflow handling in visual media.\"\n\nThe ``overflow`` property is supported, as defined in CSS2. ``overflow-x``,\n``overflow-y``, ``overflow-clip-margin``, ``overflow-inline`` and\n``overflow-block`` are **not** supported.\n\nThe ``text-overflow``, ``block-ellipsis``, ``line-clamp``, ``max-lines`` and\n``continue`` properties are supported.\n\n.. _CSS Overflow Module Level 3: https://www.w3.org/TR/2020/WD-css-overflow-3-20200603/\n\nCSS Values and Units Module Level 3 / 4\n+++++++++++++++++++++++++++++++++++++++\n\nThe `CSS Values and Units Module Level 3`_ defines various units and\nkeywords used in \"value definition field of each CSS property\".\n\nThe `CSS Values and Units Module Level 4`_ adds many new units, unit types and math\nfunctions.\n\nThe ``initial`` and ``inherit`` CSS-wide keywords are supported, but the\n``unset`` keyword is **not** supported.\n\nQuoted strings, URLs and numeric data types are supported.\n\nFont-relative lengths (``*em``, ``*ex``, ``*ch``, ``*cap``, ``*ic``, ``*lh``),\nviewport-relative lengths (``*vw``, ``*vh``, ``*vi``, ``*vb``, ``*vmin``, ``*vmax``),\nabsolute lengths (``cm``, ``mm``, ``q``, ``in``, ``pt``, ``pc``, ``px``), angles\n(``rad``, ``grad``, ``turn``, ``deg``), resolutions (``dpi``, ``dpcm``, ``dppx``, ``x``)\nare supported.\n\nUnspecified page-relative units (``pvw``, ``pvh``…) are also supported. They are\nrelative to the whole page size (including page margins), while all the other units are\nrelative to the page area size (without page margins).\n\nThe ``attr()`` functional notation is allowed in the ``content`` and\n``string-set`` properties.\n\nAll the mathematical functions (``calc()``…) are supported.\n\n.. _CSS Values and Units Module Level 3: https://www.w3.org/TR/css3-values/\n.. _CSS Values and Units Module Level 4: https://www.w3.org/TR/css4-values/\n\nCSS Multi-column Layout Module\n++++++++++++++++++++++++++++++\n\nThe `CSS Multi-column Layout Module`_ \"describes multi-column layouts in CSS, a\nstyle sheet language for the web. Using functionality described in the\nspecification, content can be flowed into multiple columns with a gap and a\nrule between them.\"\n\nSimple multi-column layouts are supported in WeasyPrint. Features such as\nconstrained height, spanning columns or column breaks are **not**\nsupported. Pagination and overflow are not seriously tested.\n\nThe ``column-width`` and ``column-count`` properties, and the ``columns``\nshorthand property are supported.\n\nThe ``column-gap``, ``column-rule-color``, ``column-rule-style`` and\n``column-rule-width`` properties, and the ``column-rule`` shorthand property\nare supported.\n\nThe ``break-before``, ``break-after`` and ``break-inside`` properties are\nsupported.\n\nThe ``column-span`` property is supported for direct children of columns.\n\nThe ``column-fill`` property is supported, with a column balancing algorithm\nthat should be efficient with simple cases.\n\n.. _CSS Multi-column Layout Module: https://www.w3.org/TR/css-multicol-1/\n\nCSS Fragmentation Module Level 3 / 4\n++++++++++++++++++++++++++++++++++++\n\nThe `CSS Fragmentation Module Level 3`_ \"describes the fragmentation model that\npartitions a flow into pages, columns, or regions. It builds on the Page model\nmodule and introduces and defines the fragmentation model. It adds\nfunctionality for pagination, breaking variable fragment size and orientation,\nwidows and orphans.\"\n\nThe `CSS Fragmentation Module Level 4`_ is a working draft on the same subject.\n\nThe ``break-before``, ``break-after`` and ``break-inside`` properties are\nsupported for pages, but **not** for columns and regions. ``page-break-*``\naliases as defined in CSS2 are supported too.\n\nThe ``orphans`` and ``widows`` properties are supported.\n\nThe ``box-decoration-break`` property is supported, but backgrounds are always\nrepeated and not extended through the whole box as it should be with 'slice'\nvalue.\n\nThe ``margin-break`` property is supported.\n\n.. _CSS Fragmentation Module Level 3: https://www.w3.org/TR/css-break-3/\n.. _CSS Fragmentation Module Level 4: https://www.w3.org/TR/css-break-4/\n\nCSS Custom Properties for Cascading Variables Module Level 1\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n\nThe `CSS Custom Properties for Cascading Variables Module Level 1`_ \"introduces\ncascading variables as a new primitive value type that is accepted by all CSS\nproperties, and custom properties for defining them.\"\n\nThe custom properties and the ``var()`` notation are supported.\n\n.. _CSS Custom Properties for Cascading Variables Module Level 1: https://www.w3.org/TR/css-variables/\n\nCSS Text Decoration Module Level 3 / 4\n++++++++++++++++++++++++++++++++++++++\n\nThe `CSS Text Decoration Module Level 3`_ \"contains the features of CSS\nrelating to text decoration, such as underlines, text shadows, and emphasis\nmarks.\"\n\nThe `CSS Text Decoration Module Level 4`_ is a working draft on the same subject.\n\nThe ``text-decoration-line``, ``text-decoration-style``,\n``text-decoration-color``, ``text-decoration-thickness`` and\n``text-underline-offset`` properties are supported. The ``text-decoration``\nshorthand is also supported.\n\nThe other properties (``text-underline-position``, ``text-emphasis-*``,\n``text-shadow``) are not supported.\n\n.. _CSS Text Decoration Module Level 3: https://www.w3.org/TR/css-text-decor-3/\n.. _CSS Text Decoration Module Level 4: https://www.w3.org/TR/css-text-decor-4/\n\nCSS Flexible Box Layout Module Level 1\n++++++++++++++++++++++++++++++++++++++\n\nThe `CSS Flexible Box Layout Module Level 1`_ \"describes a CSS box model\noptimized for user interface design\", also known as \"flexbox\".\n\nThis module works for simple use cases but is not deeply tested.\n\nAll the ``flex-*``, ``align-*``, ``justify-*`` and ``order`` properties are\nsupported. The ``flex`` and ``flex-flow`` shorthands are supported too.\n\n.. _CSS Flexible Box Layout Module Level 1: https://www.w3.org/TR/css-flexbox-1/\n\nCSS Grid Layout Module Level 2\n++++++++++++++++++++++++++++++\n\nThe `CSS Grid Layout Module Level 2`_ \"defines a two-dimensional grid-based layout\nsystem, optimized for user interface design\".\n\nThis module works for simple cases, but has some limitations. Here are\nnon-exhaustive lists of supported/unsupported features.\n\nSupported:\n\n- ``display: grid``,\n- ``grid-auto-*``, ``grid-template-*`` and other ``grid-*`` properties,\n- ``grid`` and other ``grid-*`` shorthands,\n- flexible lengths (``fr`` unit),\n- line names,\n- grid areas,\n- auto rows and auto columns,\n- ``z-index``,\n- ``repeat(X, *)``,\n- ``minmax()``,\n- ``align-*`` and ``justify-*`` alignment properties,\n- ``gap`` and ``*-gap`` properties for gutters,\n- dense auto flow,\n- ``order``,\n- margins, borders, padding on grid containers and grid items,\n- fragmentation between rows.\n\nUnsupported or untested:\n\n- ``display: inline-grid``,\n- auto content size for grid containers,\n- ``grid-auto-flow: column``,\n- subgrids,\n- ``repeat(auto-fill, *)`` and ``repeat(auto-fit, *)``,\n- auto margins for grid items,\n- ``span`` with line names,\n- ``span`` for flexible tracks,\n- ``safe`` and ``unsafe`` alignments,\n- baseline alignment,\n- grid items with intrinsic size (images),\n- distribute space beyond limits,\n- grid items larger than grid containers,\n- ``min-width``, ``max-width``, ``min-height``, ``max-height`` on grid items,\n- complex ``min-content`` and ``max-content`` cases,\n- absolutely positioned and floating grid items,\n- fragmentation in rows.\n\n.. _CSS Grid Layout Module Level 2: https://www.w3.org/TR/css-grid-2/\n\nCSS Basic User Interface Module Level 3/4\n+++++++++++++++++++++++++++++++++++++++++\n\nThe `CSS Basic User Interface Module Level 3/4`_ \"enables authors to style user\ninterface related properties and values.\"\n\nThe ``outline-width``, ``outline-style``, ``outline-color`` properties and the\n``outline`` shorthand are supported. The ``outline-offset`` property is also\nsupported.\n\nThe ``resize``, ``cursor``, ``caret-*`` and ``nav-*`` properties are **not**\nsupported.\n\nThe ``appearance`` property is supported. When set to ``auto``, it displays\nform fields as PDF form fields (supported for text inputs, check boxes, text\nareas, and select only).\n\nThe ``accent-color`` property is **not** supported.\n"
  },
  {
    "path": "docs/changelog.rst",
    "content": "Changelog\n=========\n\n\nVersion 68.1\n------------\n\nReleased on 2026-02-06.\n\nBug fixes:\n\n* `#2662 <https://github.com/Kozea/WeasyPrint/issues/2662>`_:\n  Don’t crash when SVG clip paths are not in defs tags\n* `#2665 <https://github.com/Kozea/WeasyPrint/issues/2665>`_:\n  Fix position of box bounding box\n* `#2663 <https://github.com/Kozea/WeasyPrint/issues/2663>`_:\n  Fix transparency with Acrobat and Edge\n* `#2666 <https://github.com/Kozea/WeasyPrint/issues/2666>`_:\n  Don’t rely on random default font to define test page size\n* `#2670 <https://github.com/Kozea/WeasyPrint/issues/2670>`_:\n  Fix pattern detection of URL schemes\n* `#2671 <https://github.com/Kozea/WeasyPrint/pull/2671>`_:\n  Improve API compatibility between URLFetcherResponse and addinfourl\n* `#2672 <https://github.com/Kozea/WeasyPrint/issues/2672>`_:\n  Fix charset for old URL fetcher requests\n* `#2675 <https://github.com/Kozea/WeasyPrint/pull/2675>`_,\n  `#2673 <https://github.com/Kozea/WeasyPrint/issues/2673>`_:\n  Fix calc for many properties\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Spacinov\n* Syslifters\n* Kobalt\n* Simon Sapin\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Simonsoft\n* KontextWork\n* Menutech\n* TrainingSparkle\n* Healthchecks.io\n* Method B\n* FieldHub\n* Hammerbacher\n* Yanal-Yves Fargialla\n* Morntag\n* Piloterr\n* Xavid\n* Charlie S.\n* Prothesis Dental Solutions\n* Kai DeLorenzo\n\n\nVersion 68.0\n------------\n\nReleased on 2026-01-19.\n\n**This is a security update (CVE-2025-68616).**\n\nWe strongly recommend to upgrade WeasyPrint to the latest version if you use the\n``default_url_fetcher`` function in your custom URL fetcher, or if you use the\n``allowed_protocols`` parameter of the ``default_url_fetcher`` function.\n\nSecurity:\n\n* Always use URL fetcher for HTTP redirects\n\nPython API:\n\n* ``default_url_fetcher()`` is deprecated, use the new ``URLFetcher`` class instead, see\n  :ref:`URL Fetchers` for more information about URL fetchers\n* ``DocumentMetadata.generate_rdf_metadata`` is now a method that can be overridden\n  instead of a parameter, see :ref:`Factur-X / ZUGFeRD (Electronic Invoices)` for\n  examples to create e-invoices\n\nFeatures:\n\n* `#2609 <https://github.com/Kozea/WeasyPrint/pull/2609>`_,\n  `#2603 <https://github.com/Kozea/WeasyPrint/issues/2603>`_,\n  `#351 <https://github.com/Kozea/WeasyPrint/issues/351>`_:\n  Refactor URL fetcher API\n* `#2632 <https://github.com/Kozea/WeasyPrint/pull/2632>`_:\n  Support legacy 0 value for angles\n* `#2627 <https://github.com/Kozea/WeasyPrint/pull/2627>`_:\n  Add font-face support to SVG\n* `#2646 <https://github.com/Kozea/WeasyPrint/pull/2646>`_,\n  `#2255 <https://github.com/Kozea/WeasyPrint/issues/2255>`_:\n  Add font shorthand support for SVG text elements\n* `#2590 <https://github.com/Kozea/WeasyPrint/pull/2590>`_,\n  `#1749 <https://github.com/Kozea/WeasyPrint/issues/1749>`_:\n  Honor language-specific rules for text-transform\n* `#2645 <https://github.com/Kozea/WeasyPrint/pull/2645>`_,\n  `#2613 <https://github.com/Kozea/WeasyPrint/issues/2613>`_:\n  Improve SVG and SVG emojis rendering\n* `#2658 <https://github.com/Kozea/WeasyPrint/pull/2658>`_,\n  `#2583 <https://github.com/Kozea/WeasyPrint/issues/2583>`_:\n  Add CLI for Factur-X / ZUGFeRD e-invoices\n\nBug fixes:\n\n* `#2649 <https://github.com/Kozea/WeasyPrint/issues/2649>`_:\n  Refactor URL fetcher API\n* `#2643 <https://github.com/Kozea/WeasyPrint/pull/2643>`_,\n  `#2628 <https://github.com/Kozea/WeasyPrint/issues/2628>`_:\n  Handle box-sizing: border-box in grid layout\n* `#2641 <https://github.com/Kozea/WeasyPrint/pull/2641>`_,\n  `#1875 <https://github.com/Kozea/WeasyPrint/issues/1875>`_:\n  Process whitespace after checking all pending targets\n* `#2488 <https://github.com/Kozea/WeasyPrint/pull/2488>`_,\n  `#2485 <https://github.com/Kozea/WeasyPrint/issues/2485>`_:\n  Preserve page groups during layout repagination\n* `#2642 <https://github.com/Kozea/WeasyPrint/pull/2642>`_,\n  `#2631 <https://github.com/Kozea/WeasyPrint/issues/2631>`_:\n  Don’t use isolated transparency groups\n* `#2637 <https://github.com/Kozea/WeasyPrint/issues/2637>`_:\n  Fix repeating radial gradients rendering\n* `#2622 <https://github.com/Kozea/WeasyPrint/issues/2622>`_:\n  Fix validation of colors\n* `#2626 <https://github.com/Kozea/WeasyPrint/issues/2626>`_:\n  Share grid items rendering advancement between a box and its copies\n* `#2621 <https://github.com/Kozea/WeasyPrint/issues/2621>`_:\n  Correctly handle fallback values of attr()\n* `#2619 <https://github.com/Kozea/WeasyPrint/issues/2619>`_:\n  Fix SVG fonts\n* `#2629 <https://github.com/Kozea/WeasyPrint/issues/2629>`_:\n  Always define extra skip height that may be used after\n* `#2648 <https://github.com/Kozea/WeasyPrint/issues/2648>`_:\n  Fix numbers validation in font-feature-settings\n* `#2648 <https://github.com/Kozea/WeasyPrint/issues/2660>`_:\n  Fix keyword values for text-decoration-thickness\n* `#2661 <https://github.com/Kozea/WeasyPrint/issues/2661>`_:\n  Respect inline images when defining minimum table width\n\nDocumentation:\n\n* `#2638 <https://github.com/Kozea/WeasyPrint/pull/2638>`_:\n  Update Python command for Windows installation steps\n\nContributors:\n\n* Guillaume Ayoub\n* Jurriaan Pruis\n* Mohamed Hamed\n* Alexandra Usatenko\n* Andrea Corna\n* Aoishik Khan\n* Joe\n\nBackers and sponsors:\n\n* Spacinov\n* Syslifters\n* Kobalt\n* Simon Sapin\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Simonsoft\n* KontextWork\n* Menutech\n* TrainingSparkle\n* Healthchecks.io\n* Method B\n* FieldHub\n* Hammerbacher\n* Yanal-Yves Fargialla\n* Morntag\n* Piloterr\n* Xavid\n* Charlie S.\n* Prothesis Dental Solutions\n* Kai DeLorenzo\n\n\nVersion 67.0\n------------\n\nReleased on 2025-12-02.\n\nDependencies:\n\n* Python 3.10+ is now needed, Python 3.9 is not supported anymore\n* tinycss2 1.5.0+ is now needed\n* fontTools 4.59.2+ is now needed\n\nFeatures:\n\n* `#2560 <https://github.com/Kozea/WeasyPrint/pull/2560>`_,\n  `#640 <https://github.com/Kozea/WeasyPrint/issues/640>`_,\n  `#844 <https://github.com/Kozea/WeasyPrint/issues/844>`_,\n  `#1091 <https://github.com/Kozea/WeasyPrint/issues/1091>`_,\n  `#2517 <https://github.com/Kozea/WeasyPrint/issues/2517>`_:\n  Support CMYK colors, PDF/X, color profiles and light-dark() function\n* `#2558 <https://github.com/Kozea/WeasyPrint/pull/2558>`_,\n  `#1175 <https://github.com/Kozea/WeasyPrint/issues/1175>`_:\n  Support ::first-line, with financial support from Karte Technology\n* `#2552 <https://github.com/Kozea/WeasyPrint/pull/2552>`_:\n  Support CSS layers, with financial support from Code & Co.\n* `#2564 <https://github.com/Kozea/WeasyPrint/pull/2564>`_,\n  `#2599 <https://github.com/Kozea/WeasyPrint/pull/2599>`_,\n  `#2397 <https://github.com/Kozea/WeasyPrint/issues/2397>`_:\n  Allow page breaks in grid rows, with financial support from Ocean Recap\n* `#2568 <https://github.com/Kozea/WeasyPrint/pull/2568>`_,\n  `#357 <https://github.com/Kozea/WeasyPrint/issues/357>`_:\n  Support calc() and other mathematical functions\n* `#2575 <https://github.com/Kozea/WeasyPrint/pull/2575>`_,\n  `#2574 <https://github.com/Kozea/WeasyPrint/issues/2574>`_:\n  Support PDF/A-1a, PDF/A-2a and PDF/A-3a\n* `#2611 <https://github.com/Kozea/WeasyPrint/pull/2611>`_,\n  `#2573 <https://github.com/Kozea/WeasyPrint/issues/2573>`_:\n  Support PDF/A-4e and PDF/A-4f\n* `#2523 <https://github.com/Kozea/WeasyPrint/pull/2523>`_:\n  Display tofu for missing glyphs\n* `#2581 <https://github.com/Kozea/WeasyPrint/pull/2581>`_:\n  Add option to disable protocols in URL resolution\n* `#2570 <https://github.com/Kozea/WeasyPrint/pull/2570>`_:\n  Support rch, cap, rcap, rex, ic and ric font-relative units\n* `#2547 <https://github.com/Kozea/WeasyPrint/pull/2547>`_,\n  `#2140 <https://github.com/Kozea/WeasyPrint/issues/2140>`_:\n  Support \"only\" keyword in media queries\n\nBug fixes:\n\n* `#2516 <https://github.com/Kozea/WeasyPrint/pull/2516>`_,\n  `#1510 <https://github.com/Kozea/WeasyPrint/issues/1510>`_:\n  Fix rendering of first line of text with nested right float\n* `#2510 <https://github.com/Kozea/WeasyPrint/pull/2510>`_,\n  `#1073 <https://github.com/Kozea/WeasyPrint/issues/1073>`_,\n  `#2507 <https://github.com/Kozea/WeasyPrint/issues/2507>`_:\n  Avoid Pango crashes and font mismatches with @font-face rules referencing local fonts\n* `#2532 <https://github.com/Kozea/WeasyPrint/pull/2532>`_,\n  `#2531 <https://github.com/Kozea/WeasyPrint/issues/2531>`_:\n  Use fonttools instancer instead of deprecated mutator API\n* `#2541 <https://github.com/Kozea/WeasyPrint/pull/2541>`_:\n  Fix syntax of functions\n* `#2543 <https://github.com/Kozea/WeasyPrint/pull/2543>`_:\n  Allow font-related units to access @font-face fonts\n* `#2525 <https://github.com/Kozea/WeasyPrint/pull/2525>`_:\n  Respect top margins and avoid overlapping footnotes for columns, with financial support from Code & Co.\n* `#2536 <https://github.com/Kozea/WeasyPrint/pull/2536>`_:\n  Remove Subtype key from font descriptor\n* `#2539 <https://github.com/Kozea/WeasyPrint/pull/2539>`_:\n  Fix min width for SVGs with intrinsic ratio but no intrinsic size\n* `#2537 <https://github.com/Kozea/WeasyPrint/pull/2537>`_,\n  `#2533 <https://github.com/Kozea/WeasyPrint/issues/2533>`_:\n  Fix order of operators when drawing SVGs\n* `#2538 <https://github.com/Kozea/WeasyPrint/pull/2538>`_:\n  Don’t crash with nested unknown functions\n* `#2542 <https://github.com/Kozea/WeasyPrint/pull/2542>`_:\n  Don’t crash when lh and rlh are used for line height or font size\n* `#2540 <https://github.com/Kozea/WeasyPrint/pull/2540>`_,\n  `#2528 <https://github.com/Kozea/WeasyPrint/issues/2528>`_:\n  Use locale encoding instead of filesystem encoding for font paths\n* `#2563 <https://github.com/Kozea/WeasyPrint/pull/2563>`_,\n  `#2479 <https://github.com/Kozea/WeasyPrint/issues/2479>`_:\n  Don’t avoid float collisions for atomic flex items\n* `#2569 <https://github.com/Kozea/WeasyPrint/pull/2569>`_:\n  Don’t be case-sensitive for units\n* `#2567 <https://github.com/Kozea/WeasyPrint/pull/2567>`_,\n  `#2566 <https://github.com/Kozea/WeasyPrint/issues/2566>`_:\n  Add x-default attribute for metadata description to be compliant with PDF/A\n* `#2586 <https://github.com/Kozea/WeasyPrint/pull/2586>`_,\n  `#2571 <https://github.com/Kozea/WeasyPrint/issues/2571>`_:\n  Improve formatting contexts management\n* `#2600 <https://github.com/Kozea/WeasyPrint/pull/2600>`_:\n  Fix SVG image aspect ratio when only width or height is specified\n* `#2612 <https://github.com/Kozea/WeasyPrint/pull/2612>`_,\n  `#2595 <https://github.com/Kozea/WeasyPrint/pull/2595>`_:\n  Clean block layout and fix corner cases\n* `#2522 <https://github.com/Kozea/WeasyPrint/issues/2522>`_:\n  Ignore preserveAspectRatio when SVG has no viewBox\n* `#2544 <https://github.com/Kozea/WeasyPrint/issues/2544>`_:\n  Allow to use a variable twice in a function\n* `#2555 <https://github.com/Kozea/WeasyPrint/issues/2555>`_:\n  Fix flex gap in right-to-left context\n* `#2591 <https://github.com/Kozea/WeasyPrint/issues/2591>`_:\n  Respect non-auto widths and fix padding of grid items\n* `#2601 <https://github.com/Kozea/WeasyPrint/issues/2601>`_:\n  Don’t crash when tagged tables are not displayed as tables\n* `#2607 <https://github.com/Kozea/WeasyPrint/issues/2607>`_:\n  Fix rendering of multiline textareas with PDF forms\n* `#2106 <https://github.com/Kozea/WeasyPrint/issues/2106>`_:\n  Force variable initialization to avoid crashes during column layout\n* `#2618 <https://github.com/Kozea/WeasyPrint/pull/2618>`_,\n  `#2617 <https://github.com/Kozea/WeasyPrint/issues/2617>`_:\n  Fix rendering of relative grid and flex items\n\nDocumentation:\n\n* `#2535 <https://github.com/Kozea/WeasyPrint/pull/2535>`_:\n  `#2534 <https://github.com/Kozea/WeasyPrint/issues/2534>`_:\n  Removed reference to defunct site\n\nContributors:\n\n* Guillaume Ayoub\n* Fazle Rabbi Ferdaus\n* Lucie Anglade\n* Luca Vercelli\n* ChickenF622\n* Ernie Chu\n* Mark Pullin\n* Malte Laukötter\n* Markus Mohanty\n* Yvonne Kothmeier\n* Jarom Ort\n* kuypan\n\nBackers and sponsors:\n\n* Spacinov\n* Syslifters\n* Kobalt\n* Simon Sapin\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Simonsoft\n* KontextWork\n* Menutech\n* TrainingSparkle\n* Healthchecks.io\n* Method B\n* FieldHub\n* Hammerbacher\n* Yanal-Yves Fargialla\n* Morntag\n* Piloterr\n* Xavid\n* Charlie S.\n* Prothesis Dental Solutions\n* Kai DeLorenzo\n\n\nVersion 66.0\n------------\n\nReleased on 2025-07-24.\n\nFeatures:\n\n* `#2475 <https://github.com/Kozea/WeasyPrint/pull/2475>`_:\n  Add support for 'lh' and 'rlh' units\n* `#2432 <https://github.com/Kozea/WeasyPrint/issues/2432>`_,\n  `#2437 <https://github.com/Kozea/WeasyPrint/pull/2437>`_:\n  Report footnotes when text overflows because of orphans, with financial support from Code & Co.\n* `#2256 <https://github.com/Kozea/WeasyPrint/issues/2256>`_,\n  `#2466 <https://github.com/Kozea/WeasyPrint/pull/2466>`_:\n  Handle transform-origin in SVG\n* `#2445 <https://github.com/Kozea/WeasyPrint/pull/2445>`_:\n  Add parameter to have additional HTTP headers for url_fetcher\n\nBug fixes:\n\n* `#2471 <https://github.com/Kozea/WeasyPrint/pull/2471>`_,\n  `#2506 <https://github.com/Kozea/WeasyPrint/pull/2506>`_,\n  `#2500 <https://github.com/Kozea/WeasyPrint/issues/2500>`_,\n  `#2460 <https://github.com/Kozea/WeasyPrint/issues/2460>`_,\n  `#2363 <https://github.com/Kozea/WeasyPrint/issues/2363>`_,\n  `#2470 <https://github.com/Kozea/WeasyPrint/issues/2470>`_,\n  `#1872 <https://github.com/Kozea/WeasyPrint/issues/1872>`_,\n  `#2153 <https://github.com/Kozea/WeasyPrint/issues/2153>`_,\n  `#1838 <https://github.com/Kozea/WeasyPrint/issues/1838>`_,\n  `#1837 <https://github.com/Kozea/WeasyPrint/issues/1837>`_,\n  `#1784 <https://github.com/Kozea/WeasyPrint/issues/1784>`_,\n  `#1835 <https://github.com/Kozea/WeasyPrint/issues/1835>`_,\n  `#2444 <https://github.com/Kozea/WeasyPrint/issues/2444>`_,\n  `#2497 <https://github.com/Kozea/WeasyPrint/issues/2497>`_,\n  `#2505 <https://github.com/Kozea/WeasyPrint/issues/2505>`_,\n  `#2503 <https://github.com/Kozea/WeasyPrint/issues/2503>`_,\n  `#1836 <https://github.com/Kozea/WeasyPrint/issues/1836>`_,\n  `#2467 <https://github.com/Kozea/WeasyPrint/issues/2467>`_:\n  Improve PDF/UA support, with financial support from NLnet\n* `#2425 <https://github.com/Kozea/WeasyPrint/pull/2425>`_,\n  `#1557 <https://github.com/Kozea/WeasyPrint/issues/1557>`_:\n  Improve position of outside markers\n* `#2409 <https://github.com/Kozea/WeasyPrint/pull/2409>`_,\n  `#2265 <https://github.com/Kozea/WeasyPrint/issues/2265>`_:\n  Draw circles instead of rectangles when drawing dotted borders\n* `#2416 <https://github.com/Kozea/WeasyPrint/pull/2416>`_,\n  `#2270 <https://github.com/Kozea/WeasyPrint/issues/2270>`_:\n  Correctly split words for automatic hyphenation\n* `#2439 <https://github.com/Kozea/WeasyPrint/pull/2439>`_,\n  `#2426 <https://github.com/Kozea/WeasyPrint/issues/2426>`_:\n  Don’t rely on URL protocols outside URL fetcher function\n* `#2433 <https://github.com/Kozea/WeasyPrint/pull/2433>`_:\n  Disable style for deprecated outline algorithm\n* `#2447 <https://github.com/Kozea/WeasyPrint/pull/2447>`_,\n  `#2441 <https://github.com/Kozea/WeasyPrint/issues/2441>`_,\n  `#2448 <https://github.com/Kozea/WeasyPrint/issues/2448>`_:\n  Improve min- and max-content calculation, with financial support from Menutech\n* `#2454 <https://github.com/Kozea/WeasyPrint/pull/2454>`_,\n  `#2442 <https://github.com/Kozea/WeasyPrint/issues/2442>`_,\n  `#2449 <https://github.com/Kozea/WeasyPrint/issues/2449>`_:\n  Minor fixes for flex layout\n* `#2473 <https://github.com/Kozea/WeasyPrint/pull/2473>`_,\n  `#2459 <https://github.com/Kozea/WeasyPrint/issues/2459>`_:\n  Include out-of-flow boxes in page layout progress, with financial support from Pathfindr\n* `#2458 <https://github.com/Kozea/WeasyPrint/pull/2458>`_:\n  Replace deprecated warn logger function\n* `#2494 <https://github.com/Kozea/WeasyPrint/pull/2494>`_,\n  `#1856 <https://github.com/Kozea/WeasyPrint/issues/1856>`_:\n  Fix bug with bottom margins in columns\n* `#2435 <https://github.com/Kozea/WeasyPrint/issues/2435>`_:\n  Make footnote calls inherit from footnotes\n* `#2484 <https://github.com/Kozea/WeasyPrint/issues/2484>`_,\n  `#2456 <https://github.com/Kozea/WeasyPrint/issues/2456>`_:\n  Allow to avoid page breaks after table-row-group elements\n* `#2450 <https://github.com/Kozea/WeasyPrint/issues/2450>`_:\n  Draw background and borders for relative grid containers\n* `#2453 <https://github.com/Kozea/WeasyPrint/issues/2453>`_:\n  Don’t advance position_y for collapsed margins of discarded children\n* `#2493 <https://github.com/Kozea/WeasyPrint/issues/2493>`_:\n  Fix endless loop with CSS variables referencing each other\n* `#2502 <https://github.com/Kozea/WeasyPrint/issues/2502>`_:\n  Ignore bottom margin when calculating footnote overflow\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n* Alvaro Garcia Fernandez\n* Emmanuel Ferdman\n* Gabriel Corona\n* Markus Mohanty\n* Luca Vercelli\n* Tre Huang\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Syslifters\n* Simon Sapin\n* Manuel Barkhau\n* Simonsoft\n* Menutech\n* KontextWork\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* DocRaptor\n* Yanal-Yves Fargialla\n* Method B\n* FieldHub\n* Morntag\n* Xavid\n* Kai DeLorenzo\n* Charlie S.\n* Alan Villalobos\n\n\nVersion 65.1\n------------\n\nReleased on 2025-04-14.\n\nBug fixes:\n\n* `#2414 <https://github.com/Kozea/WeasyPrint/issues/2414>`_:\n  Correctly handle flex columns split between pages\n* `1b24ad9 <https://github.com/Kozea/WeasyPrint/commit/1b24ad9>`_:\n  Include padding in outer size of item elements\n* `#2419 <https://github.com/Kozea/WeasyPrint/issues/2419>`_:\n  Set main tag as block by default\n* `#2415 <https://github.com/Kozea/WeasyPrint/issues/2415>`_:\n  Fix support of replaced block box as flex items\n* `83da2fe0 <https://github.com/Kozea/WeasyPrint/commit/83da2fe0>`_:\n  Fix margins and padding for rtl lists\n* `#2429 <https://github.com/Kozea/WeasyPrint/issues/2429>`_,\n  `#1076 <https://github.com/Kozea/WeasyPrint/issues/1076>`_,\n  `#2431 <https://github.com/Kozea/WeasyPrint/pull/2431>`_:\n  Fix page groups\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Syslifters\n* Simon Sapin\n* Manuel Barkhau\n* Simonsoft\n* Menutech\n* KontextWork\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* DocRaptor\n* Yanal-Yves Fargialla\n* Method B\n* FieldHub\n* Morntag\n* Xavid\n* Kai DeLorenzo\n* Charlie S.\n* Alan Villalobos\n\n\nVersion 65.0\n------------\n\nReleased on 2025-03-20.\n\nDependencies:\n\n* CSSSelect2 0.8.0 is now needed\n\nFeatures:\n\n* `#1665 <https://github.com/Kozea/WeasyPrint/issues/1665>`_:\n  Support gap properties in Flex layout, with financial support from NLnet\n* `#378 <https://github.com/Kozea/WeasyPrint/issues/378>`_,\n  `#2405 <https://github.com/Kozea/WeasyPrint/pull/2405>`_:\n  Handle @font-face unicode-range\n* `#2394 <https://github.com/Kozea/WeasyPrint/pull/2394>`_:\n  Modernize and improve default user agent stylesheets\n\nBug fixes:\n\n* `#2362 <https://github.com/Kozea/WeasyPrint/issues/2362>`_,\n  `#2387 <https://github.com/Kozea/WeasyPrint/pull/2387>`_,\n  `#601 <https://github.com/Kozea/WeasyPrint/issues/601>`_,\n  `#1967 <https://github.com/Kozea/WeasyPrint/issues/1967>`_,\n  `#1805 <https://github.com/Kozea/WeasyPrint/issues/1805>`_,\n  `#2163 <https://github.com/Kozea/WeasyPrint/issues/2163>`_,\n  `#2342 <https://github.com/Kozea/WeasyPrint/issues/2342>`_,\n  `#2374 <https://github.com/Kozea/WeasyPrint/issues/2374>`_,\n  `#1109 <https://github.com/Kozea/WeasyPrint/issues/1109>`_,\n  `#1356 <https://github.com/Kozea/WeasyPrint/issues/1356>`_,\n  `#1327 <https://github.com/Kozea/WeasyPrint/issues/1327>`_,\n  `#1563 <https://github.com/Kozea/WeasyPrint/issues/1563>`_,\n  `#1652 <https://github.com/Kozea/WeasyPrint/issues/1652>`_,\n  `#2351 <https://github.com/Kozea/WeasyPrint/issues/2351>`_,\n  `#2312 <https://github.com/Kozea/WeasyPrint/issues/2312>`_,\n  `#2340 <https://github.com/Kozea/WeasyPrint/issues/2340>`_,\n  `#1311 <https://github.com/Kozea/WeasyPrint/issues/1311>`_,\n  `#2066 <https://github.com/Kozea/WeasyPrint/issues/2066>`_,\n  `#2359 <https://github.com/Kozea/WeasyPrint/issues/2359>`_,\n  `#2053 <https://github.com/Kozea/WeasyPrint/issues/2053>`_:\n  Improve Flex layout, with financial support from NLnet.\n* `#1686 <https://github.com/Kozea/WeasyPrint/issues/1686>`_,\n  `#2404 <https://github.com/Kozea/WeasyPrint/pull/2404>`_:\n  Fix duplicate text selection with right-to-left text\n* `#2372 <https://github.com/Kozea/WeasyPrint/issues/2372>`_,\n  `#2389 <https://github.com/Kozea/WeasyPrint/pull/2389>`_:\n  Fix justification of right-to-left text\n* `#2403 <https://github.com/Kozea/WeasyPrint/issues/2403>`_:\n  Fix emoji rendering with older versions of Pango\n* `#2392 <https://github.com/Kozea/WeasyPrint/issues/2392>`_:\n  Fix complex cases involving nested SVG text anchors\n* `#2396 <https://github.com/Kozea/WeasyPrint/issues/2396>`_,\n  `#2398 <https://github.com/Kozea/WeasyPrint/pull/2398>`_:\n  Fix and improve font names in PDF\n* `#2269 <https://github.com/Kozea/WeasyPrint/issues/2269>`_,\n  `#2390 <https://github.com/Kozea/WeasyPrint/pull/2390>`_:\n  Apply justification to non-breaking spaces\n* `#2362 <https://github.com/Kozea/WeasyPrint/issues/2362>`_,\n  `#2387 <https://github.com/Kozea/WeasyPrint/pull/2387>`_:\n  Improve Flex layout, with financial support from NLnet.\n\nContributors:\n\n* Guillaume Ayoub\n* Luca Vercelli\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Syslifters\n* Simon Sapin\n* Manuel Barkhau\n* Simonsoft\n* Menutech\n* KontextWork\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* DocRaptor\n* Yanal-Yves Fargialla\n* Method B\n* FieldHub\n* Morntag\n* Xavid\n* Kai DeLorenzo\n* Charlie S.\n* Alan Villalobos\n\n\nVersion 64.1\n------------\n\nReleased on 2025-02-20.\n\nBug fixes:\n\n* `#2368 <https://github.com/Kozea/WeasyPrint/issues/2368>`_:\n  Fix ascent and descent font values\n* `#2370 <https://github.com/Kozea/WeasyPrint/issues/2370>`_:\n  Avoid endless recursion for variables in nested functions\n* `#2275 <https://github.com/Kozea/WeasyPrint/issues/2275>`_:\n  Use correct containing block to render waiting children\n* `#2375 <https://github.com/Kozea/WeasyPrint/issues/2375>`_:\n  Ensure that we handle text-anchor only on text content elements\n* `#2090 <https://github.com/Kozea/WeasyPrint/issues/2090>`_:\n  Only create font temporary folder when adding fonts\n* `#2383 <https://github.com/Kozea/WeasyPrint/issues/2383>`_:\n  Fix grid-template-areas validation and allow uppercase identifiers for grid lines\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Syslifters\n* Simon Sapin\n* Manuel Barkhau\n* Simonsoft\n* Menutech\n* KontextWork\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* DocRaptor\n* Yanal-Yves Fargialla\n* Method B\n* FieldHub\n* Morntag\n* Xavid\n* Kai DeLorenzo\n* Charlie S.\n\n\nVersion 64.0\n------------\n\nReleased on 2025-01-30.\n\nFeatures:\n\n* `#2338 <https://github.com/Kozea/WeasyPrint/pull/2338>`_:\n  Allow custom RDF metadata for PDF/A and eInvoices\n* `#123 <https://github.com/Kozea/WeasyPrint/issues/123>`_,\n  `#2345 <https://github.com/Kozea/WeasyPrint/pull/2345>`_:\n  Handle small-caps synthesis\n* `#2343 <https://github.com/Kozea/WeasyPrint/issues/2343>`_:\n  Support outline-offset\n* `#2361 <https://github.com/Kozea/WeasyPrint/pull/2361>`_:\n  Support text-underline-offset and text-decoration-thickness\n* `#2296 <https://github.com/Kozea/WeasyPrint/issues/2296>`_:\n  Don’t crash with tables with rounded corners split between pages\n\nBug fixes:\n\n* `#2360 <https://github.com/Kozea/WeasyPrint/issues/2360>`_:\n  Fix gradients with non-RGB colors\n* `#2355 <https://github.com/Kozea/WeasyPrint/issues/2355>`_,\n  `#2358 <https://github.com/Kozea/WeasyPrint/pull/2358>`_:\n  Align png emojis to the surrounding text\n* `#2353 <https://github.com/Kozea/WeasyPrint/issues/2353>`_:\n  Fix alignment of SVG text with multiple nested text-anchor values\n* `#2350 <https://github.com/Kozea/WeasyPrint/pull/2350>`_:\n  Fix logging restoration in capture_logs\n* `#2341 <https://github.com/Kozea/WeasyPrint/pull/2341>`_:\n  Fix page groups\n* `#2314 <https://github.com/Kozea/WeasyPrint/pulls/2314>`_:\n  Use CSS 'image-rendering' attribute for images in SVGs\n* `#2332 <https://github.com/Kozea/WeasyPrint/issues/2332>`_:\n  Fix opacity for translated SVG elements\n* `#2329 <https://github.com/Kozea/WeasyPrint/issues/2329>`_:\n  Refactor text.line_break.get_log_attrs\n* `#2325 <https://github.com/Kozea/WeasyPrint/issues/2325>`_,\n  `#2326 <https://github.com/Kozea/WeasyPrint/pull/2326>`_:\n  Fix table overflow edge cases\n\nPerformance:\n\n* `#2347 <https://github.com/Kozea/WeasyPrint/issues/2347>`_,\n  `#2364 <https://github.com/Kozea/WeasyPrint/pull/2364>`_:\n  Improve rendering speed for text\n\nDocumentation:\n\n* `#2352 <https://github.com/Kozea/WeasyPrint/pull/2352>`_:\n  Add more use cases in documentation, use Furo theme\n\nContributors:\n\n* Guillaume Ayoub\n* Kesara Rathnayake\n* Xavid Pretzer\n* David Tagatac\n* Ernesto Ruge\n* Niko Abeler\n* Noam Kushinsky\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Syslifters\n* Simon Sapin\n* Manuel Barkhau\n* Simonsoft\n* Menutech\n* KontextWork\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* DocRaptor\n* Yanal-Yves Fargialla\n* Method B\n* FieldHub\n* Morntag\n* Xavid\n* Kai DeLorenzo\n* Charlie S.\n\n\nVersion 63.1\n------------\n\nReleased on 2024-12-10.\n\nDependencies:\n\n* `#2297 <https://github.com/Kozea/WeasyPrint/issues/2297>`_:\n  Remove upper bounds for dependencies\n\nBug fixes:\n\n* `#2300 <https://github.com/Kozea/WeasyPrint/pull/2300>`_,\n  `#2292 <https://github.com/Kozea/WeasyPrint/issues/2292>`_:\n  Don’t avoid floats for flex items\n* `#2301 <https://github.com/Kozea/WeasyPrint/pull/2301>`_,\n  `#2293 <https://github.com/Kozea/WeasyPrint/issues/2293>`_:\n  Include floats in calculation of minimum cell height\n* `#2303 <https://github.com/Kozea/WeasyPrint/pull/2303>`_,\n  `#2302 <https://github.com/Kozea/WeasyPrint/issues/2302>`_:\n  Set alpha even when current color channels didn’t change\n* `#2306 <https://github.com/Kozea/WeasyPrint/issues/2306>`_:\n  Don’t try to increase column width when there’s no extra width\n* `#2304 <https://github.com/Kozea/WeasyPrint/issues/2304>`_:\n  Don’t forget skip stack when drawing flex items\n* `#2316 <https://github.com/Kozea/WeasyPrint/issues/2316>`_:\n  Don’t crash with SVG symbols\n* `#2320 <https://github.com/Kozea/WeasyPrint/issues/2320>`_:\n  Fix currentcolor detection when parsing gradient color stops\n* `#2322 <https://github.com/Kozea/WeasyPrint/pull/2322>`_,\n  `#2289 <https://github.com/Kozea/WeasyPrint/issues/2289>`_:\n  Don’t add DLL directories when using Windows executable\n* `#2323 <https://github.com/Kozea/WeasyPrint/pull/2323>`_,\n  `#2305 <https://github.com/Kozea/WeasyPrint/issues/2305>`_:\n  Fix different rendering test\n\nPerformance:\n\n* `#2319 <https://github.com/Kozea/WeasyPrint/issues/2319>`_:\n  Fix memory leaks\n\nDocumentation:\n\n* `#2299 <https://github.com/Kozea/WeasyPrint/pull/2299>`_:\n  Update install instructions for Alpine\n* `#2321 <https://github.com/Kozea/WeasyPrint/pull/2321>`_:\n  Add example invocation of WeasyPrint on the \"Contribute\" page\n\nContributors:\n\n* Guillaume Ayoub\n* Jó Ágila Bitsch\n* Lucie Anglade\n* Alexander Gitter\n* Luke Cousins\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Syslifters\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* Simon Sapin\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* Advance Insight\n* Docraptor\n* Method B\n* FieldHub\n* Yanal-Yves Fargialla\n* Morntag\n* Xavid\n\n\nVersion 63.0\n------------\n\nReleased on 2024-10-29.\n\nDependencies:\n\n* Python 3.13 is now supported\n* pydyf 0.11.0+ is now needed\n* tinycss2 1.4.0+ is now needed\n* tinyhtml5 2.0.0+ is now needed, instead of html5lib\n\nFeatures:\n\n* `#2252 <https://github.com/Kozea/WeasyPrint/pull/2252>`_,\n  `#895 <https://github.com/Kozea/WeasyPrint/issues/895>`_:\n  Handle page groups, with financial support from Code & Co.\n* `#1630 <https://github.com/Kozea/WeasyPrint/issues/1630>`_,\n  `#2286 <https://github.com/Kozea/WeasyPrint/pull/2286>`_:\n  Support CSS Color Level 4\n* `#2192 <https://github.com/Kozea/WeasyPrint/pull/2192>`_:\n  Add PDF variant for debugging purpose\n* `#2208 <https://github.com/Kozea/WeasyPrint/pull/2208>`_:\n  Support submit inputs in PDF forms\n* `#2139 <https://github.com/Kozea/WeasyPrint/pull/2139>`_:\n  Support ``mask-border-*`` properties\n* `#1831 <https://github.com/Kozea/WeasyPrint/issues/1831>`_,\n  `#2143 <https://github.com/Kozea/WeasyPrint/pull/2143>`_:\n  Support radio inputs in PDF forms\n\nBug fixes:\n\n* `#2262 <https://github.com/Kozea/WeasyPrint/issues/2262>`_:\n  Avoid integer overflows when converting units from/to doubles\n* `#2260 <https://github.com/Kozea/WeasyPrint/pull/2260>`_:\n  Avoid float collision with box establishing formatting context\n* `#2240 <https://github.com/Kozea/WeasyPrint/issues/2240>`_,\n  `#2242 <https://github.com/Kozea/WeasyPrint/pull/2242>`_:\n  Handle ``svg`` tags with no size\n* `#2231 <https://github.com/Kozea/WeasyPrint/pull/2231>`_,\n  `#1171 <https://github.com/Kozea/WeasyPrint/issues/1171>`_,\n  `#2222 <https://github.com/Kozea/WeasyPrint/issues/2222>`_,\n  `#1208 <https://github.com/Kozea/WeasyPrint/issues/1208>`_:\n  Fix several problems related to ``flex-direction: column``\n* `#2239 <https://github.com/Kozea/WeasyPrint/issues/2239>`_:\n  Don’t fail when SVG markers are undefined references\n* `#2230 <https://github.com/Kozea/WeasyPrint/issues/2230>`_,\n  `#2238 <https://github.com/Kozea/WeasyPrint/pull/2238>`_:\n  Set explicit flags when loading DLLs on Windows\n* `#2228 <https://github.com/Kozea/WeasyPrint/issues/2228>`_,\n  `#1942 <https://github.com/Kozea/WeasyPrint/issues/1942>`_:\n  Store original and PDF stream images in different cache slots\n* `#2234 <https://github.com/Kozea/WeasyPrint/issues/2234>`_:\n  Apply stylesheet and other basic operations to SVG root tag\n* `#2054 <https://github.com/Kozea/WeasyPrint/issues/2054>`_,\n  `#2233 <https://github.com/Kozea/WeasyPrint/pull/2233>`_:\n  Keep auto margins on flex layout boxes\n* `#1883 <https://github.com/Kozea/WeasyPrint/issues/1883>`_:\n  Don’t crash with empty list marker strings\n* `#2216 <https://github.com/Kozea/WeasyPrint/issues/2216>`_:\n  Fix vertical alignment of out-of-flow elements in tables\n* `#996 <https://github.com/Kozea/WeasyPrint/issues/996>`_,\n  `#2219 <https://github.com/Kozea/WeasyPrint/pull/2219>`_:\n  Don’t ignore absolutely positioned elements inside flex boxes\n* `#2217 <https://github.com/Kozea/WeasyPrint/issues/2217>`_:\n  Don’t crash with ``normal`` column gaps\n* `#1817 <https://github.com/Kozea/WeasyPrint/issues/1817>`_:\n  Don’t assume that lines break after spaces\n* `#1868 <https://github.com/Kozea/WeasyPrint/issues/1868>`_:\n  Don’t break rows with atomic cells\n* `#2166 <https://github.com/Kozea/WeasyPrint/issues/2166>`_:\n  Don’t display bottom border on cells in split rows\n* `61852c4 <https://github.com/Kozea/WeasyPrint/commit/61852c4>`_:\n  Capture fontTools logs when subsetting fonts\n* `#2190 <https://github.com/Kozea/WeasyPrint/pull/2190>`_:\n  Don’t use a pattern when drawing backgrounds for no-repeat background images\n* `#2185 <https://github.com/Kozea/WeasyPrint/issues/2185>`_:\n  Check that Harfbuzz version is at least 4.1.0 to subset fonts\n* `#2180 <https://github.com/Kozea/WeasyPrint/issues/2180>`_:\n  Store width for all glyphs when font is not subset\n* `#2183 <https://github.com/Kozea/WeasyPrint/issues/2183>`_:\n  Respect ``break-inside: avoid`` for flex items\n* `#2055 <https://github.com/Kozea/WeasyPrint/issues/2055>`_,\n  `#2058 <https://github.com/Kozea/WeasyPrint/pull/2058>`_:\n  Fix right-to-left tables with collapsed borders\n* `#2179 <https://github.com/Kozea/WeasyPrint/pull/2179>`_,\n  `#1128 <https://github.com/Kozea/WeasyPrint/issues/1128>`_:\n  Handle buggy Adobe Photoshop CMYK JPEGs\n* `#2175 <https://github.com/Kozea/WeasyPrint/issues/2175>`_:\n  Don’t compress PDF metadata for PDF/A-1\n* `#2174 <https://github.com/Kozea/WeasyPrint/issues/2174>`_:\n  Fix extra width distribution for auto table layout\n\nPerformance:\n\n* `#1155 <https://github.com/Kozea/WeasyPrint/issues/1155>`_:\n  Improve rendering speed for large colspan values\n* `#2120 <https://github.com/Kozea/WeasyPrint/issues/2120>`_,\n  `#2178 <https://github.com/Kozea/WeasyPrint/pull/2178>`_:\n  Use Harfbuzz to subset fonts by default\n\nDocumentation:\n\n* `#2282 <https://github.com/Kozea/WeasyPrint/issues/2282>`_,\n  `#2284 <https://github.com/Kozea/WeasyPrint/pull/2284>`_:\n  Simplify Alpine install instructions\n* `#2254 <https://github.com/Kozea/WeasyPrint/issues/2254>`_:\n  Add warning about antivirus false detection\n* `#2220 <https://github.com/Kozea/WeasyPrint/pull/2220>`_:\n  Add extra information to debug logs\n* `#2211 <https://github.com/Kozea/WeasyPrint/pull/2211>`_:\n  Fix link to samples\n* `#2195 <https://github.com/Kozea/WeasyPrint/pull/2195>`_:\n  Update cache argument documentation\n* `#2105 <https://github.com/Kozea/WeasyPrint/issues/2105>`_,\n  `#2151 <https://github.com/Kozea/WeasyPrint/pull/2151>`_:\n  Use MSYS2 instead of GTK+3 for Windows\n\nContributors:\n\n* Guillaume Ayoub\n* David Huggins-Daines\n* Xavid Pretzer\n* Yann Trividic\n* Kevin Kays\n* Alejandro Avilés\n* Gianluca Teti\n* Gregory Goodson\n* Lucie Anglade\n* Roman Sirokov\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Syslifters\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* Simon Sapin\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* Advance Insight\n* Docraptor\n* Method B\n* FieldHub\n* Yanal-Yves Fargialla\n* Morntag\n* Xavid\n\n\nVersion 62.3\n------------\n\nReleased on 2024-06-21.\n\nBug fixes:\n\n* `#2174 <https://github.com/Kozea/WeasyPrint/issues/2174>`_:\n  Fix extra width distribution for auto table layout\n* `#2175 <https://github.com/Kozea/WeasyPrint/issues/2175>`_:\n  Don’t compress PDF metadata for PDF/A-1\n* `61f8bb3 <https://github.com/Kozea/WeasyPrint/commit/61f8bb3>`_:\n  Set default PDF variant values in options before generating PDF\n* `2c4351e <https://github.com/Kozea/WeasyPrint/commit/2c4351e>`_:\n  Avoid PDF artifacts when drawing 0-width borders\n* `d9d7f62 <https://github.com/Kozea/WeasyPrint/commit/d9d7f62>`_:\n  Don’t duplicate column when container is split on multiple pages\n* `4617b94 <https://github.com/Kozea/WeasyPrint/commit/4617b94>`_:\n  Don’t set default Fontconfig values for unset properties\n* `4c81663 <https://github.com/Kozea/WeasyPrint/commit/4c81663>`_:\n  Fix layout when all footnotes are removed from the footnote area\n* `#2184 <https://github.com/Kozea/WeasyPrint/issues/2184>`_:\n  Make items overflowing grid wrap to the next row/column\n* `#2187 <https://github.com/Kozea/WeasyPrint/issues/2187>`_:\n  Don’t append useless tracks when grid elements are positioned\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* Simon Sapin\n* René Fritz\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* Docraptor\n* Yanal-Yves Fargialla\n* Douwe van Loenen\n* Morntag\n* Xavid\n\n\nVersion 62.2\n------------\n\nReleased on 2024-06-04.\n\nFeatures:\n\n* `#2142 <https://github.com/Kozea/WeasyPrint/issues/2142>`_,\n  `#2162 <https://github.com/Kozea/WeasyPrint/pull/2162>`_:\n  Support grid-auto-flow: column, with financial support from Menutech\n\nBug fixes:\n\n* `#2167 <https://github.com/Kozea/WeasyPrint/issues/2167>`_:\n  Fix space added by CSS gap at the end\n* `#2134 <https://github.com/Kozea/WeasyPrint/issues/2134>`_:\n  Remove absolute placeholders from discarded content\n* `#2154 <https://github.com/Kozea/WeasyPrint/issues/2154>`_:\n  Don’t crash when grid items have auto margins\n* `8cdd66f <https://github.com/Kozea/WeasyPrint/commit/8cdd66f>`_:\n  Fix CSS nesting for nested selectors with comma\n* `3359db5 <https://github.com/Kozea/WeasyPrint/commit/3359db5>`_:\n  Fix and test grid shorthand\n* `82deda4 <https://github.com/Kozea/WeasyPrint/commit/82deda4>`_:\n  Fix wrong resume_at for split floats\n* `ff2acf1 <https://github.com/Kozea/WeasyPrint/commit/ff2acf1>`_:\n  Ensure that gradient size is positive to please some PDF readers\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* Simon Sapin\n* René Fritz\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* Docraptor\n* Yanal-Yves Fargialla\n* Douwe van Loenen\n* Morntag\n* Xavid\n\n\nVersion 62.1\n------------\n\nReleased on 2024-05-06.\n\nBug fixes:\n\n* `#2144 <https://github.com/Kozea/WeasyPrint/issues/2144>`_,\n  `#2149 <https://github.com/Kozea/WeasyPrint/pull/2149>`_:\n  Avoid broken fonts when generating multiple documents\n* `c10c6892 <https://github.com/Kozea/WeasyPrint/commit/c10c6892>`_:\n  Display at least one grid row on empty pages\n* `#2146 <https://github.com/Kozea/WeasyPrint/issues/2146>`_:\n  Don’t crash when flex container’s parent’s height is auto\n\nContributors:\n\n* Guillaume Ayoub\n* Claudius Ellsel\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* Simon Sapin\n* René Fritz\n* TrainingSparkle\n* Healthchecks.io\n* Docraptor\n* Yanal-Yves Fargialla\n* Douwe van Loenen\n* Morntag\n* Xavid\n\n\nVersion 62.0\n------------\n\nReleased on 2024-04-30.\n\nDependencies:\n\n* Python 3.9+ is now needed, Python 3.7 and 3.8 are not supported anymore\n* pydyf 0.10.0+ is now needed\n* tinycss2 1.3.0+ is now needed\n\nFeatures:\n\n* `#543 <https://github.com/Kozea/WeasyPrint/issues/543>`_,\n  `#2121 <https://github.com/Kozea/WeasyPrint/pull/2121>`_:\n  Support CSS Grid layout\n* `#2124 <https://github.com/Kozea/WeasyPrint/issues/2124>`_,\n  `#2125 <https://github.com/Kozea/WeasyPrint/pull/2125>`_:\n  Support border-image-* properties\n* `#2084 <https://github.com/Kozea/WeasyPrint/issues/2084>`_,\n  `#2077 <https://github.com/Kozea/WeasyPrint/pull/2077>`_:\n  Support CSS nesting\n* `#2101 <https://github.com/Kozea/WeasyPrint/issues/2101>`_:\n  Support HTML maxlength attribute for form fields\n* `#2095 <https://github.com/Kozea/WeasyPrint/pull/2095>`_:\n  Apply overflow to replaced boxes\n* `245e4f5 <https://github.com/Kozea/WeasyPrint/commit/245e4f5>`_:\n  Add support of PDF/A-?u\n\nBug fixes:\n\n* `#2136 <https://github.com/Kozea/WeasyPrint/issues/2136>`_:\n  Don’t clip aligned text in SVG\n* `#2135 <https://github.com/Kozea/WeasyPrint/pull/2135>`_:\n  Allow column-direction flex containers to use percentage-based heights\n* `#2128 <https://github.com/Kozea/WeasyPrint/issues/2128>`_:\n  Don’t crash when a FontConfig object is destroyed early\n* `#2079 <https://github.com/Kozea/WeasyPrint/issues/2079>`_:\n  Fix executable file for some Windows versions\n* `#2131 <https://github.com/Kozea/WeasyPrint/issues/2131>`_:\n  Fix alpha for images before/after transparent text\n* `#2111 <https://github.com/Kozea/WeasyPrint/issues/2111>`_:\n  Handle auto and none values for CSS quotes property\n* `#2103 <https://github.com/Kozea/WeasyPrint/issues/2103>`_:\n  Don’t crash with overconstrained columns\n* `#2100 <https://github.com/Kozea/WeasyPrint/issues/2100>`_:\n  Fix rounding error when detecting overflows\n* `#2093 <https://github.com/Kozea/WeasyPrint/issues/2093>`_,\n  `#2097 <https://github.com/Kozea/WeasyPrint/issues/2097>`_,\n  `#2094 <https://github.com/Kozea/WeasyPrint/pull/2094>`_:\n  Mark use of md5() and sha1() as not for security\n* `#1956 <https://github.com/Kozea/WeasyPrint/issues/1956>`_,\n  `#2087 <https://github.com/Kozea/WeasyPrint/pull/2087>`_:\n  Use CSS table module level 3 to compute widths\n* `#2086 <https://github.com/Kozea/WeasyPrint/pull/2086>`_:\n  Fix selects with empty values displaying None\n* `#1112 <https://github.com/Kozea/WeasyPrint/issues/1112>`_,\n  `#2082 <https://github.com/Kozea/WeasyPrint/issues/2082>`_,\n  `#2085 <https://github.com/Kozea/WeasyPrint/pull/2085>`_:\n  Fix computation for outer min-content width for table cells\n* `016bd81 <https://github.com/Kozea/WeasyPrint/commit/016bd81>`_:\n  Fix many different bugs with SVG markers\n\nPerformance:\n\n* `#2130 <https://github.com/Kozea/WeasyPrint/issues/2130>`_:\n  Cache font key instead of whole font content\n\nDocumentation:\n\n* `#2108 <https://github.com/Kozea/WeasyPrint/pull/2108>`_:\n  Update documentation about CSS leader() function\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n* Xavid Pretzer\n* kygoh\n* Germain Gueutier\n* Vagner José Nicolodi\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* Simon Sapin\n* René Fritz\n* TrainingSparkle\n* Healthchecks.io\n* Docraptor\n* Yanal-Yves Fargialla\n* Douwe van Loenen\n* Morntag\n* Xavid\n\n\nVersion 61.2\n------------\n\nReleased on 2024-03-08.\n\n**This is a security update.**\n\nWe strongly recommend to upgrade WeasyPrint to the latest version if you use\nWeasyPrint 61.0 or 61.1. Older versions are not impacted.\n\nSecurity:\n\n- Always use URL fetcher for attachments\n\nContributors:\n\n* Guillaume Ayoub\n* Ilia Novoselov\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* René Fritz\n* Simon Sapin\n* Arcanite\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* Docraptor\n* Yanal-Yves Fargialla\n* Morntag\n* NBCO\n\n\nVersion 61.1\n------------\n\nReleased on 2024-02-26.\n\nBug fixes:\n\n- `#2075 <https://github.com/Kozea/WeasyPrint/issues/2075>`_:\n  Use default value when variable is not defined\n- `#2070 <https://github.com/Kozea/WeasyPrint/issues/2070>`_:\n  Don’t crash when rendering SVGs with non-text a children\n- Don’t crash when SVG file can’t be rendered\n\nDocumentation:\n\n- `#2067 <https://github.com/Kozea/WeasyPrint/pull/2067>`_:\n  Suggest \"dnf\" instead of \"yum\" to install Fedora packages\n- Improve documentation for Windows\n- Fix required version of TinyCSS2\n\nContributors:\n\n* Guillaume Ayoub\n* Felix Schwarz\n* Lucie Anglade\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* René Fritz\n* Simon Sapin\n* Arcanite\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* Docraptor\n* Yanal-Yves Fargialla\n* Morntag\n* NBCO\n\n\nVersion 61.0\n------------\n\nReleased on 2024-02-12.\n\nPython API:\n\n* ``DocumentMetadata.attachments`` is now a list of ``Attachment`` objects, not\n  a list of ``(url, description)`` tuples.\n\nNew features:\n\n* `#1219 <https://github.com/Kozea/WeasyPrint/issues/1219>`_,\n  `#2017 <https://github.com/Kozea/WeasyPrint/pull/2017>`_:\n  Support var() in shorthand and multiple-value functions\n* `#1986 <https://github.com/Kozea/WeasyPrint/issues/1986>`_:\n  Support percentages for opacity\n* `#2050 <https://github.com/Kozea/WeasyPrint/pull/2050>`_:\n  Build executable file for Windows\n* `#2000 <https://github.com/Kozea/WeasyPrint/pull/2000>`_:\n  Support select fields\n* `#1993 <https://github.com/Kozea/WeasyPrint/issues/1993>`_:\n  Handle background-attachment: fixed to cover the whole page\n* `#2023 <https://github.com/Kozea/WeasyPrint/issues/2023>`_,\n  `#2022 <https://github.com/Kozea/WeasyPrint/pull/2022>`_:\n  Allow text-based file objects for HTML and CSS classes\n* `#2014 <https://github.com/Kozea/WeasyPrint/pull/2014>`_:\n  Remove warnings for PDF/A and PDF/UA compatibility\n\nBug fixes:\n\n* `#2052 <https://github.com/Kozea/WeasyPrint/issues/2052>`_,\n  `#1869 <https://github.com/Kozea/WeasyPrint/pull/1869>`_:\n  Handle attachments for PDF/A documents\n* `#2013 <https://github.com/Kozea/WeasyPrint/issues/2013>`_,\n  `#2051 <https://github.com/Kozea/WeasyPrint/pull/2051>`_:\n  Apply margin to running tables\n* `#1278 <https://github.com/Kozea/WeasyPrint/issues/1278>`_,\n  `#1884 <https://github.com/Kozea/WeasyPrint/pull/1884>`_:\n  Draw collapsed borders of running tables\n* `#2029 <https://github.com/Kozea/WeasyPrint/issues/2029>`_:\n  Fix page counter in non-root absolute boxes\n* `#2043 <https://github.com/Kozea/WeasyPrint/pull/2043>`_:\n  Fix text-anchor on SVG tspan elements\n* `#1968 <https://github.com/Kozea/WeasyPrint/issues/1968>`_,\n  `#2039 <https://github.com/Kozea/WeasyPrint/pull/2039>`_:\n  Use cell's border-height to calculate table row height\n* `#2030 <https://github.com/Kozea/WeasyPrint/issues/2030>`_:\n  Ensure that bounding box is set to invisible text tags\n* `#2040 <https://github.com/Kozea/WeasyPrint/issues/2040>`_,\n  `#2041 <https://github.com/Kozea/WeasyPrint/pull/2041>`_:\n  Don’t crash on malformed URLs\n* `#2026 <https://github.com/Kozea/WeasyPrint/issues/2026>`_:\n  Don’t break pages when fixed-height elements don’t overflow page\n* `#2038 <https://github.com/Kozea/WeasyPrint/issues/2038>`_:\n  Don’t mix original streams when drawing transparent text\n* `#2016 <https://github.com/Kozea/WeasyPrint/issues/2016>`_:\n  Avoid duplication when breaking out-of-flow boxes\n* `#2012 <https://github.com/Kozea/WeasyPrint/issues/2012>`_:\n  Don’t crash when CSS properties have no value\n* `#2010 <https://github.com/Kozea/WeasyPrint/issues/2010>`_,\n  `#1287 <https://github.com/Kozea/WeasyPrint/issues/1287>`_:\n  Fix many corner cases with CSS variables\n* `#1996 <https://github.com/Kozea/WeasyPrint/issues/1996>`_:\n  Don’t crash when drawing groove/ridge collapsed borders\n* `#1982 <https://github.com/Kozea/WeasyPrint/issues/1982>`_:\n  Fix SVG markers size, position and drawing\n\nDocumentation:\n\n* `#2021 <https://github.com/Kozea/WeasyPrint/issues/2021>`_,\n  `#2048 <https://github.com/Kozea/WeasyPrint/pull/2048>`_:\n  Replace non-virtualenv installation instructions with distribution packages\n\nContributors:\n\n* Guillaume Ayoub\n* kygoh\n* Lucie Anglade\n* Timo Ramsauer\n* Alexander Gitter\n* Michael Lisitsa\n* Vagner José Nicolodi\n* Manolis Stamatogiannakis\n* Pascal de Bruijn\n* Viktor Shevtsov\n* Eduardo Gonzalez\n* Kesara Rathnayake\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* René Fritz\n* Simon Sapin\n* Arcanite\n* TrainingSparkle\n* Healthchecks.io\n* Hammerbacher\n* Docraptor\n* Yanal-Yves Fargialla\n* Morntag\n* NBCO\n\n\nVersion 60.2\n------------\n\nReleased on 2023-12-11.\n\nBug fixes:\n\n* `#1982 <https://github.com/Kozea/WeasyPrint/issues/1982>`_:\n  Fix SVG markers size, position and drawing\n* `23cfc775 <https://github.com/Kozea/WeasyPrint/commit/23cfc775>`_:\n  Draw background behind absolutely positioned replaced boxes\n* `fe2f0c69 <https://github.com/Kozea/WeasyPrint/commit/fe2f0c69>`_:\n  Don’t crash with bitmap fonts with no \"glyf\" table\n* `14605225 <https://github.com/Kozea/WeasyPrint/commit/14605225>`_:\n  Improve SVG text-anchor attribute\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* NCC Group\n* René Fritz\n* Nicola Auchmuty\n* Syslifters\n* Hammerbacher\n* TrainingSparkle\n* Daniel Kucharski\n* Healthchecks.io\n* Yanal-Yves Fargialla\n* WakaTime\n* Paheko\n* Synapsium\n* DocRaptor\n\n\nVersion 60.1\n------------\n\nReleased on 2023-09-29.\n\nBug fixes:\n\n* `#1973 <https://github.com/Kozea/WeasyPrint/issues/1973>`_:\n  Fix crash caused by wrong UTF-8 indices\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* NCC Group\n* René Fritz\n* Nicola Auchmuty\n* Syslifters\n* Hammerbacher\n* TrainingSparkle\n* Daniel Kucharski\n* Healthchecks.io\n* Yanal-Yves Fargialla\n* WakaTime\n* Paheko\n* Synapsium\n* DocRaptor\n\n\nVersion 60.0\n------------\n\nReleased on 2023-09-25.\n\nNew features:\n\n* `#1903 <https://github.com/Kozea/WeasyPrint/issues/1903>`_:\n  Print form fields\n* `#1922 <https://github.com/Kozea/WeasyPrint/pull/1922>`_:\n  Add support for textLength and lengthAdjust in SVG text elements\n* `#1965 <https://github.com/Kozea/WeasyPrint/issues/1965>`_:\n  Handle <wbr> tag\n* `#1970 <https://github.com/Kozea/WeasyPrint/pull/1970>`_:\n  Handle y offset of glyphs\n* `#1909 <https://github.com/Kozea/WeasyPrint/issues/1909>`_:\n  Add a --timeout option\n\nBug fixes:\n\n* `#1887 <https://github.com/Kozea/WeasyPrint/pull/1887>`_:\n  Fix footnote-call displayed incorrectly for some fonts\n* `#1890 <https://github.com/Kozea/WeasyPrint/pull/1890>`_:\n  Fix page-margin boxes layout algorithm\n* `#1908 <https://github.com/Kozea/WeasyPrint/pull/1908>`_:\n  Fix IndexError when rendering PDF version 1.4\n* `#1906 <https://github.com/Kozea/WeasyPrint/issues/1906>`_:\n  Apply text transformations to first-letter pseudo elements\n* `#1915 <https://github.com/Kozea/WeasyPrint/pull/1915>`_:\n  Avoid footnote appearing before its call\n* `#1934 <https://github.com/Kozea/WeasyPrint/pull/1934>`_:\n  Fix balance before \"column-span: all\"\n* `#1935 <https://github.com/Kozea/WeasyPrint/issues/1935>`_:\n  Only draw required glyph with OpenType-SVG fonts\n* `#1595 <https://github.com/Kozea/WeasyPrint/issues/1595>`_:\n  Don’t draw clipPath when defined after reference\n* `#1895 <https://github.com/Kozea/WeasyPrint/pull/1895>`_:\n  Don’t ignore min-width when computing cell size\n* `#1899 <https://github.com/Kozea/WeasyPrint/pull/1899>`_:\n  Fix named pages inheritance\n* `#1936 <https://github.com/Kozea/WeasyPrint/pull/1936>`_:\n  Avoid page breaks caused by children of overflow hidden boxes\n* `#1943 <https://github.com/Kozea/WeasyPrint/issues/1943>`_:\n  Use bleed area for page’s painting area\n* `#1946 <https://github.com/Kozea/WeasyPrint/issues/1946>`_:\n  Use margin box of children to define available width for leaders\n\nContributors:\n\n* Guillaume Ayoub\n* Sahil Rohilla\n* Azharuddin Syed\n* kygoh\n* Andy Lenards\n* Gaurav Samudra\n* Michael Wedl\n* Lucie Anglade\n* Obeida Shamoun\n* Evgeniy Krysanov\n\nBackers and sponsors:\n\n* Spacinov\n* Kobalt\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* NCC Group\n* René Fritz\n* Nicola Auchmuty\n* Syslifters\n* Hammerbacher\n* TrainingSparkle\n* Daniel Kucharski\n* Healthchecks.io\n* Yanal-Yves Fargialla\n* WakaTime\n* Paheko\n* Synapsium\n* DocRaptor\n\n\nVersion 59.0\n------------\n\nReleased on 2023-05-11.\n\nThis version also includes the changes from unstable b1 version listed\nbelow.\n\nBug fixes:\n\n* `#1864 <https://github.com/Kozea/WeasyPrint/issues/1864>`_:\n  Handle overflow for svg and symbol tags in SVG images\n* `#1867 <https://github.com/Kozea/WeasyPrint/pull/1867>`_:\n  Remove duplicate compression of attachments\n* `d0ad5c1 <https://github.com/Kozea/WeasyPrint/commit/d0ad5c1>`_:\n  Override use tag children instead of drawing their references\n* `93df1a5 <https://github.com/Kozea/WeasyPrint/commit/93df1a5>`_:\n  Don’t resize the same image twice when the --dpi option is set\n* `#1874 <https://github.com/Kozea/WeasyPrint/pull/1874>`_:\n  Drawn underline and overline behind text\n\nContributors:\n\n* Guillaume Ayoub\n* Timo Ramsauer\n* Alexander Mankuta\n\nBackers and sponsors:\n\n* Castedo Ellerman\n* Kobalt\n* Spacinov\n* Grip Angebotssoftware\n* Crisp BV\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* NCC Group\n* René Fritz\n* Moritz Mahringer\n* Yanal-Yves Fargialla\n* Piotr Horzycki\n* Healthchecks.io\n* TrainingSparkle\n* Hammerbacher\n* Synapsium\n\n\nVersion 59.0b1\n--------------\n\nReleased on 2023-04-14.\n\n**This version is experimental, don't use it in production. If you find bugs,\nplease report them!**\n\nCommand-line API:\n\n* The ``--optimize-size`` option and its short equivalent ``-O`` have been\n  deprecated. To activate or deactivate different size optimizations, you can\n  now use:\n\n  * ``--uncompressed-pdf``,\n  * ``--optimize-images``,\n  * ``--full-fonts``,\n  * ``--hinting``,\n  * ``--dpi <resolution>``, and\n  * ``--jpeg-quality <quality>``.\n\n* A new ``--cache-folder <folder>`` option has been added to store temporary\n  data in the given folder on the disk instead of keeping them in memory.\n\nPython API:\n\n* Global rendering options are now given in ``**options`` instead of dedicated\n  parameters, with slightly different names. It means that the signature of the\n  ``HTML.render()``, ``HTML.write_pdf()`` and ``Document.write_pdf()`` has\n  changed. Here are the steps to port your Python code to v59.0:\n\n  1. Use named parameters for these functions, not positioned parameters.\n  2. Rename some the parameters:\n\n     * ``image_cache`` becomes ``cache`` (see below),\n     * ``identifier`` becomes ``pdf_identifier``,\n     * ``variant`` becomes ``pdf_variant``,\n     * ``version`` becomes ``pdf_version``,\n     * ``forms`` becomes ``pdf_forms``.\n\n* The ``optimize_size`` parameter of ``HTML.render()``, ``HTML.write_pdf()``\n  and ``Document()`` has been removed and will be ignored. You can now use the\n  ``uncompressed_pdf``, ``full_fonts``, ``hinting``, ``dpi`` and\n  ``jpeg_quality`` parameters that are included in ``**options``.\n\n* The ``cache`` parameter can be included in ``**options`` to replace\n  ``image_cache``. If it is a dictionary, this dictionary will be used to store\n  temporary data in memory, and can be even shared between multiple documents.\n  If it’s a folder Path or string, WeasyPrint stores temporary data in the\n  given temporary folder on disk instead of keeping them in memory.\n\nNew features:\n\n* `#1853 <https://github.com/Kozea/WeasyPrint/pull/1853>`_,\n  `#1854 <https://github.com/Kozea/WeasyPrint/issues/1854>`_:\n  Reduce PDF size, with financial support from Code & Co.\n* `#1824 <https://github.com/Kozea/WeasyPrint/issues/1824>`_,\n  `#1829 <https://github.com/Kozea/WeasyPrint/pull/1829>`_:\n  Reduce memory use for images\n* `#1858 <https://github.com/Kozea/WeasyPrint/issues/1858>`_:\n  Add an option to keep hinting information in embedded fonts\n\nBug fixes:\n\n* `#1855 <https://github.com/Kozea/WeasyPrint/issues/1855>`_:\n  Fix position of emojis in justified text\n* `#1852 <https://github.com/Kozea/WeasyPrint/issues/1852>`_:\n  Don’t crash when line can be split before trailing spaces\n* `#1843 <https://github.com/Kozea/WeasyPrint/issues/1843>`_:\n  Fix syntax of dates in metadata\n* `#1827 <https://github.com/Kozea/WeasyPrint/issues/1827>`_,\n  `#1832 <https://github.com/Kozea/WeasyPrint/pull/1832>`_:\n  Fix word-spacing problems with nested tags\n\nDocumentation:\n\n* `#1841 <https://github.com/Kozea/WeasyPrint/issues/1841>`_:\n  Add a paragraph about unsupported calc() function\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n* Alex Ch\n* whi_ne\n* Jonas Castro\n\nBackers and sponsors:\n\n* Castedo Ellerman\n* Kobalt\n* Spacinov\n* Grip Angebotssoftware\n* Crisp BV\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* NCC Group\n* René Fritz\n* Moritz Mahringer\n* Yanal-Yves Fargialla\n* Piotr Horzycki\n* Healthchecks.io\n* TrainingSparkle\n* Hammerbacher\n* Synapsium\n\n\nVersion 58.1\n------------\n\nReleased on 2023-03-07.\n\nBug fixes:\n\n* `#1815 <https://github.com/Kozea/WeasyPrint/issues/1815>`_:\n  Fix bookmarks coordinates\n* `#1822 <https://github.com/Kozea/WeasyPrint/issues/1822>`_,\n  `#1823 <https://github.com/Kozea/WeasyPrint/pull/1823>`_:\n  Fix vertical positioning for absolute replaced elements\n\nDocumentation:\n\n* `#1814 <https://github.com/Kozea/WeasyPrint/pull/1814>`_:\n  Fix broken link pointing to samples\n\nContributors:\n\n* Guillaume Ayoub\n* Jonas Castro\n* Lucie Anglade\n* Menelaos Kotoglou\n\nBackers and sponsors:\n\n* Kobalt\n* Grip Angebotssoftware\n* Spacinov\n* Crisp BV\n* Castedo Ellerman\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* NCC Group\n* René Fritz\n* Moritz Mahringer\n* Yanal-Yves Fargialla\n* Piotr Horzycki\n* Healthchecks.io\n* Hammerbacher\n* TrainingSparkle\n* Synapsium\n\n\nVersion 58.0\n------------\n\nReleased on 2023-02-17.\n\nThis version also includes the changes from unstable b1 version listed\nbelow.\n\nBug fixes:\n\n* `#1807 <https://github.com/Kozea/WeasyPrint/issues/1807>`_:\n  Don’t crash when out-of-flow box is split in out-of-flow parent\n* `#1806 <https://github.com/Kozea/WeasyPrint/issues/1806>`_:\n  Don’t crash when fixed elements aren’t displayed yet in aborted line\n* `#1809 <https://github.com/Kozea/WeasyPrint/issues/1809>`_:\n  Fix background drawing for out-of-the-page transformed boxes\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Kobalt\n* Grip Angebotssoftware\n* Crisp BV\n* Spacinov\n* Castedo Ellerman\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* NCC Group\n* René Fritz\n* Moritz Mahringer\n* Yanal-Yves Fargialla\n* Piotr Horzycki\n* Healthchecks.io\n\n\nVersion 58.0b1\n--------------\n\nReleased on 2023-02-03.\n\n**This version is experimental, don't use it in production. If you find bugs,\nplease report them!**\n\nNew features:\n\n* `#61 <https://github.com/Kozea/WeasyPrint/issues/61>`_,\n  `#1796 <https://github.com/Kozea/WeasyPrint/pull/1796>`_:\n  Support PDF forms, with financial support from Personalkollen\n* `#1173 <https://github.com/Kozea/WeasyPrint/issues/1173>`_:\n  Add style for form fields\n\nBug fixes:\n\n* `#1777 <https://github.com/Kozea/WeasyPrint/issues/1777>`_:\n  Detect JPEG/MPO images as normal JPEG files\n* `#1771 <https://github.com/Kozea/WeasyPrint/pull/1771>`_:\n  Improve SVG gradients\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n\nBackers and sponsors:\n\n* Kobalt\n* Grip Angebotssoftware\n* Crisp BV\n* Spacinov\n* Castedo Ellerman\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* NCC Group\n* René Fritz\n* Moritz Mahringer\n* Yanal-Yves Fargialla\n* Piotr Horzycki\n* Healthchecks.io\n\n\nVersion 57.2\n------------\n\nReleased on 2022-12-23.\n\nBug fixes:\n\n* `0f2e377 <https://github.com/Kozea/WeasyPrint/commit/0f2e377>`_:\n  Print annotations with PDF/A\n* `0e9426f <https://github.com/Kozea/WeasyPrint/commit/0e9426f>`_:\n  Hide annotations with PDF/UA\n* `#1764 <https://github.com/Kozea/WeasyPrint/issues/1764>`_:\n  Use reference instead of stream for annotation appearance stream\n* `#1783 <https://github.com/Kozea/WeasyPrint/pull/1783>`_:\n  Fix multiple font weights for @font-face declarations\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* Spacinov\n* KontextWork\n* René Fritz\n* NCC Group\n* Kobalt\n* Tom Pohl\n* Castedo Ellerman\n* Moritz Mahringer\n* Piotr Horzycki\n* Gábor Nyers\n* Sidharth Kapur\n\n\nVersion 57.1\n------------\n\nReleased on 2022-11-04.\n\nDependencies:\n\n* `#1754 <https://github.com/Kozea/WeasyPrint/pull/1754>`_:\n  Pillow 9.1.0 is now needed\n\nBug fixes:\n\n* `#1756 <https://github.com/Kozea/WeasyPrint/pull/1756>`_:\n  Fix rem font size for SVG images\n* `#1755 <https://github.com/Kozea/WeasyPrint/issues/1755>`_:\n  Keep format when transposing images\n* `#1753 <https://github.com/Kozea/WeasyPrint/issues/1753>`_:\n  Don’t use deprecated ``read_text`` function when ``files`` is available\n* `#1741 <https://github.com/Kozea/WeasyPrint/issues/1741>`_:\n  Generate better manpage\n* `#1747 <https://github.com/Kozea/WeasyPrint/issues/1747>`_:\n  Correctly set target counters in pages’ absolute elements\n* `#1748 <https://github.com/Kozea/WeasyPrint/issues/1748>`_:\n  Always set font size when font is changed in line\n* `2b05137 <https://github.com/Kozea/WeasyPrint/commit/2b05137>`_:\n  Fix stability of font identifiers\n\nDocumentation:\n\n* `#1750 <https://github.com/Kozea/WeasyPrint/pull/1750>`_:\n  Fix documentation spelling\n\nContributors:\n\n* Guillaume Ayoub\n* Eli Schwartz\n* Mikhail Anikin\n* Scott Kitterman\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* Spacinov\n* KontextWork\n* René Fritz\n* NCC Group\n* Kobalt\n* Tom Pohl\n* John R Ellis\n* Castedo Ellerman\n* Moritz Mahringer\n* Gábor\n* Piotr Horzycki\n\n\nVersion 57.0\n------------\n\nReleased on 2022-10-18.\n\nThis version also includes the changes from unstable b1 version listed\nbelow.\n\nNew features:\n\n* `a4fc7a1 <https://github.com/Kozea/WeasyPrint/commit/a4fc7a1>`_:\n  Support image-orientation\n\nBug fixes:\n\n* `#1739 <https://github.com/Kozea/WeasyPrint/issues/1739>`_:\n  Set baseline on all flex containers\n* `#1740 <https://github.com/Kozea/WeasyPrint/issues/1740>`_:\n  Don’t crash when currentColor is set on root svg tag\n* `#1718 <https://github.com/Kozea/WeasyPrint/issues/1718>`_:\n  Don’t crash with empty bitmap glyphs\n* `#1736 <https://github.com/Kozea/WeasyPrint/issues/1736>`_:\n  Always use the font’s vector variant when possible\n* `eef8b4d <https://github.com/Kozea/WeasyPrint/commit/eef8b4d>`_:\n  Always set color and state before drawing\n* `#1662 <https://github.com/Kozea/WeasyPrint/issues/1662>`_:\n  Use a stable key to store stream fonts\n* `#1733 <https://github.com/Kozea/WeasyPrint/issues/1733>`_:\n  Don’t remove attachments when adding internal anchors\n* `3c4fa50 <https://github.com/Kozea/WeasyPrint/commit/3c4fa50>`_,\n  `c215697 <https://github.com/Kozea/WeasyPrint/commit/c215697>`_,\n  `d275dac <https://github.com/Kozea/WeasyPrint/commit/d275dac>`_,\n  `b04bfff <https://github.com/Kozea/WeasyPrint/commit/b04bfff>`_:\n  Fix many bugs related to PDF/UA structure\n\nPerformance:\n\n* `dfccf1b <https://github.com/Kozea/WeasyPrint/commit/dfccf1b>`_:\n  Use faces as fonts dictionary keys\n* `0dc12b6 <https://github.com/Kozea/WeasyPrint/commit/0dc12b6>`_:\n  Cache add_font to avoid calling get_face too often\n* `75e17bf <https://github.com/Kozea/WeasyPrint/commit/75e17bf>`_:\n  Don’t call process_whitespace twice on many children\n* `498d3e1 <https://github.com/Kozea/WeasyPrint/commit/498d3e1>`_:\n  Optimize __missing__ functions\n\nDocumentation:\n\n* `863b3d6 <https://github.com/Kozea/WeasyPrint/commit/863b3d6>`_:\n  Update documentation of installation on macOS with Homebrew\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* Spacinov\n* KontextWork\n* René Fritz\n* NCC Group\n* Kobalt\n* Tom Pohl\n* John R Ellis\n* Castedo Ellerman\n* Moritz Mahringer\n* Gábor\n* Piotr Horzycki\n\n\nVersion 57.0b1\n--------------\n\nReleased on 2022-09-22.\n\n**This version is experimental, don't use it in production. If you find bugs,\nplease report them!**\n\nNew features:\n\n* `#1704 <https://github.com/Kozea/WeasyPrint/pull/1704>`_:\n  Support PDF/UA, with financial support from Novareto\n* `#1454 <https://github.com/Kozea/WeasyPrint/issues/1454>`_:\n  Support variable fonts\n\nBug fixes:\n\n* `#1058 <https://github.com/Kozea/WeasyPrint/issues/1058>`_:\n  Fix bullet position after page break, with financial support from OpenZeppelin\n* `#1707 <https://github.com/Kozea/WeasyPrint/issues/1707>`_:\n  Fix footnote positioning in multicolumn layout, with financial support from Code & Co.\n* `#1722 <https://github.com/Kozea/WeasyPrint/issues/1722>`_:\n  Handle skew transformation with only one parameter\n* `#1715 <https://github.com/Kozea/WeasyPrint/issues/1715>`_:\n  Don’t crash when images are truncated\n* `#1697 <https://github.com/Kozea/WeasyPrint/issues/1697>`_:\n  Don’t crash when attr() is used in text-decoration-color\n* `#1695 <https://github.com/Kozea/WeasyPrint/pull/1695>`_:\n  Include language information in PDF metadata\n* `#1612 <https://github.com/Kozea/WeasyPrint/issues/1612>`_:\n  Don’t lowercase letters when capitalizing text\n* `#1700 <https://github.com/Kozea/WeasyPrint/issues/1700>`_:\n  Fix crash when rendering footnote with repagination\n* `#1667 <https://github.com/Kozea/WeasyPrint/issues/1667>`_:\n  Follow EXIF metadata for image rotation\n* `#1669 <https://github.com/Kozea/WeasyPrint/issues/1669>`_:\n  Take care of floats when remvoving placeholders\n* `#1638 <https://github.com/Kozea/WeasyPrint/issues/1638>`_:\n  Use the original box when breaking waiting children\n\nContributors:\n\n* Guillaume Ayoub\n* Konstantin Weddige\n* VeteraNovis\n* Lucie Anglade\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* Spacinov\n* KontextWork\n* René Fritz\n* NCC Group\n* Kobalt\n* Tom Pohl\n* John R Ellis\n* Moritz Mahringer\n* Gábor\n* Piotr Horzycki\n* Andrew Ittner\n\n\nVersion 56.1\n------------\n\nReleased on 2022-07-24.\n\nBug fixes:\n\n* `#1674 <https://github.com/Kozea/WeasyPrint/issues/1674>`_:\n  Follow max-height on footnot area, with financial support from Code & Co.\n* `#1678 <https://github.com/Kozea/WeasyPrint/issues/1678>`_:\n  Fix gradients with opacity set\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* Spacinov\n* KontextWork\n* René Fritz\n* NCC Group\n* Kobalt\n* Tom Pohl\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* Gábor\n* Piotr Horzycki\n* Andrew Ittner\n\n\nVersion 56.0\n------------\n\nReleased on 2022-07-07.\n\nThis version also includes the changes from unstable b1 version listed\nbelow.\n\nNew features:\n\n* `70f9b62 <https://github.com/Kozea/WeasyPrint/commit/70f9b62>`_:\n  Support format 5 for bitmap glyphs\n\nBug fixes:\n\n* `#1666 <https://github.com/Kozea/WeasyPrint/issues/1666>`_\n  Fix reproducible PDF generation with embedded images\n* `#1668 <https://github.com/Kozea/WeasyPrint/issues/1668>`_:\n  Fix @page:nth() selector\n* `3bd9a8e <https://github.com/Kozea/WeasyPrint/commit/3bd9a8e>`_:\n  Don’t limit the opacity groups to the original box size\n* `cb9540b <https://github.com/Kozea/WeasyPrint/commit/cb9540b>`_,\n  `76d174f <https://github.com/Kozea/WeasyPrint/commit/76d174f>`_,\n  `9ce6547 <https://github.com/Kozea/WeasyPrint/commit/9ce6547>`_:\n  Minor bugfixes for split table rows\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* Spacinov\n* KontextWork\n* René Fritz\n* NCC Group\n* Kobalt\n* Des images et des mots\n* Andreas Zettl\n* Tom Pohl\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* Gábor\n* Piotr Horzycki\n\n\nVersion 56.0b1\n--------------\n\nReleased on 2022-06-17.\n\n**This version is experimental, don't use it in production. If you find bugs,\nplease report them!**\n\nDependencies:\n\n* pydyf 0.2.0+ is now needed\n\nNew features:\n\n* `#1660 <https://github.com/Kozea/WeasyPrint/pull/1660>`_:\n  Support nested line-clamp, with financial support from Expert Germany\n* `#1644 <https://github.com/Kozea/WeasyPrint/pull/1644>`_,\n  `#1645 <https://github.com/Kozea/WeasyPrint/issues/1645>`_:\n  Support bitmap fonts, with financial support from Expert Germany\n* `#1651 <https://github.com/Kozea/WeasyPrint/pull/1651>`_,\n  `#630 <https://github.com/Kozea/WeasyPrint/issues/630>`_:\n  Support PDF/A, with financial support from Blueshoe\n\nBug fixes:\n\n* `#1656 <https://github.com/Kozea/WeasyPrint/issues/1656>`_:\n  Fix chained variables in the same selector block\n* `#1028 <https://github.com/Kozea/WeasyPrint/issues/1028>`_:\n  Fix font weight management in @font-face rules\n* `#1653 <https://github.com/Kozea/WeasyPrint/issues/1653>`_:\n  Don’t crash when @font-face’s src ends with a comma\n* `#1650 <https://github.com/Kozea/WeasyPrint/issues/1650>`_:\n  Don’t check origin when URL only contains fragment\n* `e38bff8 <https://github.com/Kozea/WeasyPrint/commit/e38bff8>`_:\n  Don’t crash when inherited SVG attributes are not set on the parent\n\nPerformance:\n\n* `e6021da <https://github.com/Kozea/WeasyPrint/commit/e6021da>`_:\n  Launch tests in parallel by default\n\nContributors:\n\n* Guillaume Ayoub\n* aschmitz\n* Lucie Anglade\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* Spacinov\n* KontextWork\n* René Fritz\n* NCC Group\n* Kobalt\n* Des images et des mots\n* Andreas Zettl\n* Tom Pohl\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* Gábor\n* Piotr Horzycki\n\n\nVersion 55.0\n------------\n\nReleased on 2022-05-12.\n\nThis version also includes the changes from unstable b1 version listed\nbelow.\n\nBug fixes:\n\n* `#1626 <https://github.com/Kozea/WeasyPrint/issues/1626>`_,\n  `3802f88 <https://github.com/Kozea/WeasyPrint/commit/3802f88>`_:\n  Fix the vertical position and available height of absolute boxes\n* `9641098 <https://github.com/Kozea/WeasyPrint/commit/9641098>`_,\n  `e5e6b88 <https://github.com/Kozea/WeasyPrint/commit/e5e6b88>`_:\n  Minor fixes for multi-column layout\n* `0fcc7de <https://github.com/Kozea/WeasyPrint/commit/0fcc7de>`_:\n  Don’t stop rendering SVG when CSS parsing fails\n* `#1636 <https://github.com/Kozea/WeasyPrint/pull/1636>`_:\n  Fix sequential footnotes that could disappear when overflowing\n* `#1637 <https://github.com/Kozea/WeasyPrint/issues/1637>`_:\n  Fix position of absolute boxes with right-to-left direction\n* `#1641 <https://github.com/Kozea/WeasyPrint/issues/1641>`_:\n  Fix relative paths for SVG files stored as data URLs\n\nContributors:\n\n* Guillaume Ayoub\n* aschmitz\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* Spacinov\n* KontextWork\n* René Fritz\n* NCC Group\n* Kobalt\n* Nathalie Gutton\n* Andreas Zettl\n* Tom Pohl\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* Gábor\n* Piotr Horzycki\n\n\nVersion 55.0b1\n--------------\n\nReleased on 2022-04-15.\n\n**This version is experimental, don't use it in production. If you find bugs,\nplease report them!**\n\nDependencies:\n\n* Python 3.7+ is now needed, Python 3.6 is not supported anymore\n\nNew features:\n\n* `#1534 <https://github.com/Kozea/WeasyPrint/pull/1534>`_:\n  Support ``word-break: break-all``\n* `#489 <https://github.com/Kozea/WeasyPrint/issues/489>`_,\n  `#1619 <https://github.com/Kozea/WeasyPrint/pull/1619>`_:\n  Support column breaks\n* `#1553 <https://github.com/Kozea/WeasyPrint/issues/1553>`_:\n  Allow reproducible PDF generation\n\nBug fixes:\n\n* `#1007 <https://github.com/Kozea/WeasyPrint/issues/1007>`_,\n  `#1524 <https://github.com/Kozea/WeasyPrint/pull/1524>`_:\n  Handle ``inherit`` in shorthand properties\n* `#1539 <https://github.com/Kozea/WeasyPrint/issues/1539>`_,\n  `#1541 <https://github.com/Kozea/WeasyPrint/pull/1541>`_:\n  Space out no-repeat patterns\n* `#1554 <https://github.com/Kozea/WeasyPrint/pull/1554>`_:\n  Avoid invalid PDF operators when drawing SVG text\n* `#1564 <https://github.com/Kozea/WeasyPrint/issues/1564>`_,\n  `#1566 <https://github.com/Kozea/WeasyPrint/pull/1566>`_,\n  `#1570 <https://github.com/Kozea/WeasyPrint/pull/1570>`_:\n  Don’t output footnotes before their call sites\n* `#1020 <https://github.com/Kozea/WeasyPrint/issues/1020>`_,\n  `#1597 <https://github.com/Kozea/WeasyPrint/pull/1597>`_:\n  Prevent infinite loops in multi-column layout\n* `#1512 <https://github.com/Kozea/WeasyPrint/issues/1512>`_,\n  `#1613 <https://github.com/Kozea/WeasyPrint/pull/1613>`_:\n  Fix position of absolute boxes in right-to-left contexts\n* `#1093 <https://github.com/Kozea/WeasyPrint/issues/1093>`_:\n  Draw borders around absolute replaced boxes\n* `#984 <https://github.com/Kozea/WeasyPrint/issues/984>`_,\n  `#1604 <https://github.com/Kozea/WeasyPrint/issues/1604>`_:\n  Fix skip stacks for columns\n* `#1621 <https://github.com/Kozea/WeasyPrint/issues/1621>`_:\n  Better support of nested ``text-decoration`` properties\n* `fe1f3d9 <https://github.com/Kozea/WeasyPrint/commit/fe1f3d9>`_:\n  Fix absolute blocks in lines\n* `4650b70 <https://github.com/Kozea/WeasyPrint/commit/4650b70>`_:\n  Clear adjoining margins when a container’s child doesn’t fit\n\nPerformance:\n\n* `#1548 <https://github.com/Kozea/WeasyPrint/pull/1548>`_:\n  Improve tests speed\n* `3b0ae92 <https://github.com/Kozea/WeasyPrint/commit/3b0ae92>`_,\n  `#1457 <https://github.com/Kozea/WeasyPrint/issues/1457>`_:\n  Improve fonts management\n* `#1597 <https://github.com/Kozea/WeasyPrint/pull/1597>`_:\n  Improve column layout speed\n* `#1587 <https://github.com/Kozea/WeasyPrint/pull/1587>`_,\n  `#1607 <https://github.com/Kozea/WeasyPrint/pull/1607>`_,\n  `#1608 <https://github.com/Kozea/WeasyPrint/pull/1608>`_:\n  Cache ``ch`` and ``ex`` units calculations\n\nContributors:\n\n* Guillaume Ayoub\n* aschmitz\n* Lucie Anglade\n* Christoph Kepper\n* Jack Lin\n* Rian McGuire\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* KontextWork\n* Maykin Media\n* René Fritz\n* NCC Group\n* Spacinov\n* Nathalie Gutton\n* Andreas Zettl\n* Tom Pohl\n* Kobalt\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* Gábor\n* Piotr Horzycki\n* DeivGuerrero\n\n\nVersion 54.3\n------------\n\nReleased on 2022-04-04.\n\nBug fixes:\n\n* `#1588 <https://github.com/Kozea/WeasyPrint/pull/1588>`_:\n  Support position: absolute in footnotes\n* `#1586 <https://github.com/Kozea/WeasyPrint/issues/1586>`_:\n  Fix discarded text-align values\n\nContributors:\n\n* aschmitz\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* KontextWork\n* Maykin Media\n* René Fritz\n* NCC Group\n* Spacinov\n* Nathalie Gutton\n* Andreas Zettl\n* Tom Pohl\n* Kobalt\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* Gábor\n* Piotr Horzycki\n* DeivGuerrero\n\n\nVersion 54.2\n------------\n\nReleased on 2022-02-27.\n\nBug fixes:\n\n* `#1575 <https://github.com/Kozea/WeasyPrint/issues/1575>`_:\n  Always store parent blocks children as lists\n* `#1574 <https://github.com/Kozea/WeasyPrint/issues/1574>`_,\n  `#1559 <https://github.com/Kozea/WeasyPrint/pull/1559>`_:\n  Fix float rounding errors\n* `#1571 <https://github.com/Kozea/WeasyPrint/issues/1571>`_:\n  Ignore unknown glyphs\n* `#1561 <https://github.com/Kozea/WeasyPrint/issues/1561>`_,\n  `#1562 <https://github.com/Kozea/WeasyPrint/issues/1562>`_:\n  Fix line break when breaks occur between a nbsp and an inline block\n* `#1560 <https://github.com/Kozea/WeasyPrint/issues/1560>`_:\n  Always set the child index\n* `#1558 <https://github.com/Kozea/WeasyPrint/issues/1558>`_:\n  Fix patterns with use tags\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n* Jack Lin\n* aschmitz\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* Crisp BV\n* SimonSoft\n* Menutech\n* KontextWork\n* Maykin Media\n* René Fritz\n* NCC Group\n* Spacinov\n* Nathalie Gutton\n* Andreas Zettl\n* Tom Pohl\n* Kobalt\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* Gábor\n* Piotr Horzycki\n* DeivGuerrero\n\n\nVersion 54.1\n------------\n\nReleased on 2022-01-31.\n\nNew features:\n\n* `#1547 <https://github.com/Kozea/WeasyPrint/issues/1547>`_:\n  Handle break-inside: avoid on tr tags\n\nBug fixes:\n\n* `#1540 <https://github.com/Kozea/WeasyPrint/issues/1540>`_,\n  `#1239 <https://github.com/Kozea/WeasyPrint/issues/1239>`_:\n  Handle absolute children in running elements\n* `#1538 <https://github.com/Kozea/WeasyPrint/issues/1538>`_:\n  Handle invalid values in text-align\n* `#1536 <https://github.com/Kozea/WeasyPrint/issues/1536>`_:\n  Handle absolute flex boxes\n\nContirbutors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n\nBackers and sponsors:\n\n* H-Net: Humanities and Social Sciences Online\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* Crisp BV\n* Maykin Media\n* René Fritz\n* Simon Sapin\n* NCC Group\n* Nathalie Gutton\n* Andreas Zettl\n* Tom Pohl\n* Spacinov\n* Des images et des mots\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* Gábor\n* Piotr Horzycki\n\n\nVersion 54.0\n------------\n\nReleased on 2022-01-08.\n\nThis version also includes the changes from unstable b1 version listed\nbelow.\n\nBug fixes:\n\n* `#1531 <https://github.com/Kozea/WeasyPrint/issues/1531>`_:\n  Always use absolute paths to get hrefs in SVG\n* `#1523 <https://github.com/Kozea/WeasyPrint/issues/1523>`_:\n  Fix many rendering problems of broken tables\n* `e1aee70 <https://github.com/Kozea/WeasyPrint/commit/e1aee70>`_:\n  Fix support of fonts with SVG emojis\n\nContirbutors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* Crisp BV\n* Maykin Media\n* René Fritz\n* Simon Sapin\n* NCC Group\n* Nathalie Gutton\n* Andreas Zettl\n* Tom Pohl\n* Des images et des mots\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* Gábor\n* Piotr Horzycki\n\n\nVersion 54.0b1\n--------------\n\nReleased on 2021-12-13.\n\n**This version is experimental, don't use it in production. If you find bugs,\nplease report them!**\n\nDependencies:\n\n* html5lib 1.1+ is now needed.\n\nNew features:\n\n* `#1509 <https://github.com/Kozea/WeasyPrint/pull/1509>`_:\n  Support footnotes, with financial support from Code & Co.\n* `#36 <https://github.com/Kozea/WeasyPrint/issues/36>`_:\n  Handle parallel flows for floats, absolutes, table-cells\n* `#1389 <https://github.com/Kozea/WeasyPrint/pull/1389>`_:\n  Support ``text-align-last`` and ``text-align-all`` properties\n* `#1434 <https://github.com/Kozea/WeasyPrint/pull/1434>`_:\n  Draw SVG and PNG emojis\n* `#1520 <https://github.com/Kozea/WeasyPrint/pull/1520>`_:\n  Support ``overflow-wrap: anywhere``\n* `#1435 <https://github.com/Kozea/WeasyPrint/issues/1435>`_:\n  Add environment variable to set DLL folder on Windows\n\nPerformance:\n\n* `#1439 <https://github.com/Kozea/WeasyPrint/issues/1439>`_:\n  Cache SVG ``use`` tags\n* `#1481 <https://github.com/Kozea/WeasyPrint/pull/1481>`_:\n  Encode non-JPEG images as PNGs instead of JPEG2000s\n\nBug fixes:\n\n* `#137 <https://github.com/Kozea/WeasyPrint/issues/137>`_:\n  Don’t use ``text-transform`` text for content-based uses\n* `#1443 <https://github.com/Kozea/WeasyPrint/issues/1443>`_:\n  Don’t serialize and parse again inline SVG files\n* `#607 <https://github.com/Kozea/WeasyPrint/issues/607>`_:\n  Correctly handle whitespaces in bookmark labels\n* `#1094 <https://github.com/Kozea/WeasyPrint/issues/1094>`_:\n  Fix column height with ``column-span`` content\n* `#1473 <https://github.com/Kozea/WeasyPrint/issues/1473>`_:\n  Fix absolutely positioned boxes in duplicated pages\n* `#1491 <https://github.com/Kozea/WeasyPrint/issues/1491>`_:\n  Fix ``target-counter`` attribute in flex items\n* `#1515 <https://github.com/Kozea/WeasyPrint/issues/1515>`_,\n  `#1508 <https://github.com/Kozea/WeasyPrint/issues/1508>`_:\n  Don’t draw empty glyphs\n* `#1499 <https://github.com/Kozea/WeasyPrint/issues/1499>`_:\n  Don’t crash when font size is really small\n\nDocumentation:\n\n* `#1519 <https://github.com/Kozea/WeasyPrint/issues/1519>`_:\n  Fix typo\n\nPackaging:\n\n* The source package does not include a ``setup.py`` file anymore. You can find\n  more information about this in\n  `issue #1410 <https://github.com/Kozea/WeasyPrint/issues/1410>`_.\n\nContirbutors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n* Colin Kinloch\n* aschmitz\n* Pablo González\n* Rian McGuire\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* Manuel Barkhau\n* SimonSoft\n* Menutech\n* KontextWork\n* Crisp BV\n* Maykin Media\n* René Fritz\n* Simon Sapin\n* NCC Group\n* Nathalie Gutton\n* Andreas Zettl\n* Tom Pohl\n* Des images et des mots\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* Gábor\n* Piotr Horzycki\n\n\nVersion 53.4\n------------\n\nReleased on 2021-11-14.\n\nBug fixes:\n\n* `#1446 <https://github.com/Kozea/WeasyPrint/issues/1446>`_:\n  Fix background on pages with a bleed property\n* `#1455 <https://github.com/Kozea/WeasyPrint/issues/1455>`_:\n  Use SVG width/height as inner size when no viewBox is given\n* `#1469 <https://github.com/Kozea/WeasyPrint/issues/1469>`_:\n  Only enable letter- and word-spacing when needed\n* `#1471 <https://github.com/Kozea/WeasyPrint/issues/1471>`_:\n  Don’t display inputs with \"hidden\" type\n* `#1485 <https://github.com/Kozea/WeasyPrint/issues/1485>`_:\n  Allow quotes in url() syntax for SVG,\n  Use better approximations for font ascent and descent values in SVG\n* `#1486 <https://github.com/Kozea/WeasyPrint/issues/1486>`_:\n  Fix images embedded from multiple pages\n* `#1489 <https://github.com/Kozea/WeasyPrint/issues/1489>`_:\n  Use a better hash for fonts to avoid collisions\n* `abd54c4 <https://github.com/Kozea/WeasyPrint/commit/abd54c4>`_:\n  Set SVG ratio when width and height are 0\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* SimonSoft\n* Menutech\n* Manuel Barkhau\n* Simon Sapin\n* KontextWork\n* René Fritz\n* Maykin Media\n* NCC Group\n* Crisp BV\n* Des images et des mots\n* Andreas Zettl\n* Nathalie Gutton\n* Tom Pohl\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n* G. Allard\n* Gábor\n\n\nVersion 53.3\n------------\n\nReleased on 2021-09-10.\n\nBug fixes:\n\n* `#1431 <https://github.com/Kozea/WeasyPrint/issues/1431>`_,\n  `#1440 <https://github.com/Kozea/WeasyPrint/issues/1440>`_:\n  Fix crashes and malformed PDF files\n* `#1430 <https://github.com/Kozea/WeasyPrint/issues/1430>`_:\n  Handle cx and cy in SVG rotations\n* `#1436 <https://github.com/Kozea/WeasyPrint/pull/1436>`_:\n  Fix marker-start being drawn on mid vertices\n\nContributors:\n\n* Guillaume Ayoub\n* Rian McGuire\n* Lucie Anglade\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* SimonSoft\n* Menutech\n* Manuel Barkhau\n* Simon Sapin\n* KontextWork\n* René Fritz\n* Maykin Media\n* NCC Group\n* Des images et des mots\n* Andreas Zettl\n* Nathalie Gutton\n* Tom Pohl\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n\n\nVersion 53.2\n------------\n\nReleased on 2021-08-27.\n\nNew features:\n\n* `#1428 <https://github.com/Kozea/WeasyPrint/issues/1428>`_:\n  Re-add the ``make_bookmark_tree()`` method\n\nBug fixes:\n\n* `#1429 <https://github.com/Kozea/WeasyPrint/issues/1429>`_:\n  Fix package deployed on PyPI\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* PDF Blocks\n* SimonSoft\n* Menutech\n* Manuel Barkhau\n* Simon Sapin\n* KontextWork\n* René Fritz\n* Maykin Media\n* NCC Group\n* Des images et des mots\n* Andreas Zettl\n* Nathalie Gutton\n* Tom Pohl\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n\n\nVersion 53.1\n------------\n\nReleased on 2021-08-22.\n\nBug fixes:\n\n* `#1409 <https://github.com/Kozea/WeasyPrint/issues/1409>`_:\n  Don’t crash when leaders are in floats\n* `#1414 <https://github.com/Kozea/WeasyPrint/issues/1414>`_:\n  Embed images once\n* `#1417 <https://github.com/Kozea/WeasyPrint/issues/1417>`_:\n  Fix crash with SVG intrinsic ratio\n\nDocumentation:\n\n* `#1422 <https://github.com/Kozea/WeasyPrint/issues/1422>`_:\n  Include ``weasyprint.tools`` removal in documentation\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Grip Angebotssoftware\n* PDF Blocks\n* SimonSoft\n* Menutech\n* Manuel Barkhau\n* Simon Sapin\n* KontextWork\n* René Fritz\n* Maykin Media\n* NCC Group\n* Des images et des mots\n* Andreas Zettl\n* Nathalie Gutton\n* Tom Pohl\n* Moritz Mahringer\n* Florian Demmer\n* Yanal-Yves Fargialla\n\n\nVersion 53.0\n------------\n\nReleased on 2021-07-31.\n\nThis version also includes the changes from unstable b1 and b2 versions listed\nbelow.\n\nDependencies:\n\n* Pango 1.44.0+ is now needed.\n* pydyf 0.0.3+ is now needed.\n* fontTools 4.0.0+ is now needed.\n* html5lib 1.0.1+ is now needed.\n\nAPI changes:\n\n* ``FontConfiguration`` is now in the ``weasyprint.text.fonts`` module.\n* ``--format`` and ``--resolution`` options have been deprecated, PDF is the\n  only output format supported.\n* ``--optimize-images`` option has been deprecated and replaced by\n  ``--optimize-size``, allowing ``images``, ``fonts``, ``all`` and ``none``\n  values.\n* ``weasyprint.tools`` have been removed.\n* ``Document.resolve_links``, ``Document.make_bookmark_tree`` and\n  ``Document.add_hyperlinks`` have been removed.\n\nPerformance:\n\n* Improve image management\n\nNew features:\n\n* `#1374 <https://github.com/Kozea/WeasyPrint/issues/1374>`_:\n  Support basic \"clipPath\" in SVG\n\nBug fixes:\n\n* `#1369 <https://github.com/Kozea/WeasyPrint/issues/1369>`_:\n  Render use path in SVG\n* `#1370 <https://github.com/Kozea/WeasyPrint/issues/1370>`_:\n  Fix fill color on use path in SVG\n* `#1371 <https://github.com/Kozea/WeasyPrint/issues/1371>`_:\n  Handle stroke-opacity and fill-opacity\n* `#1378 <https://github.com/Kozea/WeasyPrint/issues/1378>`_:\n  Fix crash with borders whose widths are in em\n* `#1394 <https://github.com/Kozea/WeasyPrint/issues/1394>`_:\n  Fix crash on draw_pattern\n* `#880 <https://github.com/Kozea/WeasyPrint/issues/880>`_:\n  Handle stacking contexts put in contexts by previous generations\n* `#1386 <https://github.com/Kozea/WeasyPrint/issues/1386>`_:\n  Catch font subsetting errors\n* `#1403 <https://github.com/Kozea/WeasyPrint/issues/1403>`_:\n  Fix how x and y attributes are handled in SVG\n* `#1399 <https://github.com/Kozea/WeasyPrint/issues/1399>`_,\n  `#1401 <https://github.com/Kozea/WeasyPrint/pull/1401>`_:\n  Don’t crash when use tags reference non-existing element\n* `#1393 <https://github.com/Kozea/WeasyPrint/issues/1393>`_:\n  Handle font collections\n* `#1408 <https://github.com/Kozea/WeasyPrint/issues/1408>`_:\n  Handle x and y attributes in use tags\n\nDocumentation:\n\n* `#1391 <https://github.com/Kozea/WeasyPrint/issues/1391>`_,\n  `#1405 <https://github.com/Kozea/WeasyPrint/pull/1405>`_:\n  Add documentation for installation\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n* Pelle Bo Regener\n* aschmitz\n* John Jackson\n* Felix Schwarz\n* Syrus Dark\n* Christoph Päper\n\nBackers and sponsors:\n\n* OpenEdition\n* Grip Angebotssoftware\n* Simonsoft\n* PDF Blocks\n* Menutech\n* Manuel Barkhau\n* print-css.rocks\n* Simon Sapin\n* KontextWork\n* René Fritz\n* Maykin Media\n* Nathalie Gutton\n* Andreas Zettl\n* Tom Pohl\n* NCC Group\n* Moritz Mahringer\n* Florian Demmer\n* Des images et des mots\n* Mohammed Y. Alnajdi\n* Yanal-Yves Fargialla\n* Yevhenii Hyzyla\n\n\nVersion 53.0b2\n--------------\n\nReleased on 2021-05-30.\n\n**This version is experimental, don't use it in production. If you find bugs,\nplease report them!**\n\nNew features:\n\n* `#359 <https://github.com/Kozea/WeasyPrint/issues/359>`_:\n  Embed full sets of fonts in PDF\n\nBug fixes:\n\n* `#1345 <https://github.com/Kozea/WeasyPrint/issues/1345>`_:\n  Fix position of SVG use tags\n* `#1346 <https://github.com/Kozea/WeasyPrint/pull/1346>`_:\n  Handle \"stroke-dasharray: none\"\n* `#1352 <https://github.com/Kozea/WeasyPrint/issues/1352>`_,\n  `#1358 <https://github.com/Kozea/WeasyPrint/pull/1358>`_:\n  Sort link target identifiers\n* `#1357 <https://github.com/Kozea/WeasyPrint/issues/1357>`_:\n  Fix font information\n* `#1362 <https://github.com/Kozea/WeasyPrint/issues/1362>`_:\n  Handle visibility and display properties in SVG\n* `#1365 <https://github.com/Kozea/WeasyPrint/issues/1365>`_:\n  Cascade inherited attributes for use tags\n* `#1366 <https://github.com/Kozea/WeasyPrint/issues/1366>`_:\n  Correctly handle style attributes in SVG\n* `#1367 <https://github.com/Kozea/WeasyPrint/issues/1367>`_:\n  Include line stroke in box bounding\n\nDocumentation:\n\n* `#1341 <https://github.com/Kozea/WeasyPrint/pull/1341>`_:\n  Fix typos\n\nContributors:\n\n* Guillaume Ayoub\n* aschmitz\n* John Jackson\n* Lucie Anglade\n* Pelle Bo Regener\n\nBackers and sponsors:\n\n* OpenEdition\n* print-css.rocks\n* Simonsoft\n* PDF Blocks\n* Menutech\n* Manuel Barkhau\n* Simon Sapin\n* Grip Angebotssoftware\n* KontextWork\n* René Fritz\n* Nathalie Gutton\n* Andreas Zettl\n* Tom Pohl\n* Maykin Media\n* Moritz Mahringer\n* Florian Demmer\n* Mohammed Y. Alnajdi\n* NCC Group\n* Des images et des mots\n* Yanal-Yves Fargialla\n* Yevhenii Hyzyla\n\n\nVersion 53.0b1\n--------------\n\nReleased on 2021-04-22.\n\n**This version is experimental, don't use it in production. If you find bugs,\nplease report them!**\n\nDependencies:\n\n* This version uses its own PDF generator instead of Cairo. Rendering may be\n  different for text, gradients, SVG images…\n* Packaging is now done with Flit.\n\nNew features:\n\n* `#1328 <https://github.com/Kozea/WeasyPrint/pull/1328>`_:\n  Add ISO and JIS paper sizes\n* `#1309 <https://github.com/Kozea/WeasyPrint/pull/1309>`_:\n  Leader support, with financial support from Simonsoft\n\nBug fixes:\n\n* `#504 <https://github.com/Kozea/WeasyPrint/issues/504>`_:\n  Fix rendering bugs with PDF gradients\n* `#606 <https://github.com/Kozea/WeasyPrint/issues/606>`_:\n  Fix rounding errors on PDF dimensions\n* `#1264 <https://github.com/Kozea/WeasyPrint/issues/1264>`_:\n  Include witdh/height when calculating auto margins of absolute boxes\n* `#1191 <https://github.com/Kozea/WeasyPrint/issues/1191>`_:\n  Don’t try to get an earlier page break between columns\n* `#1235 <https://github.com/Kozea/WeasyPrint/issues/1235>`_:\n  Include padding, border, padding when calculating inline-block width\n* `#1199 <https://github.com/Kozea/WeasyPrint/issues/1199>`_:\n  Fix kerning issues with small fonts\n\nDocumentation:\n\n* `#1298 <https://github.com/Kozea/WeasyPrint/pull/1298>`_:\n  Rewrite documentation\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n* Felix Schwarz\n* Syrus Dark\n* Christoph Päper\n\nBackers and sponsors:\n\n* Simonsoft\n* PDF Blocks\n* Menutech\n* Manuel Barkhau\n* Simon Sapin\n* Nathalie Gutton\n* Andreas Zettl\n* René Fritz\n* Tom Pohl\n* KontextWork\n* Moritz Mahringer\n* Florian Demmer\n* Maykin Media\n* Yanal-Yves Fargialla\n* Des images et des mots\n* Yevhenii Hyzyla\n\n\nVersion 52.5\n------------\n\nReleased on 2021-04-17.\n\nBug fixes:\n\n* `#1336 <https://github.com/Kozea/WeasyPrint/issues/1336>`_:\n  Fix text breaking exception\n* `#1318 <https://github.com/Kozea/WeasyPrint/issues/1318>`_:\n  Fix @font-face rules with Pango 1.48.3+\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Simonsoft\n* PDF Blocks\n* Menutech\n* Manuel Barkhau\n* Simon Sapin\n* Nathalie Gutton\n* Andreas Zettl\n* René Fritz\n* Tom Pohl\n* KontextWork\n* Moritz Mahringer\n* Florian Demmer\n* Maykin Media\n* Yanal-Yves Fargialla\n* Des images et des mots\n* Yevhenii Hyzyla\n\n\nVersion 52.4\n------------\n\nReleased on 2021-03-11.\n\nBug fixes:\n\n* `#1304 <https://github.com/Kozea/WeasyPrint/issues/1304>`_:\n  Don’t try to draw SVG files with no size\n* `ece5f066 <https://github.com/Kozea/WeasyPrint/commit/ece5f066>`_:\n  Avoid crash on last word detection\n* `4ee42e48 <https://github.com/Kozea/WeasyPrint/commit/4ee42e48>`_:\n  Remove last word before ellipses when hyphenated\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* PDF Blocks\n* Simonsoft\n* Menutech\n* Simon Sapin\n* Manuel Barkhau\n* Andreas Zettl\n* Nathalie Gutton\n* Tom Pohl\n* René Fritz\n* Moritz Mahringer\n* Florian Demmer\n* KontextWork\n* Michele Mostarda\n\n\nVersion 52.3\n------------\n\nReleased on 2021-03-02.\n\nBug fixes:\n\n* `#1299 <https://github.com/Kozea/WeasyPrint/issues/1299>`_:\n  Fix imports with url() and quotes\n\nNew features:\n\n* `#1300 <https://github.com/Kozea/WeasyPrint/pull/1300>`_:\n  Add support of line-clamp, with financial support from\n  expert Germany\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n\nBackers and sponsors:\n\n* PDF Blocks\n* Simonsoft\n* Menutech\n* Simon Sapin\n* Manuel Barkhau\n* Andreas Zettl\n* Nathalie Gutton\n* Tom Pohl\n* Moritz Mahringer\n* Florian Demmer\n* KontextWork\n* Michele Mostarda\n\n\nVersion 52.2\n------------\n\nReleased on 2020-12-06.\n\nBug fixes:\n\n* `238e214 <https://github.com/Kozea/WeasyPrint/commit/238e214>`_:\n  Fix URL handling with tinycss2\n* `#1248 <https://github.com/Kozea/WeasyPrint/issues/1248>`_:\n  Include missing test data\n* `#1254 <https://github.com/Kozea/WeasyPrint/issues/1254>`_:\n  Top margins removed from children when tables are displayed on multiple pages\n* `#1250 <https://github.com/Kozea/WeasyPrint/issues/1250>`_:\n  Correctly draw borders on the last line of split tables\n* `a6f9c80 <https://github.com/Kozea/WeasyPrint/commit/a6f9c80>`_:\n  Add a nice gif to please gdk-pixbuf 2.42.0\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n* Felix Schwarz\n\nBackers and sponsors:\n\n* PDF Blocks\n* Simonsoft\n* Menutech\n* Simon Sapin\n* Nathalie Gutton\n* Andreas Zetti\n* Tom Pohl\n* Florian Demmer\n* Moritz Mahringer\n\n\nVersion 52.1\n------------\n\nReleased on 2020-11-02.\n\nBug fixes:\n\n* `238e214 <https://github.com/Kozea/WeasyPrint/commit/238e214>`_:\n  Fix URL handling with tinycss2\n\nContributors:\n\n* Guillaume Ayoub\n\nBackers and sponsors:\n\n* Simonsoft\n* Simon Sapin\n* Nathalie Gutton\n* Andreas Zettl\n* Florian Demmer\n* Moritz Mahringer\n\n\nVersion 52\n----------\n\nReleased on 2020-10-29.\n\nDependencies:\n\n* Python 3.6+ is now needed, Python 3.5 is not supported anymore\n* WeasyPrint now depends on Pillow\n\nNew features:\n\n* `#1019 <https://github.com/Kozea/WeasyPrint/issues/1019>`_:\n  Implement ``counter-set``\n* `#1080 <https://github.com/Kozea/WeasyPrint/issues/1080>`_:\n  Don’t display ``template`` tags\n* `#1210 <https://github.com/Kozea/WeasyPrint/pull/1210>`_:\n  Use ``download`` attribute in ``a`` tags for attachment's filename\n* `#1206 <https://github.com/Kozea/WeasyPrint/issues/1206>`_:\n  Handle strings in ``list-style-type``\n* `#1165 <https://github.com/Kozea/WeasyPrint/pull/1165>`_:\n  Add support for concatenating ``var()`` functions in ``content`` declarations\n* `c56b96b <https://github.com/Kozea/WeasyPrint/commit/c56b96b>`_:\n  Add an option to optimize embedded images size, with financial support from\n  Hashbang\n* `#969 <https://github.com/Kozea/WeasyPrint/issues/969>`_:\n  Add an image cache that can be shared between documents, with financial\n  support from Hashbang\n\nBug fixes:\n\n* `#1141 <https://github.com/Kozea/WeasyPrint/pull/1141>`_:\n  Don’t clip page margins on account of ``body`` overflow\n* `#1000 <https://github.com/Kozea/WeasyPrint/issues/1000>`_:\n  Don’t apply ``text-indent`` twice on inline blocks\n* `#1051 <https://github.com/Kozea/WeasyPrint/issues/1051>`_:\n  Avoid random line breaks\n* `#1120 <https://github.com/Kozea/WeasyPrint/pull/1120>`_:\n  Gather target counters in page margins\n* `#1110 <https://github.com/Kozea/WeasyPrint/issues/1110>`_:\n  Handle most cases for boxes avoiding floats in rtl containers, with financial\n  support from Innovative Software\n* `#1111 <https://github.com/Kozea/WeasyPrint/issues/1111>`_:\n  Fix horizontal position of last rtl line, with financial support from\n  Innovative Software\n* `#1114 <https://github.com/Kozea/WeasyPrint/issues/1114>`_:\n  Fix bug with transparent borders in tables\n* `#1146 <https://github.com/Kozea/WeasyPrint/pull/1146>`_:\n  Don’t gather bookmarks twice for blocks that are displayed on two pages\n* `#1237 <https://github.com/Kozea/WeasyPrint/issues/1237>`_:\n  Use fallback fonts on unsupported WOFF2 and WOFF fonts\n* `#1025 <https://github.com/Kozea/WeasyPrint/issues/1025>`_:\n  Don’t insert the same layout attributes multiple times\n* `#1027 <https://github.com/Kozea/WeasyPrint/issues/1027>`_:\n  Don’t try to break tables after the header or before the footer\n* `#1050 <https://github.com/Kozea/WeasyPrint/issues/1050>`_:\n  Don’t crash on absolute SVG files with no intrinsic size\n* `#1204 <https://github.com/Kozea/WeasyPrint/issues/1204>`_:\n  Fix a crash with a flexbox corner case\n* `#1030 <https://github.com/Kozea/WeasyPrint/pull/1030>`_:\n  Fix frozen builds\n* `#1089 <https://github.com/Kozea/WeasyPrint/pull/1089>`_:\n  Fix Pyinstaller builds\n* `#1216 <https://github.com/Kozea/WeasyPrint/pull/1213>`_:\n  Fix embedded files\n* `#1225 <https://github.com/Kozea/WeasyPrint/pull/1225>`_:\n  Initial support of RTL direction in flexbox layout\n\nDocumentation:\n\n* `#1149 <https://github.com/Kozea/WeasyPrint/issues/1149>`_:\n  Add the ``--quiet`` CLI option in the documentation\n* `#1061 <https://github.com/Kozea/WeasyPrint/pull/1061>`_:\n  Update install instructions on Windows\n\nTests:\n\n* `#1209 <https://github.com/Kozea/WeasyPrint/pull/1209>`_:\n  Use GitHub Actions instead of Travis\n\nContributors:\n\n* Guillaume Ayoub\n* Lucie Anglade\n* Tontyna\n* Mohammed Y. Alnajdi\n* Mike Voets\n* Bjarni Þórisson\n* Balázs Dukai\n* Bart Broere\n* Endalkachew\n* Felix Schwarz\n* Julien Sanchez\n* Konstantin Alekseev\n* Nicolas Hart\n* Nikolaus Schlemm\n* Thomas J. Lampoltshammer\n* mPyth\n* nempoBu4\n* saddy001\n\nBackers and sponsors:\n\n* Hashbang\n* Innovative Software\n* Screenbreak\n* Simon Sapin\n* Lisa Warshaw\n* Nathalie Gutton\n* Andreas Zettl\n* Florian Demmer\n* Moritz Mahringer\n\n\nVersion 51\n----------\n\nReleased on 2019-12-23.\n\nDependencies:\n\n* Pyphen 0.9.1+ is now needed\n\nNew features:\n\n* `#882 <https://github.com/Kozea/WeasyPrint/pull/882>`_:\n  Add support of ``element()`` and ``running()``\n* `#972 <https://github.com/Kozea/WeasyPrint/pull/972>`_:\n  Add HTML element to Box class\n* `7a4d6f8 <https://github.com/Kozea/WeasyPrint/commit/7a4d6f8>`_:\n  Support ``larger`` and ``smaller`` values for ``font-size``\n\nBug fixes:\n\n* `#960 <https://github.com/Kozea/WeasyPrint/pull/960>`_:\n  Fix how fonts used for macOS tests are installed\n* `#956 <https://github.com/Kozea/WeasyPrint/pull/956>`_:\n  Fix various crashes due to line breaking bugs\n* `#983 <https://github.com/Kozea/WeasyPrint/issues/983>`_:\n  Fix typo in variable name\n* `#975 <https://github.com/Kozea/WeasyPrint/pull/975>`_:\n  Don’t crash when ``string-set`` is set to ``none``\n* `#998 <https://github.com/Kozea/WeasyPrint/pull/998>`_:\n  Keep font attributes when text lines are modified\n* `#1005 <https://github.com/Kozea/WeasyPrint/issues/1005>`_:\n  Don’t let presentational hints add decorations on tables with no borders\n* `#974 <https://github.com/Kozea/WeasyPrint/pull/974>`_:\n  Don’t crash on improper ``var()`` values\n* `#1012 <https://github.com/Kozea/WeasyPrint/pull/1012>`_:\n  Fix rendering of header and footer for empty tables\n* `#1013 <https://github.com/Kozea/WeasyPrint/issues/1013>`_:\n  Avoid quadratic time relative to tree depth when setting page names\n\nContributors:\n\n- Lucie Anglade\n- Guillaume Ayoub\n- Guillermo Bonvehí\n- Holger Brunn\n- Felix Schwarz\n- Tontyna\n\n\nVersion 50\n----------\n\nReleased on 2019-09-19.\n\nNew features:\n\n* `#209 <https://github.com/Kozea/WeasyPrint/issues/209>`_:\n  Make ``break-*`` properties work inside tables\n* `#661 <https://github.com/Kozea/WeasyPrint/issues/661>`_:\n  Make blocks with ``overflow: auto`` grow to include floating children\n\nBug fixes:\n\n* `#945 <https://github.com/Kozea/WeasyPrint/issues/945>`_:\n  Don't break pages between a list item and its marker\n* `#727 <https://github.com/Kozea/WeasyPrint/issues/727>`_:\n  Avoid tables lost between pages\n* `#831 <https://github.com/Kozea/WeasyPrint/issues/831>`_:\n  Ignore auto margins on flex containers\n* `#923 <https://github.com/Kozea/WeasyPrint/issues/923>`_:\n  Fix a couple of crashes when splitting a line twice\n* `#896 <https://github.com/Kozea/WeasyPrint/issues/896>`_:\n  Fix skip stack order when using a reverse flex direction\n\nContributors:\n\n- Lucie Anglade\n- Guillaume Ayoub\n\n\nVersion 49\n----------\n\nReleased on 2019-09-11.\n\nPerformance:\n\n* Speed and memory use have been largely improved.\n\nNew features:\n\n* `#700 <https://github.com/Kozea/WeasyPrint/issues/700>`_:\n  Handle ``::marker`` pseudo-selector\n* `135dc06c <https://github.com/Kozea/WeasyPrint/commit/135dc06c>`_:\n  Handle ``recto`` and ``verso`` parameters for page breaks\n* `#907 <https://github.com/Kozea/WeasyPrint/pull/907>`_:\n  Provide a clean way to build layout contexts\n\nBug fixes:\n\n* `#937 <https://github.com/Kozea/WeasyPrint/issues/937>`_:\n  Fix rendering of tables with empty lines and rowspans\n* `#897 <https://github.com/Kozea/WeasyPrint/issues/897>`_:\n  Don't crash when small columns are wrapped in absolute blocks\n* `#913 <https://github.com/Kozea/WeasyPrint/issues/913>`_:\n  Fix a test about gradient colors\n* `#924 <https://github.com/Kozea/WeasyPrint/pull/924>`_:\n  Fix title for document with attachments\n* `#917 <https://github.com/Kozea/WeasyPrint/issues/917>`_:\n  Fix tests with Pango 1.44\n* `#919 <https://github.com/Kozea/WeasyPrint/issues/919>`_:\n  Fix padding and margin management for column flex boxes\n* `#901 <https://github.com/Kozea/WeasyPrint/issues/901>`_:\n  Fix width of replaced boxes with no intrinsic width\n* `#906 <https://github.com/Kozea/WeasyPrint/issues/906>`_:\n  Don't respect table cell width when content doesn't fit\n* `#927 <https://github.com/Kozea/WeasyPrint/pull/927>`_:\n  Don't use deprecated ``logger.warn`` anymore\n* `a8662794 <https://github.com/Kozea/WeasyPrint/commit/a8662794>`_:\n  Fix margin collapsing between caption and table wrapper\n* `87d9e84f <https://github.com/Kozea/WeasyPrint/commit/87d9e84f>`_:\n  Avoid infinite loops when rendering columns\n* `789b80e6 <https://github.com/Kozea/WeasyPrint/commit/789b80e6>`_:\n  Only use in flow children to set columns height\n* `615e298a <https://github.com/Kozea/WeasyPrint/commit/615e298a>`_:\n  Don't include floating elements each time we try to render a column\n* `48d8632e <https://github.com/Kozea/WeasyPrint/commit/48d8632e>`_:\n  Avoid not in flow children to compute column height\n* `e7c452ce <https://github.com/Kozea/WeasyPrint/commit/e7c452ce>`_:\n  Fix collapsing margins for columns\n* `fb0887cf <https://github.com/Kozea/WeasyPrint/commit/fb0887cf>`_:\n  Fix crash when using currentColor in gradients\n* `f66df067 <https://github.com/Kozea/WeasyPrint/commit/f66df067>`_:\n  Don't crash when using ex units in word-spacing in letter-spacing\n* `c790ff20 <https://github.com/Kozea/WeasyPrint/commit/c790ff20>`_:\n  Don't crash when properties needing base URL use var functions\n* `d63eac31 <https://github.com/Kozea/WeasyPrint/commit/d63eac31>`_:\n  Don't crash with object-fit: non images with no intrinsic size\n\nDocumentation:\n\n* `#900 <https://github.com/Kozea/WeasyPrint/issues/900>`_:\n  Add documentation about semantic versioning\n* `#692 <https://github.com/Kozea/WeasyPrint/issues/692>`_:\n  Add a snippet about PDF magnification\n* `#899 <https://github.com/Kozea/WeasyPrint/pull/899>`_:\n  Add .NET wrapper link\n* `#893 <https://github.com/Kozea/WeasyPrint/pull/893>`_:\n  Fixed wrong nested list comprehension example\n* `#902 <https://github.com/Kozea/WeasyPrint/pull/902>`_:\n  Add ``state`` to the ``make_bookmark_tree`` documentation\n* `#921 <https://github.com/Kozea/WeasyPrint/pull/921>`_:\n  Fix typos in the documentation\n* `#328 <https://github.com/Kozea/WeasyPrint/issues/328>`_:\n  Add CSS sample for forms\n\nContributors:\n\n- Lucie Anglade\n- Guillaume Ayoub\n- Raphael Gaschignard\n- Stani\n- Szmen\n- Thomas Dexter\n- Tontyna\n\n\nVersion 48\n----------\n\nReleased on 2019-07-08.\n\nDependencies:\n\n* CairoSVG 2.4.0+ is now needed\n\nNew features:\n\n* `#891 <https://github.com/Kozea/WeasyPrint/pull/891>`_:\n  Handle ``text-overflow``\n* `#878 <https://github.com/Kozea/WeasyPrint/pull/878>`_:\n  Handle ``column-span``\n* `#855 <https://github.com/Kozea/WeasyPrint/pull/855>`_:\n  Handle all the ``text-decoration`` features\n* `#238 <https://github.com/Kozea/WeasyPrint/issues/238>`_:\n  Don't repeat background images when it's not needed\n* `#875 <https://github.com/Kozea/WeasyPrint/issues/875>`_:\n  Handle ``object-fit`` and ``object-position``\n* `#870 <https://github.com/Kozea/WeasyPrint/issues/870>`_:\n  Handle ``bookmark-state``\n\nBug fixes:\n\n* `#686 <https://github.com/Kozea/WeasyPrint/issues/686>`_:\n  Fix column balance when children are not inline\n* `#885 <https://github.com/Kozea/WeasyPrint/issues/885>`_:\n  Actually use the content box to resolve flex items percentages\n* `#867 <https://github.com/Kozea/WeasyPrint/issues/867>`_:\n  Fix rendering of KaTeX output, including (1) set row baseline of tables when\n  no cells are baseline-aligned, (2) set baseline for inline tables, (3) don't\n  align lines larger than their parents, (4) force CairoSVG to respect image\n  size defined by CSS.\n* `#873 <https://github.com/Kozea/WeasyPrint/issues/873>`_:\n  Set a minimum height for empty list elements with outside marker\n* `#811 <https://github.com/Kozea/WeasyPrint/issues/811>`_:\n  Don't use translations to align flex items\n* `#851 <https://github.com/Kozea/WeasyPrint/issues/851>`_,\n  `#860 <https://github.com/Kozea/WeasyPrint/issues/860>`_:\n  Don't cut pages when content overflows a very little bit\n* `#862 <https://github.com/Kozea/WeasyPrint/issues/862>`_:\n  Don't crash when using UTC dates in metadata\n\nDocumentation:\n\n* `#854 <https://github.com/Kozea/WeasyPrint/issues/854>`_:\n  Add a \"Tips & Tricks\" section\n\nContributors:\n\n- Gabriel Corona\n- Guillaume Ayoub\n- Manuel Barkhau\n- Nathan de Maestri\n- Lucie Anglade\n- theopeek\n\n\nVersion 47\n----------\n\nReleased on 2019-04-12.\n\nNew features:\n\n* `#843 <https://github.com/Kozea/WeasyPrint/pull/843>`_:\n  Handle CSS variables\n* `#846 <https://github.com/Kozea/WeasyPrint/pull/846>`_:\n  Handle ``:nth()`` page selector\n* `#847 <https://github.com/Kozea/WeasyPrint/pull/847>`_:\n  Allow users to use a custom SSL context for HTTP requests\n\nBug fixes:\n\n* `#797 <https://github.com/Kozea/WeasyPrint/issues/797>`_:\n  Fix underlined justified text\n* `#836 <https://github.com/Kozea/WeasyPrint/issues/836>`_:\n  Fix crash when flex items are replaced boxes\n* `#835 <https://github.com/Kozea/WeasyPrint/issues/835>`_:\n  Fix ``margin-break: auto``\n\n\nVersion 46\n----------\n\nReleased on 2019-03-20.\n\nNew features:\n\n* `#771 <https://github.com/Kozea/WeasyPrint/issues/771>`_:\n  Handle ``box-decoration-break``\n* `#115 <https://github.com/Kozea/WeasyPrint/issues/115>`_:\n  Handle ``margin-break``\n* `#821 <https://github.com/Kozea/WeasyPrint/issues/821>`_:\n  Continuous integration includes tests on Windows\n\nBug fixes:\n\n* `#765 <https://github.com/Kozea/WeasyPrint/issues/765>`_,\n  `#754 <https://github.com/Kozea/WeasyPrint/issues/754>`_,\n  `#800 <https://github.com/Kozea/WeasyPrint/issues/800>`_:\n  Fix many crashes related to the flex layout\n* `#783 <https://github.com/Kozea/WeasyPrint/issues/783>`_:\n  Fix a couple of crashes with strange texts\n* `#827 <https://github.com/Kozea/WeasyPrint/pull/827>`_:\n  Named strings and counters are case-sensitive\n* `#823 <https://github.com/Kozea/WeasyPrint/pull/823>`_:\n  Shrink min/max-height/width according to box-sizing\n* `#728 <https://github.com/Kozea/WeasyPrint/issues/728>`_,\n  `#171 <https://github.com/Kozea/WeasyPrint/issues/171>`_:\n  Don't crash when fixed boxes are nested\n* `#610 <https://github.com/Kozea/WeasyPrint/issues/610>`_,\n  `#828 <https://github.com/Kozea/WeasyPrint/issues/828>`_:\n  Don't crash when preformatted text lines end with a space\n* `#808 <https://github.com/Kozea/WeasyPrint/issues/808>`_,\n  `#387 <https://github.com/Kozea/WeasyPrint/issues/387>`_:\n  Fix position of some images\n* `#813 <https://github.com/Kozea/WeasyPrint/issues/813>`_:\n  Don't crash when long preformatted text lines end with ``\\n``\n\nDocumentation:\n\n* `#815 <https://github.com/Kozea/WeasyPrint/pull/815>`_:\n  Add documentation about custom ``url_fetcher``\n\n\nVersion 45\n----------\n\nReleased on 2019-02-20.\n\nWeasyPrint now has a `code of conduct\n<https://github.com/Kozea/WeasyPrint/blob/master/CODE_OF_CONDUCT.rst>`_.\n\nA new website has been launched, with beautiful and useful graphs about speed\nand memory use across versions: check `WeasyPerf\n<https://kozea.github.io/WeasyPerf/index.html>`_.\n\nDependencies:\n\n* Python 3.5+ is now needed, Python 3.4 is not supported anymore\n\nBug fixes:\n\n* `#798 <https://github.com/Kozea/WeasyPrint/pull/798>`_:\n  Prevent endless loop and index out of range in pagination\n* `#767 <https://github.com/Kozea/WeasyPrint/issues/767>`_:\n  Add a ``--quiet`` CLI parameter\n* `#784 <https://github.com/Kozea/WeasyPrint/pull/784>`_:\n  Fix library loading on Alpine\n* `#791 <https://github.com/Kozea/WeasyPrint/pull/791>`_:\n  Use path2url in tests for Windows\n* `#789 <https://github.com/Kozea/WeasyPrint/pull/789>`_:\n  Add LICENSE file to distributed sources\n* `#788 <https://github.com/Kozea/WeasyPrint/pull/788>`_:\n  Fix pending references\n* `#780 <https://github.com/Kozea/WeasyPrint/issues/780>`_:\n  Don't draw patterns for empty page backgrounds\n* `#774 <https://github.com/Kozea/WeasyPrint/issues/774>`_:\n  Don't crash when links include quotes\n* `#637 <https://github.com/Kozea/WeasyPrint/issues/637>`_:\n  Fix a problem with justified text\n* `#763 <https://github.com/Kozea/WeasyPrint/pull/763>`_:\n  Launch tests with Python 3.7\n* `#704 <https://github.com/Kozea/WeasyPrint/issues/704>`_:\n  Fix a corner case with tables\n* `#804 <https://github.com/Kozea/WeasyPrint/pull/804>`_:\n  Don't logger handlers defined before importing WeasyPrint\n* `#109 <https://github.com/Kozea/WeasyPrint/issues/109>`_,\n  `#748 <https://github.com/Kozea/WeasyPrint/issues/748>`_:\n  Don't include punctuation for hyphenation\n* `#770 <https://github.com/Kozea/WeasyPrint/issues/770>`_:\n  Don't crash when people use uppercase words from old-fashioned Microsoft\n  fonts in tables, especially when there's an 5th column\n* Use a `separate logger\n  <https://weasyprint.readthedocs.io/en/latest/tutorial.html#logging>`_ to\n  report the rendering process\n* Add a ``--debug`` CLI parameter and set debug level for unknown prefixed CSS\n  properties\n* Define minimal versions of Python and setuptools in setup.cfg\n\nDocumentation:\n\n* `#796 <https://github.com/Kozea/WeasyPrint/pull/796>`_:\n  Fix a small typo in the tutorial\n* `#792 <https://github.com/Kozea/WeasyPrint/pull/792>`_:\n  Document no alignment character support\n* `#773 <https://github.com/Kozea/WeasyPrint/pull/773>`_:\n  Fix phrasing in Hacking section\n* `#402 <https://github.com/Kozea/WeasyPrint/issues/402>`_:\n  Add a paragraph about fontconfig error\n* `#764 <https://github.com/Kozea/WeasyPrint/pull/764>`_:\n  Fix list of dependencies for Alpine\n* Fix API documentation of HTML and CSS classes\n\n\nVersion 44\n----------\n\nReleased on 2018-12-29.\n\nBug fixes:\n\n* `#742 <https://github.com/Kozea/WeasyPrint/issues/742>`_:\n  Don't crash during PDF generation when locale uses commas as decimal separator\n* `#746 <https://github.com/Kozea/WeasyPrint/issues/746>`_:\n  Close file when reading VERSION\n* Improve speed and memory usage for long texts.\n\nDocumentation:\n\n* `#733 <https://github.com/Kozea/WeasyPrint/pull/733>`_:\n  Small documentation fixes\n* `#735 <https://github.com/Kozea/WeasyPrint/pull/735>`_:\n  Fix broken links in NEWS.rst\n\n\nVersion 43\n----------\n\nReleased on 2018-11-09.\n\nBug fixes:\n\n* `#726 <https://github.com/Kozea/WeasyPrint/issues/726>`_:\n  Make empty strings clear previous values of named strings\n* `#729 <https://github.com/Kozea/WeasyPrint/issues/729>`_:\n  Include tools in packaging\n\nThis version also includes the changes from unstable rc1 and rc2 versions\nlisted below.\n\n\nVersion 43rc2\n-------------\n\nReleased on 2018-11-02.\n\n**This version is experimental, don't use it in production. If you find bugs,\nplease report them!**\n\nBug fixes:\n\n* `#706 <https://github.com/Kozea/WeasyPrint/issues/706>`_:\n  Fix text-indent at the beginning of a page\n* `#687 <https://github.com/Kozea/WeasyPrint/issues/687>`_:\n  Allow query strings in file:// URIs\n* `#720 <https://github.com/Kozea/WeasyPrint/issues/720>`_:\n  Optimize minimum size calculation of long inline elements\n* `#717 <https://github.com/Kozea/WeasyPrint/issues/717>`_:\n  Display <details> tags as blocks\n* `#691 <https://github.com/Kozea/WeasyPrint/issues/691>`_:\n  Don't recalculate max content widths when distributing extra space for tables\n* `#722 <https://github.com/Kozea/WeasyPrint/issues/722>`_:\n  Fix bookmarks and strings set on images\n* `#723 <https://github.com/Kozea/WeasyPrint/issues/723>`_:\n  Warn users when string() is not used in page margin\n\n\nVersion 43rc1\n-------------\n\nReleased on 2018-10-15.\n\n**This version is experimental, don't use it in production. If you find bugs,\nplease report them!**\n\nDependencies:\n\n* Python 3.4+ is now needed, Python 2.x is not supported anymore\n* Cairo 1.15.4+ is now needed, but 1.10+ should work with missing features\n  (such as links, outlines and metadata)\n* Pdfrw is not needed anymore\n\nNew features:\n\n* `Beautiful website <https://weasyprint.org>`_\n* `#579 <https://github.com/Kozea/WeasyPrint/issues/579>`_:\n  Initial support of flexbox\n* `#592 <https://github.com/Kozea/WeasyPrint/pull/592>`_:\n  Support @font-face on Windows\n* `#306 <https://github.com/Kozea/WeasyPrint/issues/306>`_:\n  Add a timeout parameter to the URL fetcher functions\n* `#594 <https://github.com/Kozea/WeasyPrint/pull/594>`_:\n  Split tests using modern pytest features\n* `#599 <https://github.com/Kozea/WeasyPrint/pull/599>`_:\n  Make tests pass on Windows\n* `#604 <https://github.com/Kozea/WeasyPrint/pull/604>`_:\n  Handle target counters and target texts\n* `#631 <https://github.com/Kozea/WeasyPrint/pull/631>`_:\n  Enable counter-increment and counter-reset in page context\n* `#622 <https://github.com/Kozea/WeasyPrint/issues/622>`_:\n  Allow pathlib.Path objects for HTML, CSS and Attachment classes\n* `#674 <https://github.com/Kozea/WeasyPrint/issues/674>`_:\n  Add extensive installation instructions for Windows\n\nBug fixes:\n\n* `#558 <https://github.com/Kozea/WeasyPrint/issues/558>`_:\n  Fix attachments\n* `#565 <https://github.com/Kozea/WeasyPrint/issues/565>`_,\n  `#596 <https://github.com/Kozea/WeasyPrint/issues/596>`_,\n  `#539 <https://github.com/Kozea/WeasyPrint/issues/539>`_:\n  Fix many PDF rendering, printing and compatibility problems\n* `#614 <https://github.com/Kozea/WeasyPrint/issues/614>`_:\n  Avoid crashes and endless loops caused by a Pango bug\n* `#662 <https://github.com/Kozea/WeasyPrint/pull/662>`_:\n  Fix warnings and errors when generating documentation\n* `#666 <https://github.com/Kozea/WeasyPrint/issues/666>`_,\n  `#685 <https://github.com/Kozea/WeasyPrint/issues/685>`_:\n  Fix many table layout rendering problems\n* `#680 <https://github.com/Kozea/WeasyPrint/pull/680>`_:\n  Don't crash when there's no font available\n* `#662 <https://github.com/Kozea/WeasyPrint/pull/662>`_:\n  Fix support of some align values in tables\n\n\nVersion 0.42.3\n--------------\n\nReleased on 2018-03-27.\n\nBug fixes:\n\n* `#583 <https://github.com/Kozea/WeasyPrint/issues/583>`_:\n  Fix floating-point number error to fix floating box layout\n* `#586 <https://github.com/Kozea/WeasyPrint/issues/586>`_:\n  Don't optimize resume_at when splitting lines with trailing spaces\n* `#582 <https://github.com/Kozea/WeasyPrint/issues/582>`_:\n  Fix table layout with no overflow\n* `#580 <https://github.com/Kozea/WeasyPrint/issues/580>`_:\n  Fix inline box breaking function\n* `#576 <https://github.com/Kozea/WeasyPrint/issues/576>`_:\n  Split replaced_min_content_width and replaced_max_content_width\n* `#574 <https://github.com/Kozea/WeasyPrint/issues/574>`_:\n  Respect text direction and don't translate rtl columns twice\n* `#569 <https://github.com/Kozea/WeasyPrint/issues/569>`_:\n  Get only first line's width of inline children to get linebox width\n\n\nVersion 0.42.2\n--------------\n\nReleased on 2018-02-04.\n\nBug fixes:\n\n* `#560 <https://github.com/Kozea/WeasyPrint/issues/560>`_:\n  Fix a couple of crashes and endless loops when breaking lines.\n\n\nVersion 0.42.1\n--------------\n\nReleased on 2018-02-01.\n\nBug fixes:\n\n* `#566 <https://github.com/Kozea/WeasyPrint/issues/566>`_:\n  Don't crash when using @font-config.\n* `#567 <https://github.com/Kozea/WeasyPrint/issues/567>`_:\n  Fix text-indent with text-align: justify.\n* `#465 <https://github.com/Kozea/WeasyPrint/issues/465>`_:\n  Fix string(\\*, start).\n* `#562 <https://github.com/Kozea/WeasyPrint/issues/562>`_:\n  Handle named pages with pseudo-class.\n* `#507 <https://github.com/Kozea/WeasyPrint/issues/507>`_:\n  Fix running headers.\n* `#557 <https://github.com/Kozea/WeasyPrint/issues/557>`_:\n  Avoid infinite loops in inline_line_width.\n* `#555 <https://github.com/Kozea/WeasyPrint/issues/555>`_:\n  Fix margins, borders and padding in column layouts.\n\n\nVersion 0.42\n------------\n\nReleased on 2017-12-26.\n\nWeasyPrint is not tested with (end-of-life) Python 3.3 anymore.\n\n**This release is probably the last version of the 0.x series.**\n\nNext version may include big changes:\n\n- end of Python 2.7 support,\n- initial support of bidirectional text,\n- initial support of flexbox,\n- improvements for speed and memory usage.\n\nNew features:\n\n* `#532 <https://github.com/Kozea/WeasyPrint/issues/532>`_:\n  Support relative file URIs when using CLI.\n\nBug fixes:\n\n* `#553 <https://github.com/Kozea/WeasyPrint/issues/553>`_:\n  Fix slow performance for pre-formatted boxes with a lot of children.\n* `#409 <https://github.com/Kozea/WeasyPrint/issues/409>`_:\n  Don't crash when rendering some tables.\n* `#39 <https://github.com/Kozea/WeasyPrint/issues/39>`_:\n  Fix rendering of floats in inlines.\n* `#301 <https://github.com/Kozea/WeasyPrint/issues/301>`_:\n  Split lines carefully.\n* `#530 <https://github.com/Kozea/WeasyPrint/issues/530>`_:\n  Fix root when frozen with Pyinstaller.\n* `#534 <https://github.com/Kozea/WeasyPrint/issues/534>`_:\n  Handle SVGs containing images embedded as data URIs.\n* `#360 <https://github.com/Kozea/WeasyPrint/issues/360>`_:\n  Fix border-radius rendering problem with some PDF readers.\n* `#525 <https://github.com/Kozea/WeasyPrint/issues/525>`_:\n  Fix pipenv support.\n* `#227 <https://github.com/Kozea/WeasyPrint/issues/227>`_:\n  Smartly handle replaced boxes with percentage width in auto-width parents.\n* `#520 <https://github.com/Kozea/WeasyPrint/issues/520>`_:\n  Don't ignore CSS @page rules that are imported by an @import rule.\n\n\nVersion 0.41\n------------\n\nReleased on 2017-10-05.\n\nWeasyPrint now depends on pdfrw >= 0.4.\n\nNew features:\n\n* `#471 <https://github.com/Kozea/WeasyPrint/issues/471>`_:\n  Support page marks and bleed.\n\nBug fixes:\n\n* `#513 <https://github.com/Kozea/WeasyPrint/issues/513>`_:\n  Don't crash on unsupported image-resolution values.\n* `#506 <https://github.com/Kozea/WeasyPrint/issues/506>`_:\n  Fix @font-face use with write_* methods.\n* `#500 <https://github.com/Kozea/WeasyPrint/pull/500>`_:\n  Improve readability of _select_source function.\n* `#498 <https://github.com/Kozea/WeasyPrint/issues/498>`_:\n  Use CSS prefixes as recommended by the CSSWG.\n* `#441 <https://github.com/Kozea/WeasyPrint/issues/441>`_:\n  Fix rendering problems and crashes when using @font-face.\n* `bb3a4db <https://github.com/Kozea/WeasyPrint/commit/bb3a4db>`_:\n  Try to break pages after a block before trying to break inside it.\n* `1d1654c <https://github.com/Kozea/WeasyPrint/commit/1d1654c>`_:\n  Fix and test corner cases about named pages.\n\nDocumentation:\n\n* `#508 <https://github.com/Kozea/WeasyPrint/pull/508>`_:\n  Add missing libpangocairo dependency for Debian and Ubuntu.\n* `a7b17fb <https://github.com/Kozea/WeasyPrint/commit/a7b17fb>`_:\n  Add documentation on logged rendering steps.\n\n\nVersion 0.40\n------------\n\nReleased on 2017-08-17.\n\nWeasyPrint now depends on cssselect2 instead of cssselect and lxml.\n\nNew features:\n\n* `#57 <https://github.com/Kozea/WeasyPrint/issues/57>`_:\n  Named pages.\n* Unprefix properties, see\n  `#498 <https://github.com/Kozea/WeasyPrint/issues/498>`_.\n* Add a \"verbose\" option logging the document generation steps.\n\nBug fixes:\n\n* `#483 <https://github.com/Kozea/WeasyPrint/issues/483>`_:\n  Fix slow performance with long pre-formatted texts.\n* `#70 <https://github.com/Kozea/WeasyPrint/issues/70>`_:\n  Improve speed and memory usage for long documents.\n* `#487 <https://github.com/Kozea/WeasyPrint/issues/487>`_:\n  Don't crash on local() fonts with a space and no quotes.\n\n\nVersion 0.39\n------------\n\nReleased on 2017-06-24.\n\nBug fixes:\n\n* Fix the use of WeasyPrint's URL fetcher with CairoSVG.\n\n\nVersion 0.38\n------------\n\nReleased on 2017-06-16.\n\nBug fixes:\n\n* `#477 <https://github.com/Kozea/WeasyPrint/issues/477>`_:\n  Don't crash on font-face's src attributes with local functions.\n\n\nVersion 0.37\n------------\n\nReleased on 2017-06-15.\n\nWeasyPrint now depends on tinycss2 instead of tinycss.\n\nNew features:\n\n* `#437 <https://github.com/Kozea/WeasyPrint/issues/437>`_:\n  Support local links in generated PDFs.\n\nBug fixes:\n\n* `#412 <https://github.com/Kozea/WeasyPrint/issues/412>`_:\n  Use a NullHandler log handler when WeasyPrint is used as a library.\n* `#417 <https://github.com/Kozea/WeasyPrint/issues/417>`_,\n  `#472 <https://github.com/Kozea/WeasyPrint/issues/472>`_:\n  Don't crash on some line breaks.\n* `#327 <https://github.com/Kozea/WeasyPrint/issues/327>`_:\n  Don't crash with replaced elements with height set in percentages.\n* `#467 <https://github.com/Kozea/WeasyPrint/issues/467>`_:\n  Remove incorrect line breaks.\n* `#446 <https://github.com/Kozea/WeasyPrint/pull/446>`_:\n  Let the logging module do the string interpolation.\n\n\nVersion 0.36\n------------\n\nReleased on 2017-02-25.\n\nNew features:\n\n* `#407 <https://github.com/Kozea/WeasyPrint/pull/407>`_:\n  Handle ::first-letter.\n* `#423 <https://github.com/Kozea/WeasyPrint/pull/423>`_:\n  Warn user about broken cairo versions.\n\nBug fixes:\n\n* `#411 <https://github.com/Kozea/WeasyPrint/pull/411>`_:\n  Typos fixed in command-line help.\n\n\nVersion 0.35\n------------\n\nReleased on 2017-02-25.\n\nBug fixes:\n\n* `#410 <https://github.com/Kozea/WeasyPrint/pull/410>`_:\n  Fix AssertionError in split_text_box.\n\n\nVersion 0.34\n------------\n\nReleased on 2016-12-21.\n\nBug fixes:\n\n* `#398 <https://github.com/Kozea/WeasyPrint/issues/398>`_:\n  Honor the presentational_hints option for PDFs.\n* `#399 <https://github.com/Kozea/WeasyPrint/pull/399>`_:\n  Avoid CairoSVG-2.0.0rc* on Python 2.\n* `#396 <https://github.com/Kozea/WeasyPrint/issues/396>`_:\n  Correctly close files open by mkstemp.\n* `#403 <https://github.com/Kozea/WeasyPrint/issues/403>`_:\n  Cast the number of columns into int.\n* Fix multi-page multi-columns and add related tests.\n\n\nVersion 0.33\n------------\n\nReleased on 2016-11-28.\n\nNew features:\n\n* `#393 <https://github.com/Kozea/WeasyPrint/issues/393>`_:\n  Add tests on MacOS.\n* `#370 <https://github.com/Kozea/WeasyPrint/issues/370>`_:\n  Enable @font-face on MacOS.\n\nBug fixes:\n\n* `#389 <https://github.com/Kozea/WeasyPrint/issues/389>`_:\n  Always update resume_at when splitting lines.\n* `#394 <https://github.com/Kozea/WeasyPrint/issues/394>`_:\n  Don't build universal wheels.\n* `#388 <https://github.com/Kozea/WeasyPrint/issues/388>`_:\n  Fix logic when finishing block formatting context.\n\n\nVersion 0.32\n------------\n\nReleased on 2016-11-17.\n\nNew features:\n\n* `#28 <https://github.com/Kozea/WeasyPrint/issues/28>`_:\n  Support @font-face on Linux.\n* Support CSS fonts level 3 almost entirely, including OpenType features.\n* `#253 <https://github.com/Kozea/WeasyPrint/issues/253>`_:\n  Support presentational hints (optional).\n* Support break-after, break-before and break-inside for pages and columns.\n* `#384 <https://github.com/Kozea/WeasyPrint/issues/384>`_:\n  Major performance boost.\n\nBux fixes:\n\n* `#368 <https://github.com/Kozea/WeasyPrint/issues/368>`_:\n  Respect white-space for shrink-to-fit.\n* `#382 <https://github.com/Kozea/WeasyPrint/issues/382>`_:\n  Fix the preferred width for column groups.\n* Handle relative boxes in column-layout boxes.\n\nDocumentation:\n\n* Add more and more documentation about Windows installation.\n* `#355 <https://github.com/Kozea/WeasyPrint/issues/355>`_:\n  Add fonts requirements for tests.\n\n\nVersion 0.31\n------------\n\nReleased on 2016-08-28.\n\nNew features:\n\n* `#124 <https://github.com/Kozea/WeasyPrint/issues/124>`_:\n  Add MIME sniffing for images.\n* `#60 <https://github.com/Kozea/WeasyPrint/issues/60>`_:\n  CSS Multi-column Layout.\n* `#197 <https://github.com/Kozea/WeasyPrint/pull/197>`_:\n  Add hyphens at line breaks activated by a soft hyphen.\n\nBux fixes:\n\n* `#132 <https://github.com/Kozea/WeasyPrint/pull/132>`_:\n  Fix Python 3 compatibility on Windows.\n\nDocumentation:\n\n* `#329 <https://github.com/Kozea/WeasyPrint/issues/329>`_:\n  Add documentation about installation on Windows.\n\n\nVersion 0.30\n------------\n\nReleased on 2016-07-18.\n\nWeasyPrint now depends on html5lib-0.999999999.\n\nBux fixes:\n\n* Fix Acid2\n* `#325 <https://github.com/Kozea/WeasyPrint/issues/325>`_:\n  Cutting lines is broken in page margin boxes.\n* `#334 <https://github.com/Kozea/WeasyPrint/issues/334>`_:\n  Newest html5lib 0.999999999 breaks rendering.\n\n\nVersion 0.29\n------------\n\nReleased on 2016-06-17.\n\nBug fixes:\n\n* `#263 <https://github.com/Kozea/WeasyPrint/pull/263>`_:\n  Don't crash with floats with percents in positions.\n* `#323 <https://github.com/Kozea/WeasyPrint/pull/323>`_:\n  Fix CairoSVG 2.0 pre-release dependency in Python 2.x.\n\n\nVersion 0.28\n------------\n\nReleased on 2016-05-16.\n\nBug fixes:\n\n* `#189 <https://github.com/Kozea/WeasyPrint/issues/189>`_:\n  ``white-space: nowrap`` still wraps on hyphens\n* `#305 <https://github.com/Kozea/WeasyPrint/issues/305>`_:\n  Fix crashes on some tables\n* Don't crash when transform matrix isn't invertible\n* Don't crash when rendering ratio-only SVG images\n* Fix margins and borders on some tables\n\n\nVersion 0.27\n------------\n\nReleased on 2016-04-08.\n\nNew features:\n\n* `#295 <https://github.com/Kozea/WeasyPrint/pull/295>`_:\n  Support the 'rem' unit.\n* `#299 <https://github.com/Kozea/WeasyPrint/pull/299>`_:\n  Enhance the support of SVG images.\n\nBug fixes:\n\n* `#307 <https://github.com/Kozea/WeasyPrint/issues/307>`_:\n  Fix the layout of cells larger than their tables.\n\nDocumentation:\n\n* The website is now on GitHub Pages, the documentation is on Read the Docs.\n* `#297 <https://github.com/Kozea/WeasyPrint/issues/297>`_:\n  Rewrite the CSS chapter of the documentation.\n\n\nVersion 0.26\n------------\n\nReleased on 2016-01-29.\n\nNew features:\n\n* Support the `empty-cells` attribute.\n* Respect table, column and cell widths.\n\nBug fixes:\n\n* `#172 <https://github.com/Kozea/WeasyPrint/issues/172>`_:\n  Unable to set table column width on tables td's.\n* `#151 <https://github.com/Kozea/WeasyPrint/issues/151>`_:\n  Table background colour bleeds beyond table cell boundaries.\n* `#260 <https://github.com/Kozea/WeasyPrint/issues/260>`_:\n  TypeError: unsupported operand type(s) for +: 'float' and 'str'.\n* `#288 <https://github.com/Kozea/WeasyPrint/issues/288>`_:\n  Unwanted line-breaks in bold text.\n* `#286 <https://github.com/Kozea/WeasyPrint/issues/286>`_:\n  AttributeError: 'Namespace' object has no attribute 'attachments'.\n\n\nVersion 0.25\n------------\n\nReleased on 2015-12-17.\n\nNew features:\n\n* Support the 'q' unit.\n\nBug fixes:\n\n* `#285 <https://github.com/Kozea/WeasyPrint/issues/285>`_:\n  Fix a crash happening when splitting lines.\n* `#284 <https://github.com/Kozea/WeasyPrint/issues/284>`_:\n  Escape parenthesis in PDF links.\n* `#280 <https://github.com/Kozea/WeasyPrint/pull/280>`_:\n  Replace utf8 with utf-8 for gettext/django compatibility.\n* `#269 <https://github.com/Kozea/WeasyPrint/pull/269>`_:\n  Add support for use when frozen.\n* `#250 <https://github.com/Kozea/WeasyPrint/issues/250>`_:\n  Don't crash when attachments are not available.\n\n\nVersion 0.24\n------------\n\nReleased on 2015-08-04.\n\nNew features:\n\n* `#174 <https://github.com/Kozea/WeasyPrint/issues/174>`_:\n  Basic support for Named strings.\n\nBug fixes:\n\n* `#207 <https://github.com/Kozea/WeasyPrint/issues/207>`_:\n  Draw rounded corners on replaced boxes.\n* `#224 <https://github.com/Kozea/WeasyPrint/pull/224>`_:\n  Rely on the font size for rounding bug workaround.\n* `#31 <https://github.com/Kozea/WeasyPrint/issues/31>`_:\n  Honor the vertical-align property in fixed-height cells.\n* `#202 <https://github.com/Kozea/WeasyPrint/issues/202>`_:\n  Remove unreachable area/border at bottom of page.\n* `#225 <https://github.com/Kozea/WeasyPrint/issues/225>`_:\n  Don't allow unknown units during line-height validation.\n* Fix some wrong conflict resolutions for table borders with inset\n  and outset styles.\n\n\nVersion 0.23\n------------\n\nReleased on 2014-09-16.\n\nBug fixes:\n\n* `#196 <https://github.com/Kozea/WeasyPrint/issues/196>`_:\n  Use the default image sizing algorithm for images’s preferred size.\n* `#194 <https://github.com/Kozea/WeasyPrint/pull/194>`_:\n  Try more library aliases with ``dlopen()``.\n* `#201 <https://github.com/Kozea/WeasyPrint/pull/201>`_:\n  Consider ``page-break-after-avoid`` when pushing floats to the next page.\n* `#217 <https://github.com/Kozea/WeasyPrint/issues/217>`_:\n  Avoid a crash on zero-sized background images.\n\nRelease process:\n\n* Start testing on Python 3.4 on Travis-CI.\n\n\nVersion 0.22\n------------\n\nReleased on 2014-05-05.\n\nNew features:\n\n* `#86 <https://github.com/Kozea/WeasyPrint/pull/86>`_:\n  Support gzip and deflate encoding in HTTP responses\n* `#177 <https://github.com/Kozea/WeasyPrint/pull/177>`_:\n  Support for PDF attachments.\n\nBug fixes:\n\n* `#169 <https://github.com/Kozea/WeasyPrint/issues/169>`_:\n  Fix a crash on percentage-width columns in an auto-width table.\n* `#168 <https://github.com/Kozea/WeasyPrint/issues/168>`_:\n  Make ``<fieldset>`` a block in the user-agent stylesheet.\n* `#175 <https://github.com/Kozea/WeasyPrint/issues/175>`_:\n  Fix some ``dlopen()`` library loading issues on OS X.\n* `#183 <https://github.com/Kozea/WeasyPrint/issues/183>`_:\n  Break to the next page before a float that would overflow the page.\n  (It might still overflow if it’s bigger than the page.)\n* `#188 <https://github.com/Kozea/WeasyPrint/issues/188>`_:\n  Require a recent enough version of Pyphen\n\nRelease process:\n\n* Drop Python 3.1 support.\n* Set up [Travis CI](https://travis-ci.org/)\n  to automatically test all pushes and pull requests.\n* Start testing on Python 3.4 locally. (Travis does not support 3.4 yet.)\n\n\nVersion 0.21\n------------\n\nReleased on 2014-01-11.\n\nNew features:\n\n* Add the `overflow-wrap <https://drafts.csswg.org/css-text/#overflow-wrap>`_\n  property, allowing line breaks inside otherwise-unbreakable words.\n  Thanks Frédérick Deslandes!\n* Add the `image-resolution\n  <https://drafts.csswg.org/css-images-3/#the-image-resolution>`_ property,\n  allowing images to be sized proportionally to their intrinsic size\n  at a resolution other than 96 image pixels per CSS ``in``\n  (ie. one image pixel per CSS ``px``)\n\nBug fixes:\n\n* `#145 <https://github.com/Kozea/WeasyPrint/issues/145>`_:\n  Fix parsing HTML from an HTTP URL on Python 3.x\n* `#40 <https://github.com/Kozea/WeasyPrint/issues/40>`_:\n  Use more general hyphenation dictionaries for specific document languages.\n  (E.g. use ``hyph_fr.dic`` for ``lang=\"fr_FR\"``.)\n* `#26 <https://github.com/Kozea/WeasyPrint/issues/26>`_:\n  Fix ``min-width`` and ``max-width`` on floats.\n* `#100 <https://github.com/Kozea/WeasyPrint/issues/100>`_:\n  Fix a crash on trailing whitespace with ``font-size: 0``\n* `#82 <https://github.com/Kozea/WeasyPrint/issues/82>`_:\n  Borders on tables with ``border-collapse: collapse`` were sometimes\n  drawn at an incorrect position.\n* `#30 <https://github.com/Kozea/WeasyPrint/issues/30>`_:\n  Fix positioning of images with ``position: absolute``.\n* `#118 <https://github.com/Kozea/WeasyPrint/issues/118>`_:\n  Fix a crash when using ``position: absolute``\n  inside a ``position: relative`` element.\n* Fix ``visibility: collapse`` to behave like ``visibility: hidden``\n  on elements other than table rows and table columns.\n* `#147 <https://github.com/Kozea/WeasyPrint/issues/147>`_ and\n  `#153 <https://github.com/Kozea/WeasyPrint/issues/153>`_:\n  Fix dependencies to require lxml 3.0 or a more recent version.\n  Thanks gizmonerd and Thomas Grainger!\n* `#152 <https://github.com/Kozea/WeasyPrint/issues/152>`_:\n  Fix a crash on percentage-sized table cells in auto-sized tables.\n  Thanks Johannes Duschl!\n\n\nVersion 0.20.2\n--------------\n\nReleased on 2013-12-18.\n\n* Fix `#146 <https://github.com/Kozea/WeasyPrint/issues/146>`_: don't crash\n  when drawing really small boxes with dotted/dashed borders\n\n\nVersion 0.20.1\n--------------\n\nReleased on 2013-12-16.\n\n* Depend on html5lib >= 0.99 instead of 1.0b3 to fix pip 1.4 support.\n* Fix `#74 <https://github.com/Kozea/WeasyPrint/issues/74>`_: don't crash on\n  space followed by dot at line break.\n* Fix `#78 <https://github.com/Kozea/WeasyPrint/issues/78>`_: nicer colors for\n  border-style: ridge/groove/inset/outset.\n\n\nVersion 0.20\n------------\n\nReleased on 2013-12-14.\n\n* Add support for ``border-radius``.\n* Feature `#77 <https://github.com/Kozea/WeasyPrint/issues/77>`_: Add PDF\n  metadata from HTML.\n* Feature `#12 <https://github.com/Kozea/WeasyPrint/pull/12>`_: Use html5lib.\n* Tables: handle percentages for column groups, columns and cells, and values\n  for row height.\n* Bug fixes:\n\n  * Fix `#84 <https://github.com/Kozea/WeasyPrint/pull/84>`_: don't crash when\n    stylesheets are not available.\n  * Fix `#101 <https://github.com/Kozea/WeasyPrint/issues/101>`_: use page ids\n    instead of page numbers in PDF bookmarks.\n  * Use ``logger.warning`` instead of deprecated ``logger.warn``.\n  * Add 'font-stretch' in the 'font' shorthand.\n\n\nVersion 0.19.2\n--------------\n\nReleased on 2013-06-18.\n\nBug fix release:\n\n* Fix `#88 <https://github.com/Kozea/WeasyPrint/issues/88>`_:\n  ``text-decoration: overline`` not being drawn above the text\n* Bug fix: Actually draw multiple lines when multiple values are given\n  to ``text-decoration``.\n* Use the font metrics for text decoration positioning.\n* Bug fix: Don't clip the border with ``overflow: hidden``.\n* Fix `#99 <https://github.com/Kozea/WeasyPrint/issues/99>`_:\n  Regression: JPEG images not loading with cairo 1.8.x.\n\n\nVersion 0.19.1\n--------------\n\nReleased on 2013-04-30.\n\nBug fix release:\n\n* Fix incorrect intrinsic width calculation\n  leading to unnecessary line breaks in floats, tables, etc.\n* Tweak border painting to look better\n* Fix unnecessary page break before big tables.\n* Fix table row overflowing at the bottom of the page\n  when there are margins above the table.\n* Fix ``position: fixed`` to actually repeat on every page.\n* Fix `#76 <https://github.com/Kozea/WeasyPrint/issues/76>`_:\n  repeat ``<thead>`` and ``<tfoot>`` elements on every page,\n  even with table border collapsing.\n\n\nVersion 0.19\n------------\n\nReleased on 2013-04-18.\n\n* Add support for ``linear-gradient()`` and ``radial-gradient``\n  in background images.\n* Add support for the ``ex`` and ``ch`` length units.\n  (``1ex`` is based on the font instead of being always ``0.5em`` as before.)\n* Add experimental support for Level 4 hyphenation properties.\n* Drop support for CFFI < 0.6 and cairocffi < 0.4.\n* Many bug fixes, including:\n\n * Fix `#54 <https://github.com/Kozea/WeasyPrint/issues/54>`_:\n   min/max-width/height on block-level images.\n * Fix `#71 <https://github.com/Kozea/WeasyPrint/issues/71>`_:\n   Crash when parsing nested functional notation.\n\n\nVersion 0.18\n------------\n\nReleased on 2013-03-30.\n\n* Add support for Level 3 backgrounds,\n  including multiple background layers per element/box.\n* Forward-compatibility with (future releases of) cairocffi 0.4+ and CFFI 0.6+.\n* Bug fixes:\n\n  * Avoid some unnecessary line breaks\n    for elements sized based on their content (aka. “shrink-to-fit”)\n    such as floats and page headers.\n  * Allow page breaks between empty blocks.\n  * Fix `#66 <https://github.com/Kozea/WeasyPrint/issues/66>`_:\n    Resolve images’ auto width from non-auto height and intrinsic ratio.\n  * Fix `#21 <https://github.com/Kozea/WeasyPrint/issues/21>`_:\n    The ``data:`` URL scheme is case-insensitive.\n  * Fix `#53 <https://github.com/Kozea/WeasyPrint/issues/53>`_:\n    Crash when backtracking for ``break-before/after: avoid``.\n\n\nVersion 0.17.1\n--------------\n\nReleased on 2013-03-18.\n\nBug fixes:\n\n* Fix `#41 <https://github.com/Kozea/WeasyPrint/issues/41>`_:\n  GObject initialization when GDK-PixBuf is not installed.\n* Fix `#42 <https://github.com/Kozea/WeasyPrint/issues/42>`_:\n  absolute URLs without a base URL (ie. document parsed from a string.)\n* Fix some whitespace collapsing bugs.\n* Fix absolutely-positioned elements inside inline elements.\n* Fix URL escaping of image references from CSS.\n* Fix `#49 <https://github.com/Kozea/WeasyPrint/issues/49>`_:\n  Division by 0 on dashed or dotted border smaller than one dot/dash.\n* Fix `#44 <https://github.com/Kozea/WeasyPrint/issues/44>`_:\n  bad interaction of ``page-break-before/after: avoid`` and floats.\n\n\nVersion 0.17\n------------\n\nReleased on 2013-02-27.\n\n* Added `text hyphenation`_ with the ``-weasy-hyphens`` property.\n* When a document includes JPEG images, embed them as JPEG in the PDF output.\n  This often results in smaller PDF file size\n  compared to the default *deflate* compression.\n* Switched to using CFFI instead of PyGTK or PyGObject-introspection.\n* Layout bug fixes:\n\n  - Correctly trim whitespace at the end of lines.\n  - Fix some cases with floats within inline content.\n\n.. _text hyphenation: https://weasyprint.readthedocs.io/en/latest/features.html#css-text-module-level-3-4\n\n\nVersion 0.16\n------------\n\nReleased on 2012-12-13.\n\n* Add the ``zoom`` parameter to ``HTML.write_pdf`` and\n  ``Document.write_pdf() <weasyprint.document.Document.write_pdf>``\n* Fix compatibility with old (and buggy) pycairo versions.\n  WeasyPrint is now tested on 1.8.8 in addition to the latest.\n* Fix layout bugs related to line trailing spaces.\n\n\nVersion 0.15\n------------\n\nReleased on 2012-10-09.\n\n* Add a low-level API that enables painting pages individually on any\n  cairo surface.\n* **Backward-incompatible change**: remove the ``HTML.get_png_pages``\n  method. The new low-level API covers this functionality and more.\n* Add support for the ``font-stretch`` property.\n* Add support for ``@page:blank`` to select blank pages.\n* New Sphinx-based and improved docs\n* Bug fixes:\n\n  - Importing Pango in some PyGTK installations.\n  - Layout of inline-blocks with `vertical-align: top` or `bottom`.\n  - Do not repeat a block’s margin-top or padding-top after a page break.\n  - Performance problem with large tables split across many pages.\n  - Anchors and hyperlinks areas now follow CSS transforms.\n    Since PDF links have to be axis-aligned rectangles, the bounding box\n    is used. This may be larger than expected with rotations that are\n    not a multiple of 90 degrees.\n\n\nVersion 0.14\n------------\n\nReleased on 2012-08-03.\n\n* Add a public API to choose media type used for @media.\n  (It still defaults to ``print``). Thanks Chung Lu!\n* Add ``--base-url`` and ``--resolution`` to the command-line API, making it\n  as complete as the Python one.\n* Add support for the ``<base href=\"...\">`` element in HTML.\n* Add support for CSS outlines\n* Switch to gdk-pixbuf instead of Pystacia for loading raster images.\n* Bug fixes:\n\n  - Handling of filenames and URLs on Windows\n  - Unicode filenames with older version of py2cairo\n  - ``base_url`` now behaves as expected when set to a directory name.\n  - Make some tests more robust\n\n\nVersion 0.13\n------------\n\nReleased on 2012-07-23.\n\n* Add support for PyGTK, as an alternative to PyGObject + introspection.\n  This should make WeasyPrint easier to run on platforms that not not have\n  packages for PyGObject 3.x yet.\n* Bug fix: crash in PDF outlines for some malformed HTML documents\n\n\nVersion 0.12\n------------\n\nReleased on 2012-07-19.\n\n* Add support for collapsed borders on tables. This is currently incompatible\n  with repeating header and footer row groups on each page: headers and footers\n  are treated as normal row groups on table with ``border-collapse: collapse``.\n* Add ``url_fetcher`` to the public API. This enables users to hook into\n  WeasyPrint for fetching linked stylesheets or images, eg. to generate them\n  on the fly without going through the network.\n  This enables the creation of `Flask-WeasyPrint\n  <https://packages.python.org/Flask-WeasyPrint/>`_.\n\n\nVersion 0.11\n------------\n\nReleased on 2012-07-04.\n\n* Add support for floats and clear.\n  Together with various bug fixes, this enables WeasyPrint to pass the Acid2\n  test! Acid2 is now part of our automated test suite.\n* Add support for the width, min-width, max-width, height, min-height and\n  max-height properties in @page. The size property is now the size of the\n  page’s containing block.\n* Switch the Variable Dimension rules to `the new proposal\n  <https://github.com/SimonSapin/css/blob/master/margin-boxes-variable-dimension>`_.\n  The previous implementation was broken in many cases.\n* The ``image-rendering``, ``transform``, ``transform-origin`` and ``size``\n  properties are now unprefixed. The prefixed form (eg. -weasy-size) is ignored\n  but gives a specific warning.\n\n\nVersion 0.10\n------------\n\nReleased on 2012-06-25.\n\n* Add ``get_png_pages()`` to the public API. It returns each page as\n  a separate PNG image.\n* Add a ``resolution`` parameter for PNG.\n* Add *WeasyPrint Navigator*, a web application that shows WeasyPrint’s\n  output with clickable links. Yes, that’s a browser in your browser.\n  Start it with ``python -m weasyprint.navigator``\n* Add support for `vertical-align: top` and `vertical-align: bottom`\n* Add support for `page-break-before: avoid` and `page-break-after: avoid`\n* Bug fixes\n\n\nVersion 0.9\n-----------\n\nReleased on 2012-06-04.\n\n* Relative, absolute and fixed positioning\n* Proper painting order (z-index)\n* In PDF: support for internal and external hyperlinks as well as bookmarks.\n* Added the ``tree`` parameter to the ``HTML`` class: accepts a parsed lxml\n  object.\n* Bug fixes, including many crashes.\n\nBookmarks can be controlled by the ``-weasy-bookmark-level`` and\n``-weasy-bookmark-label`` properties, as described in `CSS Generated Content\nfor Paged Media Module <https://drafts.csswg.org/css-gcpm-3/#bookmarks>`_.\n\nThe default UA stylesheet sets a matching bookmark level on all ``<h1>``\nto ``<h6>`` elements.\n\n\nVersion 0.8\n-----------\n\nReleased on 2012-05-07.\n\n* Switch from cssutils to tinycss_ as the CSS parser.\n* Switch to the new cssselect_, almost all level 3 selectors are supported now.\n* Support for inline blocks and inline tables\n* Automatic table layout (column widths)\n* Support for the ``min-width``, ``max-width``, ``min-height`` and\n  ``max-height`` properties, except on table-related and page-related boxes.\n* Speed improvements on big stylesheets / small documents thanks to tinycss.\n* Many bug fixes\n\n.. _tinycss: https://packages.python.org/tinycss/\n.. _cssselect: https://packages.python.org/cssselect/\n\n\nVersion 0.7.1\n-------------\n\nReleased on 2012-03-21.\n\nChange the license from AGPL to BSD.\n\n\nVersion 0.7\n-----------\n\nReleased on 2012-03-21.\n\n* Support page breaks between table rows\n* Support for the ``orphans`` and ``widows`` properties.\n* Support for ``page-break-inside: avoid``\n* Bug fixes\n\nOnly avoiding page breaks before/after an element is still missing.\n\n\nVersion 0.6.1\n-------------\n\nReleased on 2012-03-01.\n\nFix a packaging bug. (Remove use_2to3 in setup.py. We use the same\ncodebase for Python 2 and 3.)\n\n\nVersion 0.6\n-----------\n\nReleased on 2012-02-29.\n\n* *Backward incompatible*: completely change the Python API. See the\n  documentation:\n  https://weasyprint.readthedocs.io/en/latest/tutorial.html#as-a-python-library\n* *Backward incompatible*: Proper margin collapsing.\n  This changes how blocks are rendered: adjoining margins \"collapse\"\n  (their maximum is used) instead of accumulating.\n* Support images in ``embed`` or ``object`` elements.\n* Switch to pystacia instead of PIL for raster images\n* Add compatibility with CPython 2.6 and 3.2. (Previously only 2.7\n  was supported)\n* Many bug fixes\n\n\nVersion 0.5\n-----------\n\nReleased on 2012-02-08.\n\n* Support for the ``overflow`` and ``clip`` properties.\n* Support for the ``opacity`` property from CSS3 Colors.\n* Support for CSS 2D Transforms. These are prefixed, so you need to use\n  ``-weasy-transform`` and ``-weasy-transform-origin``.\n\n\nVersion 0.4\n-----------\n\nReleased on 2012-02-07.\n\n* Support ``text-align: justify``, ``word-spacing`` and ``letter-spacing``.\n* Partial support for CSS3 Paged Media: page size and margin boxes with\n  page-based counters.\n* All CSS 2.1 border styles\n* Fix SVG images with non-pixel units. Requires CairoSVG 0.3\n* Support for ``page-break-before`` and ``page-break-after``, except for\n  the value ``avoid``.\n* Support for the ``background-clip``, ``background-origin`` and\n  ``background-size`` from CSS3 (but still with a single background\n  per element)\n* Support for the ``image-rendering`` from SVG. This one is prefixed,\n  use ``-weasy-image-rendering``. It only has an effect on PNG output.\n\n\nVersion 0.3.1\n-------------\n\nReleased on 2011-12-14.\n\nCompatibility with CairoSVG 0.1.2\n\n\nVersion 0.3\n-----------\n\nReleased on 2011-12-13.\n\n* **Backward-incompatible change:** the 'size' property is now prefixed (since\n  it is in an experimental specification). Use '-weasy-size' instead.\n* cssutils 0.9.8 or higher is now required.\n* Support SVG images with CairoSVG\n* Support generated content: the ``:before`` and ``:after`` pseudo-elements,\n  the ``content``, ``quotes`` and ``counter-*`` properties.\n* Support ordered lists: all CSS 2.1 values of the ``list-style-type`` property.\n* New user-agent stylesheet with HTML 5 elements and automatic quotes for many\n  languages. Thanks Peter Moulder!\n* Disable cssutils validation warnings, they are redundant with WeasyPrint’s.\n* Add ``--version`` to the command-line script.\n* Various bug fixes\n\n\nVersion 0.2\n-----------\n\nReleased on 2011-11-25.\n\n* Support for tables.\n* Support the `box-sizing` property from CSS 3 Basic User Interface\n* Support all values of vertical-align except top and bottom. They are\n  interpreted as text-top and text-bottom.\n* Minor bug fixes\n\nTables have some limitations:\nOnly the fixed layout and separate border model are supported.\nThere are also no page break inside tables so a table higher\nthan a page will overflow.\n\n\nVersion 0.1\n-----------\n\nReleased on 2011-10-28.\n\nFirst packaged release. Supports \"simple\" CSS 2.1 pages: there is no\nsupport for floats, tables, or absolute positioning. Other than that\nmost of CSS 2.1 is supported, as well as CSS 3 Colors and Selectors.\n"
  },
  {
    "path": "docs/common_use_cases.rst",
    "content": "Common Use Cases\n================\n\n\nInclude in Web Applications\n---------------------------\n\nUsing WeasyPrint in web applications sometimes requires attention on some\ndetails.\n\nSecurity Problems\n.................\n\nFirst of all, rendering untrusted HTML and CSS files can lead to :ref:`security\nproblems <Security>`. Please be sure to carefully follow the different proposed\nsolutions if you allow your users to modify the source of the rendered\ndocuments in any way.\n\nRights Management\n.................\n\nAnother problem is rights management: you often need to render templates that\ncan only be accessed by authenticated users, and WeasyPrint installed on the\nserver doesn’t send the same cookies as the ones sent by the users. Extensions\nsuch as Flask-WeasyPrint_ (for Flask_) or Django-WeasyPrint_ (for Django_)\nsolve this issue with a small amount of code. If you use another framework, you\ncan read these extensions and probably find an equivalent workaround.\n\n.. _Flask-Weasyprint: https://github.com/Kozea/Flask-WeasyPrint\n.. _Flask: https://flask.palletsprojects.com/\n.. _Django-WeasyPrint: https://github.com/fdemmer/django-weasyprint\n.. _Django: https://www.djangoproject.com/\n\nServer Side Requests & Self-Signed SSL Certificates\n...................................................\n\nIf your server is requesting data from itself, you may encounter a self-signed\ncertificate error, even if you have a valid certificate.\n\nYou need to add yourself as a Certificate Authority, so that your self-signed\nSSL certificates can be requested.\n\n.. code-block:: bash\n\n   # If you have not yet created a certificate.\n   sudo openssl req -x509 \\\n       -sha256 \\\n       -nodes \\\n       -newkey rsa:4096 \\\n       -days 365 \\\n       -keyout localhost.key \\\n       -out localhost.crt\n\n   # Follow the prompts about your certificate and the domain name.\n   openssl x509 -text -noout -in localhost.crt\n\nAdd your new self-signed SSL certificate to your nginx.conf, below the line\n``server_name 123.123.123.123;``:\n\n.. code-block:: bash\n\n   ssl_certificate /etc/ssl/certs/localhost.crt;\n   ssl_certificate_key /etc/ssl/private/localhost.key;\n\nThe SSL certificate will be valid when accessing your website from the\ninternet. However, images will not render when requesting files from the same\nserver.\n\nYou will need to add your new self-signed certificates as trusted:\n\n.. code-block:: bash\n\n   sudo cp /etc/ssl/certs/localhost.crt /usr/local/share/ca-certificates/localhost.crt\n   sudo cp /etc/ssl/private/localhost.key /usr/local/share/ca-certificates/localhost.key\n\n   # Update the certificate authority trusted certificates.\n   sudo update-ca-certificates\n\n   # Export your newly updated Certificate Authority Bundle file.\n   # If using Django, it will use the newly signed certificate authority as\n   # valid and images will load properly.\n   sudo tee -a /etc/environment <<< 'export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt'\n\n\nAdjust Document Dimensions\n--------------------------\n\nWeasyPrint does not provide support for adjusting page size or document margins\nvia command-line flags. This is best accomplished with the CSS ``@page``\nat-rule. Consider the following example:\n\n.. code-block:: css\n\n  @page {\n    size: Letter; /* Change from the default size of A4 */\n    margin: 3cm; /* Set margin on each page */\n  }\n\nThere is much more which can be achieved with the ``@page`` at-rule,\nsuch as page numbers, headers, etc. Read more about the page_ at-rule.\n\n.. _page: https://developer.mozilla.org/en-US/docs/Web/CSS/@page\n\n\nGenerate Specialized PDFs\n-------------------------\n\nWeasyPrint can generate different PDF variants, including PDF/UA and PDF/A. The\nfeature is available by using the ``--pdf-variant`` CLI option, or the\n``pdf_variant`` Python parameter of :func:`HTML.write_pdf\n<weasyprint.HTML.write_pdf>`.\n\n.. code-block:: python\n\n  from weasyprint import HTML\n  HTML(string=\"<p>document</p>\").write_pdf(\"document.pdf\", pdf_variant=\"pdf/a-3u\")\n\n.. code-block:: sh\n\n  $ weasyprint document.html --pdf-variant=\"pdf/ua-1\" document.pdf\n\nThe different supported variants can be listed using ``weasyprint --help``.\n\nEven if WeasyPrint tries to generate valid documents, the result is not\nguaranteed: the HTML, CSS and PDF features chosen by the user must follow the\nlimitations defined by the different specifications.\n\nPDF/A (Archiving)\n.................\n\nPDF/A documents are specialized for archiving purposes. They are a simple\nsubset of PDF, with a lot of limitations: no audio, video or JavaScript,\ndefined color spaces, embedded fonts, etc.\n\nIf possible, PDF/A-3u should be preferred: it allows transparency layers that\nare forbidden in A-1, and arbitrary formats for attached files that are\nforbidden in A-2. The \"u\" part of the variant indicates that the PDF text is\navailable as Unicode.\n\nPDF/A documents include a PDF identifier, that is mainly useful to indicate\nthat a PDF is a new version of another PDF. By default, WeasyPrint generates a\nvalid PDF identifier, but you can provide your own with the\n``--pdf-identifier`` CLI option or ``pdf_identifier`` Python parameter.\n\nIf your document includes images, you must set the ``image-rendering:\ncrisp-edges`` property to avoid anti-aliasing, that is forbidden by PDF/A.\n\nPDF/UA (Universal Accessibility)\n................................\n\nPDF/UA documents are specialized for accessibility purposes. They include extra\nmetadata that define document information and content structure.\n\nThe main constraint to get valid PDF/UA documents is to use a correct HTML\nstructure, to avoid inconsistencies in the PDF structure. The HTML order is\nalso used to define the order of the PDF content.\n\nSome information is required in your HTML file, including a ``<title>`` tag,\nand a ``lang`` attribute set on the ``<html>`` tag.\n\nPDF/X (Graphics Exchange)\n.........................\n\nPDF/X documents facilitate graphics exchange and adds printing-related requirements such\nas color profiles.\n\nThe easiest way to fulfill these requirements is to use device-dependent CMYK colors and\nimages everywhere in your document. For colors, you can use the ``device-cmyk()``\nfunction:\n\n.. code-block:: css\n\n   body { color: device-cmyk(0% 10% 0% 80%) }\n\nYou also have to define the output profile color to use.\n\n.. code-block:: css\n\n   @color-profile device-cmyk {\n     components: cyan, magenta, yellow, black;\n     src: url(path/to/cmyk-profile.icc);\n   }\n\nIf possible, PDF/X-4 should be preferred: it allows transparency layers that are\nforbidden by previous variants.\n\nFactur-X / ZUGFeRD (Electronic Invoices)\n........................................\n\nFactur-X / ZUGFeRD is a Franco-German standard for hybrid e-invoice, the first\nimplementation of the European Semantic Standard EN 16931. It enables users to\ninclude normalized metadata in PDF invoices, such as companies information or\ninvoice amounts, so that compatible software can automatically read this\ninformation. This standard is based on PDF/A-3b.\n\nWeasyPrint can generate Factur-X / ZUGFeRD documents. Invoice metadata must be\ngenerated by the user and included in the PDF document when rendered. Two\ndifferent metadata files are required:\n\n- the first one is RDF metadata, containing document metadata and PDF/A\n  extension information;\n- the second one is Factur-X / ZUGFeRD metadata, containing invoice amounts,\n  plus seller and buyer information.\n\nHere is an example of Factur-X document generation.\n\n``rdf.xml``:\n\n.. code-block:: xml\n\n  <rdf:RDF\n      xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n      xmlns:fx=\"urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#\"\n      xmlns:pdfaExtension=\"http://www.aiim.org/pdfa/ns/extension/\"\n      xmlns:pdfaSchema=\"http://www.aiim.org/pdfa/ns/schema#\"\n      xmlns:pdfaProperty=\"http://www.aiim.org/pdfa/ns/property#\">\n    <rdf:Description rdf:about=\"\">\n      <fx:ConformanceLevel>MINIMUM</fx:ConformanceLevel>\n      <fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>\n      <fx:DocumentType>INVOICE</fx:DocumentType>\n      <fx:Version>1.0</fx:Version>\n    </rdf:Description>\n    <rdf:Description rdf:about=\"\">\n      <pdfaExtension:schemas>\n        <rdf:Bag>\n          <rdf:li rdf:parseType=\"Resource\">\n            <pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>\n            <pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>\n            <pdfaSchema:prefix>fx</pdfaSchema:prefix>\n            <pdfaSchema:property>\n              <rdf:Seq>\n                <rdf:li rdf:parseType=\"Resource\">\n                  <pdfaProperty:name>DocumentFileName</pdfaProperty:name>\n                  <pdfaProperty:valueType>Text</pdfaProperty:valueType>\n                  <pdfaProperty:category>external</pdfaProperty:category>\n                  <pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>\n                </rdf:li>\n                <rdf:li rdf:parseType=\"Resource\">\n                  <pdfaProperty:name>DocumentType</pdfaProperty:name>\n                  <pdfaProperty:valueType>Text</pdfaProperty:valueType>\n                  <pdfaProperty:category>external</pdfaProperty:category>\n                  <pdfaProperty:description>INVOICE</pdfaProperty:description>\n                </rdf:li>\n                <rdf:li rdf:parseType=\"Resource\">\n                  <pdfaProperty:name>Version</pdfaProperty:name>\n                  <pdfaProperty:valueType>Text</pdfaProperty:valueType>\n                  <pdfaProperty:category>external</pdfaProperty:category>\n                  <pdfaProperty:description>The actual version of the Factur-X XML schema</pdfaProperty:description>\n                </rdf:li>\n                <rdf:li rdf:parseType=\"Resource\">\n                  <pdfaProperty:name>ConformanceLevel</pdfaProperty:name>\n                  <pdfaProperty:valueType>Text</pdfaProperty:valueType>\n                  <pdfaProperty:category>external</pdfaProperty:category>\n                  <pdfaProperty:description>The conformance level of the embedded Factur-X data</pdfaProperty:description>\n                </rdf:li>\n              </rdf:Seq>\n            </pdfaSchema:property>\n          </rdf:li>\n        </rdf:Bag>\n      </pdfaExtension:schemas>\n    </rdf:Description>\n  </rdf:RDF>\n\n``factur-x.xml``:\n\n.. code-block:: xml\n\n  <rsm:CrossIndustryInvoice\n      xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n      xmlns:qdt=\"urn:un:unece:uncefact:data:standard:QualifiedDataType:100\"\n      xmlns:udt=\"urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100\"\n      xmlns:rsm=\"urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100\"\n      xmlns:ram=\"urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100\">\n    <rsm:ExchangedDocumentContext>\n      <ram:BusinessProcessSpecifiedDocumentContextParameter>\n        <ram:ID>A1</ram:ID>\n      </ram:BusinessProcessSpecifiedDocumentContextParameter>\n      <ram:GuidelineSpecifiedDocumentContextParameter>\n        <ram:ID>urn:factur-x.eu:1p0:minimum</ram:ID>\n      </ram:GuidelineSpecifiedDocumentContextParameter>\n    </rsm:ExchangedDocumentContext>\n    <rsm:ExchangedDocument>\n      <ram:ID>123</ram:ID>\n      <ram:TypeCode>380</ram:TypeCode>\n      <ram:IssueDateTime>\n        <udt:DateTimeString format=\"102\">20200131</udt:DateTimeString>\n      </ram:IssueDateTime>\n    </rsm:ExchangedDocument>\n    <rsm:SupplyChainTradeTransaction>\n      <ram:ApplicableHeaderTradeAgreement>\n        <ram:BuyerReference>Buyer</ram:BuyerReference>\n        <ram:SellerTradeParty>\n          <ram:Name>Supplyer Corp</ram:Name>\n          <ram:SpecifiedLegalOrganization>\n            <ram:ID schemeID=\"0002\">123456782</ram:ID>\n          </ram:SpecifiedLegalOrganization>\n          <ram:PostalTradeAddress>\n            <ram:CountryID>FR</ram:CountryID>\n          </ram:PostalTradeAddress>\n          <ram:SpecifiedTaxRegistration>\n            <ram:ID schemeID=\"VA\">FR11123456782</ram:ID>\n          </ram:SpecifiedTaxRegistration>\n        </ram:SellerTradeParty>\n        <ram:BuyerTradeParty>\n          <ram:Name>Buyer Corp</ram:Name>\n          <ram:SpecifiedLegalOrganization>\n            <ram:ID schemeID=\"0002\">987654324</ram:ID>\n          </ram:SpecifiedLegalOrganization>\n        </ram:BuyerTradeParty>\n        <ram:BuyerOrderReferencedDocument >\n          <ram:IssuerAssignedID>456</ram:IssuerAssignedID>\n        </ram:BuyerOrderReferencedDocument>\n      </ram:ApplicableHeaderTradeAgreement>\n      <ram:ApplicableHeaderTradeDelivery/>\n      <ram:ApplicableHeaderTradeSettlement>\n        <ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>\n        <ram:SpecifiedTradeSettlementHeaderMonetarySummation>\n          <ram:TaxBasisTotalAmount>100.00</ram:TaxBasisTotalAmount>\n          <ram:TaxTotalAmount currencyID=\"EUR\">20.00</ram:TaxTotalAmount>\n          <ram:GrandTotalAmount>120.00</ram:GrandTotalAmount>\n          <ram:DuePayableAmount>120.00</ram:DuePayableAmount>\n        </ram:SpecifiedTradeSettlementHeaderMonetarySummation>\n      </ram:ApplicableHeaderTradeSettlement>\n    </rsm:SupplyChainTradeTransaction>\n  </rsm:CrossIndustryInvoice>\n\n``invoice.html``:\n\n.. code-block:: html\n\n  <h1>Invoice</h1>\n\nCommand-line::\n\n  weasyprint invoice.html invoice.pdf --attachment=factur-x.xml --attachment-relationship=Data --xmp-metadata=rdf.xml --pdf-variant=pdf/a-3a\n\nOr Python API:\n\n.. code-block:: python\n\n  from weasyprint import Attachment, HTML\n\n  document = HTML(\"invoice.html\").render()\n\n  factur_x_xml = Path(\"factur-x.xml\").read_text()\n  attachment = Attachment(string=factur_x_xml, name=\"factur-x.xml\", relationship=\"Data\")\n  document.metadata.attachments = [attachment]\n\n  xmp_metadata = Path(\"rdf.xml\").read_text().encode()\n  document.metadata.xmp_metadata = [xmp_metadata]\n\n  document.write_pdf(\"invoice.pdf\", pdf_variant=\"pdf/a-3b\")\n\nOf course, the content of these files has to be adapted to the content of real\ninvoices.\n\n\nInclude PDF Forms\n-----------------\n\nBy default, form fields are transformed into pure text and graphical shapes\nwhen exported to PDF. But WeasyPrint gives the possibility to generate real PDF\nforms that can be filled with a PDF reader. These forms can even send requests\nwith the data filled in the PDF, just as the same form would do in a web\nbrowser.\n\nTo transform all HTML forms into PDF forms, you can use the ``--pdf-forms`` CLI\noption or ``pdf_forms`` Python parameter.\n\n.. code-block:: python\n\n  from weasyprint import HTML\n  HTML(string=\"<input value='test'>\").write_pdf(\"test.pdf\", pdf_forms=True)\n\n.. code-block:: sh\n\n  $ weasyprint document.html --pdf-forms document.pdf\n\nYou can also define which specific fields (``input``, ``select``, ``textarea``,\n``button``) have to be transformed into PDF forms by setting the ``appearance``\nCSS property to ``auto`` on them. In this case, as for browsers, you’ll have to\nmanually override the default style set by the user agent stylesheet. Reading\n`the stylesheet set by the --pdf-forms option\n<https://github.com/Kozea/WeasyPrint/blob/main/weasyprint/css/html5_ua_form.css>`_\ncan help to override this style.\n\n.. code-block:: html\n\n  <style>\n    label { display: block }\n    .pdf-form { appearance: auto }\n    .pdf-form::before { visibility: hidden }\n  </style>\n  <label>\n    Can't be modified in PDF\n    <input value=\"static\">\n  </label>\n  <label>\n    Can be modified in PDF\n    <input class=\"pdf-form\" value=\"dynamic\">\n  </label>\n\nPDF forms support can be quite poor depending on the PDF reader you use. If a\nfeature doesn’t work for you, please check that this feature is actually\nsupported by your PDF reader before reporting a bug.\n\n\nDefine PDF Metadata\n-------------------\n\nPDF documents can include various metadata, such as title, authors or creation\ndate. The easiest way to define them is to include them in your HTML file:\nthese fields are normalized and can be automatically picked up by WeasyPrint.\n\n.. code-block:: html\n\n  <html lang=\"en\">\n    <head>\n      <title>PDF Sample with Metadata</title>\n      <meta name=\"author\" content=\"Jane Doe\">\n      <meta name=\"author\" content=\"John Doe\">\n      <meta name=\"generator\" content=\"HTML generator\">\n      <meta name=\"keywords\" content=\"HTML, CSS, PDF\">\n      <meta name=\"dcterms.created\" content=\"2000-12-31T12:34:56+02:00\">\n      <meta name=\"dcterms.modified\" content=\"2010-07-14\">\n      <meta name=\"description\" content=\"This is a simple sample\">\n    </head>\n  </html>\n\nHTML metadata values listed here, including language and title, are stored in\nthe corresponding, normalized fields in PDF.\n\nIf you use custom metadata fields, they are not stored in PDF by default. You\ncan include them in the PDF info dictionary using the ``--custom-metadata`` CLI\noption or the ``custsom_metadata`` Python parameter.\n\n.. code-block:: python\n\n  from weasyprint import HTML\n  HTML(string=\"<meta name=\"recipe\" content=\"fries\">\").write_pdf(\"recipe.pdf\", custom_metadata=True)\n\n.. code-block:: sh\n\n  $ weasyprint document.html --custom-metadata document.pdf\n\n\nAttach Files\n------------\n\nYou can attach files to your generated PDF. These files can be opened when a\nlink is clicked in the PDF, or just available in the list of attached files in\nyour PDF reader.\n\nTo attach a file with a regular link, you can use a regular anchor with the\n``rel`` attribute set to ``attachment``.\n\n.. code-block:: html\n\n  <a rel=\"attachment\" href=\"note.txt\">view attached note</a>\n\nTo attach a file globally to the document, you can add a ``link`` tag in your\n``head``:\n\n.. code-block:: html\n\n  <link rel=\"attachment\" href=\"note.txt\">\n\nIf you don’t want to attach your files using HTML tags, you can also use the\n``--attachment`` CLI option, multiple times if needed.\n\n.. code-block:: sh\n\n  $ weasyprint document.html --attachment note.txt --attachment photo.jpg document.pdf\n\nIn a Python script, you can also attach files using the\n:class:`weasyprint.Attachment` class.\n\n.. code-block:: python\n\n  from weasyprint import Attachment, HTML\n  attachments = [Attachment(\"note.txt\"), Attachment(\"photo.jpg\")]\n  HTML(string=\"<p>PDF with attachments</p>\").write_pdf(\"recipe.pdf\", attachments=attachments)\n\n\nCache and Optimize Images\n-------------------------\n\nWeasyPrint provides many options to deal with images: ``optimize_images``,\n``jpeg_quality``, ``dpi`` and ``cache``.\n\n``optimize_images`` can enable size optimization for images. When enabled, the\ngenerated PDF will include smaller images with no quality penalty, but the\nrendering time may be slightly increased.\n\nThe ``jpeg_quality`` option can be set to decrease the quality of JPEG images\nincluded in the PDF. You can set a value between 95 (best quality) to 0\n(smaller image size), depending on your needs.\n\nThe ``dpi`` option offers the possibility to reduce the size (in pixels, and\nthus in bytes) of all included raster images. The resolution, set in dots per\ninch, indicates the maximum number of pixels included in one inch on the\ngenerated PDF.\n\n.. code-block:: python\n\n    # Original high-quality images, faster, but generated PDF is larger\n    HTML('https://weasyprint.org/').write_pdf('weasyprint.pdf')\n\n    # Optimized lower-quality images, a bit slower, but generated PDF is smaller\n    HTML('https://weasyprint.org/').write_pdf(\n        'weasyprint.pdf', optimize_images=True, jpeg_quality=60, dpi=150)\n\n``cache`` gives the possibility to use a cache for images, avoiding to\ndownload, parse and optimize them each time they are used.\n\nBy default, the cache is used document by document, but you can share it\nbetween documents if needed. This feature can save a lot of network and CPU\ntime when you render a lot of documents that use the same images.\n\n.. code-block:: python\n\n    cache = {}\n    for i in range(10):\n        HTML(f'https://weasyprint.org/').write_pdf(\n            f'example-{i}.pdf', cache=cache)\n\nIt’s also possible to cache images on disk instead of keeping them in memory.\nThe ``--cache-folder`` CLI option can be used to define the folder used to\nstore temporary images. You can also provide this folder path as a string for\n``cache``.\n\n\nImprove Rendering Speed and Memory Use\n--------------------------------------\n\nWeasyPrint is often slower than other web engines. Python is the usual suspect,\nbut it’s not the main culprit here. :ref:`Optimization is not the main goal of\nWeasyPrint <Why Python?>` and it may lead to unbearable long rendering times.\n\nFirst of all: WeasyPrint’s performance gets generally better with time. You can\ncheck WeasyPerf_ to compare time and memory needed across versions.\n\nSome tips may help you to get better results.\n\n- A high number of CSS properties with a high number of HTML tags can lead to a\n  huge amount of time spent for the cascade. Avoiding large CSS frameworks can\n  drastically reduce the rendering time.\n- Tables are known to be slow, especially when they are rendered on multiple\n  pages. When possible, using a common block layout instead gives much faster\n  renderings.\n- Optimizing images and fonts can reduce the PDF size, but increase the\n  rendering time. Moreover, caching images gives the possibility to read and\n  optimize images only once, and thus to save time when the same image is used\n  multiple times. See :ref:`Cache and Optimize Images`.\n\n.. _WeasyPerf: https://kozea.github.io/WeasyPerf/\n\n\nShow Log Messages\n-----------------\n\nMost errors (unsupported CSS property, missing image…) are not fatal and will\nnot prevent a document from being rendered. WeasyPrint uses the :mod:`logging`\nmodule from the Python standard library to log these errors and let you know\nabout them.\n\nWhen WeasyPrint is launched in a terminal, logged messages will go to the\nstandard error stream (``stderr``) by default. When used as a library, logs are\nnot displayed at all. You can change that by configuring the ``weasyprint``\nlogger object:\n\n.. code-block:: python\n\n    import logging\n    logger = logging.getLogger('weasyprint')\n\n    # Display warnings, errors and critical messages.\n    logger.setLevel(logging.WARNING)\n\n    # Save logs to the weasyprint.log file.\n    logger.addHandler(logging.FileHandler('weasyprint.log'))\n    # Print logs on console.\n    logger.addHandler(logging.StreamHandler())\n\nThe ``weasyprint.progress`` logger is used to report the rendering progress. It\nis useful to get feedback when WeasyPrint is launched in a terminal (using the\n``--verbose`` or ``--debug`` option), or to give this feedback to end users\nwhen used as a library.\n\nSee the documentation of the :mod:`logging` module for details.\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# WeasyPrint documentation build configuration file.\n\nimport weasyprint\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.\nextensions = [\n    'sphinx.ext.autodoc', 'sphinx.ext.intersphinx',\n    'sphinx.ext.autosectionlabel']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix of source filenames.\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = 'WeasyPrint'\ncopyright = 'Simon Sapin and contributors'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The full version, including alpha/beta/rc tags.\nrelease = weasyprint.__version__\n\n# The short X.Y version.\nversion = release\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\nexclude_patterns = ['_build']\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'monokai'\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\nhtml_theme = 'furo'\n\nhtml_theme_options = {\n    'top_of_page_buttons': ['edit'],\n    'source_edit_link':\n    'https://github.com/Kozea/WeasyPrint/edit/main/docs/{filename}',\n}\n\n# Favicon URL\nhtml_favicon = 'https://www.courtbouillon.org/static/images/favicon.png'\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = []\n\n# These paths are either relative to html_static_path\n# or fully qualified paths (eg. https://...)\nhtml_css_files = [\n    'https://www.courtbouillon.org/static/docs-furo.css',\n]\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'weasyprintdoc'\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    ('manpage', 'weasyprint', 'The Awesome Document Factory',\n     ['Simon Sapin and contributors'], 1)\n]\n\n# Don’t autolabel man page titles.\nautosectionlabel_maxdepth = 4\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author, dir menu entry, description, category)\ntexinfo_documents = [(\n    'index', 'WeasyPrint', 'WeasyPrint Documentation',\n    'Simon Sapin and contributors', 'WeasyPrint',\n    'The Awesome Document Factory', 'Miscellaneous'),\n]\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/3/', None),\n    'pydyf': ('https://doc.courtbouillon.org/pydyf/stable/', None),\n}\n"
  },
  {
    "path": "docs/contribute.rst",
    "content": "Contribute\n==========\n\nYou want to add some code to WeasyPrint, launch its tests or improve its\ndocumentation? Thank you very much! Here are some tips to help you play with\nWeasyPrint in good conditions.\n\nThe first step is to clone the repository, create a virtual environment and\ninstall WeasyPrint dependencies:\n\n.. code-block:: shell\n\n   git clone https://github.com/Kozea/WeasyPrint.git\n   cd WeasyPrint\n   python -m venv venv\n   venv/bin/pip install -e '.[doc,test]'\n\nYou can then launch Python to test your changes:\n\n.. code-block:: shell\n\n   venv/bin/python\n\nRunning WeasyPrint might look something like this:\n\n.. code-block:: shell\n\n   venv/bin/python -m weasyprint example.html example.pdf\n\n\nCode & Issues\n-------------\n\nIf you’ve found a bug in WeasyPrint, it’s time to report it, and to fix it if you\ncan!\n\nYou can report bugs and feature requests on `GitHub`_. If you want to add or\nfix some code, please fork the repository and create a pull request, we’ll be\nhappy to review your work.\n\nYou can find more information about the code architecture in the :ref:`Dive\ninto the Source` section.\n\n.. _GitHub: https://github.com/Kozea/WeasyPrint\n\n\nTests\n-----\n\nTests are stored in the ``tests`` folder at the top of the repository. They use\nthe pytest_ library.\n\nTests require Ghostscript_ to be installed and available on the local path. You\nshould also install all the `DejaVu fonts`_ if you’re on Linux.\n\nYou can launch tests using the following command::\n\n  venv/bin/python -m pytest\n\nWeasyPrint also uses ruff_ to check the coding style::\n\n  venv/bin/python -m ruff check\n\n.. _pytest: https://docs.pytest.org/\n.. _Ghostscript: https://www.ghostscript.com/\n.. _DejaVu fonts: https://dejavu-fonts.github.io/\n.. _ruff: https://docs.astral.sh/ruff/\n\n\nDocumentation\n-------------\n\nDocumentation is stored in the ``docs`` folder at the top of the repository. It\nrelies on the `Sphinx`_ library.\n\nYou can build the documentation using the following command::\n\n  venv/bin/sphinx-build docs docs/_build\n\nThe documentation home page can now be found in the\n``/path/to/weasyprint/docs/_build/index.html`` file. You can open this file in a\nbrowser to see the final rendering.\n\n.. _Sphinx: https://www.sphinx-doc.org/\n"
  },
  {
    "path": "docs/first_steps.rst",
    "content": "First Steps\n===========\n\n.. currentmodule:: weasyprint\n\n\nInstallation\n------------\n\nWeasyPrint |version| depends on:\n\n* Python_ ≥ 3.10.0\n* Pango_ ≥ 1.44.0\n* pydyf_ ≥ 0.11.0\n* CFFI_ ≥ 0.6\n* tinyhtml5_ ≥ 2.0.0b1\n* tinycss2_ ≥ 1.5.0\n* cssselect2_ ≥ 0.8.0\n* Pyphen_ ≥ 0.9.1\n* Pillow_ ≥ 9.1.0\n* fontTools_ ≥ 4.59.2\n\n.. _Python: https://www.python.org/\n.. _Pango: https://pango.gnome.org/\n.. _CFFI: https://cffi.readthedocs.io/\n.. _pydyf: https://doc.courtbouillon.org/pydyf/\n.. _tinyhtml5: https://doc.courtbouillon.org/tinyhtml5/\n.. _tinycss2: https://doc.courtbouillon.org/tinycss2/\n.. _cssselect2: https://doc.courtbouillon.org/cssselect2/\n.. _Pyphen: https://pyphen.org/\n.. _Pillow: https://python-pillow.org/\n.. _fontTools: https://github.com/fonttools/fonttools\n\nThere are many ways to install WeasyPrint, depending on the system you use.\n\n\nLinux\n~~~~~\n\nThe easiest way to install WeasyPrint on Linux is to use the package manager of\nyour distribution. WeasyPrint is packaged for recent versions of Debian_,\nUbuntu_, Fedora_, Archlinux_, Gentoo_…\n\n.. _Debian: https://packages.debian.org/search?keywords=weasyprint&searchon=names&suite=all&section=all\n.. _Ubuntu: https://packages.ubuntu.com/search?keywords=weasyprint&searchon=names&suite=all&section=all\n.. _Fedora: https://src.fedoraproject.org/rpms/weasyprint\n.. _Archlinux: https://aur.archlinux.org/packages/python-weasyprint\n.. _Gentoo: https://packages.gentoo.org/packages/dev-python/weasyprint\n\nIf WeasyPrint is not available on your distribution, or if you want to use a\nmore recent version of WeasyPrint, you have to be sure that Python_ and Pango_\nare installed on your system, and that they are recent enough. You can verify\nthis by launching::\n\n  python3 --version\n  pango-view --version\n\nIf the version of Pango provided by your distribution is too old, you can use\nversion 52.5 of WeasyPrint which does not need recent Pango features.\n\nWhen everything is OK, you can install WeasyPrint directly on your system or\nin a `virtual environment`_ using `pip`_::\n\n  python3 -m venv venv\n  source venv/bin/activate\n  pip install weasyprint\n  weasyprint --info\n\n.. _virtual environment: https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/\n.. _pip: https://pip.pypa.io/\n\n\nAlpine ≥ 3.17\n+++++++++++++\n\nTo install WeasyPrint using your distribution’s package::\n\n  apk add weasyprint\n\nTo install WeasyPrint inside a virtualenv using wheels (if possible), you need\nthe following packages::\n\n  apk add py3-pip so:libgobject-2.0.so.0 so:libpango-1.0.so.0 so:libharfbuzz.so.0 so:libharfbuzz-subset.so.0 so:libfontconfig.so.1 so:libpangoft2-1.0.so.0\n\nTo install WeasyPrint inside a virtualenv without using wheels, you need the\nfollowing packages::\n\n  apk add py3-pip so:libgobject-2.0.so.0 so:libpango-1.0.so.0 so:libharfbuzz.so.0 so:libharfbuzz-subset.so.0 so:libfontconfig.so.1 so:libpangoft2-1.0.so.0\n  apk add gcc musl-dev python3-dev zlib-dev jpeg-dev openjpeg-dev libwebp-dev g++ libffi-dev\n\n\nArchlinux\n+++++++++\n\nTo install WeasyPrint using your distribution’s package::\n\n  pacman -S python-weasyprint\n\nTo install WeasyPrint inside a virtualenv using wheels (if possible), you need\nthe following packages::\n\n  pacman -S python-pip pango\n\nTo install WeasyPrint inside a virtualenv without using wheels, you need the\nfollowing packages::\n\n  pacman -S python-pip pango gcc libjpeg-turbo openjpeg2\n\n\nDebian ≥ 11\n+++++++++++\n\nTo install WeasyPrint using your distribution’s package::\n\n  apt install weasyprint\n\nTo install WeasyPrint inside a virtualenv using wheels (if possible), you need\nthe following packages::\n\n  apt install python3-pip libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0\n\nTo install WeasyPrint inside a virtualenv without using wheels, you need the\nfollowing packages::\n\n  apt install python3-pip libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 libjpeg-dev libopenjp2-7-dev libffi-dev\n\n\nFedora ≥ 39\n+++++++++++\n\nTo install WeasyPrint using your distribution’s package::\n\n  dnf install weasyprint\n\nTo install WeasyPrint inside a virtualenv using wheels (if possible), you need\nthe following packages::\n\n  dnf install python-pip pango\n\nTo install WeasyPrint inside a virtualenv without using wheels, you need the\nfollowing packages::\n\n  dnf install python3-pip pango gcc python3-devel gcc-c++ zlib-devel libjpeg-devel openjpeg2-devel libffi-devel\n\n\nUbuntu ≥ 20.04\n++++++++++++++\n\nTo install WeasyPrint using your distribution’s package::\n\n  apt install weasyprint\n\nTo install WeasyPrint inside a virtualenv using wheels (if possible), you need\nthe following packages::\n\n  apt install python3-pip libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 libharfbuzz-subset0\n\nTo install WeasyPrint inside a virtualenv without using wheels, you need the\nfollowing packages::\n\n  apt install python3-pip libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 libharfbuzz-subset0 libffi-dev libjpeg-dev libopenjp2-7-dev\n\n\nmacOS\n~~~~~\n\nThe easiest way to install WeasyPrint on macOS is to use Homebrew_::\n\n  brew install weasyprint\n\n.. _Homebrew: https://brew.sh/\n\n\nWindows\n~~~~~~~\n\nTo use WeasyPrint on Windows, the easiest way is to use the `executable`_ of\nthe latest release.\n\n.. warning::\n\n   WeasyPrint is regularly marked as malware by different antivirus companies.\n   See `#2081`_ or `#2092`_ to get more information on this topic. Don’t\n   hesitate to report the false positive detection to your antivirus company in\n   order to improve malware detection for future versions.\n\nIf you want to use WeasyPrint as a Python library, you’ll have to follow a few\nextra steps. Please read this chapter carefully.\n\nThe first step is to install the `Python Install Manager`_ from the Microsoft\nStore.\n\nWhen Python is installed, you have to install Pango and its dependencies. The\neasiest way to install these libraries is to use MSYS2. Here are the steps you\nhave to follow:\n\n- Install `MSYS2`_ keeping the default options.\n- After installation, in MSYS2’s shell, execute ``pacman -S mingw-w64-x86_64-pango``.\n- Close MSYS2’s shell.\n\nYou can then launch a Windows command prompt by clicking on the Start menu,\ntyping ``cmd`` and clicking the \"Command Prompt\" icon. Install WeasyPrint in a\n`virtual environment`_ using `pip`_::\n\n  python -m venv venv\n  venv\\Scripts\\activate.bat\n  python -m pip install weasyprint\n  python -m weasyprint --info\n\n.. _executable: https://github.com/Kozea/WeasyPrint/releases\n.. _#2081: https://github.com/Kozea/WeasyPrint/issues/2081\n.. _#2092: https://github.com/Kozea/WeasyPrint/issues/2092\n.. _Python Install Manager: https://apps.microsoft.com/detail/9nq7512cxl7t\n.. _MSYS2: https://www.msys2.org/#installation\n\n\nOther Solutions\n~~~~~~~~~~~~~~~\n\nOther solutions are available to install WeasyPrint. These solutions are not\ntested but they are known to work for some use cases on specific platforms.\n\nMacports\n++++++++\n\nOn macOS, you can install WeasyPrint’s dependencies with Macports_::\n\n  sudo port install py-pip pango libffi\n\nYou can then install WeasyPrint in a `virtual environment`_ using `pip`_::\n\n  python3 -m venv venv\n  source venv/bin/activate\n  pip install weasyprint\n  weasyprint --info\n\n.. _Macports: https://www.macports.org/\n\nConda\n+++++\n\nOn Linux and macOS, WeasyPrint is available on Conda_, with `a WeasyPrint\npackage on Conda Forge`_.\n\n.. _Conda: https://docs.conda.io/projects/conda/en/latest/\n.. _a WeasyPrint package on Conda Forge: https://anaconda.org/conda-forge/weasyprint\n\nWSL\n+++\n\nOn Windows, you can also use WSL_ and install WeasyPrint the same way it has to\nbe installed on Linux.\n\n.. _WSL: https://docs.microsoft.com/en-us/windows/wsl/\n\n.NET Wrapper\n++++++++++++\n\nOn Windows, Bader Albarrak maintains `a .NET wrapper`_.\n\n.. _a .NET wrapper: https://github.com/balbarak/WeasyPrint-netcore\n\nAWS\n+++\n\nKotify maintains `an AWS Lambda layer`_, see issue `#1003`_ for more\ninformation.\n\n.. _an AWS Lambda layer: https://github.com/kotify/cloud-print-utils\n.. _#1003: https://github.com/Kozea/WeasyPrint/issues/1003\n\nDocker\n++++++\n\nLuca Vercelli maintains `Docker images`_.\n\n.. _an AWS Lambda layer: https://github.com/kotify/cloud-print-utils\n.. _Docker images: https://github.com/luca-vercelli/WeasyPrint-docker-images/\n\n\nTroubleshooting\n~~~~~~~~~~~~~~~\n\nMost of the installation problems have already been met, and some `issues on\nGitHub`_ could help you to solve them. If the solutions here don’t solve your\nproblem, please open a new issue (and don’t add comments to closed issues).\n\n.. _issues on GitHub: https://github.com/Kozea/WeasyPrint/issues\n\nMissing Library\n+++++++++++++++\n\nOn Windows or macOS, most of the problems come from unreachable libraries. If\nyou get an error like ``cannot load library 'xxx': error xxx``, it means that\nWeasyPrint can’t find this library.\n\nOn Windows, you can set the ``WEASYPRINT_DLL_DIRECTORIES`` environment variable\nto list the folders where the DLL files can be found. For example, in\n``cmd.exe``::\n\n  set WEASYPRINT_DLL_DIRECTORIES=C:\\msys64\\mingw64\\bin\n\nOn macOS, you can set the ``DYLD_FALLBACK_LIBRARY_PATH`` environment variable::\n\n  export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_FALLBACK_LIBRARY_PATH\n\nOf course, check that the folders you set actually contain the ``.dll`` (on\nWindows) or ``.dylib`` (on macOS) files WeasyPrint requires.\n\nMissing Fonts\n+++++++++++++\n\nIf no character is drawn in the generated PDF, or if you get squares instead of\nletters, you have to install fonts and make them available to WeasyPrint.\nFollowing the standard way to install fonts on your system should be enough.\nYou can also use ``@font-face`` rules to explicitly reference fonts using URLs.\n\n\nCommand-Line\n------------\n\nUsing the WeasyPrint command line interface can be as simple as this:\n\n.. code-block:: sh\n\n    weasyprint https://weasyprint.org /tmp/weasyprint-website.pdf\n\nYou may see warnings on the standard error output about unsupported CSS\nproperties. See :ref:`Command-Line API` for the details of all available\noptions.\n\nIn particular, the ``-s`` option can add a filename for a\n:ref:`user stylesheet <Stylesheet Origins>`. For quick experimentation\nhowever, you may not want to create a file. In bash or zsh, you can\nuse the shell’s redirection instead:\n\n.. code-block:: sh\n\n    weasyprint https://weasyprint.org /tmp/weasyprint-website.pdf \\\n        -s <(echo 'body { font-family: serif !important }')\n\nIf you have many documents to convert you may prefer using the Python API\nin long-lived processes to avoid paying the start-up costs every time.\n\n\nPython Library\n--------------\n\n.. attention::\n\n    Using WeasyPrint with untrusted HTML or untrusted CSS may lead to various\n    :ref:`security problems <security>`.\n\nQuickstart\n~~~~~~~~~~\n\nThe Python version of the above example goes like this:\n\n.. code-block:: python\n\n    from weasyprint import HTML\n    HTML('https://weasyprint.org/').write_pdf('/tmp/weasyprint-website.pdf')\n\n… or with the inline stylesheet:\n\n.. code-block:: python\n\n    from weasyprint import HTML, CSS\n    HTML('https://weasyprint.org/').write_pdf('/tmp/weasyprint-website.pdf',\n        stylesheets=[CSS(string='body { font-family: serif !important }')])\n\nInstantiating HTML and CSS Objects\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf you have a file name, an absolute URL or a readable :term:`file object`,\nyou can just pass it to :class:`HTML` or :class:`CSS` to create an instance.\nAlternatively, use a named argument so that no guessing is involved:\n\n.. code-block:: python\n\n    from weasyprint import HTML\n\n    HTML('../foo.html')  # Same as …\n    HTML(filename='../foo.html')\n\n    HTML('https://weasyprint.org')  # Same as …\n    HTML(url='https://weasyprint.org')\n\n    HTML(sys.stdin)  # Same as …\n    HTML(file_obj=sys.stdin)\n\nIf you have a byte string or Unicode string already in memory you can also pass\nthat, although the argument must be named:\n\n.. code-block:: python\n\n    from weasyprint import HTML, CSS\n\n    # HTML('<h1>foo') would be filename\n    HTML(string='''\n        <h1>The title</h1>\n        <p>Content goes here\n    ''')\n    CSS(string='@page { size: A3; margin: 1cm }')\n\nIf you have ``@font-face`` rules in your CSS, you have to create a\n``FontConfiguration`` object:\n\n.. code-block:: python\n\n    from weasyprint import HTML, CSS\n    from weasyprint.text.fonts import FontConfiguration\n\n    font_config = FontConfiguration()\n    html = HTML(string='<h1>The title</h1>')\n    css = CSS(string='''\n        @font-face {\n            font-family: Gentium;\n            src: url(https://example.com/fonts/Gentium.otf);\n        }\n        h1 { font-family: Gentium }''', font_config=font_config)\n    html.write_pdf(\n        '/tmp/example.pdf', stylesheets=[css],\n        font_config=font_config)\n\nRendering to a Single File\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nOnce you have a :class:`HTML` object, call its :meth:`HTML.write_pdf` method to\nget the rendered document in a single PDF file.\n\nWithout arguments, this method returns a byte string in memory. If you pass a\nfile name or a writable :term:`file object`, they will write there directly\ninstead. (**Warning**: with a filename, these methods will overwrite existing\nfiles silently.)\n\nRendering Individual Pages\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf you want more than a single PDF, the :meth:`HTML.render` method gives you a\n:class:`document.Document` object with access to individual\n:class:`document.Page` objects. Thus you can get the number of pages, their\nsize\\ [#]_, the details of hyperlinks and bookmarks, etc. Documents also have a\n:meth:`document.Document.write_pdf` method, and you can get a subset of the\npages with :meth:`document.Document.copy()`.\n\n.. [#] Pages in the same document do not always have the same size.\n\nSee the :ref:`Python API` for details. A few random examples:\n\n.. code-block:: python\n\n    # Write odd and even pages separately:\n    #   Lists count from 0 but page numbers usually from 1\n    #   [::2] is a slice of even list indexes but odd-numbered pages.\n    document.copy(document.pages[::2]).write_pdf('odd_pages.pdf')\n    document.copy(document.pages[1::2]).write_pdf('even_pages.pdf')\n\n.. code-block:: python\n\n    # Print the outline of the document.\n    # Output on https://www.w3.org/TR/CSS21/intro.html\n    #     1. Introduction to CSS 2.1 (page 2)\n    #       1. A brief CSS 2.1 tutorial for HTML (page 2)\n    #       2. A brief CSS 2.1 tutorial for XML (page 5)\n    #       3. The CSS 2.1 processing model (page 6)\n    #         1. The canvas (page 7)\n    #         2. CSS 2.1 addressing model (page 7)\n    #       4. CSS design principles (page 8)\n    def print_outline(bookmarks, indent=0):\n        for i, bookmark in enumerate(bookmarks, 1):\n            page = bookmark.destination[0]\n            print('%s%d. %s (page %d)' % (\n                ' ' * indent, i, bookmark.label.lstrip('0123456789. '), page))\n            print_outline(bookmark.children, indent + 2)\n    print_outline(document.make_bookmark_tree())\n\nURL Fetchers\n~~~~~~~~~~~~\n\nWeasyPrint goes through a *URL fetcher* to fetch external resources such as\nimages or CSS stylesheets. The default fetcher can natively open file and\nHTTP URLs, but the HTTP client does not support advanced features like cookies\nor authentication. This can be worked-around by passing a custom\n:class:`URLFetcher <urls.URLFetcher>` to the :class:`HTML` or :class:`CSS`\nclasses.\n\nSome features, such as the timeout delay, can be configured as parameters:\n\n.. code-block:: python\n\n    from weasyprint import HTML\n    from weasyprint.urls import URLFetcher\n\n    HTML(string='<html>', url_fetcher=URLFetcher(timeout=20)).write_pdf('out.pdf')\n\nCustom fetchers can also choose to handle some URLs and defer others to the default\nfetcher:\n\n.. code-block:: python\n\n    from weasyprint import HTML\n    from weasyprint.urls import URLFetcher, URLFetcherResponse\n\n    class MyFetcher(URLFetcher):\n        def fetch(self, url, headers=None):\n            if url.startswith('graph:'):\n                graph_data = [float(value) for value in url[6:].split(',')]\n                string = generate_graph(graph_data)\n                return URLFetcherResponse(url, string, {'Content-Type': 'image/png'})\n            return super().fetch(url, headers)\n\n    source = '<img src=\"graph:42,10.3,87\">'\n    HTML(string=source, url_fetcher=MyFetcher()).write_pdf('out.pdf')\n\nBy default, all errors raised by the fetcher are caught by WeasyPrint and emit a\nwarning. If you want some errors to be fatal and stop the rendering, you can raise a\n:class:`urls.FatalURLFetchingError`.\n\n.. code-block:: python\n\n    from weasyprint import HTML\n    from weasyprint.urls import URLFetcher, FatalURLFetchingError\n\n    class MyFetcher(URLFetcher):\n        def fetch(self, url, headers=None):\n            try:\n                return super().fetch(url, headers)\n            except Exception as exception:\n                if url.endswith('.css'):\n                    # Stop the rendering if a problem happens with a stylesheet.\n                    message = f'Problem with stylesheet at {url}'\n                    raise FatalURLFetchingError(message) from exception\n                else:\n                    # Raise original error that will be caught by WeasyPrint.\n                    raise exception\n\n    HTML(string='<html>', url_fetcher=MyFetcher()).write_pdf('out.pdf')\n\nFlask-WeasyPrint_ for Flask_ and Django-Weasyprint_ for Django_ both make\nuse of a custom URL fetcher to integrate WeasyPrint and use the filesystem\ninstead of a network call for static and media files.\n\nA custom fetcher should be returning a :class:`urls.URLFetcherResponse`.\n\nIf a ``file_obj`` is given, the resource will be closed automatically by\nthe function internally used by WeasyPrint to retrieve data.\n\n.. _Flask-Weasyprint: https://github.com/Kozea/Flask-WeasyPrint\n.. _Flask: https://flask.pocoo.org/\n.. _Django-WeasyPrint: https://github.com/fdemmer/django-weasyprint\n.. _Django: https://www.djangoproject.com/\n\n\nSecurity\n--------\n\n*This section has been added thanks to the very useful reports and advice from\nRaz Becker.*\n\nWhen used with untrusted HTML or untrusted CSS, WeasyPrint can meet security\nproblems. You will need extra configuration in your Python application to avoid\nhigh memory use, endless renderings or local files leaks.\n\nAs for any service dealing with untrusted data, you should at least follow\nbasic security rules with WeasyPrint: don’t launch the service as root, launch\nit as a user with limited access to filesystem, network and memory. Using a\ncontainer can also be a simple way to limit the possibilities given to an\nattacker in case of security breach.\n\nLong Renderings\n~~~~~~~~~~~~~~~\n\nWeasyPrint is pretty slow and can take a long time to render long documents or\nspecially crafted HTML pages.\n\nWhen WeasyPrint used on a server with HTML or CSS files from untrusted sources,\nthis problem can lead to very long time renderings, with processes with high\nCPU and memory use. Even small documents may lead to really long rendering\ntimes, restricting HTML document size is not enough.\n\nIf you use WeasyPrint on a server with HTML or CSS samples coming from\nuntrusted users, you should:\n\n- limit rendering time and memory use of your process, for example using\n  ``evil-reload-on-as`` and ``harakiri`` options if you use uWSGI,\n- limit memory use at the OS level, for example with ``ulimit`` on Linux,\n- automatically kill the process when it uses too much memory or when the\n  rendering time is too high, by regularly launching a script to do so if no\n  better option is available,\n- truncate and sanitize HTML and CSS input to avoid very long documents and\n  access to external URLs.\n\nInfinite Requests\n~~~~~~~~~~~~~~~~~\n\nWeasyPrint can reach files on the network, for example using ``https://``\nURIs. For various reasons, HTTP requests may take a long time and lead to\nproblems similar to :ref:`Long Renderings`.\n\nWeasyPrint has a default timeout of 10 seconds for HTTP, HTTPS and FTP\nresources. This timeout has no effect with other protocols, including access to\n``file://`` URIs.\n\nIf you use WeasyPrint on a server with HTML or CSS samples coming from\nuntrusted users, or need to reach network resources, you should:\n\n- use a custom :ref:`URL fetcher <URL Fetchers>`,\n- follow solutions listed in :ref:`Long Renderings`.\n\nInfinite Loops\n~~~~~~~~~~~~~~\n\nWeasyPrint has been hit by a large number of bugs, including infinite\nloops. Specially crafted HTML and CSS files can quite easily lead to infinite\nloops and infinite rendering times.\n\nIf you use WeasyPrint on a server with HTML or CSS samples coming from\nuntrusted users, you should:\n\n- follow solutions listed in :ref:`Long Renderings`.\n\nHuge Values\n~~~~~~~~~~~\n\nWeasyPrint doesn't restrict integer and float values used in CSS. Using huge\nvalues for some properties (page sizes, font sizes, block sizes) can lead to\nvarious problems, including infinite rendering times, huge PDF files, high\nmemory use and crashes.\n\nThis problem is really hard to avoid. Even parsing CSS stylesheets and\nsearching for huge values is not enough, as it is quite easy to trick CSS\npre-processors using relative units (``em`` and ``%`` for example).\n\nIf you use WeasyPrint on a server with HTML or CSS samples coming from\nuntrusted users, you should:\n\n- follow solutions listed in :ref:`Long Renderings`.\n\nAccess to Local Files\n~~~~~~~~~~~~~~~~~~~~~\n\nAs any web renderer, WeasyPrint can reach files on the local filesystem using\n``file://`` URIs. These files can be shown in ``img`` or ``embed`` tags for\nexample.\n\nWhen WeasyPrint used on a server with HTML or CSS files from untrusted sources,\nthis feature may be used to know if files are present on the server filesystem,\nand to embed them in generated documents.\n\nUnix-like systems also have special local files with infinite size, like\n``/dev/urandom``. Referencing these files in HTML or CSS files obviously lead\nto infinite time renderings.\n\nIf you use WeasyPrint on a server with HTML or CSS samples coming from\nuntrusted users, you should:\n\n- restrict your process access to trusted files using sandboxing solutions,\n- use a custom :ref:`URL fetcher <URL Fetchers>` that doesn't allow ``file://``\n  URLs or filters access depending on given paths.\n- follow solutions listed in :ref:`Long Renderings`.\n\nSystem Information Leaks\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nWeasyPrint relies on many libraries that can leak hardware and software\ninformation. Even when this information looks useless, it can be used by\nattackers to exploit other security breaches.\n\nLeaks can include (but are not restricted to):\n\n- locally installed fonts (using ``font-family`` and ``@font-face``),\n- network configuration (IPv4 and IPv6 support, IP addressing, firewall\n  configuration, using ``https://`` URIs and tracking time used to render\n  documents),\n- Python, Pango and other libraries versions (implementation details\n  lead to different renderings).\n\nSVG Images\n~~~~~~~~~~\n\nRendering SVG images more or less suffers from the same problems as the ones\nlisted here for WeasyPrint.\n\nSecurity advices apply for untrusted SVG files as they apply for untrusted HTML\nand CSS documents.\n\nNote that WeasyPrint’s URL fetcher is used to render SVG files.\n"
  },
  {
    "path": "docs/going_further.rst",
    "content": "Going Further\n=============\n\n.. currentmodule:: weasyprint\n\n\nWhy WeasyPrint?\n---------------\n\nAutomatic document generation is a common need of many applications, even if a\nlot of operations do not require printed paper anymore.\n\nInvoices, tickets, leaflets, diplomas, documentation, books… All these\ndocuments are read and used on paper, but also on electronical readers, on\nsmartphones, on computers. PDF is a great format to store and display them in\na reliable way, with pagination.\n\nUsing HTML and CSS to generate static and paged content can be strange at first\nglance: browsers display only one page, with variable dimensions, often in a\nvery dynamic way. But paged media layout is actually included in CSS2_, which\nwas already a W3C recommendation in 1998.\n\nOther well-known tools can be used to automatically generate PDF documents,\nlike LaTeX and LibreOffice, but they miss many advantages that HTML and CSS\noffer. HTML and CSS are very widely known, by developers but also by\nwebdesigners. They are specified in a backwards-compatible way, and regularly\nadapted to please the use of billions of people. They are really easy to write\nand generate, with a ridiculous amount of tools that are finely adapted to the\nneeds and taste of their users.\n\nHowever, the web engines that are used for browsers were very limited for\npagination when WeasyPrint was created in 2011. Even now, they lack a lot of\nbasic features. That’s why projects such as wkhtmltopdf_ and PagedJS_ have been\ncreated: they add some of these features to existing browsers.\n\nOther solutions have beed developed, including web engine dedicated to paged\nmedia. Prince_, Antennahouse_ or `Typeset.sh`_ created original renderers\nsupporting many features related to pagination. These tools are very powerful,\nbut they are not open source.\n\nBuilding a free and open source web renderer generating high-quality documents\nis the main goal of WeasyPrint. Do you think that it was a little bit crazy to\ncreate such a big project from scratch? Here is what `Simon Sapin`_ wrote\nin WeasyPrint’s documentation one month after the beginning:\n\n  Are we crazy? Yes. But not that much. Each modern web browser did take many\n  developers’ many years of work to get where they are now, but WeasyPrint’s\n  scope is much smaller: there is no user-interaction, no JavaScript, no live\n  rendering (the document doesn’t changed after it was first parsed) and no\n  quirks mode (we don’t need to support every broken page of the web.)\n\n  We still need however to implement the whole CSS box model and visual\n  rendering. This is a lot of work, but we feel we can get something useful\n  much quicker than “Let’s build a rendering engine!” may seem.\n\nSimon is often right.\n\n.. _CSS2: https://www.w3.org/TR/1998/REC-CSS2-19980512/\n.. _wkhtmltopdf: https://wkhtmltopdf.org/\n.. _PagedJS: https://www.pagedjs.org/\n.. _Prince: https://www.princexml.com/\n.. _Antennahouse: https://www.antennahouse.com/\n.. _Typeset.sh: https://typeset.sh/\n.. _Simon Sapin: https://exyr.org/\n\n\nWhy Python?\n-----------\n\nPython is a really good language to design a small, OS-agnostic parser. As it\nis object-oriented, it gives the possibility to follow the specification with\nhigh-level classes and a small amount of very simple code.\n\nSpeed is not WeasyPrint’s main goal. Web rendering is a very complex task, and\nfollowing :pep:`the Zen of Python <20>` helped a lot to keep our sanity (both in our\ncode and in our heads): code simplicity, maintainability and flexibility are\nthe most important goals for this library, as they give the ability to stay\nreally close to the specification and to fix bugs easily.\n\n\nDive into the Source\n--------------------\n\nThis chapter is a high-level overview of WeasyPrint’s source code. For more\ndetails, see the various docstrings or even the code itself. When in doubt,\nfeel free to :ref:`ask <Support>`!\n\nMuch `like in web browsers`_, the rendering of a document in WeasyPrint goes\nlike this:\n\n1. The HTML document is fetched and parsed into a tree of elements (like DOM).\n2. CSS stylesheets (either found in the HTML or supplied by the user) are\n   fetched and parsed.\n3. The stylesheets are applied to the DOM-like tree.\n4. The DOM-like tree with styles is transformed into a *formatting structure*\n   made of rectangular boxes.\n5. These boxes are *laid-out* with fixed dimensions and position onto pages.\n6. For each page, the boxes are re-ordered to observe stacking rules, and are\n   drawn on a PDF page.\n7. Metadata −such as document information, attachments, embedded files,\n   hyperlinks, and PDF trim and bleed boxes− are added to the PDF.\n\n.. _like in web browsers: https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/#The_main_flow\n\n\nParsing HTML\n............\n\nNot much to see here. The :class:`HTML` class handles step 1 and\ngives a tree of HTML *elements*. Although the actual API is different, this\ntree is conceptually the same as what web browsers call *the DOM*.\n\n\nParsing CSS\n...........\n\nAs with HTML, CSS stylesheets are parsed in the :class:`CSS` class\nwith an external library, tinycss2_.\n\nIn addition to the actual parsing, the ``css`` and ``css.validation``\nmodules do some pre-processing:\n\n* Unknown and unsupported declarations are ignored with warnings.\n  Remaining property values are parsed in a property-specific way\n  from raw tinycss2 tokens into a higher-level form.\n* Shorthand properties are expanded. For example, ``margin`` becomes\n  ``margin-top``, ``margin-right``, ``margin-bottom`` and ``margin-left``.\n* Hyphens in property names are replaced by underscores (``margin-top`` becomes\n  ``margin_top``). This transformation is safe since none of the known (not\n  ignored) properties have an underscore character.\n* Selectors are pre-compiled with cssselect2_.\n\n.. _tinycss2: https://pypi.python.org/pypi/tinycss2\n.. _cssselect2: https://pypi.python.org/pypi/cssselect2\n\n\nThe Cascade\n...........\n\nAfter that and still in the ``css`` package, the cascade_\n(that’s the C in CSS!) applies the stylesheets to the element tree.\nSelectors associate property declarations to elements. In case of conflicting\ndeclarations (different values for the same property on the same element),\nthe one with the highest *weight* wins. Weights are based on the stylesheet’s\n:ref:`origin <Stylesheet Origins>`, ``!important`` markers, selector\nspecificity and source order. Missing values are filled in through\n*inheritance* (from the parent element) or the property’s *initial value*,\nso that every element has a *specified value* for every property.\n\n.. _cascade: https://www.w3.org/TR/CSS21/cascade.html\n\nThese *specified values* are turned into *computed values* in the\n``css.computed_values`` module. Keywords and lengths in various units are\nconverted to pixels, etc. At this point the value for some properties can be\nrepresented by a single number or string, but some require more complex\nobjects. For example, a ``Dimension`` object can be either an absolute length\nor a percentage.\n\nThe final result of the ``css.get_all_computed_styles`` function is a big dict\nwhere keys are ``(element, pseudo_element_type)`` tuples, and keys are style\ndict objects. Elements are ElementTree elements, while the type of\npseudo-element is a string for eg. ``::first-line`` selectors, or :obj:`None`\nfor “normal” elements. Style dict objects are dicts mapping property names to\nthe computed values. (The return value is not the dict itself, but a\nconvenience ``style_for`` function for accessing it.)\n\n\nFormatting Structure\n....................\n\nThe `visual formatting model`_ explains how *elements* (from the ElementTree\ntree) generate *boxes* (in the formatting structure). This is step 4 above.\nBoxes may have children and thus form a tree, much like elements. This tree is\ngenerally close but not identical to the ElementTree tree: some elements\ngenerate more than one box or none.\n\n.. _visual formatting model: https://www.w3.org/TR/CSS21/visuren.html\n\nBoxes are of a lot of different kinds. For example you should not confuse\n*block-level boxes* and *block containers*, though *block boxes* are both.  The\n``formatting_structure.boxes`` module has a whole hierarchy of classes to\nrepresent all these boxes. We won’t go into the details here, see the module\nand class docstrings.\n\nThe ``formatting_structure.build`` module takes an ElementTree tree with\nassociated computed styles, and builds a formatting structure. It generates the\nright boxes for each element and ensures they conform to the models rules\n(eg. an inline box can not contain a block). Each box has a ``style``\nattribute containing the style dict of computed values.\n\nThe main logic is based on the ``display`` property, but it can be overridden\nfor some elements by adding a handler in the ``html`` module.\nThis is how ``<img>`` and ``<td colspan=3>`` are currently implemented,\nfor example.\n\nThis module is rather short as most of HTML is defined in CSS rather than\nin Python, in the `user agent stylesheet`_.\n\nThe ``formatting_structure.build.build_formatting_structure`` function returns\nthe box for the root element (and, through its ``children`` attribute, the\nwhole tree).\n\n.. _user agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/main/weasyprint/css/html5_ua.css\n\n\nLayout\n......\n\nStep 5 is the layout. You could say the everything else is glue code and\nthis is where the magic happens.\n\nDuring the layout the document’s content is, well, laid out on pages.\nThis is when we decide where to do line breaks and page breaks. If a break\nhappens inside of a box, that box is split into two (or more) boxes in the\nlayout result.\n\nAccording to the `box model`_, each box has rectangular margin, border,\npadding and content areas:\n\n.. _box model: https://www.w3.org/TR/CSS21/box.html\n\n.. image:: https://www.w3.org/TR/CSS21/images/boxdim.png\n   :alt: CSS Box Model\n   :class: dark-invert\n\nWhile ``box.style`` contains computed values, the `used values`_ are set as\nattributes of the ``Box`` object itself during the layout. This include\nresolving percentages and especially ``auto`` values into absolute, pixel\nlengths. Once the layout done, each box has used values for margins, border\nwidth, padding of each four sides, as well as the ``width`` and ``height`` of\nthe content area. They also have ``position_x`` and ``position_y``, the\nabsolute coordinates of the top-left corner of the margin box (**not** the\ncontent box) from the top-left corner of the page.\\ [#]_\n\nBoxes also have helpers methods such as ``content_box_y`` and ``margin_width``\nthat give other metrics that can be useful in various parts of the code.\n\nThe final result of the layout is a list of ``PageBox`` objects.\n\n.. [#] These are the coordinates *if* no `CSS transform`_ applies.\n       Transforms change the actual location of boxes, but they are applied\n       later during drawing and do not affect layout.\n.. _used values: https://www.w3.org/TR/CSS21/cascade.html#used-value\n.. _CSS transform: https://www.w3.org/TR/css-transforms-1/\n\n\nStacking & Drawing\n..................\n\nIn step 6, the boxes are reordered by the ``stacking`` module to observe\n`stacking rules`_ such as the ``z-index`` property.  The result is a tree of\n*stacking contexts*.\n\nNext, each laid-out page is *drawn* onto a PDF page. Since each box has\nabsolute coordinates on the page from the layout step, the logic here should be\nminimal. If you find yourself adding a lot of logic here, maybe it should go in\nthe layout or stacking instead.\n\nThe code lives in the ``draw`` module.\n\n.. _stacking rules: https://www.w3.org/TR/CSS21/zindex.html\n\n\nMetadata\n........\n\nFinally (step 7), the ``pdf`` adds metadata to the PDF file: document\ninformation, attachments, hyperlinks, embedded files, trim box and bleed box.\n"
  },
  {
    "path": "docs/index.rst",
    "content": "WeasyPrint\n==========\n\n.. currentmodule:: weasyprint\n\n.. include:: ../README.rst\n\n.. toctree::\n   :caption: Documentation\n   :maxdepth: 2\n\n   first_steps\n   common_use_cases\n   api_reference\n   going_further\n\n.. toctree::\n   :caption: Extra Information\n   :maxdepth: 2\n\n   changelog\n   contribute\n   support\n"
  },
  {
    "path": "docs/manpage.rst",
    "content": ":orphan:\n\n.. currentmodule:: weasyprint\n\n\nDescription\n-----------\n\n.. autofunction:: weasyprint.__main__.main(argv=sys.argv)\n   :noindex:\n\n\nAbout\n-----\n\n.. include:: ../README.rst\n"
  },
  {
    "path": "docs/support.rst",
    "content": "Support\n=======\n\n\nSponsorship\n-----------\n\nWith `donations and sponsorship`, you help make the projects\nbetter. Donations allow the CourtBouillon team to have more time dedicated to\nadd new features, fix bugs, and improve documentation.\n\n.. _donations and sponsorship: https://opencollective.com/courtbouillon\n\n\nProfessional Support\n--------------------\n\nYou can improve your experience with CourtBouillon’s tools thanks to our\nprofessional support. You want bugs fixed as soon as possible? Your projects\nwould highly benefit from some new features? You or your team would like to get\nnew skills with one of the technologies we master?\n\nPlease contact us by mail, by chat, or by tweet to get in touch and find the\nbest way we can help you.\n\n.. _mail: mailto:contact@courtbouillon.org\n.. _chat: https://gitter.im/CourtBouillon/tinycss2\n.. _tweet: https://twitter.com/BouillonCourt\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = ['flit_core >=3.2,<4']\nbuild-backend = 'flit_core.buildapi'\n\n[project]\nname = 'weasyprint'\ndescription = 'The Awesome Document Factory'\nkeywords = ['html', 'css', 'pdf', 'converter']\nauthors = [{name = 'Simon Sapin', email = 'simon.sapin@exyr.org'}]\nmaintainers = [{name = 'CourtBouillon', email = 'contact@courtbouillon.org'}]\nrequires-python = '>=3.10'\nreadme = {file = 'README.rst', content-type = 'text/x-rst'}\nlicense = {file = 'LICENSE'}\ndependencies = [\n  'pydyf >=0.11.0',\n  'cffi >=0.6',\n  'tinyhtml5 >=2.0.0b1',\n  'tinycss2 >=1.5.0',\n  'cssselect2 >=0.8.0',\n  'Pyphen >=0.9.1',\n  'Pillow >=9.1.0',\n  'fonttools[woff] >=4.59.2',\n]\nclassifiers = [\n  'Development Status :: 5 - Production/Stable',\n  'Intended Audience :: Developers',\n  'License :: OSI Approved :: BSD License',\n  'Operating System :: OS Independent',\n  'Programming Language :: Python',\n  'Programming Language :: Python :: 3',\n  'Programming Language :: Python :: 3 :: Only',\n  'Programming Language :: Python :: 3.10',\n  'Programming Language :: Python :: 3.11',\n  'Programming Language :: Python :: 3.12',\n  'Programming Language :: Python :: 3.13',\n  'Programming Language :: Python :: 3.14',\n  'Programming Language :: Python :: Implementation :: CPython',\n  'Programming Language :: Python :: Implementation :: PyPy',\n  'Topic :: Internet :: WWW/HTTP',\n  'Topic :: Text Processing :: Markup :: HTML',\n  'Topic :: Multimedia :: Graphics :: Graphics Conversion',\n  'Topic :: Printing',\n]\ndynamic = ['version']\n\n[project.urls]\nHomepage = 'https://weasyprint.org/'\nDocumentation = 'https://doc.courtbouillon.org/weasyprint/'\nCode = 'https://github.com/Kozea/WeasyPrint'\nIssues = 'https://github.com/Kozea/WeasyPrint/issues'\nChangelog = 'https://github.com/Kozea/WeasyPrint/releases'\nDonation = 'https://opencollective.com/courtbouillon'\n\n[project.optional-dependencies]\ndoc = ['sphinx', 'furo']\ntest = ['pytest', 'ruff', 'Pillow >=12.1.0']\n\n[project.scripts]\nweasyprint = 'weasyprint.__main__:main'\n\n[tool.flit.sdist]\nexclude = ['.*']\n\n[tool.coverage.run]\nbranch = true\ninclude = ['tests/*', 'weasyprint/*']\n\n[tool.coverage.report]\nexclude_lines = ['pragma: no cover', 'def __repr__', 'raise NotImplementedError']\n\n[tool.ruff.lint]\nselect = ['E', 'W', 'F', 'I', 'N', 'RUF', 'T20', 'PIE', 'PT', 'RSE', 'UP', 'Q']\nignore = ['E226', 'RUF001', 'RUF002', 'RUF003', 'RUF039', 'RUF059', 'UP031']\n\n[tool.ruff.lint.flake8-quotes]\ninline-quotes = 'single'\nmultiline-quotes = 'single'"
  },
  {
    "path": "tests/__init__.py",
    "content": "\"\"\"The Weasyprint test suite.\"\"\"\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "\"\"\"Configuration for WeasyPrint tests.\n\nThis module adds a PNG export based on Ghostscript.\n\nNote that Ghostscript is released under AGPL.\n\n\"\"\"\n\nimport io\nimport os\nimport shutil\nfrom subprocess import PIPE, run\nfrom tempfile import NamedTemporaryFile\n\nimport pytest\nfrom PIL import Image\n\nfrom weasyprint import HTML\nfrom weasyprint.document import Document\n\nfrom . import draw\n\nMAGIC_NUMBER = b'\\x89\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a'\n\n\ndef document_write_png(document, target=None, resolution=96, antialiasing=1,\n                       zoom=4/30, split_images=False):\n    # Use temporary files because gs on Windows doesn’t accept binary on stdin\n    with NamedTemporaryFile(delete=False) as pdf:\n        document.write_pdf(pdf, zoom=zoom)\n    command = (\n        'gs', '-q', '-sDEVICE=png16m', f'-dTextAlphaBits={antialiasing}',\n        f'-dGraphicsAlphaBits={antialiasing}', '-dBATCH', '-dNOPAUSE',\n        '-dPDFSTOPONERROR', f'-r{resolution / zoom}', '-dUsePDFX3Profile',\n        '-sOutputFile=-', pdf.name)\n    pngs = run(command, stdout=PIPE).stdout\n    os.remove(pdf.name)\n\n    error = pngs.split(MAGIC_NUMBER)[0].decode().strip() or 'no output'\n    assert pngs.startswith(MAGIC_NUMBER), f'Ghostscript error: {error}'\n\n    if split_images:\n        assert target is None\n\n    # TODO: use a different way to find PNG files in stream\n    magic_numbers = pngs.count(MAGIC_NUMBER)\n    if magic_numbers == 1:\n        if target is None:\n            return [pngs] if split_images else pngs\n        png = io.BytesIO(pngs)\n    else:\n        images = [MAGIC_NUMBER + png for png in pngs[8:].split(MAGIC_NUMBER)]\n        if split_images:\n            return images\n        images = [Image.open(io.BytesIO(image)) for image in images]\n        width = max(image.width for image in images)\n        height = sum(image.height for image in images)\n        output_image = Image.new('RGBA', (width, height))\n        top = 0\n        for image in images:\n            output_image.paste(image, (int((width - image.width) / 2), top))\n            top += image.height\n        png = io.BytesIO()\n        output_image.save(png, format='png')\n\n    png.seek(0)\n\n    if target is None:\n        return png.read()\n\n    if hasattr(target, 'write'):\n        shutil.copyfileobj(png, target)\n    else:\n        with open(target, 'wb') as fd:\n            shutil.copyfileobj(png, fd)\n\n\ndef html_write_png(document, target=None, font_config=None, counter_style=None,\n                   resolution=96, **options):\n    document = document.render(font_config, counter_style, **options)\n    return document.write_png(target, resolution)\n\n\nDocument.write_png = document_write_png\nHTML.write_png = html_write_png\n\n\ndef test_filename(filename):\n    return ''.join(\n        character if character.isalnum() else '_'\n        for character in filename[5:50]).rstrip('_')\n\n\n@pytest.fixture\ndef assert_pixels(request, *args, **kwargs):\n    return lambda *args, **kwargs: draw.assert_pixels(\n        test_filename(request.node.name), *args, **kwargs)\n\n\n@pytest.fixture\ndef assert_same_renderings(request, *args, **kwargs):\n    return lambda *args, **kwargs: draw.assert_same_renderings(\n        test_filename(request.node.name), *args, **kwargs)\n\n\n@pytest.fixture\ndef assert_different_renderings(request, *args, **kwargs):\n    return lambda *args, **kwargs: draw.assert_different_renderings(\n        test_filename(request.node.name), *args, **kwargs)\n\n\n@pytest.fixture\ndef assert_pixels_equal(request, *args, **kwargs):\n    return lambda *args, **kwargs: draw.assert_pixels_equal(\n        test_filename(request.node.name), *args, **kwargs)\n"
  },
  {
    "path": "tests/css/__init__.py",
    "content": "\"\"\"Test CSS features.\"\"\"\n"
  },
  {
    "path": "tests/css/test_common.py",
    "content": "\"\"\"Test the CSS parsing, cascade, inherited and computed values.\"\"\"\n\nfrom math import isclose\n\nimport pytest\n\nfrom weasyprint import CSS\nfrom weasyprint.css import find_stylesheets, get_all_computed_styles\nfrom weasyprint.urls import URLFetcher, path2url\n\nfrom ..testing_utils import (  # isort:skip\n    BASE_URL, FakeHTML, assert_no_logs, capture_logs, resource_path)\n\n\n@assert_no_logs\ndef test_find_stylesheets():\n    html = FakeHTML(resource_path('doc1.html'))\n\n    sheets = list(find_stylesheets(\n        html.wrapper_element, 'print', URLFetcher(), html.base_url, font_config=None,\n        counter_style=None, color_profiles=None, page_rules=None, layers=None))\n    assert len(sheets) == 2\n    # Also test that stylesheets are in tree order.\n    sheet_names = [\n        sheet.base_url.rsplit('/', 1)[-1].rsplit(',', 1)[-1]\n        for sheet in sheets]\n    assert sheet_names == ['a%7Bcolor%3AcurrentColor%7D', 'doc1.html']\n\n    rules = []\n    for sheet in sheets:\n        for sheet_rules in sheet.matcher.lower_local_name_selectors.values():\n            for rule in sheet_rules:\n                rules.append(rule)\n        for rule in sheet.page_rules:\n            rules.append(rule)\n    assert len(rules) == 10\n\n    # TODO: Test that the values are correct too.\n\n\n@assert_no_logs\ndef test_annotate_document():\n    document = FakeHTML(resource_path('doc1.html'))\n    document._ua_stylesheets = (\n        lambda *_, **__: [CSS(resource_path('mini_ua.css'))])\n    style_for = get_all_computed_styles(\n        document, user_stylesheets=[CSS(resource_path('user.css'))])\n\n    # Element objects behave as lists of their children.\n    _head, body = document.etree_element\n    h1, p, ul, div = body\n    li_0, _li_1 = ul\n    a, = li_0\n    span1, = div\n    span2, = span1\n\n    h1 = style_for(h1)\n    p = style_for(p)\n    ul = style_for(ul)\n    li_0 = style_for(li_0)\n    div = style_for(div)\n    after = style_for(a, 'after')\n    a = style_for(a)\n    span1 = style_for(span1)\n    span2 = style_for(span2)\n\n    assert h1['background_image'] == (\n        ('url', path2url(resource_path('logo_small.png'))),)\n\n    assert h1['font_weight'] == 700\n    assert h1['font_size'] == 40  # 2em\n\n    # x-large * initial = 3/2 * 16 = 24\n    assert p['margin_top'] == (24, 'px')\n    assert p['margin_right'] == (0, 'px')\n    assert p['margin_bottom'] == (24, 'px')\n    assert p['margin_left'] == (0, 'px')\n    assert p['background_color'] == 'currentcolor'\n\n    # 2em * 1.25ex = 2 * 20 * 1.25 * 0.8 = 40\n    # 2.5ex * 1.25ex = 2.5 * 0.8 * 20 * 1.25 * 0.8 = 40\n    # TODO: ex unit doesn't work with @font-face fonts, see computed_values.py\n    # assert ul['margin_top'] == (40, 'px')\n    # assert ul['margin_right'] == (40, 'px')\n    # assert ul['margin_bottom'] == (40, 'px')\n    # assert ul['margin_left'] == (40, 'px')\n\n    assert ul['font_weight'] == 400\n    # thick = 5px, 0.25 inches = 96*.25 = 24px\n    assert ul['border_top_width'] == 0\n    assert ul['border_right_width'] == 5\n    assert ul['border_bottom_width'] == 0\n    assert ul['border_left_width'] == 24\n\n    assert li_0['font_weight'] == 700\n    assert li_0['font_size'] == 8  # 6pt\n    assert li_0['margin_top'] == (16, 'px')  # 2em\n    assert li_0['margin_right'] == (0, 'px')\n    assert li_0['margin_bottom'] == (16, 'px')\n    assert li_0['margin_left'] == (32, 'px')  # 4em\n\n    assert a['text_decoration_line'] == {'underline'}\n    assert a['font_weight'] == 900\n    assert a['font_size'] == 24  # 300% of 8px\n    assert a['padding_top'] == (1, 'px')\n    assert a['padding_right'] == (2, 'px')\n    assert a['padding_bottom'] == (3, 'px')\n    assert a['padding_left'] == (4, 'px')\n    assert a['border_top_width'] == 42\n    assert a['border_bottom_width'] == 42\n\n    assert a['color'] == (1, 0, 0, 1)\n    assert a['border_top_color'] == 'currentcolor'\n\n    assert div['font_size'] == 40  # 2 * 20px\n    assert span1['width'] == (160, 'px')  # 10 * 16px (root default is 16px)\n    assert span1['height'] == (400, 'px')  # 10 * (2 * 20px)\n    assert span2['font_size'] == 32\n\n    # The href attr should be as in the source, not made absolute.\n    assert after['content'] == (\n        ('string', ' ['), ('string', 'home.html'), ('string', ']'))\n    assert after['background_color'] == (1, 0, 0, 1)\n    assert after['border_top_width'] == 42\n    assert after['border_bottom_width'] == 3\n\n    # TODO: much more tests here: test that origin and selector precedence\n    # and inheritance are correct…\n\n\n@assert_no_logs\ndef test_important():\n    document = FakeHTML(string='''\n      <style>\n        p:nth-child(1) { color: lime }\n        body p:nth-child(2) { color: red }\n\n        p:nth-child(3) { color: lime !important }\n        body p:nth-child(3) { color: red }\n\n        body p:nth-child(5) { color: lime }\n        p:nth-child(5) { color: red }\n\n        p:nth-child(6) { color: red }\n        p:nth-child(6) { color: lime }\n      </style>\n      <p></p>\n      <p></p>\n      <p></p>\n      <p></p>\n      <p></p>\n      <p></p>\n    ''')\n    page, = document.render(stylesheets=[CSS(string='''\n      body p:nth-child(1) { color: red }\n      p:nth-child(2) { color: lime !important }\n\n      p:nth-child(4) { color: lime !important }\n      body p:nth-child(4) { color: red }\n    ''')]).pages\n    html, = page._page_box.children\n    body, = html.children\n    for paragraph in body.children:\n        assert paragraph.style['color'] == (0, 1, 0, 1)  # lime (light green)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('value', 'width'), [\n    # Absolute units.\n    ('96px', 96),\n    ('1in', 96),\n    ('72pt', 96),\n    ('6pc', 96),\n    ('2.54cm', 96),\n    ('25.4mm', 96),\n    ('101.6q', 96),\n    # Font-relative units.\n    ('1.1em', 11),\n    ('1.1rem', 17.6),\n    ('1.1ch', 11),\n    ('1.1rch', 17.6),\n    ('1.1cap', 11),\n    ('1.1rcap', 17.6),\n    ('1.5ex', 12),\n    ('2rex', 25.6),\n    ('1ic', 20),\n    ('1ric', 32),\n    ('1.1lh', 13.2),\n    ('1.1rlh', 26.4),\n    # Viewport-relative units.\n    ('50vh', 50),\n    ('50vw', 100),\n    ('25vb', 25),\n    ('1.25vi', 2.5),\n    ('10vmin', 10),\n    ('20vmax', 40),\n    ('50lvh', 50),\n    ('50lvw', 100),\n    ('25lvb', 25),\n    ('1.25lvi', 2.5),\n    ('10lvmin', 10),\n    ('20lvmax', 40),\n    ('50svh', 50),\n    ('50svw', 100),\n    ('25svb', 25),\n    ('1.25svi', 2.5),\n    ('10svmin', 10),\n    ('20svmax', 40),\n    ('50dvh', 50),\n    ('50dvw', 100),\n    ('25dvb', 25),\n    ('1.25dvi', 2.5),\n    ('10dvmin', 10),\n    ('20dvmax', 40),\n    ('50pvh', 100),\n    ('50pvw', 150),\n    ('25pvb', 50),\n    ('1.5pvi', 4.5),\n    ('10pvmin', 20),\n    ('20pvmax', 60),\n])\ndef test_units(value, width):\n    document = FakeHTML(base_url=BASE_URL, string='''\n      <html style=\"font: 16px / 1.5 weasyprint\">\n      <style>@page { size: 300px 200px; margin: 50px }</style>\n      <body style=\"font: 10px / 1.2 weasyprint\">\n      <p style=\"margin-left: %s\"></p>\n      <p style=\"margin-left: %s\"></p>\n      ''' % (value, value.upper()))\n    page, = document.render().pages\n    html, = page._page_box.children\n    body, = html.children\n    p1, p2 = body.children\n    assert p1.margin_left == p2.margin_left\n    assert isclose(p1.margin_left, width, rel_tol=0.01)\n\n\n@assert_no_logs\n@pytest.mark.parametrize('property', ['line-height', 'font-size'])\ndef test_recursive_lh(property):\n    document = FakeHTML(base_url=BASE_URL, string='''\n      <html style=\"%s: 1lh\">\n      <body style=\"%s: 1rlh\">a''' % (property, property))\n    document.render().pages\n    document = FakeHTML(base_url=BASE_URL, string='''\n      <html style=\"%s: 1rlh\">\n      <body style=\"%s: 1lh\">a''' % (property, property))\n    document.render().pages\n\n\n@pytest.mark.parametrize(('media', 'width', 'warning'), [\n    ('@media screen { @page { size: 10px } }', 20, False),\n    ('@media print { @page { size: 10px } }', 10, False),\n    ('@media (\"unknown content\") { @page { size: 10px } }', 20, True),\n])\ndef test_media_queries(media, width, warning):\n    document = FakeHTML(string='<p>a<span>b')\n    with capture_logs() as logs:\n        page, = document.render(\n            stylesheets=[CSS(string='@page{size:20px}%s' % media)]).pages\n    html, = page._page_box.children\n    assert html.width == width\n    assert (logs if warning else not logs)\n"
  },
  {
    "path": "tests/css/test_counters.py",
    "content": "\"\"\"Test CSS counters.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import (  # isort:skip\n    FakeHTML, assert_no_logs, assert_tree, parse_all, render_pages)\n\nRENDER = FakeHTML(string='')._ua_counter_style()[0].render_value\n\n\n@assert_no_logs\ndef test_counters_1():\n    assert_tree(parse_all('''\n      <style>\n        p { counter-increment: p 2 }\n        p:before { content: counter(p); }\n        p:nth-child(1) { counter-increment: none; }\n        p:nth-child(2) { counter-increment: p; }\n      </style>\n      <p></p>\n      <p></p>\n      <p></p>\n      <p style=\"counter-reset: p 117 p\"></p>\n      <p></p>\n      <p></p>\n      <p style=\"counter-reset: p -13\"></p>\n      <p></p>\n      <p></p>\n      <p style=\"counter-reset: p 42\"></p>\n      <p></p>\n      <p></p>'''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p::before', 'Inline', [\n                    ('p::before', 'Text', counter)])])])\n        for counter in '0 1 3  2 4 6  -11 -9 -7  44 46 48'.split()])\n\n\n@assert_no_logs\ndef test_counters_2():\n    assert_tree(parse_all('''\n      <ol style=\"list-style-position: inside\">\n        <li></li>\n        <li></li>\n        <li></li>\n        <li><ol>\n          <li></li>\n          <li style=\"counter-increment: none\"></li>\n          <li></li>\n        </ol></li>\n        <li></li>\n      </ol>'''), [\n        ('ol', 'Block', [\n            ('li', 'Block', [\n                ('li', 'Line', [\n                    ('li::marker', 'Inline', [\n                        ('li::marker', 'Text', '1. ')])])]),\n            ('li', 'Block', [\n                ('li', 'Line', [\n                    ('li::marker', 'Inline', [\n                        ('li::marker', 'Text', '2. ')])])]),\n            ('li', 'Block', [\n                ('li', 'Line', [\n                    ('li::marker', 'Inline', [\n                        ('li::marker', 'Text', '3. ')])])]),\n            ('li', 'Block', [\n                ('li', 'Block', [\n                    ('li', 'Line', [\n                        ('li::marker', 'Inline', [\n                            ('li::marker', 'Text', '4. ')])])]),\n                ('ol', 'Block', [\n                    ('li', 'Block', [\n                        ('li', 'Line', [\n                            ('li::marker', 'Inline', [\n                                ('li::marker', 'Text', '1. ')])])]),\n                    ('li', 'Block', [\n                        ('li', 'Line', [\n                            ('li::marker', 'Inline', [\n                                ('li::marker', 'Text', '1. ')])])]),\n                    ('li', 'Block', [\n                        ('li', 'Line', [\n                            ('li::marker', 'Inline', [\n                                ('li::marker', 'Text', '2. ')])])])])]),\n            ('li', 'Block', [\n                ('li', 'Line', [\n                    ('li::marker', 'Inline', [\n                        ('li::marker', 'Text', '5. ')])])])])])\n\n\n@assert_no_logs\ndef test_counters_3():\n    assert_tree(parse_all('''\n      <style>\n        p { display: list-item; list-style: inside decimal }\n      </style>\n      <div>\n        <p></p>\n        <p></p>\n        <p style=\"counter-reset: list-item 7 list-item -56\"></p>\n      </div>\n      <p></p>'''), [\n        ('div', 'Block', [\n            ('p', 'Block', [\n                ('p', 'Line', [\n                    ('p::marker', 'Inline', [\n                        ('p::marker', 'Text', '1. ')])])]),\n            ('p', 'Block', [\n                ('p', 'Line', [\n                    ('p::marker', 'Inline', [\n                        ('p::marker', 'Text', '2. ')])])]),\n            ('p', 'Block', [\n                ('p', 'Line', [\n                    ('p::marker', 'Inline', [\n                        ('p::marker', 'Text', '-55. ')])])])]),\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p::marker', 'Inline', [\n                    ('p::marker', 'Text', '1. ')])])])])\n\n\n@assert_no_logs\ndef test_counters_4():\n    assert_tree(parse_all('''\n      <style>\n        section:before { counter-reset: h; content: '' }\n        h1:before { counter-increment: h; content: counters(h, '.') }\n      </style>\n      <body>\n        <section><h1></h1>\n          <h1></h1>\n          <section><h1></h1>\n            <h1></h1>\n          </section>\n          <h1></h1>\n        </section>\n      </body>'''), [\n        ('section', 'Block', [\n            ('section', 'Block', [\n                ('section', 'Line', [\n                    ('section::before', 'Inline', [])])]),\n            ('h1', 'Block', [\n                ('h1', 'Line', [\n                    ('h1::before', 'Inline', [\n                        ('h1::before', 'Text', '1')])])]),\n            ('h1', 'Block', [\n                ('h1', 'Line', [\n                    ('h1::before', 'Inline', [\n                        ('h1::before', 'Text', '2')])])]),\n            ('section', 'Block', [\n                ('section', 'Block', [\n                    ('section', 'Line', [\n                        ('section::before', 'Inline', [])])]),\n                ('h1', 'Block', [\n                    ('h1', 'Line', [\n                        ('h1::before', 'Inline', [\n                            ('h1::before', 'Text', '2.1')])])]),\n                ('h1', 'Block', [\n                    ('h1', 'Line', [\n                        ('h1::before', 'Inline', [\n                            ('h1::before', 'Text', '2.2')])])])]),\n            ('h1', 'Block', [\n                ('h1', 'Line', [\n                    ('h1::before', 'Inline', [\n                        ('h1::before', 'Text', '3')])])])])])\n\n\n@assert_no_logs\ndef test_counters_5():\n    assert_tree(parse_all('''\n      <style>\n        p:before { content: counter(c) }\n      </style>\n      <div>\n        <span style=\"counter-reset: c\">\n          Scope created now, deleted after the div\n        </span>\n      </div>\n      <p></p>'''), [\n        ('div', 'Block', [\n            ('div', 'Line', [\n                ('span', 'Inline', [\n                    ('span', 'Text',\n                     'Scope created now, deleted after the div ')])])]),\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p::before', 'Inline', [\n                    ('p::before', 'Text', '0')])])])])\n\n\n@assert_no_logs\ndef test_counters_6():\n    # counter-increment may interfere with display: list-item\n    assert_tree(parse_all('''\n      <p style=\"counter-increment: c;\n                display: list-item; list-style: inside decimal\">'''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p::marker', 'Inline', [\n                    ('p::marker', 'Text', '0. ')])])])])\n\n\n@assert_no_logs\ndef test_counters_7():\n    # Regression test for #827.\n    # Test that counters are case-sensitive.\n    assert_tree(parse_all('''\n      <style>\n        p { counter-increment: p 2 }\n        p:before { content: counter(p) '.' counter(P); }\n      </style>\n      <p></p>\n      <p style=\"counter-increment: P 3\"></p>\n      <p></p>'''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p::before', 'Inline', [\n                    ('p::before', 'Text', counter)])])])\n        for counter in '2.0 2.3 4.3'.split()])\n\n\n@assert_no_logs\ndef test_counters_8():\n    assert_tree(parse_all('''\n      <style>\n        p:before { content: 'a'; display: list-item }\n      </style>\n      <p></p>\n      <p></p>'''), 2 * [\n        ('p', 'Block', [\n          ('p::before', 'Block', [\n            ('p::marker', 'Block', [\n              ('p::marker', 'Line', [\n                ('p::marker', 'Text', '• ')])]),\n            ('p::before', 'Block', [\n                ('p::before', 'Line', [\n                    ('p::before', 'Text', 'a')])])])])])\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_counters_9():\n    page, = render_pages('''\n      <ol>\n        <li></li>\n        <li>\n          <ol style=\"counter-reset: a\">\n            <li></li>\n            <li></li>\n          </ol>\n        </li>\n        <li></li>\n      </ol>\n    ''')\n    html, = page._page_box.children\n    body, = html.children\n    ol1, = body.children\n    oli1, oli2, oli3 = ol1.children\n    marker, ol2 = oli2.children\n    oli21, oli22 = ol2.children\n    assert oli1.children[0].children[0].children[0].text == '1. '\n    assert oli2.children[0].children[0].children[0].text == '2. '\n    assert oli21.children[0].children[0].children[0].text == '1. '\n    assert oli22.children[0].children[0].children[0].text == '2. '\n    assert oli3.children[0].children[0].children[0].text == '3. '\n\n\n@assert_no_logs\ndef test_counter_styles_1():\n    assert_tree(parse_all('''\n      <style>\n        body { --var: 'Counter'; counter-reset: p -12 }\n        p { counter-increment: p }\n        p:nth-child(1):before { content: '-' counter(p, none) '-'; }\n        p:nth-child(2):before { content: counter(p, disc); }\n        p:nth-child(3):before { content: counter(p, circle); }\n        p:nth-child(4):before { content: counter(p, square); }\n        p:nth-child(5):before { content: counter(p); }\n        p:nth-child(6):before { content: var(--var) ':' counter(p); }\n        p:nth-child(7):before { content: counter(p) ':' var(--var); }\n      </style>\n      <p></p>\n      <p></p>\n      <p></p>\n      <p></p>\n      <p></p>\n      <p></p>\n      <p></p>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p::before', 'Inline', [\n                    ('p::before', 'Text', counter)])])])\n        for counter in '--  •  ◦  ▪  -7 Counter:-6 -5:Counter'.split()])\n\n\n@assert_no_logs\ndef test_counter_styles_2():\n    assert_tree(parse_all('''\n      <style>\n        p { counter-increment: p }\n        p::before { content: counter(p, decimal-leading-zero); }\n      </style>\n      <p style=\"counter-reset: p -1987\"></p>\n      <p></p>\n      <p style=\"counter-reset: p -12\"></p>\n      <p></p>\n      <p></p>\n      <p></p>\n      <p style=\"counter-reset: p -2\"></p>\n      <p></p>\n      <p></p>\n      <p></p>\n      <p style=\"counter-reset: p 8\"></p>\n      <p></p>\n      <p></p>\n      <p style=\"counter-reset: p 98\"></p>\n      <p></p>\n      <p></p>\n      <p style=\"counter-reset: p 4134\"></p>\n      <p></p>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p::before', 'Inline', [\n                    ('p::before', 'Text', counter)])])])\n        for counter in '''-1986 -1985  -11 -10 -9 -8  -1 00 01 02  09 10 11\n                            99 100 101  4135 4136'''.split()])\n\n\n@assert_no_logs\ndef test_counter_styles_3():\n    assert [RENDER(value, 'decimal-leading-zero') for value in [\n        -1986, -1985,\n        -11, -10, -9, -8,\n        -1, 0, 1, 2,\n        9, 10, 11,\n        99, 100, 101,\n        4135, 4136\n    ]] == '''\n        -1986 -1985  -11 -10 -9 -8  -1 00 01 02  09 10 11\n        99 100 101  4135 4136\n    '''.split()\n\n\n@assert_no_logs\ndef test_counter_styles_4():\n    assert [RENDER(value, 'lower-roman') for value in [\n        -1986, -1985,\n        -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,\n        49, 50,\n        389, 390,\n        3489, 3490, 3491,\n        4999, 5000, 5001\n     ]] == '''\n        -1986 -1985  -1 0 i ii iii iv v vi vii viii ix x xi xii\n        xlix l  ccclxxxix cccxc  mmmcdlxxxix mmmcdxc mmmcdxci\n        4999 5000 5001\n    '''.split()\n\n\n@assert_no_logs\ndef test_counter_styles_5():\n    assert [RENDER(value, 'upper-roman') for value in [\n         -1986, -1985,\n         -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,\n         49, 50,\n         389, 390,\n         3489, 3490, 3491,\n         4999, 5000, 5001\n    ]] == '''\n        -1986 -1985  -1 0 I II III IV V VI VII VIII IX X XI XII\n        XLIX L  CCCLXXXIX CCCXC  MMMCDLXXXIX MMMCDXC MMMCDXCI\n        4999 5000 5001\n    '''.split()\n\n\n@assert_no_logs\ndef test_counter_styles_6():\n    assert [RENDER(value, 'lower-alpha') for value in [\n        -1986, -1985,\n        -1, 0, 1, 2, 3, 4,\n        25, 26, 27, 28, 29,\n        2002, 2003\n    ]] == '''\n        -1986 -1985  -1 0 a b c d  y z aa ab ac bxz bya\n    '''.split()\n\n\n@assert_no_logs\ndef test_counter_styles_7():\n    assert [RENDER(value, 'upper-alpha') for value in [\n        -1986, -1985,\n        -1, 0, 1, 2, 3, 4,\n        25, 26, 27, 28, 29,\n        2002, 2003\n    ]] == '''\n        -1986 -1985  -1 0 A B C D  Y Z AA AB AC BXZ BYA\n    '''.split()\n\n\n@assert_no_logs\ndef test_counter_styles_8():\n    assert [RENDER(value, 'lower-latin') for value in [\n        -1986, -1985,\n        -1, 0, 1, 2, 3, 4,\n        25, 26, 27, 28, 29,\n        2002, 2003\n    ]] == '''\n        -1986 -1985  -1 0 a b c d  y z aa ab ac bxz bya\n    '''.split()\n\n\n@assert_no_logs\ndef test_counter_styles_9():\n    assert [RENDER(value, 'upper-latin') for value in [\n        -1986, -1985,\n        -1, 0, 1, 2, 3, 4,\n        25, 26, 27, 28, 29,\n        2002, 2003\n    ]] == '''\n        -1986 -1985  -1 0 A B C D  Y Z AA AB AC BXZ BYA\n    '''.split()\n\n\n@assert_no_logs\ndef test_counter_styles_10():\n    assert [RENDER(value, 'georgian') for value in [\n        -1986, -1985,\n        -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,\n        20, 30, 40, 50, 60, 70, 80, 90, 100,\n        200, 300, 400, 500, 600, 700, 800, 900, 1000,\n        2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000,\n        19999, 20000, 20001\n    ]] == '''\n        -1986 -1985  -1 0 ა\n        ბ გ დ ე ვ ზ ჱ თ ი ია იბ\n        კ ლ მ ნ ჲ ო პ ჟ რ\n        ს ტ ჳ ფ ქ ღ ყ შ ჩ\n        ც ძ წ ჭ ხ ჴ ჯ ჰ ჵ\n        ჵჰშჟთ 20000 20001\n    '''.split()\n\n\n@assert_no_logs\ndef test_counter_styles_11():\n    assert [RENDER(value, 'armenian') for value in [\n        -1986, -1985,\n        -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,\n        20, 30, 40, 50, 60, 70, 80, 90, 100,\n        200, 300, 400, 500, 600, 700, 800, 900, 1000,\n        2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000,\n        9999, 10000, 10001\n    ]] == '''\n        -1986 -1985  -1 0 Ա\n        Բ Գ Դ Ե Զ Է Ը Թ Ժ ԺԱ ԺԲ\n        Ի Լ Խ Ծ Կ Հ Ձ Ղ Ճ\n        Մ Յ Ն Շ Ո Չ Պ Ջ Ռ\n        Ս Վ Տ Ր Ց Ւ Փ Ք\n        ՔՋՂԹ 10000 10001\n    '''.split()\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('arguments', 'values'), [\n    ('cyclic \"a\" \"b\" \"c\"', ('a ', 'b ', 'c ', 'a ')),\n    ('symbolic \"a\" \"b\"', ('a ', 'b ', 'aa ', 'bb ')),\n    ('\"a\" \"b\"', ('a ', 'b ', 'aa ', 'bb ')),\n    ('alphabetic \"a\" \"b\"', ('a ', 'b ', 'aa ', 'ab ')),\n    ('fixed \"a\" \"b\"', ('a ', 'b ', '3 ', '4 ')),\n    ('numeric \"0\" \"1\" \"2\"', ('1 ', '2 ', '10 ', '11 ')),\n])\ndef test_counter_symbols(arguments, values):\n    page, = render_pages('''\n      <style>\n        ol { list-style-type: symbols(%s) }\n      </style>\n      <ol>\n        <li>abc</li>\n        <li>abc</li>\n        <li>abc</li>\n        <li>abc</li>\n      </ol>\n    ''' % arguments)\n    html, = page.children\n    body, = html.children\n    ol, = body.children\n    li_1, li_2, li_3, li_4 = ol.children\n    assert li_1.children[0].children[0].children[0].text == values[0]\n    assert li_2.children[0].children[0].children[0].text == values[1]\n    assert li_3.children[0].children[0].children[0].text == values[2]\n    assert li_4.children[0].children[0].children[0].text == values[3]\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('style_type', 'values'), [\n    ('decimal', ('1. ', '2. ', '3. ', '4. ')),\n    ('\"/\"', ('/', '/', '/', '/')),\n])\ndef test_list_style_types(style_type, values):\n    page, = render_pages('''\n      <style>\n        ol { list-style-type: %s }\n      </style>\n      <ol>\n        <li>abc</li>\n        <li>abc</li>\n        <li>abc</li>\n        <li>abc</li>\n      </ol>\n    ''' % style_type)\n    html, = page.children\n    body, = html.children\n    ol, = body.children\n    li_1, li_2, li_3, li_4 = ol.children\n    assert li_1.children[0].children[0].children[0].text == values[0]\n    assert li_2.children[0].children[0].children[0].text == values[1]\n    assert li_3.children[0].children[0].children[0].text == values[2]\n    assert li_4.children[0].children[0].children[0].text == values[3]\n\n\ndef test_list_style_type_empty_string():\n    # Regression test for #1883.\n    render_pages('<ul><li style=\"list-style-type: \\'\\'\">')\n\n\ndef test_counter_set():\n    page, = render_pages('''\n      <style>\n        body { counter-reset: h2 0 h3 4; font-size: 1px }\n        article { counter-reset: h2 2 }\n        h1 { counter-increment: h1 }\n        h1::before { content: counter(h1) }\n        h2 { counter-increment: h2; counter-set: h3 3 }\n        h2::before { content: counter(h2) }\n        h3 { counter-increment: h3 }\n        h3::before { content: counter(h3) }\n      </style>\n      <article>\n        <h1></h1>\n      </article>\n      <article>\n        <h2></h2>\n        <h3></h3>\n      </article>\n      <article>\n        <h3></h3>\n      </article>\n      <article>\n        <h2></h2>\n      </article>\n      <article>\n        <h3></h3>\n        <h3></h3>\n      </article>\n      <article>\n        <h1></h1>\n        <h2></h2>\n        <h3></h3>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    art_1, art_2, art_3, art_4, art_5, art_6 = body.children\n\n    h1, = art_1.children\n    assert h1.children[0].children[0].children[0].text == '1'\n\n    h2, h3, = art_2.children\n    assert h2.children[0].children[0].children[0].text == '3'\n    assert h3.children[0].children[0].children[0].text == '4'\n\n    h3, = art_3.children\n    assert h3.children[0].children[0].children[0].text == '5'\n\n    h2, = art_4.children\n    assert h2.children[0].children[0].children[0].text == '3'\n\n    h3_1, h3_2 = art_5.children\n    assert h3_1.children[0].children[0].children[0].text == '4'\n    assert h3_2.children[0].children[0].children[0].text == '5'\n\n    h1, h2, h3 = art_6.children\n    assert h1.children[0].children[0].children[0].text == '1'\n    assert h2.children[0].children[0].children[0].text == '3'\n    assert h3.children[0].children[0].children[0].text == '4'\n\n\ndef test_counter_multiple_extends():\n    # Inspired by W3C failing test system-extends-invalid\n    page, = render_pages('''\n      <style>\n        @counter-style a {\n          system: extends b;\n          prefix: a;\n        }\n        @counter-style b {\n          system: extends c;\n          suffix: b;\n        }\n        @counter-style c {\n          system: extends b;\n          pad: 2 c;\n        }\n        @counter-style d {\n          system: extends d;\n          prefix: d;\n        }\n        @counter-style e {\n          system: extends unknown;\n          prefix: e;\n        }\n        @counter-style f {\n          system: extends decimal;\n          symbols: a;\n        }\n        @counter-style g {\n          system: extends decimal;\n          additive-symbols: 1 a;\n        }\n      </style>\n      <ol>\n        <li style=\"list-style-type: a\"></li>\n        <li style=\"list-style-type: b\"></li>\n        <li style=\"list-style-type: c\"></li>\n        <li style=\"list-style-type: d\"></li>\n        <li style=\"list-style-type: e\"></li>\n        <li style=\"list-style-type: f\"></li>\n        <li style=\"list-style-type: g\"></li>\n        <li style=\"list-style-type: h\"></li>\n      </ol>\n    ''')\n    html, = page.children\n    body, = html.children\n    ol, = body.children\n    li_1, li_2, li_3, li_4, li_5, li_6, li_7, li_8 = ol.children\n    assert li_1.children[0].children[0].children[0].text == 'a1b'\n    assert li_2.children[0].children[0].children[0].text == '2b'\n    assert li_3.children[0].children[0].children[0].text == 'c3. '\n    assert li_4.children[0].children[0].children[0].text == 'd4. '\n    assert li_5.children[0].children[0].children[0].text == 'e5. '\n    assert li_6.children[0].children[0].children[0].text == '6. '\n    assert li_7.children[0].children[0].children[0].text == '7. '\n    assert li_8.children[0].children[0].children[0].text == '8. '\n"
  },
  {
    "path": "tests/css/test_descriptors.py",
    "content": "\"\"\"Test CSS descriptors.\"\"\"\n\nimport pytest\nimport tinycss2\n\nfrom weasyprint.css import preprocess_stylesheet\nfrom weasyprint.css.validation.descriptors import preprocess_descriptors\n\nfrom ..testing_utils import assert_no_logs, capture_logs\n\n\n@assert_no_logs\ndef test_font_face_1():\n    stylesheet = tinycss2.parse_stylesheet(\n        '@font-face {'\n        '  font-family: Gentium Hard;'\n        '  src: url(https://example.com/fonts/Gentium.woff);'\n        '}')\n    at_rule, = stylesheet\n    assert at_rule.at_keyword == 'font-face'\n    font_family, src = list(preprocess_descriptors(\n        'font-face', 'https://weasyprint.org/foo/',\n        tinycss2.parse_blocks_contents(at_rule.content)))\n    assert font_family == ('font_family', 'Gentium Hard')\n    assert src == (\n        'src', (('external', 'https://example.com/fonts/Gentium.woff'),))\n\n\n@assert_no_logs\ndef test_font_face_2():\n    stylesheet = tinycss2.parse_stylesheet(\n        '@font-face {'\n        '  font-family: \"Fonty Smiley\";'\n        '  src: url(Fonty-Smiley.woff);'\n        '  font-style: italic;'\n        '  font-weight: 200;'\n        '  font-stretch: condensed;'\n        '}')\n    at_rule, = stylesheet\n    assert at_rule.at_keyword == 'font-face'\n    font_family, src, font_style, font_weight, font_stretch = list(\n        preprocess_descriptors(\n            'font-face', 'https://weasyprint.org/foo/',\n            tinycss2.parse_blocks_contents(at_rule.content)))\n    assert font_family == ('font_family', 'Fonty Smiley')\n    assert src == (\n        'src', (('external', 'https://weasyprint.org/foo/Fonty-Smiley.woff'),))\n    assert font_style == ('font_style', 'italic')\n    assert font_weight == ('font_weight', 200)\n    assert font_stretch == ('font_stretch', 'condensed')\n\n\n@assert_no_logs\ndef test_font_face_3():\n    stylesheet = tinycss2.parse_stylesheet(\n        '@font-face {'\n        '  font-family: Gentium Hard;'\n        '  src: local();'\n        '}')\n    at_rule, = stylesheet\n    assert at_rule.at_keyword == 'font-face'\n    font_family, src = list(preprocess_descriptors(\n        'font-face', 'https://weasyprint.org/foo/',\n        tinycss2.parse_blocks_contents(at_rule.content)))\n    assert font_family == ('font_family', 'Gentium Hard')\n    assert src == ('src', (('local', None),))\n\n\n@assert_no_logs\ndef test_font_face_4():\n    # Regression test for #487.\n    stylesheet = tinycss2.parse_stylesheet(\n        '@font-face {'\n        '  font-family: Gentium Hard;'\n        '  src: local(Gentium Hard);'\n        '}')\n    at_rule, = stylesheet\n    assert at_rule.at_keyword == 'font-face'\n    font_family, src = list(preprocess_descriptors(\n        'font-face', 'https://weasyprint.org/foo/',\n        tinycss2.parse_blocks_contents(at_rule.content)))\n    assert font_family == ('font_family', 'Gentium Hard')\n    assert src == ('src', (('local', 'Gentium Hard'),))\n\n\n@assert_no_logs\ndef test_font_face_5():\n    # Regression test for #1653.\n    stylesheet = tinycss2.parse_stylesheet(\n        '@font-face {'\n        '  font-family: Gentium Hard;'\n        '  src: local(Gentium Hard);'\n        '  src: local(Gentium Soft),'\n        '}')\n    at_rule, = stylesheet\n    assert at_rule.at_keyword == 'font-face'\n    with capture_logs() as logs:\n        font_family, src = list(preprocess_descriptors(\n            'font-face', 'https://weasyprint.org/foo/',\n            tinycss2.parse_blocks_contents(at_rule.content)))\n    assert font_family == ('font_family', 'Gentium Hard')\n    assert src == ('src', (('local', 'Gentium Hard'),))\n    assert len(logs) == 1\n    assert 'invalid value' in logs[0]\n\n\ndef test_font_face_bad_1():\n    stylesheet = tinycss2.parse_stylesheet(\n        '@font-face {'\n        '  font-family: \"Bad Font\";'\n        '  src: url(BadFont.woff);'\n        '  font-stretch: expanded;'\n        '  font-style: wrong;'\n        '  font-weight: bolder;'\n        '  font-stretch: wrong;'\n        '}')\n    at_rule, = stylesheet\n    assert at_rule.at_keyword == 'font-face'\n    with capture_logs() as logs:\n        font_family, src, font_stretch = list(\n            preprocess_descriptors(\n                'font-face', 'https://weasyprint.org/foo/',\n                tinycss2.parse_blocks_contents(at_rule.content)))\n    assert font_family == ('font_family', 'Bad Font')\n    assert src == (\n        'src', (('external', 'https://weasyprint.org/foo/BadFont.woff'),))\n    assert font_stretch == ('font_stretch', 'expanded')\n    assert logs == [\n        'WARNING: Ignored `font-style: wrong` at 1:91, invalid value.',\n        'WARNING: Ignored `font-weight: bolder` at 1:111, invalid value.',\n        'WARNING: Ignored `font-stretch: wrong` at 1:133, invalid value.']\n\n\ndef test_font_face_bad_2():\n    stylesheet = tinycss2.parse_stylesheet('@font-face{}')\n    with capture_logs() as logs:\n        preprocess_stylesheet(\n            'print', 'https://wp.org/foo/', stylesheet, None, None, None,\n            None, None, None, None)\n    assert logs == [\n        \"WARNING: Missing src descriptor in '@font-face' rule at 1:1\"]\n\n\ndef test_font_face_bad_3():\n    stylesheet = tinycss2.parse_stylesheet('@font-face{src: url(test.woff)}')\n    with capture_logs() as logs:\n        preprocess_stylesheet(\n            'print', 'https://wp.org/foo/', stylesheet, None, None, None,\n            None, None, None, None)\n    assert logs == [\n        \"WARNING: Missing font-family descriptor in '@font-face' rule at 1:1\"]\n\n\ndef test_font_face_bad_4():\n    stylesheet = tinycss2.parse_stylesheet('@font-face{font-family: test}')\n    with capture_logs() as logs:\n        preprocess_stylesheet(\n            'print', 'https://wp.org/foo/', stylesheet, None, None, None,\n            None, None, None, None)\n    assert logs == [\n        \"WARNING: Missing src descriptor in '@font-face' rule at 1:1\"]\n\n\ndef test_font_face_bad_5():\n    stylesheet = tinycss2.parse_stylesheet(\n        '@font-face { font-family: test; src: wrong }')\n    with capture_logs() as logs:\n        preprocess_stylesheet(\n            'print', 'https://wp.org/foo/', stylesheet, None, None, None,\n            None, None, None, None)\n    assert logs == [\n        'WARNING: Ignored `src: wrong ` at 1:33, invalid value.',\n        \"WARNING: Missing src descriptor in '@font-face' rule at 1:1\"]\n\n\ndef test_font_face_bad_6():\n    stylesheet = tinycss2.parse_stylesheet(\n        '@font-face { font-family: good, bad; src: url(test.woff) }')\n    with capture_logs() as logs:\n        preprocess_stylesheet(\n            'print', 'https://wp.org/foo/', stylesheet, None, None, None,\n            None, None, None, None)\n    assert logs == [\n        'WARNING: Ignored `font-family: good, bad` at 1:14, invalid value.',\n        \"WARNING: Missing font-family descriptor in '@font-face' rule at 1:1\"]\n\n\ndef test_font_face_bad_7():\n    stylesheet = tinycss2.parse_stylesheet(\n        '@font-face { font-family: good, bad; src: really bad }')\n    with capture_logs() as logs:\n        preprocess_stylesheet(\n            'print', 'https://wp.org/foo/', stylesheet, None, None, None,\n            None, None, None, None)\n    assert logs == [\n        'WARNING: Ignored `font-family: good, bad` at 1:14, invalid value.',\n        'WARNING: Ignored `src: really bad ` at 1:38, invalid value.',\n        \"WARNING: Missing src descriptor in '@font-face' rule at 1:1\"]\n\n\n@pytest.mark.parametrize('rule', [\n    '@counter-style test {system: alphabetic; symbols: a}',\n    '@counter-style test {system: cyclic}',\n    '@counter-style test {system: additive; additive-symbols: a 1}',\n    '@counter-style test {system: additive; additive-symbols: 10 x, 1 i, 5 v}',\n])\ndef test_counter_style_invalid(rule):\n    stylesheet = tinycss2.parse_stylesheet(rule)\n    with capture_logs() as logs:\n        preprocess_stylesheet(\n            'print', 'https://wp.org/foo/', stylesheet, None, None, None,\n            None, None, None, {})\n    assert len(logs) >= 1\n"
  },
  {
    "path": "tests/css/test_errors.py",
    "content": "\"\"\"Test CSS errors and warnings.\"\"\"\n\nimport pytest\n\nfrom weasyprint import CSS\n\nfrom ..testing_utils import assert_no_logs, capture_logs, render_pages\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('source', 'messages'), [\n    (':lipsum { margin: 2cm', ['WARNING: Invalid or unsupported selector']),\n    ('::lipsum { margin: 2cm', ['WARNING: Invalid or unsupported selector']),\n    ('foo { margin-color: red', ['WARNING: Ignored', 'unknown property']),\n    ('foo { margin-top: red', ['WARNING: Ignored', 'invalid value']),\n    ('@import \"relative-uri.css\"',\n     ['ERROR: Relative URI reference without a base URI']),\n    ('@import \"invalid-protocol://absolute-URL\"',\n     ['ERROR: Failed to load stylesheet at']),\n    ('test', ['WARNING: Parse error']),\n    ('@test', ['WARNING: Unknown empty rule']),\n    ('@test {}', ['WARNING: Unknown rule']),\n])\ndef test_warnings(source, messages):\n    with capture_logs() as logs:\n        CSS(string=source)\n    assert len(logs) == 1, source\n    for message in messages:\n        assert message in logs[0]\n\n\n@assert_no_logs\ndef test_warnings_stylesheet():\n    with capture_logs() as logs:\n        render_pages('<link rel=stylesheet href=invalid-protocol://absolute>')\n    assert len(logs) == 1\n    assert 'ERROR: Failed to load stylesheet at' in logs[0]\n\n\n@assert_no_logs\n@pytest.mark.parametrize('style', [\n    '<style> html { color red; color: blue; color',\n    '<html style=\"color; color: blue; color red\">',\n])\ndef test_error_recovery(style):\n    with capture_logs() as logs:\n        page, = render_pages(style)\n        html, = page.children\n        assert html.style['color'] == (0, 0, 1, 1)  # blue\n    assert len(logs) == 2\n"
  },
  {
    "path": "tests/css/test_expanders.py",
    "content": "\"\"\"Test expanders for shorthand properties.\"\"\"\n\nimport pytest\nimport tinycss2\nfrom tinycss2.color5 import parse_color\n\nfrom weasyprint.css import preprocess_declarations\nfrom weasyprint.css.properties import INITIAL_VALUES, ZERO_PIXELS\nfrom weasyprint.css.validation.expanders import EXPANDERS\n\nfrom ..testing_utils import assert_no_logs, capture_logs\n\n\ndef expand_to_dict(css, expected_error=None):\n    \"\"\"Helper to test shorthand properties expander functions.\"\"\"\n    declarations = tinycss2.parse_blocks_contents(css)\n\n    with capture_logs() as logs:\n        base_url = 'https://weasyprint.org/foo/'\n        declarations = list(preprocess_declarations(base_url, declarations))\n\n    if expected_error:\n        assert len(logs) == 1\n        assert expected_error in logs[0]\n    else:\n        assert not logs\n\n    return {\n        name: parse_color(value)\n        if name.endswith('_color') and value != 'inherit' else value\n        for name, value, _ in declarations if value != 'initial'}\n\n\ndef assert_invalid(css, message='invalid'):\n    assert expand_to_dict(css, message) == {}\n\n\n@assert_no_logs\n@pytest.mark.parametrize('expander', EXPANDERS)\ndef test_empty_expander_value(expander):\n    assert_invalid(f'{expander}:', message='Ignored')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('none', {'text_decoration_line': 'none'}),\n    ('overline', {'text_decoration_line': {'overline'}}),\n    ('overline blink line-through', {\n        'text_decoration_line': {'blink', 'line-through', 'overline'},\n    }),\n    ('red', {'text_decoration_color': parse_color('red')}),\n    ('blue 1px', {\n        'text_decoration_color': parse_color('blue'),\n        'text_decoration_thickness': (1, 'px'),\n    }),\n    ('100% none', {\n        'text_decoration_line': 'none',\n        'text_decoration_thickness': (100, '%'),\n    }),\n    ('inherit', {\n        f'text_decoration_{key}': 'inherit'\n        for key in ('color', 'line', 'style', 'thickness')}),\n])\ndef test_text_decoration(rule, result):\n    assert expand_to_dict(f'text-decoration: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'solid solid',\n    'red red',\n    'underline none',\n    '1px 100%',\n    'none none',\n])\ndef test_text_decoration_invalid(rule):\n    assert_invalid(f'text-decoration: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('margin: inherit', {\n        'margin_top': 'inherit',\n        'margin_right': 'inherit',\n        'margin_bottom': 'inherit',\n        'margin_left': 'inherit',\n    }),\n    ('margin: 1em', {\n        'margin_top': (1, 'em'),\n        'margin_right': (1, 'em'),\n        'margin_bottom': (1, 'em'),\n        'margin_left': (1, 'em'),\n    }),\n    ('margin: -1em auto 20%', {\n        'margin_top': (-1, 'em'),\n        'margin_right': 'auto',\n        'margin_bottom': (20, '%'),\n        'margin_left': 'auto',\n    }),\n    ('padding: 1em 0', {\n        'padding_top': (1, 'em'),\n        'padding_right': (0, None),\n        'padding_bottom': (1, 'em'),\n        'padding_left': (0, None),\n    }),\n    ('padding: 1em 0 2%', {\n        'padding_top': (1, 'em'),\n        'padding_right': (0, None),\n        'padding_bottom': (2, '%'),\n        'padding_left': (0, None),\n    }),\n    ('padding: 1em 0 2em 5px', {\n        'padding_top': (1, 'em'),\n        'padding_right': (0, None),\n        'padding_bottom': (2, 'em'),\n        'padding_left': (5, 'px'),\n    }),\n])\ndef test_four_sides(rule, result):\n    assert expand_to_dict(rule) == result\n\n\n@assert_no_logs\ndef test_four_sides_warning():\n    assert expand_to_dict('padding: 1 2 3 4 5', 'Expected 1 to 4 tokens, got 5') == {}\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'margin: rgb(0, 0, 0)',\n    'padding: auto',\n    'padding: -12px',\n    'border-width: -3em',\n    'border-width: 12%',\n])\ndef test_four_sides_invalid(rule):\n    assert_invalid(rule)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('border-top: 3px dotted red', {\n        'border_top_width': (3, 'px'),\n        'border_top_style': 'dotted',\n        'border_top_color': (1, 0, 0, 1),  # red\n    }),\n    ('border-top: 3px dotted', {\n        'border_top_width': (3, 'px'),\n        'border_top_style': 'dotted',\n    }),\n    ('border-top: 3px red', {\n        'border_top_width': (3, 'px'),\n        'border_top_color': (1, 0, 0, 1),  # red\n    }),\n    ('border-top: solid', {'border_top_style': 'solid'}),\n    ('border: 6px dashed lime', {\n        'border_top_width': (6, 'px'),\n        'border_top_style': 'dashed',\n        'border_top_color': (0, 1, 0, 1),  # lime\n\n        'border_left_width': (6, 'px'),\n        'border_left_style': 'dashed',\n        'border_left_color': (0, 1, 0, 1),  # lime\n\n        'border_bottom_width': (6, 'px'),\n        'border_bottom_style': 'dashed',\n        'border_bottom_color': (0, 1, 0, 1),  # lime\n\n        'border_right_width': (6, 'px'),\n        'border_right_style': 'dashed',\n        'border_right_color': (0, 1, 0, 1),  # lime\n    }),\n])\ndef test_borders(rule, result):\n    assert expand_to_dict(rule) == result\n\n\n@assert_no_logs\ndef test_borders_invalid():\n    assert_invalid('border: 6px dashed left')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('list-style: inherit', {\n        'list_style_position': 'inherit',\n        'list_style_image': 'inherit',\n        'list_style_type': 'inherit',\n    }),\n    ('list-style: url(../bar/lipsum.png)', {\n        'list_style_image': ('url', 'https://weasyprint.org/bar/lipsum.png'),\n    }),\n    ('list-style: square', {\n        'list_style_type': 'square',\n    }),\n    ('list-style: circle inside', {\n        'list_style_position': 'inside',\n        'list_style_type': 'circle',\n    }),\n    ('list-style: none circle inside', {\n        'list_style_position': 'inside',\n        'list_style_image': ('none', None),\n        'list_style_type': 'circle',\n    }),\n    ('list-style: none inside none', {\n        'list_style_position': 'inside',\n        'list_style_image': ('none', None),\n        'list_style_type': 'none',\n    }),\n    ('list-style: inside special none', {\n        'list_style_position': 'inside',\n        'list_style_image': ('none', None),\n        'list_style_type': 'special',\n    }),\n])\ndef test_list_style(rule, result):\n    assert expand_to_dict(rule) == result\n\n\n@assert_no_logs\ndef test_list_style_warning():\n    assert_invalid(\n        'list-style: circle disc',\n        'got multiple type values in a list-style shorthand')\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'list-style: none inside none none',\n    'list-style: 1px',\n])\ndef test_list_style_invalid(rule):\n    assert_invalid(rule)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('red', {'background_color': (1, 0, 0, 1)}),\n    ('url(lipsum.png)', {\n        'background_image': [\n            ('url', 'https://weasyprint.org/foo/lipsum.png')]}),\n    ('no-repeat', {\n        'background_repeat': [('no-repeat', 'no-repeat')]}),\n    ('fixed', {\n        'background_attachment': ['fixed']}),\n    ('repeat no-repeat fixed', {\n        'background_repeat': [('repeat', 'no-repeat')],\n        'background_attachment': ['fixed']}),\n    ('inherit', {\n        'background_repeat': 'inherit',\n        'background_attachment': 'inherit',\n        'background_image': 'inherit',\n        'background_position': 'inherit',\n        'background_size': 'inherit',\n        'background_clip': 'inherit',\n        'background_origin': 'inherit',\n        'background_color': 'inherit'}),\n    ('top', {\n        'background_position': [('left', (50, '%'), 'top', (0, '%'))]}),\n    ('top right', {\n        'background_position': [('left', (100, '%'), 'top', (0, '%'))]}),\n    ('top right 20px', {\n        'background_position': [('right', (20, 'px'), 'top', (0, '%'))]}),\n    ('top 1% right 20px', {\n        'background_position': [('right', (20, 'px'), 'top', (1, '%'))]}),\n    ('top no-repeat', {\n        'background_repeat': [('no-repeat', 'no-repeat')],\n        'background_position': [('left', (50, '%'), 'top', (0, '%'))]}),\n    ('top right no-repeat', {\n        'background_repeat': [('no-repeat', 'no-repeat')],\n        'background_position': [('left', (100, '%'), 'top', (0, '%'))]}),\n    ('top right 20px no-repeat', {\n        'background_repeat': [('no-repeat', 'no-repeat')],\n        'background_position': [('right', (20, 'px'), 'top', (0, '%'))]}),\n    ('top 1% right 20px no-repeat', {\n        'background_repeat': [('no-repeat', 'no-repeat')],\n        'background_position': [('right', (20, 'px'), 'top', (1, '%'))]}),\n    ('url(bar) #f00 repeat-y center left fixed', {\n        'background_color': (1, 0, 0, 1),\n        'background_image': [('url', 'https://weasyprint.org/foo/bar')],\n        'background_repeat': [('no-repeat', 'repeat')],\n        'background_attachment': ['fixed'],\n        'background_position': [('left', (0, '%'), 'top', (50, '%'))]}),\n    ('#00f 10% 200px', {\n        'background_color': (0, 0, 1, 1),\n        'background_position': [('left', (10, '%'), 'top', (200, 'px'))]}),\n    ('right 78px fixed', {\n        'background_attachment': ['fixed'],\n        'background_position': [('left', (100, '%'), 'top', (78, 'px'))]}),\n    ('center / cover red', {\n        'background_size': ['cover'],\n        'background_position': [('left', (50, '%'), 'top', (50, '%'))],\n        'background_color': (1, 0, 0, 1)}),\n    ('center / auto red', {\n        'background_size': [('auto', 'auto')],\n        'background_position': [('left', (50, '%'), 'top', (50, '%'))],\n        'background_color': (1, 0, 0, 1)}),\n    ('center / 42px', {\n        'background_size': [((42, 'px'), 'auto')],\n        'background_position': [('left', (50, '%'), 'top', (50, '%'))]}),\n    ('center / 7% 4em', {\n        'background_size': [((7, '%'), (4, 'em'))],\n        'background_position': [('left', (50, '%'), 'top', (50, '%'))]}),\n    ('red content-box', {\n        'background_color': (1, 0, 0, 1),\n        'background_origin': ['content-box'],\n        'background_clip': ['content-box']}),\n    ('red border-box content-box', {\n        'background_color': (1, 0, 0, 1),\n        'background_origin': ['border-box'],\n        'background_clip': ['content-box']}),\n    ('border-box red', {\n        'background_color': (1, 0, 0, 1),\n        'background_origin': ['border-box']}),\n    ('url(bar) center, no-repeat', {\n        'background_color': (0, 0, 0, 0),\n        'background_image': [\n            ('url', 'https://weasyprint.org/foo/bar'), ('none', None)],\n        'background_position': [\n            ('left', (50, '%'), 'top', (50, '%')),\n            ('left', (0, '%'), 'top', (0, '%'))],\n        'background_repeat': [\n            ('repeat', 'repeat'), ('no-repeat', 'no-repeat')]}),\n])\ndef test_background(rule, result):\n    expanded = expand_to_dict(f'background: {rule}')\n    assert expanded.pop('background_color') == result.pop(\n        'background_color', parse_color(INITIAL_VALUES['background_color']))\n    nb_layers = len(expanded['background_image'])\n    for name, value in result.items():\n        assert expanded.pop(name) == value\n    for name, value in expanded.items():\n        assert tuple(value) == INITIAL_VALUES[name] * nb_layers\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'red, url(foo)',\n    '10px lipsum',\n    'content-box red content-box',\n])\ndef test_background_invalid(rule):\n    assert_invalid(f'background: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('1px', {\n        'border_top_left_radius': ((1, 'px'), (1, 'px')),\n        'border_top_right_radius': ((1, 'px'), (1, 'px')),\n        'border_bottom_right_radius': ((1, 'px'), (1, 'px')),\n        'border_bottom_left_radius': ((1, 'px'), (1, 'px')),\n    }),\n    ('1px 2em', {\n        'border_top_left_radius': ((1, 'px'), (1, 'px')),\n        'border_top_right_radius': ((2, 'em'), (2, 'em')),\n        'border_bottom_right_radius': ((1, 'px'), (1, 'px')),\n        'border_bottom_left_radius': ((2, 'em'), (2, 'em')),\n    }),\n    ('1px / 2em', {\n        'border_top_left_radius': ((1, 'px'), (2, 'em')),\n        'border_top_right_radius': ((1, 'px'), (2, 'em')),\n        'border_bottom_right_radius': ((1, 'px'), (2, 'em')),\n        'border_bottom_left_radius': ((1, 'px'), (2, 'em')),\n    }),\n    ('1px 3px / 2em 4%', {\n        'border_top_left_radius': ((1, 'px'), (2, 'em')),\n        'border_top_right_radius': ((3, 'px'), (4, '%')),\n        'border_bottom_right_radius': ((1, 'px'), (2, 'em')),\n        'border_bottom_left_radius': ((3, 'px'), (4, '%')),\n    }),\n    ('1px 2em 3%', {\n        'border_top_left_radius': ((1, 'px'), (1, 'px')),\n        'border_top_right_radius': ((2, 'em'), (2, 'em')),\n        'border_bottom_right_radius': ((3, '%'), (3, '%')),\n        'border_bottom_left_radius': ((2, 'em'), (2, 'em')),\n    }),\n    ('1px 2em 3% 4rem', {\n        'border_top_left_radius': ((1, 'px'), (1, 'px')),\n        'border_top_right_radius': ((2, 'em'), (2, 'em')),\n        'border_bottom_right_radius': ((3, '%'), (3, '%')),\n        'border_bottom_left_radius': ((4, 'rem'), (4, 'rem')),\n    }),\n    ('inherit', {\n        'border_top_left_radius': 'inherit',\n        'border_top_right_radius': 'inherit',\n        'border_bottom_right_radius': 'inherit',\n        'border_bottom_left_radius': 'inherit',\n    }),\n])\ndef test_border_radius(rule, result):\n    assert expand_to_dict(f'border-radius: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'message'), [\n    ('1px 1px 1px 1px 1px', '1 to 4 token'),\n    ('1px 1px 1px 1px 1px / 1px', '1 to 4 token'),\n    ('1px / 1px / 1px', 'only one \"/\"'),\n    ('12deg', 'invalid'),\n    ('1px 1px 1px 12deg', 'invalid'),\n    ('super', 'invalid'),\n    ('1px, 1px', 'invalid'),\n    ('1px /', 'value after \"/\"'),\n])\ndef test_border_radius_invalid(rule, message):\n    assert_invalid(f'border-radius: {rule}', message)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('url(border.png) 27', {\n        'border_image_source': ('url', 'https://weasyprint.org/foo/border.png'),\n        'border_image_slice': ((27, None),),\n    }),\n    ('url(border.png) 10 / 4 / 2 round stretch', {\n        'border_image_source': ('url', 'https://weasyprint.org/foo/border.png'),\n        'border_image_slice': ((10, None),),\n        'border_image_width': ((4, None),),\n        'border_image_outset': ((2, None),),\n        'border_image_repeat': (('round', 'stretch')),\n    }),\n    ('10 // 2', {\n        'border_image_slice': ((10, None),),\n        'border_image_outset': ((2, None),),\n    }),\n    ('5.5%', {\n        'border_image_slice': ((5.5, '%'),),\n    }),\n    ('stretch 2 url(\"border.png\")', {\n        'border_image_source': ('url', 'https://weasyprint.org/foo/border.png'),\n        'border_image_slice': ((2, None),),\n        'border_image_repeat': (('stretch',)),\n    }),\n    ('1/2 round', {\n        'border_image_slice': ((1, None),),\n        'border_image_width': ((2, None),),\n        'border_image_repeat': (('round',)),\n    }),\n    ('none', {\n        'border_image_source': ('none', None),\n    }),\n])\ndef test_border_image(rule, result):\n    assert expand_to_dict(f'border-image: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'reason'), [\n    ('url(border.png) url(border.png)', 'multiple source'),\n    ('10 10 10 10 10', 'multiple slice'),\n    ('1 / 2 / 3 / 4', 'invalid'),\n    ('/1', 'invalid'),\n    ('round round round', 'invalid'),\n    ('-1', 'invalid'),\n    ('1 repeat 2', 'multiple slice'),\n    ('1% // 1%', 'invalid'),\n    ('1 / repeat', 'invalid'),\n    ('', 'no value'),\n])\ndef test_border_image_invalid(rule, reason):\n    assert_invalid(f'border-image: {rule}', reason)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('url(border.png) 27', {\n        'mask_border_source': ('url', 'https://weasyprint.org/foo/border.png'),\n        'mask_border_slice': ((27, None),),\n    }),\n    ('url(border.png) 10 / 4 / 2 round stretch', {\n        'mask_border_source': ('url', 'https://weasyprint.org/foo/border.png'),\n        'mask_border_slice': ((10, None),),\n        'mask_border_width': ((4, None),),\n        'mask_border_outset': ((2, None),),\n        'mask_border_repeat': (('round', 'stretch')),\n    }),\n    ('10 // 2', {\n        'mask_border_slice': ((10, None),),\n        'mask_border_outset': ((2, None),),\n    }),\n    ('5.5%', {\n        'mask_border_slice': ((5.5, '%'),),\n    }),\n    ('stretch 2 url(\"border.png\")', {\n        'mask_border_source': ('url', 'https://weasyprint.org/foo/border.png'),\n        'mask_border_slice': ((2, None),),\n        'mask_border_repeat': (('stretch',)),\n    }),\n    ('1/2 round', {\n        'mask_border_slice': ((1, None),),\n        'mask_border_width': ((2, None),),\n        'mask_border_repeat': (('round',)),\n    }),\n    ('none', {\n        'mask_border_source': ('none', None),\n    }),\n    ('url(border.png) 27 alpha', {\n        'mask_border_source': ('url', 'https://weasyprint.org/foo/border.png'),\n        'mask_border_slice': ((27, None),),\n        'mask_border_mode': 'alpha',\n    }),\n    ('url(border.png) 27 luminance', {\n        'mask_border_source': ('url', 'https://weasyprint.org/foo/border.png'),\n        'mask_border_slice': ((27, None),),\n        'mask_border_mode': 'luminance',\n    }),\n])\ndef test_mask_border(rule, result):\n    assert expand_to_dict(f'mask-border: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'reason'), [\n    ('url(border.png) url(border.png)', 'multiple source'),\n    ('10 10 10 10 10', 'multiple slice'),\n    ('1 / 2 / 3 / 4', 'invalid'),\n    ('/1', 'invalid'),\n    ('round round round', 'invalid'),\n    ('-1', 'invalid'),\n    ('1 repeat 2', 'multiple slice'),\n    ('1% // 1%', 'invalid'),\n    ('1 / repeat', 'invalid'),\n    ('', 'no value'),\n    ('alpha alpha', 'multiple mode'),\n    ('alpha luminance', 'multiple mode'),\n])\ndef test_mask_border_invalid(rule, reason):\n    assert_invalid(f'mask-border: {rule}', reason)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('12px My Fancy Font, serif', {\n        'font_size': (12, 'px'),\n        'font_family': ('My Fancy Font', 'serif'),\n    }),\n    ('small/1.2 \"Some Font\", serif', {\n        'font_size': 'small',\n        'line_height': (1.2, None),\n        'font_family': ('Some Font', 'serif'),\n    }),\n    ('small-caps italic 700 large serif', {\n        'font_style': 'italic',\n        'font_variant_caps': 'small-caps',\n        'font_weight': 700,\n        'font_size': 'large',\n        'font_family': ('serif',),\n    }),\n    ('small-caps condensed normal 700 large serif', {\n        'font_stretch': 'condensed',\n        'font_variant_caps': 'small-caps',\n        'font_weight': 700,\n        'font_size': 'large',\n        'font_family': ('serif',),\n    }),\n])\ndef test_font(rule, result):\n    assert expand_to_dict(f'font: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'message'), [\n    ('menu', 'System fonts are not supported'),\n    ('12deg My Fancy Font, serif', 'invalid'),\n    ('12px', 'invalid'),\n    ('12px/foo serif', 'invalid'),\n    ('12px \"Invalid\" family', 'invalid'),\n    ('normal normal normal normal normal large serif', 'invalid'),\n    ('normal small-caps italic 700 condensed large serif', 'invalid'),\n    ('small-caps italic 700 normal condensed large serif', 'invalid'),\n    ('small-caps italic 700 condensed normal large serif', 'invalid'),\n    ('normal normal normal normal', 'invalid'),\n    ('normal normal normal italic', 'invalid'),\n    ('caption', 'System fonts'),\n])\ndef test_font_invalid(rule, message):\n    assert_invalid(f'font: {rule}', message)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('normal', {\n        'font_variant_alternates': 'normal',\n        'font_variant_caps': 'normal',\n        'font_variant_east_asian': 'normal',\n        'font_variant_ligatures': 'normal',\n        'font_variant_numeric': 'normal',\n        'font_variant_position': 'normal',\n    }),\n    ('none', {\n        'font_variant_alternates': 'normal',\n        'font_variant_caps': 'normal',\n        'font_variant_east_asian': 'normal',\n        'font_variant_ligatures': 'none',\n        'font_variant_numeric': 'normal',\n        'font_variant_position': 'normal',\n    }),\n    ('historical-forms petite-caps', {\n        'font_variant_alternates': 'historical-forms',\n        'font_variant_caps': 'petite-caps',\n    }),\n    ('lining-nums contextual small-caps common-ligatures', {\n        'font_variant_ligatures': ('contextual', 'common-ligatures'),\n        'font_variant_numeric': ('lining-nums',),\n        'font_variant_caps': 'small-caps',\n    }),\n    ('jis78 ruby proportional-width', {\n        'font_variant_east_asian': ('jis78', 'ruby', 'proportional-width'),\n    }),\n    # CSS2-style font-variant\n    ('small-caps', {'font_variant_caps': 'small-caps'}),\n])\ndef test_font_variant(rule, result):\n    assert expand_to_dict(f'font-variant: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'normal normal',\n    '2',\n    '\"\"',\n    'extra',\n    'jis78 jis04',\n    'full-width lining-nums ordinal normal',\n    'diagonal-fractions stacked-fractions',\n    'common-ligatures contextual no-common-ligatures',\n    'sub super',\n    'slashed-zero slashed-zero',\n])\ndef test_font_variant_invalid(rule):\n    assert_invalid(f'font-variant: {rule}')\n\n\n@assert_no_logs\ndef test_word_wrap():\n    assert expand_to_dict('word-wrap: normal') == {\n        'overflow_wrap': 'normal'}\n    assert expand_to_dict('word-wrap: break-word') == {\n        'overflow_wrap': 'break-word'}\n    assert expand_to_dict('word-wrap: inherit') == {\n        'overflow_wrap': 'inherit'}\n    assert_invalid('word-wrap: none')\n    assert_invalid('word-wrap: normal, break-word')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('auto', {'flex_grow': 1, 'flex_shrink': 1, 'flex_basis': 'auto'}),\n    ('none', {'flex_grow': 0, 'flex_shrink': 0, 'flex_basis': 'auto'}),\n    ('10', {'flex_grow': 10, 'flex_shrink': 1, 'flex_basis': ZERO_PIXELS}),\n    ('2 2', {'flex_grow': 2, 'flex_shrink': 2, 'flex_basis': ZERO_PIXELS}),\n    ('2 2 1px', {'flex_grow': 2, 'flex_shrink': 2, 'flex_basis': (1, 'px')}),\n    ('2 2 auto', {'flex_grow': 2, 'flex_shrink': 2, 'flex_basis': 'auto'}),\n    ('2 auto', {'flex_grow': 2, 'flex_shrink': 1, 'flex_basis': 'auto'}),\n    ('0 auto', {'flex_grow': 0, 'flex_shrink': 1, 'flex_basis': 'auto'}),\n    ('inherit', {\n        'flex_grow': 'inherit',\n        'flex_shrink': 'inherit',\n        'flex_basis': 'inherit'}),\n])\ndef test_flex(rule, result):\n    assert expand_to_dict(f'flex: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'auto 0 0 0',\n    '1px 2px',\n    'auto auto',\n    'auto 1 auto',\n])\ndef test_flex_invalid(rule):\n    assert_invalid(f'flex: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('column', {'flex_direction': 'column'}),\n    ('wrap', {'flex_wrap': 'wrap'}),\n    ('wrap column', {'flex_direction': 'column', 'flex_wrap': 'wrap'}),\n    ('row wrap', {'flex_direction': 'row', 'flex_wrap': 'wrap'}),\n    ('inherit', {'flex_direction': 'inherit', 'flex_wrap': 'inherit'}),\n])\ndef test_flex_flow(rule, result):\n    assert expand_to_dict(f'flex-flow: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    '1px',\n    'wrap 1px',\n    'row row',\n    'wrap nowrap',\n    'column wrap nowrap row',\n])\ndef test_flex_flow_invalid(rule):\n    assert_invalid(f'flex-flow: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('auto', {'start': 'auto', 'end': 'auto'}),\n    ('auto / auto', {'start': 'auto', 'end': 'auto'}),\n    ('4', {'start': (None, 4, None), 'end': 'auto'}),\n    ('c', {'start': (None, None, 'c'), 'end': (None, None, 'c')}),\n    ('4 / -4', {'start': (None, 4, None), 'end': (None, -4, None)}),\n    ('c / d', {'start': (None, None, 'c'), 'end': (None, None, 'd')}),\n    ('ab / cd 4', {'start': (None, None, 'ab'), 'end': (None, 4, 'cd')}),\n    ('ab 2 span', {'start': ('span', 2, 'ab'), 'end': 'auto'}),\n])\ndef test_grid_column_row(rule, result):\n    assert expand_to_dict(f'grid-column: {rule}') == dict(\n        (f'grid_column_{key}', value) for key, value in result.items())\n    assert expand_to_dict(f'grid-row: {rule}') == dict(\n        (f'grid_row_{key}', value) for key, value in result.items())\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'auto auto',\n    '4 / 2 / c',\n    'span',\n    '4 / span',\n    'c /',\n    '/4',\n    'col / 2.1',\n])\ndef test_grid_column_row_invalid(rule):\n    assert_invalid(f'grid-column: {rule}')\n    assert_invalid(f'grid-row: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('auto', {\n        'row_start': 'auto', 'row_end': 'auto',\n        'column_start': 'auto', 'column_end': 'auto'}),\n    ('auto / auto', {\n        'row_start': 'auto', 'row_end': 'auto',\n        'column_start': 'auto', 'column_end': 'auto'}),\n    ('auto / auto / auto', {\n        'row_start': 'auto', 'row_end': 'auto',\n        'column_start': 'auto', 'column_end': 'auto'}),\n    ('auto / auto / auto / auto', {\n        'row_start': 'auto', 'row_end': 'auto',\n        'column_start': 'auto', 'column_end': 'auto'}),\n    ('1/c/2 d/span 2 ab', {\n        'row_start': (None, 1, None), 'column_start': (None, None, 'c'),\n        'row_end': (None, 2, 'd'), 'column_end': ('span', 2, 'ab')}),\n    ('1  /  c', {\n        'row_start': (None, 1, None), 'column_start': (None, None, 'c'),\n        'row_end': 'auto', 'column_end': (None, None, 'c')}),\n    ('a / c 2', {\n        'row_start': (None, None, 'a'), 'column_start': (None, 2, 'c'),\n        'row_end': (None, None, 'a'), 'column_end': 'auto'}),\n    ('a', {\n        'row_start': (None, None, 'a'), 'row_end': (None, None, 'a'),\n        'column_start': (None, None, 'a'), 'column_end': (None, None, 'a')}),\n    ('span 2', {\n        'row_start': ('span', 2, None), 'row_end': 'auto',\n        'column_start': 'auto', 'column_end': 'auto'}),\n])\ndef test_grid_area(rule, result):\n    assert expand_to_dict(f'grid-area: {rule}') == dict(\n        (f'grid_{key}', value) for key, value in result.items())\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'auto auto',\n    'auto / auto auto',\n    '4 / 2 / c / d / e',\n    'span',\n    '4 / span',\n    'c /',\n    '/4',\n    'c//4',\n    '/',\n    '1 / 2 / 4 / 0.5',\n])\ndef test_grid_area_invalid(rule):\n    assert_invalid(f'grid-area: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('none', {\n        'rows': 'none', 'columns': 'none', 'areas': 'none',\n    }),\n    ('subgrid / [outer-edge] 20px [main-start]', {\n        'rows': ('subgrid', ()),\n        'columns': (('outer-edge',), (20, 'px'), ('main-start',)),\n        'areas': 'none',\n    }),\n    ('repeat(2, [e] 40px) repeat(5, auto) / subgrid [a] repeat(auto-fill, [b])', {\n        'rows': (\n            (), ('repeat()', 2, (('e',), (40, 'px'), ())), (),\n            ('repeat()', 5, ((), 'auto', ())), ()),\n        'columns': ('subgrid', (('a',), ('repeat()', 'auto-fill', (('b',),)))),\n        'areas': 'none',\n    }),\n    # TODO: support last syntax\n    # ('[a b] \"x y y\" [c] [d] \"x y y\" 1fr [e] / auto 2fr auto', {\n    #     'rows': 'none', 'columns': 'none', 'areas': 'none',\n    # }),\n    # ('[a b c] \"x x x\" 2fr', {\n    #     'rows': 'none', 'columns': 'none', 'areas': 'none',\n    # }),\n])\ndef test_grid_template(rule, result):\n    assert expand_to_dict(f'grid-template: {rule}') == dict(\n        (f'grid_template_{key}', value) for key, value in result.items())\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none none',\n    'auto',\n    'subgrid / subgrid / subgrid',\n    '[a] 1px [b] / none /',\n    '[a] 1px [b] // none',\n    '[a] 1px [b] none',\n])\ndef test_grid_template_invalid(rule):\n    assert_invalid(f'grid-template: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('none', {\n        'template_rows': 'none', 'template_columns': 'none',\n        'template_areas': 'none',\n        'auto_rows': ('auto',), 'auto_columns': ('auto',),\n        'auto_flow': ('row',),\n    }),\n    ('subgrid / [outer-edge] 20px [main-start]', {\n        'template_rows': ('subgrid', ()),\n        'template_columns': (('outer-edge',), (20, 'px'), ('main-start',)),\n        'template_areas': 'none',\n        'auto_rows': ('auto',), 'auto_columns': ('auto',),\n        'auto_flow': ('row',),\n    }),\n    ('repeat(2, [e] 40px) repeat(5, auto) / subgrid [a] repeat(auto-fill, [b])', {\n        'template_rows': (\n            (), ('repeat()', 2, (('e',), (40, 'px'), ())), (),\n            ('repeat()', 5, ((), 'auto', ())), ()),\n        'template_columns': ('subgrid', (('a',), ('repeat()', 'auto-fill', (('b',),)))),\n        'template_areas': 'none',\n        'auto_rows': ('auto',), 'auto_columns': ('auto',),\n        'auto_flow': ('row',),\n    }),\n    ('auto-flow 1fr / 100px', {\n        'template_rows': 'none', 'template_columns': ((), (100, 'px'), ()),\n        'template_areas': 'none',\n        'auto_rows': ((1, 'fr'),), 'auto_columns': ('auto',),\n        'auto_flow': ('row',),\n    }),\n    ('none / dense auto-flow 1fr', {\n        'template_rows': 'none', 'template_columns': 'none',\n        'template_areas': 'none',\n        'auto_rows': ('auto',), 'auto_columns': ((1, 'fr'),),\n        'auto_flow': ('column', 'dense'),\n    }),\n    # TODO: support last grid-template syntax\n    # ('[a b] \"x y y\" [c] [d] \"x y y\" 1fr [e] / auto 2fr auto', {\n    # }),\n    # ('[a b c] \"x x x\" 2fr', {\n    # }),\n])\ndef test_grid(rule, result):\n    assert expand_to_dict(f'grid: {rule}') == dict(\n        (f'grid_{key}', value) for key, value in result.items())\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none none',\n    'auto',\n    'subgrid / subgrid / subgrid',\n    '[a] 1px [b] / none /',\n    '[a] 1px [b] // none',\n    '[a] 1px [b] none',\n    'none / auto-flow 1fr dense',\n    'none / dense 1fr auto-flow',\n    '100px auto-flow / none',\n    'dense 100px / auto-flow 1fr'\n])\ndef test_grid_invalid(rule):\n    assert_invalid(f'grid: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('page-break-after: left', {'break_after': 'left'}),\n    ('page-break-before: always', {'break_before': 'page'}),\n    ('page-break-after: inherit', {'break_after': 'inherit'}),\n    ('page-break-before: inherit', {'break_before': 'inherit'}),\n])\ndef test_page_break(rule, result):\n    assert expand_to_dict(rule) == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'page-break-after: top',\n    'page-break-before: 1px',\n])\ndef test_page_break_invalid(rule):\n    assert_invalid(rule)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('avoid', {'break_inside': 'avoid'}),\n    ('inherit', {'break_inside': 'inherit'}),\n])\ndef test_page_break_inside(rule, result):\n    assert expand_to_dict(f'page-break-inside: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'top',\n])\ndef test_page_break_inside_invalid(rule):\n    assert_invalid(f'page-break-inside: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('1em', {'column_width': (1, 'em'), 'column_count': 'auto'}),\n    ('auto', {'column_width': 'auto', 'column_count': 'auto'}),\n    ('auto auto', {'column_width': 'auto', 'column_count': 'auto'}),\n])\ndef test_columns(rule, result):\n    assert expand_to_dict(f'columns: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'reason'), [\n    ('1px 2px', 'invalid'),\n    ('auto auto auto', 'multiple'),\n])\ndef test_columns_invalid(rule, reason):\n    assert_invalid(f'columns: {rule}', reason)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('none', {\n        'max_lines': 'none', 'continue': 'auto', 'block_ellipsis': 'none'}),\n    ('2', {\n        'max_lines': 2, 'continue': 'discard', 'block_ellipsis': 'auto'}),\n    ('3 \"…\"', {\n        'max_lines': 3, 'continue': 'discard',\n        'block_ellipsis': ('string', '…')}),\n    ('inherit', {\n        'max_lines': 'inherit', 'continue': 'inherit',\n        'block_ellipsis': 'inherit'}),\n])\ndef test_line_clamp(rule, result):\n    assert expand_to_dict(f'line-clamp: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'reason'), [\n    ('none none none', 'invalid'),\n    ('1px', 'invalid'),\n    ('0 \"…\"', 'invalid'),\n    ('1px 2px', 'invalid'),\n])\ndef test_line_clamp_invalid(rule, reason):\n    assert_invalid(f'line-clamp: {rule}', reason)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'result'), [\n    ('start', {'text_align_all': 'start', 'text_align_last': 'start'}),\n    ('right', {'text_align_all': 'right', 'text_align_last': 'right'}),\n    ('justify', {'text_align_all': 'justify', 'text_align_last': 'start'}),\n    ('justify-all', {\n        'text_align_all': 'justify', 'text_align_last': 'justify'}),\n    ('inherit', {'text_align_all': 'inherit', 'text_align_last': 'inherit'}),\n])\ndef test_text_align(rule, result):\n    assert expand_to_dict(f'text-align: {rule}') == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'reason'), [\n    ('none', 'invalid'),\n    ('start end', 'invalid'),\n    ('1', 'invalid'),\n    ('left left', 'invalid'),\n    ('top', 'invalid'),\n    ('\"right\"', 'invalid'),\n    ('1px', 'invalid'),\n])\ndef test_text_align_invalid(rule, reason):\n    assert_invalid(f'text-align: {rule}', reason)\n"
  },
  {
    "path": "tests/css/test_fonts.py",
    "content": "\"\"\"Test CSS font-related properties.\"\"\"\n\nfrom math import isclose\n\nimport pytest\n\nfrom weasyprint import CSS\nfrom weasyprint.css import get_all_computed_styles\nfrom weasyprint.text.line_break import strut\n\nfrom ..testing_utils import FakeHTML, assert_no_logs, render_pages\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('parent_css', 'parent_size', 'child_css', 'child_size'), [\n    ('10px', 10, '10px', 10),\n    ('x-small', 12, 'xx-large', 32),\n    ('x-large', 24, '2em', 48),\n    ('1em', 16, '1em', 16),\n    ('1em', 16, 'larger', 6 / 5 * 16),\n    ('medium', 16, 'larger', 6 / 5 * 16),\n    ('x-large', 24, 'larger', 32),\n    ('xx-large', 32, 'larger', 1.2 * 32),\n    ('1px', 1, 'larger', 3 / 5 * 16),\n    ('28px', 28, 'larger', 32),\n    ('100px', 100, 'larger', 120),\n    ('xx-small', 3 / 5 * 16, 'larger', 12),\n    ('1em', 16, 'smaller', 8 / 9 * 16),\n    ('medium', 16, 'smaller', 8 / 9 * 16),\n    ('x-large', 24, 'smaller', 6 / 5 * 16),\n    ('xx-large', 32, 'smaller', 24),\n    ('xx-small', 3 / 5 * 16, 'smaller', 0.8 * 3 / 5 * 16),\n    ('1px', 1, 'smaller', 0.8),\n    ('28px', 28, 'smaller', 24),\n    ('100px', 100, 'smaller', 32),\n])\ndef test_font_size(parent_css, parent_size, child_css, child_size):\n    document = FakeHTML(string='<p>a<span>b')\n    style_for = get_all_computed_styles(document, user_stylesheets=[CSS(\n        string='p{font-size:%s}span{font-size:%s}' % (parent_css, child_css))])\n\n    _head, body = document.etree_element\n    p, = body\n    span, = p\n    assert isclose(style_for(p)['font_size'], parent_size)\n    assert isclose(style_for(span)['font_size'], child_size)\n\n\n@assert_no_logs\ndef test_line_height_inheritance():\n    page, = render_pages('''\n      <style>\n        html { font-size: 10px; line-height: 140% }\n        section { font-size: 10px; line-height: 1.4 }\n        div, p { font-size: 20px; vertical-align: 50% }\n      </style>\n      <body><div><section><p></p></section></div></body>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    section, = div.children\n    paragraph, = section.children\n    assert html.style['font_size'] == 10\n    assert div.style['font_size'] == 20\n    # 140% of 10px = 14px is inherited from html\n    assert strut(div.style)[0] == 14\n    assert div.style['vertical_align'] == 7  # 50 % of 14px\n\n    assert paragraph.style['font_size'] == 20\n    # 1.4 is inherited from p, 1.4 * 20px on em = 28px\n    assert strut(paragraph.style)[0] == 28\n    assert paragraph.style['vertical_align'] == 14  # 50% of 28px\n"
  },
  {
    "path": "tests/css/test_layers.py",
    "content": "\"\"\"Test CSS layers.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\n@pytest.mark.parametrize('style', [\n    '@layer { div { width: 100px } }',\n    '@layer a { div { width: 100px } }',\n    '''\n    div { width: 100px }\n    @layer a { div { width: 200px } }\n    ''',\n    '''\n    @layer { div { width: 200px } }\n    @layer a { div { width: 100px } }\n    ''',\n    '''\n    @layer a { div { width: 200px } }\n    @layer { div { width: 100px } }\n    ''',\n    '''\n    @layer a { div { width: 200px } }\n    @layer a { div { width: 100px } }\n    ''',\n    '''\n    @layer a { div { width: 100px } }\n    @layer a.b { div { width: 200px } }\n    ''',\n    '''\n    @layer a.b { div { width: 200px } }\n    @layer a { div { width: 100px } }\n    ''',\n    '''\n    @layer a { div { width: 200px } }\n    @layer b { div { width: 100px } }\n    ''',\n    '''\n    @layer b, a;\n    @layer a { div { width: 100px } }\n    @layer b { div { width: 200px } }\n    ''',\n    '''\n    @layer b;\n    @layer a { div { width: 100px } }\n    @layer b { div { width: 200px } }\n    ''',\n    '''\n    @import url(data:text/css,div{width:100px});\n    @layer a { div { width: 200px } }\n    ''',\n    '''\n    @import url(data:text/css,div{width:200px}) layer;\n    @layer a { div { width: 100px } }\n    ''',\n    '''\n    @import url(data:text/css,div{width:200px}) layer(b);\n    @layer a { div { width: 100px } }\n    ''',\n    '''\n    @import url(data:text/css,div{width:100px}) layer(a);\n    @layer a.b { div { width: 200px } }\n    ''',\n    '''\n    @import url(data:text/css,div{width:200px}) layer(a.b);\n    @layer a { div { width: 100px } }\n    ''',\n    '''\n    @layer a { div { width: 100px } }\n    @import url(data:text/css,div{width:200px}) layer(a.b);\n    ''',\n])\ndef test_layers(style):\n    page, = render_pages('''\n      <style>\n        %s\n      </style>\n      <div>abc</div>\n    ''' % style)\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 100\n"
  },
  {
    "path": "tests/css/test_math.py",
    "content": "\"\"\"Test CSS math functions.\"\"\"\n\nfrom math import isclose\n\nimport pytest\n\nfrom weasyprint.css.validation.properties import PROPERTIES\n\nfrom ..testing_utils import assert_no_logs, capture_logs, render_pages\n\n\n@assert_no_logs\n@pytest.mark.parametrize('width', [\n    'calc(100px)',\n    'calc(10em)',\n    'calc(50vw)',\n    'calc(20pvh)',\n    'calc(50%)',\n    'calc(10px + 90px)',\n    'calc(5em + 50px)',\n    'calc(2 * 5em)',\n    'calc(2 * (3em + 20px))',\n    'calc(25% * (1 + 1))',\n    'calc(20% * (1 + 1) + 20px)',\n    'calc(100px',\n    'max(100px)',\n    'max(30%, 2em, 100px)',\n    'max(-30%, -2em, 10em)',\n    'calc(max(-1, 1, 2) * 50px)',\n    'min(100px)',\n    'min(100%, 20em, 100px)',\n    'calc(min(4, 2) * 50px)',\n    'calc(sqrt(4) * 50px)',\n    'calc(pow(2, 2) * 25px)',\n    'calc(hypot(2) * 50px)',\n    'calc(hypot(3, 4) * 20px)',\n    'calc(hypot(2px) * 50)',\n    'calc(hypot(3px, 4px) * 20)',\n    'calc(log(e) * 100px)',\n    'calc(log(100, 10) * 50px)',\n    'calc(exp(1) / e * 100px)',\n    'abs(-100px)',\n    'calc(abs(-100) * 1px)',\n    'calc(sign(-100) * -100px)',\n    'calc(sign(-100px) * -100px)',\n    'calc(sqrt(16) * min(25px, 100%))',\n    'clamp(calc(-infinity * 1px), 10em, calc(infinity * 1px))',\n    'clamp(50px, 10em, 500px)',\n    'clamp(100px, 2em, 500px)',\n    'clamp(10px, 100em, 10em)',\n    'clamp(10px, 100%, 10em)',\n    'round(100.4px)',\n    'round(145.4px, 100px)',\n    'round(nearest, 100px)',\n    'round(down, 195px, 100px)',\n    'round(up, 5px, 100px)',\n    'round(to-zero, 195px, 100px)',\n    'mod(300px, 200px)',\n    'calc(mod(300px, -200px) * -1)',\n    'calc(mod(-300px, -200px) * -1)',\n    'rem(300px, 200px)',\n    'rem(300px, -200px)',\n    'calc(rem(-300px, -200px) * -1)',\n    'calc(sin(30deg) * 200px)',\n    'calc(cos(60deg) * 200px)',\n    'calc(tan(45deg) * 100px)',\n    'calc(tan(calc(pi / 4)) * 100px)',\n    'calc(sin(asin(0.5)) * 200px)',\n    'calc(cos(acos(0.5)) * 200px)',\n    'calc(tan(atan(1)) * 100px)',\n    'calc(tan(atan2(1, 1)) * 100px)',\n    'calc(100px * var(--one))',\n    'calc(50% * var(--one))',\n    'calc(100px * sqrt(var(--one)))',\n])\ndef test_math_functions(width):\n    page, = render_pages('''\n      <style>\n        @page { size: 400px 500px; margin: 100px }\n        body { font-size: 10px; width: 200px }\n      </style>\n      <div style=\"--one: 1; height: 1px; width: %s\"></div>\n    ''' % width)\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert isclose(div.width, 100)\n\n\n@assert_no_logs\n@pytest.mark.parametrize('width', [\n    'calc',\n    '(calc)',\n    'calc(',\n    'calc()',\n    'calc(\"100px\")',\n    'calc(100)',\n    'calc(100px 100px)',\n    'calc(100px, 100px)',\n    'calc(100px * 100px)',\n    'calc(100 * 100)',\n    'calc(calc(100unknown))',\n    'calc(0.1)',\n    'calc(-1)',\n    'min()',\n    'min(10)',\n    'min(\"10px\")',\n    'min(10, 5px)',\n    'calc(min(1, 5px) * 10px)',\n    'max()',\n    'max(10)',\n    'max(\"10px\")',\n    'max(10, 50px)',\n    'calc(max(100, 5px) * 10px)',\n    'calc(100* - max(56px, 1rem)',\n    'clamp()',\n    'clamp(10px)',\n    'clamp(10px, 50px)',\n    'clamp(10px, 50px, 100px, 200px)',\n    'clamp(10px, \"50px\", 100px)',\n    'round()',\n    'round(100)',\n    'round(100, 10)',\n    'round(nearest, 100, 10)',\n    'round(100px, 10)',\n    'round(100px, \"10px\")',\n    'round(nearest, 100px, 10)',\n    'round(100px, 10px, 1)',\n    'round(nearest, 100px, 10px, 1)',\n    'round(unknown, 100px)',\n    'round(unknown, 100px, 10px)',\n    'mod()',\n    'mod(10px)',\n    'mod(100px, 10)',\n    'mod(100px, \"10px\")',\n    'calc(mod(300px, 200) * -1)',\n    'mod(100px, 10px, 1px)',\n    'rem()',\n    'rem(10px)',\n    'rem(100px, 10)',\n    'rem(100px, \"10px\")',\n    'calc(rem(300px, 200) * -1)',\n    'rem(100px, 10px, 1px)',\n    'sin()',\n    'sin(10)',\n    'sin(10%)',\n    'sin(10deg)',\n    'calc(sin(10) * 1)',\n    'cos()',\n    'cos(10)',\n    'cos(10%)',\n    'cos(10deg)',\n    'calc(cos(10) * 1)',\n    'tan()',\n    'tan(10)',\n    'tan(10%)',\n    'tan(10deg)',\n    'calc(tan(10) * 1)',\n    'asin()',\n    'asin(0)',\n    'asin(0.5)',\n    'asin(50deg)',\n    'calc(sin(asin(50deg)) * 200px)',\n    'calc(sin(asin(0.5)) * 200)',\n    'calc(sin(asin(0.5, 2)) * 200px)',\n    'calc(sin(asin(5)) * 200px)',\n    'acos()',\n    'acos(0)',\n    'acos(0.5)',\n    'acos(50deg)',\n    'calc(cos(acos(50deg)) * 200px)',\n    'calc(cos(acos(0.5)) * 200)',\n    'calc(cos(acos(0.5, 2)) * 200px)',\n    'calc(cos(acos(5)) * 200px)',\n    'atan()',\n    'atan(0)',\n    'atan(0.5)',\n    'atan(50deg)',\n    'calc(tan(atan(50deg)) * 200px)',\n    'calc(tan(atan(0.5)) * 200)',\n    'calc(tan(atan(0.5, 2)) * 200px)',\n    'atan2()',\n    'atan2(0.5)',\n    'atan2(0.5, 1)',\n    'atan2(50deg, 1)',\n    'calc(tan(atan2(50deg, 1)) * 200px)',\n    'calc(tan(atan2(0.5, 1)) * 200)',\n    'pow()',\n    'pow(4, 3)',\n    'pow(4px, 3)',\n    'pow(4, 3, 4)',\n    'sqrt()',\n    'sqrt(4)',\n    'sqrt(4px)',\n    'sqrt(4, 2)',\n    'hypoth()',\n    'hypoth(3)',\n    'hypoth(3, 4)',\n    'log()',\n    'log(10)',\n    'log(10px)',\n    'log(10, 10)',\n    'log(10px, 10)',\n    'log(10, 10, 10)',\n    'exp()',\n    'exp(10)',\n    'exp(10px)',\n    'exp(10, 10)',\n    'exp(10px, 10)',\n    'exp(10, 10, 10)',\n    'abs()',\n    'abs(10)',\n    'abs(10px, 100)',\n    'sign()',\n    'sign(10)',\n    'sign(10px)',\n    'sign(10px, 10)',\n])\ndef test_math_functions_error(width):\n    with capture_logs() as logs:\n        page, = render_pages('''\n          <style>body { font-size: 10px; width: 200px }</style>\n          <div style=\"--one: 1; height: 1px; width: %s\"></div>\n        ''' % width)\n    assert len(logs) == 1\n\n\n@pytest.mark.parametrize('css_property', PROPERTIES)\ndef test_math_functions_percentage_and_font_unit(css_property):\n    with capture_logs() as math_logs:\n        render_pages(f'''\n          <div style=\"{css_property}: calc(50% + 1em)\"></div>\n        ''')\n    with capture_logs() as logs:\n        render_pages(f'''\n          <div style=\"{css_property}: 50%\"></div>\n        ''')\n        if not logs:\n            # Happens when property accepts percentages but not lengths.\n            render_pages(f'''\n              <div style=\"{css_property}: 1em\"></div>\n            ''')\n    assert len(math_logs) == len(logs)\n\n\n@pytest.mark.parametrize('display', [\n    'block', 'inline', 'flex', 'grid',\n    'list', 'list-item',\n    'table', 'table-row-group', 'table-cell',\n    'inline-block', 'inline-table', 'inline-flex', 'inline-grid',\n])\ndef test_math_functions_display_size(display):\n    # Regression test for #2673.\n    render_pages(f'''\n    <div style=\"display: {display};\n     min-width: calc(50% + 1em); max-width: calc(50% + 1em); width: calc(50% + 1em);\n     min-height: calc(50% + 1em); max-height: calc(50% + 1em); height: calc(50% + 1em)\n    \">\n      <div style=\"\n       min-width: calc(50% + 1em); max-width: calc(50% + 1em); width: calc(50% + 1em);\n       min-height: calc(50% + 1em); max-height: calc(50% + 1em); height: calc(50% + 1em)\n      \"></div>\n    </div>\n    ''')\n\n\n@assert_no_logs\ndef test_math_functions_hyphenate():\n    render_pages('''\n      <div lang=\"en\"\n        style=\"hyphens: auto; hyphenate-limit-zone: calc(1em + 100%); width: 2em\">\n        absolute\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_math_functions_gradient():\n    render_pages('''\n      <div style=\"width: 10px; height: 10px; background: linear-gradient(\n        blue calc(20% + 1em),\n        red calc(80% + 1em))\"></div>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_math_functions_color():\n    render_pages('''\n      <div style=\"width: 10px; height: 10px;\n                  background: rgba(10, 20, calc(30), calc(80%))\"></div>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_math_functions_gradient_color():\n    render_pages('''\n      <div style=\"width: 10px; height: 10px; background: linear-gradient(\n        rgba(10, 20, calc(30), calc(80%)) 10%,\n        hsl(calc(10 + 10), 20%, 20%) 80%\"></div>\n    ''')\n\n\n@assert_no_logs\ndef test_math_image_min_content_calc():\n    render_pages('''\n      <table>\n        <td>\n          <img src=\"pattern.png\" style=\"\n            height: calc(10% + 1em);\n            width: calc(10% + 1em);\n            max-height: calc(10% + 1em);\n            max-width: calc(10% + 1em);\n            min-height: calc(10% + 1em);\n            min-width: calc(10% + 1em);\n          \">\n    ''')\n\n\n@assert_no_logs\ndef test_math_image_min_content_auto_width_calc():\n    render_pages('''\n      <table>\n        <td>\n          <img src=\"pattern.png\" style=\"\n            height: calc(10% + 1em);\n            max-height: calc(10% + 1em);\n            max-width: calc(10% + 1em);\n            min-height: calc(10% + 1em);\n            min-width: calc(10% + 1em);\n          \">\n    ''')\n\n\n@assert_no_logs\ndef test_math_image_min_content_auto_width_height_calc():\n    render_pages('''\n      <table>\n        <td>\n          <img src=\"pattern.png\" style=\"\n            max-height: calc(10% + 1em);\n            max-width: calc(10% + 1em);\n            min-height: calc(10% + 1em);\n            min-width: calc(10% + 1em);\n          \">\n    ''')\n\n\n@assert_no_logs\ndef test_math_table_margin():\n    render_pages('<table style=\"margin: calc(1em + 10%)\">')\n\n\n@assert_no_logs\ndef test_math_grid_padding():\n    render_pages('''\n      <article style=\"display: grid\">\n        <div style=\"box-sizing: border-box; border: 1px solid;\n                    padding: calc(2px + 10%); width: 7px\">a</div>\n      </article>\n    ''')\n\n\n@assert_no_logs\ndef test_math_table_column():\n    render_pages('''\n      <table style=\"width: 200px\">\n        <colgroup style=\"width: calc(1em + 10%)\">\n          <col />\n        </colgroup>\n        <col style=\"width: calc(1em + 10%)\" />\n        <tbody>\n          <tr>\n            <td>a</td>\n            <td>a</td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n"
  },
  {
    "path": "tests/css/test_nesting.py",
    "content": "\"\"\"Test CSS nesting.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\n@pytest.mark.parametrize('style', [\n    'div { p { width: 10px } }',\n    'p { div & { width: 10px } }',\n    'p { width: 20px; div & { width: 10px } }',\n    'p { div & { width: 10px } width: 20px }',\n    'div { & { & { p { & { width: 10px } } } } }',\n    '@media print { div { p { width: 10px } } }',\n    'div { em, p { width: 10px } }',\n    'p { a, div & { width: 10px } }',\n])\ndef test_nesting_block(style):\n    page, = render_pages('''\n      <style>%s</style>\n      <div><p></p></div><p></p>\n    ''' % style)\n    html, = page.children\n    body, = html.children\n    div, p = body.children\n    div_p, = div.children\n    assert div_p.width == 10\n    assert p.width != 10\n"
  },
  {
    "path": "tests/css/test_pages.py",
    "content": "\"\"\"Test CSS pages.\"\"\"\n\nimport pytest\nimport tinycss2\n\nfrom weasyprint import CSS\nfrom weasyprint.css import get_all_computed_styles, parse_page_selectors\nfrom weasyprint.layout.page import PageType, set_page_type_computed_styles\n\nfrom ..testing_utils import FakeHTML, assert_no_logs, render_pages, resource_path\n\n\n@assert_no_logs\ndef test_page():\n    document = FakeHTML(resource_path('doc1.html'))\n    style_for = get_all_computed_styles(\n        document, user_stylesheets=[CSS(string='''\n          html { color: red }\n          @page { margin: 10px }\n          @page :right {\n            color: blue;\n            margin-bottom: 12pt;\n            font-size: 20px;\n            @top-left { width: 10em }\n            @top-right { font-size: 10px}\n          }\n        ''')])\n\n    page_type = PageType(side='left', blank=False, name='', index=0, groups=())\n    set_page_type_computed_styles(page_type, document, style_for)\n    style = style_for(page_type)\n    assert style['margin_top'] == (5, 'px')\n    assert style['margin_left'] == (10, 'px')\n    assert style['margin_bottom'] == (10, 'px')\n    assert style['color'] == (1, 0, 0, 1)  # red, inherited from html\n\n    page_type = PageType(side='right', blank=False, name='', index=0, groups=())\n    set_page_type_computed_styles(page_type, document, style_for)\n    style = style_for(page_type)\n    assert style['margin_top'] == (5, 'px')\n    assert style['margin_left'] == (10, 'px')\n    assert style['margin_bottom'] == (16, 'px')\n    assert style['color'] == (0, 0, 1, 1)  # blue\n\n    page_type = PageType(side='left', blank=False, name='', index=1, groups=())\n    set_page_type_computed_styles(page_type, document, style_for)\n    style = style_for(page_type)\n    assert style['margin_top'] == (10, 'px')\n    assert style['margin_left'] == (10, 'px')\n    assert style['margin_bottom'] == (10, 'px')\n    assert style['color'] == (1, 0, 0, 1)  # red, inherited from html\n\n    page_type = PageType(side='right', blank=False, name='', index=1, groups=())\n    set_page_type_computed_styles(page_type, document, style_for)\n    style = style_for(page_type)\n    assert style['margin_top'] == (10, 'px')\n    assert style['margin_left'] == (10, 'px')\n    assert style['margin_bottom'] == (16, 'px')\n    assert style['color'] == (0, 0, 1, 1)  # blue\n\n    page_type = PageType(side='left', blank=False, name='', index=0, groups=())\n    set_page_type_computed_styles(page_type, document, style_for)\n    style = style_for(page_type, '@top-left')\n    assert style is None\n\n    page_type = PageType(side='right', blank=False, name='', index=0, groups=())\n    set_page_type_computed_styles(page_type, document, style_for)\n    style = style_for(page_type, '@top-left')\n    assert style['font_size'] == 20  # inherited from @page\n    assert style['width'] == (200, 'px')\n\n    page_type = PageType(side='right', blank=False, name='', index=0, groups=())\n    set_page_type_computed_styles(page_type, document, style_for)\n    style = style_for(page_type, '@top-right')\n    assert style['font_size'] == 10\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('style', 'selectors'), [\n    ('@page {}', [{\n        'side': None, 'blank': None, 'first': None, 'name': None,\n        'index': None, 'specificity': [0, 0, 0]}]),\n    ('@page :left {}', [{\n        'side': 'left', 'blank': None, 'first': None, 'name': None,\n        'index': None, 'specificity': [0, 0, 1]}]),\n    ('@page:first:left {}', [{\n        'side': 'left', 'blank': None, 'first': True, 'name': None,\n        'index': None, 'specificity': [0, 1, 1]}]),\n    ('@page pagename {}', [{\n        'side': None, 'blank': None, 'first': None, 'name': 'pagename',\n        'index': None, 'specificity': [1, 0, 0]}]),\n    ('@page pagename:first:right:blank {}', [{\n        'side': 'right', 'blank': True, 'first': True, 'name': 'pagename',\n        'index': None, 'specificity': [1, 2, 1]}]),\n    ('@page pagename, :first {}', [\n        {'side': None, 'blank': None, 'first': None, 'name': 'pagename',\n         'index': None, 'specificity': [1, 0, 0]},\n        {'side': None, 'blank': None, 'first': True, 'name': None,\n         'index': None, 'specificity': [0, 1, 0]}]),\n    ('@page :first:first {}', [{\n        'side': None, 'blank': None, 'first': True, 'name': None,\n        'index': None, 'specificity': [0, 2, 0]}]),\n    ('@page :left:left {}', [{\n        'side': 'left', 'blank': None, 'first': None, 'name': None,\n        'index': None, 'specificity': [0, 0, 2]}]),\n    ('@page :nth(2) {}', [{\n        'side': None, 'blank': None, 'first': None, 'name': None,\n        'index': (0, 2, None), 'specificity': [0, 1, 0]}]),\n    ('@page :nth(2n + 4) {}', [{\n        'side': None, 'blank': None, 'first': None, 'name': None,\n        'index': (2, 4, None), 'specificity': [0, 1, 0]}]),\n    ('@page :nth(3n) {}', [{\n        'side': None, 'blank': None, 'first': None, 'name': None,\n        'index': (3, 0, None), 'specificity': [0, 1, 0]}]),\n    ('@page :nth( n+2 ) {}', [{\n        'side': None, 'blank': None, 'first': None, 'name': None,\n        'index': (1, 2, None), 'specificity': [0, 1, 0]}]),\n    ('@page :nth(even) {}', [{\n        'side': None, 'blank': None, 'first': None, 'name': None,\n        'index': (2, 0, None), 'specificity': [0, 1, 0]}]),\n    ('@page pagename:nth(2) {}', [{\n        'side': None, 'blank': None, 'first': None, 'name': 'pagename',\n        'index': (0, 2, None), 'specificity': [1, 1, 0]}]),\n    ('@page page page {}', None),\n    ('@page :left page {}', None),\n    ('@page :left, {}', None),\n    ('@page , {}', None),\n    ('@page :left, test, {}', None),\n    ('@page :wrong {}', None),\n    ('@page :left:wrong {}', None),\n    ('@page :left:right {}', None),\n])\ndef test_page_selectors(style, selectors):\n    at_rule, = tinycss2.parse_stylesheet(style)\n    assert parse_page_selectors(at_rule) == selectors\n\n\n@assert_no_logs\ndef test_named_pages():\n    page, = render_pages('''\n      <style>\n        @page NARRow { size: landscape }\n        div { page: AUTO }\n        p { page: NARRow }\n      </style>\n      <div><p><span>a</span></p></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    p, = div.children\n    span, = p.children\n    assert html.style['page'] == ''\n    assert body.style['page'] == ''\n    assert div.style['page'] == ''\n    assert p.style['page'] == 'NARRow'\n    assert span.style['page'] == 'NARRow'\n"
  },
  {
    "path": "tests/css/test_target.py",
    "content": "\"\"\"Test the CSS cross references using target-*() functions.\"\"\"\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\ndef test_target_counter():\n    page, = render_pages('''\n      <style>\n        div:first-child { counter-reset: div }\n        div { counter-increment: div }\n        #id1::before { content: target-counter('#id4', div) }\n        #id2::before { content: 'test ' target-counter('#id1', div) }\n        #id3::before { content: target-counter(url(#id4), div, lower-roman) }\n        #id4::before { content: target-counter('#id3', div) }\n      </style>\n      <body>\n        <div id=\"id1\"></div>\n        <div id=\"id2\"></div>\n        <div id=\"id3\"></div>\n        <div id=\"id4\"></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div1, div2, div3, div4 = body.children\n    before = div1.children[0].children[0].children[0]\n    assert before.text == '4'\n    before = div2.children[0].children[0].children[0]\n    assert before.text == 'test 1'\n    before = div3.children[0].children[0].children[0]\n    assert before.text == 'iv'\n    before = div4.children[0].children[0].children[0]\n    assert before.text == '3'\n\n\n@assert_no_logs\ndef test_target_counter_attr():\n    page, = render_pages('''\n      <style>\n        div:first-child { counter-reset: div }\n        div { counter-increment: div }\n        div::before { content: target-counter(attr(data-count), div) }\n        #id2::before { content: target-counter(attr(data-count url), div) }\n        #id4::before {\n          content: target-counter(attr(data-count), div, lower-alpha) }\n      </style>\n      <body>\n        <div id=\"id1\" data-count=\"#id4\"></div>\n        <div id=\"id2\" data-count=\"#id1\"></div>\n        <div id=\"id3\" data-count=\"#id2\"></div>\n        <div id=\"id4\" data-count=\"#id3\"></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div1, div2, div3, div4 = body.children\n    before = div1.children[0].children[0].children[0]\n    assert before.text == '4'\n    before = div2.children[0].children[0].children[0]\n    assert before.text == '1'\n    before = div3.children[0].children[0].children[0]\n    assert before.text == '2'\n    before = div4.children[0].children[0].children[0]\n    assert before.text == 'c'\n\n\n@assert_no_logs\ndef test_target_counters():\n    page, = render_pages('''\n      <style>\n        div:first-child { counter-reset: div }\n        div { counter-increment: div }\n        #id1-2::before { content: target-counters('#id4-2', div, '.') }\n        #id2-1::before { content: target-counters(url(#id3), div, '++') }\n        #id3::before {\n          content: target-counters('#id2-1', div, '.', lower-alpha) }\n        #id4-2::before {\n          content: target-counters(attr(data-count, url), div, '') }\n      </style>\n      <body>\n        <div id=\"id1\"><div></div><div id=\"id1-2\"></div></div>\n        <div id=\"id2\"><div id=\"id2-1\"></div><div></div></div>\n        <div id=\"id3\"></div>\n        <div id=\"id4\">\n          <div></div><div id=\"id4-2\" data-count=\"#id1-2\"></div>\n        </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div1, div2, div3, div4 = body.children\n    before = div1.children[1].children[0].children[0].children[0]\n    assert before.text == '4.2'\n    before = div2.children[0].children[0].children[0].children[0]\n    assert before.text == '3'\n    before = div3.children[0].children[0].children[0]\n    assert before.text == 'b.a'\n    before = div4.children[1].children[0].children[0].children[0]\n    assert before.text == '12'\n\n\n@assert_no_logs\ndef test_target_text():\n    page, = render_pages('''\n      <style>\n        a { display: block; color: red }\n        div:first-child { counter-reset: div }\n        div { counter-increment: div }\n        #id2::before { content: 'wow' }\n        #link1::before { content: 'test ' target-text('#id4') }\n        #link2::before { content: target-text(attr(data-count, url), before) }\n        #link3::before { content: target-text('#id3', after) }\n        #link4::before { content: target-text(url(#id1), first-letter) }\n      </style>\n      <body>\n        <a id=\"link1\"></a>\n        <div id=\"id1\">1 Chapter 1</div>\n        <a id=\"link2\" data-count=\"#id2\"></a>\n        <div id=\"id2\">2 Chapter 2</div>\n        <div id=\"id3\">3 Chapter 3</div>\n        <a id=\"link3\"></a>\n        <div id=\"id4\">4 Chapter 4</div>\n        <a id=\"link4\"></a>\n    ''')\n    html, = page.children\n    body, = html.children\n    a1, div1, a2, div2, div3, a3, div4, a4 = body.children\n    before = a1.children[0].children[0].children[0]\n    assert before.text == 'test 4 Chapter 4'\n    before = a2.children[0].children[0].children[0]\n    assert before.text == 'wow'\n    assert len(a3.children[0].children[0].children) == 0\n    before = a4.children[0].children[0].children[0]\n    assert before.text == '1'\n\n\n@assert_no_logs\ndef test_target_text_whitespace_around_target():\n    # Regression test for #1875.\n    page, = render_pages('''\n      <style>\n        a::before { content: target-text(attr(href)) }\n      </style>\n      <p>before <a href=\"#ref\"></a> after</p>\n      <h2 id=\"ref\">text</h2>\n    ''')\n    html, = page.children\n    body, = html.children\n    p, h2 = body.children\n    line, = p.children\n    before, a, after = line.children\n    assert before.text == 'before '\n    assert after.text == ' after'\n    assert a.children[0].children[0].text == 'text'\n\n\n@assert_no_logs\ndef test_target_float():\n    page, = render_pages('''\n      <style>\n        a::after {\n          content: target-counter('#h', page);\n          float: right;\n        }\n      </style>\n      <div><a id=\"span\">link</a></div>\n      <h1 id=\"h\">abc</h1>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, h1 = body.children\n    line, = div.children\n    inline, = line.children\n    text_box, after = inline.children\n    assert text_box.text == 'link'\n    assert after.children[0].children[0].text == '1'\n\n\n@assert_no_logs\ndef test_target_absolute():\n    page, = render_pages('''\n      <style>\n        a::after {\n          content: target-counter('#h', page);\n        }\n        div {\n          position: absolute;\n        }\n      </style>\n      <div><a id=\"span\">link</a></div>\n      <h1 id=\"h\">abc</h1>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, h1 = body.children\n    line, = div.children\n    inline, = line.children\n    text_box, after = inline.children\n    assert text_box.text == 'link'\n    assert after.children[0].text == '1'\n\n\n@assert_no_logs\ndef test_target_absolute_non_root():\n    page, = render_pages('''\n      <style>\n        a::after {\n          content: target-counter('#h', page);\n        }\n        section {\n          position: relative;\n        }\n        div {\n          position: absolute;\n        }\n      </style>\n      <section><div><a id=\"span\">link</a></div></section>\n      <h1 id=\"h\">abc</h1>\n    ''')\n    html, = page.children\n    body, = html.children\n    section, h1 = body.children\n    div, = section.children\n    line, = div.children\n    inline, = line.children\n    text_box, after = inline.children\n    assert text_box.text == 'link'\n    assert after.children[0].text == '1'\n"
  },
  {
    "path": "tests/css/test_ua.py",
    "content": "\"\"\"Test the user-agent stylesheet.\"\"\"\n\nimport pytest\n\nfrom weasyprint.html import CSS, HTML5_PH, HTML5_UA, HTML5_UA_FORM\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\n@pytest.mark.parametrize('css', [HTML5_UA, HTML5_UA_FORM, HTML5_PH])\ndef test_ua_stylesheets(css):\n    CSS(string=css)\n"
  },
  {
    "path": "tests/css/test_validation.py",
    "content": "\"\"\"Test validation of properties.\"\"\"\n\nimport logging\nfrom math import pi\n\nimport pytest\nimport tinycss2\n\nfrom weasyprint.css import preprocess_declarations\nfrom weasyprint.css.validation.properties import PROPERTIES\nfrom weasyprint.images import LinearGradient, RadialGradient\n\nfrom ..testing_utils import assert_no_logs, capture_logs\n\n\ndef get_value(css, expected_error=None, level=None):\n    declarations = tinycss2.parse_blocks_contents(css)\n\n    with capture_logs(level=level) as logs:\n        base_url = 'https://weasyprint.org/foo/'\n        declarations = list(preprocess_declarations(base_url, declarations))\n\n    if expected_error:\n        assert len(logs) == 1\n        assert expected_error in logs[0]\n    else:\n        assert not logs\n\n    if declarations:\n        assert len(declarations) == 1\n        return declarations[0][1]\n\n\ndef get_default_value(values, index, default):\n    if index > len(values) - 1:\n        return default\n    return values[index] or default\n\n\ndef assert_invalid(css, message='invalid', level=None):\n    assert get_value(css, message, level) is None\n\n\n@assert_no_logs\ndef test_not_print():\n    assert_invalid('volume: 42', 'does not apply for the print media', logging.DEBUG)\n\n\n@assert_no_logs\ndef test_unstable_prefix():\n    assert get_value(\n        '-weasy-max-lines: 3',\n        'prefixes on unstable attributes are deprecated') == 3\n\n\n@assert_no_logs\ndef test_normal_prefix():\n    assert_invalid(\n        '-weasy-display: block', 'prefix on this attribute is not supported')\n\n\n@assert_no_logs\ndef test_unknown_prefix():\n    assert_invalid(\n        '-unknown-display: block',\n        'prefixed selectors are ignored',\n        logging.DEBUG)\n\n\n@assert_no_logs\n@pytest.mark.parametrize('prop', PROPERTIES)\ndef test_empty_property_value(prop):\n    assert_invalid(f'{prop}:', message='Ignored')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('1px, 3EM, auto, auto', ((1, 'px'), (3, 'em'), 'auto', 'auto')),\n    ('1px,3em,auto,1px', ((1, 'px'), (3, 'em'), 'auto', (1, 'px'))),\n])\ndef test_clip(rule, value):\n    assert get_value(f'clip: rect({rule})') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'clip: square(1px, 3em, auto, auto)',\n    'clip: rect(1px 3em auto auto)',\n    'clip: rect(1px, 3em, auto)',\n    'clip: rect(1px, 3em / auto)',\n])\ndef test_clip_invalid(rule):\n    assert_invalid(rule)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('counter-reset: foo bar 2 baz', (('foo', 0), ('bar', 2), ('baz', 0))),\n    ('counter-increment: foo bar 2 baz', (('foo', 1), ('bar', 2), ('baz', 1))),\n    ('counter-reset: foo', (('foo', 0),)),\n    ('counter-set: FoO', (('FoO', 0),)),\n    ('counter-increment: foo bAr 2 Bar', (('foo', 1), ('bAr', 2), ('Bar', 1))),\n    ('counter-reset: none', ()),\n])\ndef test_counters(rule, value):\n    assert get_value(rule) == value\n\n\n@pytest.mark.parametrize(('rule', 'warning'), [\n    ('counter-reset: foo initial', 'Invalid counter name: initial.'),\n    ('counter-reset: foo none', 'Invalid counter name: none.'),\n])\ndef test_counters_warning(rule, warning):\n    assert_invalid(rule, warning)\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'counter-reset: foo 3pX',\n    'counter-reset: 3',\n])\ndef test_counters_invalid(rule):\n    assert_invalid(rule)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('letter-spacing: normal', 'normal'),\n    ('letter-spacing: 3px', (3, 'px')),\n    ('word-spacing: normal', 'normal'),\n    ('word-spacing: 3px', (3, 'px')),\n])\ndef test_spacing(rule, value):\n    assert get_value(rule) == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'letter-spacing: 3',\n    'word-spacing: 3',\n])\ndef test_spacing_invalid(rule):\n    assert_invalid(rule)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('none', 'none'),\n    ('overline', {'overline'}),\n    ('overline blink line-through', {'blink', 'line-through', 'overline'}),\n])\ndef test_text_decoration_line(rule, value):\n    assert get_value(f'text-decoration-line: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('solid', 'solid'),\n    ('double', 'double'),\n    ('dotted', 'dotted'),\n    ('dashed', 'dashed'),\n])\ndef test_text_decoration_style(rule, value):\n    assert get_value(f'text-decoration-style: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('200px', ((200, 'px'), (200, 'px'))),\n    ('200px 300pt', ((200, 'px'), (300, 'pt'))),\n    ('auto', ((210, 'mm'), (297, 'mm'))),\n    ('portrait', ((210, 'mm'), (297, 'mm'))),\n    ('landscape', ((297, 'mm'), (210, 'mm'))),\n    ('A3 portrait', ((297, 'mm'), (420, 'mm'))),\n    ('A3 landscape', ((420, 'mm'), (297, 'mm'))),\n    ('portrait A3', ((297, 'mm'), (420, 'mm'))),\n    ('landscape A3', ((420, 'mm'), (297, 'mm'))),\n])\ndef test_size(rule, value):\n    assert get_value(f'size: {rule}') == value\n\n\n@pytest.mark.parametrize('rule', [\n    'A3 landscape A3',\n    'A12',\n    'foo',\n    'foo bar',\n    '20%',\n])\ndef test_size_invalid(rule):\n    assert_invalid(f'size: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('none', ()),\n    ('translate(6px) rotate(90deg)', (\n        ('translate', ((6, 'px'), (0, 'px'))), ('rotate', pi / 2))),\n    ('rotate(0)', (('rotate', 0),)),\n    ('translate(-4px, 0)', (('translate', ((-4, 'px'), (0, None))),)),\n    ('translate(6px, 20%)', (('translate', ((6, 'px'), (20, '%'))),)),\n    ('scale(2)', (('scale', (2, 2)),)),\n])\ndef test_transform(rule, value):\n    assert get_value(f'transform: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'lipsumize(6px)',\n    'foo',\n    'scale(2) foo',\n    '6px',\n    'translate(6px 20%)',\n])\ndef test_transform_invalid(rule):\n    assert_invalid(f'transform: {rule}')\n\n\n@pytest.mark.parametrize('rule', [\n    'inexistent-gradient(blue, green)',\n])\ndef test_background_image_invalid(rule):\n    assert_invalid(f'background-image: {rule}')\n\n\n@pytest.mark.parametrize(('rule', 'value'), [\n    # One token, vertical\n    ('top', (('left', (50, '%'), 'top', (0, '%')),)),\n    ('bottom', (('left', (50, '%'), 'top', (100, '%')),)),\n\n    # Three tokens\n    ('center top 10%', (('left', (50, '%'), 'top', (10, '%')),)),\n    ('top 10% center', (('left', (50, '%'), 'top', (10, '%')),)),\n    ('center bottom 10%', (('left', (50, '%'), 'bottom', (10, '%')),)),\n    ('bottom 10% center', (('left', (50, '%'), 'bottom', (10, '%')),)),\n\n    ('right top 10%', (('right', (0, '%'), 'top', (10, '%')),)),\n    ('top 10% right', (('right', (0, '%'), 'top', (10, '%')),)),\n    ('right bottom 10%', (('right', (0, '%'), 'bottom', (10, '%')),)),\n    ('bottom 10% right', (('right', (0, '%'), 'bottom', (10, '%')),)),\n\n    ('center left 10%', (('left', (10, '%'), 'top', (50, '%')),)),\n    ('left 10% center', (('left', (10, '%'), 'top', (50, '%')),)),\n    ('center right 10%', (('right', (10, '%'), 'top', (50, '%')),)),\n    ('right 10% center', (('right', (10, '%'), 'top', (50, '%')),)),\n\n    ('bottom left 10%', (('left', (10, '%'), 'bottom', (0, '%')),)),\n    ('left 10% bottom', (('left', (10, '%'), 'bottom', (0, '%')),)),\n    ('bottom right 10%', (('right', (10, '%'), 'bottom', (0, '%')),)),\n    ('right 10% bottom', (('right', (10, '%'), 'bottom', (0, '%')),)),\n\n    # Four tokens\n    ('left 10% bottom 3PX', (('left', (10, '%'), 'bottom', (3, 'px')),)),\n    ('bottom 3px left 10%', (('left', (10, '%'), 'bottom', (3, 'px')),)),\n    ('right 10% top 3px', (('right', (10, '%'), 'top', (3, 'px')),)),\n    ('top 3px right 10%', (('right', (10, '%'), 'top', (3, 'px')),)),\n    *tuple(\n        (css_x, (('left', val_x, 'top', (50, '%')),))\n        for css_x, val_x in (\n                ('left', (0, '%')), ('center', (50, '%')), ('right', (100, '%')),\n                ('4.5%', (4.5, '%')), ('12px', (12, 'px')))\n    ),\n    *tuple(\n        (f'{css_x} {css_y}', (('left', val_x, 'top', val_y),))\n        for css_x, val_x in (\n                ('left', (0, '%')), ('center', (50, '%')), ('right', (100, '%')),\n                ('4.5%', (4.5, '%')), ('12px', (12, 'px')))\n        for css_y, val_y in (\n                ('top', (0, '%')), ('center', (50, '%')), ('bottom', (100, '%')),\n                ('7%', (7, '%')), ('1.5px', (1.5, 'px')))\n    ),\n])\ndef test_background_position(rule, value):\n    assert get_value(f'background-position: {rule}') == value\n\n\n@pytest.mark.parametrize('rule', [\n    '10px lipsum',\n    'left center 3px',\n    '3px left',\n    'bottom 4%',\n    'bottom top'\n])\ndef test_background_position_invalid(rule):\n    assert_invalid(f'background-position: {rule}')\n\n\n@pytest.mark.parametrize('rule', [\n    ('\"My\" Font, serif'),\n    ('\"My\" \"Font\", serif'),\n    ('\"My\", 12pt, serif'),\n])\ndef test_font_family_invalid(rule):\n    assert_invalid(f'font-family: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('1px', (1, 'px')),\n    ('1.1%', (1.1, '%')),\n    ('1Em', (1, 'em')),\n    ('1', (1, None)),\n    ('1.3', (1.3, None)),\n    ('-0', (0, None)),\n    ('0px', (0, 'px')),\n])\ndef test_line_height(rule, value):\n    assert get_value(f'line-height: {rule}') == value\n\n\n@pytest.mark.parametrize('rule', [\n    '1deg',\n    '-1px',\n    '-1',\n    '-0.5%',\n    '1px 1px',\n])\ndef test_line_height_invalid(rule):\n    assert_invalid(f'line-height: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'symbols()',\n    'symbols(cyclic)',\n    'symbols(symbolic)',\n    'symbols(fixed)',\n    'symbols(alphabetic \"a\")',\n    'symbols(numeric \"1\")',\n    'symbols(test \"a\" \"b\")',\n    'symbols(fixed symbolic \"a\" \"b\")',\n])\ndef test_list_style_type_invalid(rule):\n    assert_invalid(f'list-style-type: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('none', 'none'),\n    ('from-image', 'from-image'),\n    ('90deg', (pi / 2, False)),\n    ('30deg', (pi / 6, False)),\n    ('180deg flip', (pi, True)),\n    ('0deg flip', (0, True)),\n    ('flip 90deg', (pi / 2, True)),\n    ('flip', (0, True)),\n])\ndef test_image_orientation(rule, value):\n    assert get_value(f'image-orientation: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none none',\n    'unknown',\n    'none flip',\n    'from-image flip',\n    '10',\n    '10 flip',\n    'flip 10',\n    'flip flip',\n    '90deg flop',\n    '90deg 180deg',\n])\ndef test_image_orientation_invalid(rule):\n    assert_invalid(f'image-orientation: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('1', ((1, None),)),\n    ('1 2    3 4', ((1, None), (2, None), (3, None), (4, None))),\n    ('50% 1000.1 0', ((50, '%'), (1000.1, None), (0, None))),\n    ('1% 2% 3% 4%', ((1, '%'), (2, '%'), (3, '%'), (4, '%'))),\n    ('fill 10% 20', ('fill', (10, '%'), (20, None))),\n    ('0 1 0.5 fill', ((0, None), (1, None), (0.5, None), 'fill')),\n])\ndef test_border_image_slice(rule, value):\n    assert get_value(f'border-image-slice: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    '1, 2',\n    '-10',\n    '-10%',\n    '1 2 3 -10%',\n    '-0.3',\n    '1 fill 2',\n    'fill 1 2 3 fill',\n])\ndef test_border_image_slice_invalid(rule):\n    assert_invalid(f'border-image-slice: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('1', ((1, None),)),\n    ('1 2    3 4', ((1, None), (2, None), (3, None), (4, None))),\n    ('50% 1000.1 0', ((50, '%'), (1000.1, None), (0, None))),\n    ('1% 2px 3em 4', ((1, '%'), (2, 'px'), (3, 'em'), (4, None))),\n    ('auto', ('auto',)),\n    ('1 auto', ((1, None), 'auto')),\n    ('auto auto', ('auto', 'auto')),\n    ('auto auto auto 2', ('auto', 'auto', 'auto', (2, None))),\n])\ndef test_border_image_width(rule, value):\n    assert get_value(f'border-image-width: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    '1, 2',\n    '1 -2',\n    '-10',\n    '-10%',\n    '1px 2px 3px -10%',\n    '-3px',\n    'auto auto auto auto auto',\n    '1 2 3 4 5',\n])\ndef test_border_image_width_invalid(rule):\n    assert_invalid(f'border-image-width: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('1', ((1, None),)),\n    ('1 2    3 4', ((1, None), (2, None), (3, None), (4, None))),\n    ('50px 1000.1 0', ((50, 'px'), (1000.1, None), (0, None))),\n    ('1in 2px 3em 4', ((1, 'in'), (2, 'px'), (3, 'em'), (4, None))),\n])\ndef test_border_image_outset(rule, value):\n    assert get_value(f'border-image-outset: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    'auto',\n    '1, 2',\n    '-10',\n    '1 -2',\n    '10%',\n    '1px 2px 3px -10px',\n    '-3px',\n    '1 2 3 4 5',\n])\ndef test_border_image_outset_invalid(rule):\n    assert_invalid(f'border-image-outset: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('stretch', ('stretch',)),\n    ('repeat repeat', ('repeat', 'repeat')),\n    ('round     space', ('round', 'space')),\n])\ndef test_border_image_repeat(rule, value):\n    assert get_value(f'border-image-repeat: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    'test',\n    'round round round',\n    'stretch space round',\n    'repeat test',\n])\ndef test_border_image_repeat_invalid(rule):\n    assert_invalid(f'border-image-repeat: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('1', ((1, None),)),\n    ('1 2    3 4', ((1, None), (2, None), (3, None), (4, None))),\n    ('50% 1000.1 0', ((50, '%'), (1000.1, None), (0, None))),\n    ('1% 2% 3% 4%', ((1, '%'), (2, '%'), (3, '%'), (4, '%'))),\n    ('fill 10% 20', ('fill', (10, '%'), (20, None))),\n    ('0 1 0.5 fill', ((0, None), (1, None), (0.5, None), 'fill')),\n])\ndef test_mask_border_slice(rule, value):\n    assert get_value(f'mask-border-slice: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    '1, 2',\n    '-10',\n    '-10%',\n    '1 2 3 -10%',\n    '-0.3',\n    '1 fill 2',\n    'fill 1 2 3 fill',\n])\ndef test_mask_border_slice_invalid(rule):\n    assert_invalid(f'mask-border-slice: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('1', ((1, None),)),\n    ('1 2    3 4', ((1, None), (2, None), (3, None), (4, None))),\n    ('50% 1000.1 0', ((50, '%'), (1000.1, None), (0, None))),\n    ('1% 2px 3em 4', ((1, '%'), (2, 'px'), (3, 'em'), (4, None))),\n    ('auto', ('auto',)),\n    ('1 auto', ((1, None), 'auto')),\n    ('auto auto', ('auto', 'auto')),\n    ('auto auto auto 2', ('auto', 'auto', 'auto', (2, None))),\n])\ndef test_mask_border_width(rule, value):\n    assert get_value(f'mask-border-width: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    '1, 2',\n    '1 -2',\n    '-10',\n    '-10%',\n    '1px 2px 3px -10%',\n    '-3px',\n    'auto auto auto auto auto',\n    '1 2 3 4 5',\n])\ndef test_mask_border_width_invalid(rule):\n    assert_invalid(f'mask-border-width: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('1', ((1, None),)),\n    ('1 2    3 4', ((1, None), (2, None), (3, None), (4, None))),\n    ('50px 1000.1 0', ((50, 'px'), (1000.1, None), (0, None))),\n    ('1in 2px 3em 4', ((1, 'in'), (2, 'px'), (3, 'em'), (4, None))),\n])\ndef test_mask_border_outset(rule, value):\n    assert get_value(f'mask-border-outset: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    'auto',\n    '1, 2',\n    '-10',\n    '1 -2',\n    '10%',\n    '1px 2px 3px -10px',\n    '-3px',\n    '1 2 3 4 5',\n])\ndef test_mask_border_outset_invalid(rule):\n    assert_invalid(f'mask-border-outset: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('stretch', ('stretch',)),\n    ('repeat repeat', ('repeat', 'repeat')),\n    ('round     space', ('round', 'space')),\n])\ndef test_mask_border_repeat(rule, value):\n    assert get_value(f'mask-border-repeat: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    'test',\n    'round round round',\n    'stretch space round',\n    'repeat test',\n])\ndef test_mask_border_repeat_invalid(rule):\n    assert_invalid(f'mask-border-repeat: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('alpha', 'alpha'),\n    ('luminance', 'luminance'),\n    ('alpha     ', 'alpha'),\n])\ndef test_mask_border_mode(rule, value):\n    assert get_value(f'mask-border-mode: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    'test',\n    'alpha alpha',\n    'alpha luminance',\n])\ndef test_mask_border_mode_invalid(rule):\n    assert_invalid(f'mask-border-mode: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('test content(text)', (('test', (('content()', 'text'),)),)),\n    ('test content(before)', (('test', (('content()', 'before'),)),)),\n    ('test \"string\"', (('test', (('string', 'string'),)),)),\n    ('test1 \"string\", test2 \"string\"', (\n        ('test1', (('string', 'string'),)),\n        ('test2', (('string', 'string'),)))),\n    ('test attr(class)', (('test', (('attr()', ('class', 'string', '')),)),)),\n    ('test attr(class url)', (('test', (('attr()', ('class', 'url', '')),)),)),\n    ('test attr(class, \"test\")', (\n        ('test', (('attr()', ('class', 'string', 'test')),)),)),\n    ('test attr(class string, \"test\")', (\n        ('test', (('attr()', ('class', 'string', 'test')),)),)),\n    ('test counter(count)', (\n        ('test', (('counter()', ('count', 'decimal')),)),)),\n    ('test counter(count, upper-roman)', (\n        ('test', (('counter()', ('count', 'upper-roman')),)),)),\n    ('test counters(count, \".\")', (\n        ('test', (('counters()', ('count', '.', 'decimal')),)),)),\n    ('test counters(count, \".\", upper-roman)', (\n        ('test', (('counters()', ('count', '.', 'upper-roman')),)),)),\n    ('test content(text) \"string\" attr(title) attr(title) counter(count)', (\n        ('test', (\n            ('content()', 'text'), ('string', 'string'),\n            ('attr()', ('title', 'string', '')),\n            ('attr()', ('title', 'string', '')),\n            ('counter()', ('count', 'decimal')))),)),\n])\ndef test_string_set(rule, value):\n    assert get_value(f'string-set: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'test',\n    'test test1',\n    'test content(test)',\n    'test unknown()',\n])\ndef test_string_set_invalid(rule):\n    assert_invalid(f'string-set: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('normal', 'normal'),\n    ('break-word', 'break-word'),\n    ('inherit', 'inherit'),\n])\ndef test_overflow_wrap(rule, value):\n    assert get_value(f'overflow-wrap: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    'normal, break-word',\n])\ndef test_overflow_wrap_invalid(rule):\n    assert_invalid(f'overflow-wrap: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('blue', ()),\n    ('red', (None, ((1, 0, 0, 1),))),\n    ('blue 1%, lime,red 2em ', (\n        None,\n        ((0, 0, 1, 1), (0, 1, 0, 1), (1, 0, 0, 1)),\n        ((1, '%'), None, (2, 'em')))),\n    ('18deg, blue', (('angle', pi / 10),)),\n    ('4rad, blue', (('angle', 4),)),\n    ('.25turn, blue', (('angle', pi / 2),)),\n    ('100grad, blue', (('angle', pi / 2),)),\n    ('12rad, blue 1%, lime,red 2em ', (\n        ('angle', 12),\n        ((0, 0, 1, 1), (0, 1, 0, 1), (1, 0, 0, 1)),\n        ((1, '%'), None, (2, 'em')))),\n    ('to top, blue', (('angle', 0),)),\n    ('to right, blue', (('angle', pi / 2),)),\n    ('to bottom, blue', (('angle', pi),)),\n    ('to left, blue', (('angle', pi * 3 / 2),)),\n    ('to right, blue 1%, lime,red 2em ', (\n        ('angle', pi / 2),\n        ((0, 0, 1, 1), (0, 1, 0, 1), (1, 0, 0, 1)),\n        ((1, '%'), None, (2, 'em')))),\n    ('to top left, blue', (('corner', 'top_left'),)),\n    ('to left top, blue', (('corner', 'top_left'),)),\n    ('to top right, blue', (('corner', 'top_right'),)),\n    ('to right top, blue', (('corner', 'top_right'),)),\n    ('to bottom left, blue', (('corner', 'bottom_left'),)),\n    ('to left bottom, blue', (('corner', 'bottom_left'),)),\n    ('to bottom right, blue', (('corner', 'bottom_right'),)),\n    ('to right bottom, blue', (('corner', 'bottom_right'),)),\n])\ndef test_linear_gradient(rule, value):\n    direction = get_default_value(value, 0, ('angle', pi))\n    colors = get_default_value(value, 1, ((0, 0, 1, 1),))\n    stop_positions = get_default_value(value, 2, (None,))\n    for repeating, prefix in ((False, ''), (True, 'repeating-')):\n        (type_, image), = get_value(\n            f'background-image: {prefix}linear-gradient({rule})')\n        assert type_ == 'linear-gradient'\n        assert isinstance(image, LinearGradient)\n        assert image.repeating == repeating\n        assert image.direction_type == direction[0]\n        if isinstance(image.direction, str):\n            image.direction == direction[1]\n        else:\n            assert image.direction == pytest.approx(direction[1])\n        assert image.colors == tuple(colors)\n        assert image.stop_positions == tuple(stop_positions)\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    ' ',\n    '1% blue',\n    'blue 10deg',\n    'blue 4',\n    'soylent-green 4px',\n    'red 4px 2px',\n    '18deg',\n    '10arc-minutes, blue',\n    '10px, blue',\n    'to 90deg, blue',\n    'to the top, blue',\n    'to up, blue',\n    'into top, blue',\n    'top, blue',\n    'to bottom up, blue',\n    'bottom left, blue',\n])\ndef test_linear_gradient_invalid(rule):\n    assert_invalid(f'background-image: linear-gradient({rule})')\n    assert_invalid(f'background-image: repeating-linear-gradient({rule})')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('blue', ()),\n    ('red', (None, None, None, ((1, 0, 0, 1),))),\n    ('blue 1%, lime,red 2em ', (\n        None, None, None, ((0, 0, 1, 1), (0, 1, 0, 1), (1, 0, 0, 1)),\n        ((1, '%'), None, (2, 'em')))),\n    ('circle, blue', ('circle',)),\n    ('ellipse, blue', ()),\n    ('ellipse closest-corner, blue', (\n        'ellipse', ('keyword', 'closest-corner'))),\n    ('circle closest-side, blue', (\n        'circle', ('keyword', 'closest-side'))),\n    ('farthest-corner circle, blue', (\n        'circle', ('keyword', 'farthest-corner'))),\n    ('farthest-side, blue', (None, (('keyword', 'farthest-side')))),\n    ('5ch, blue', ('circle', ('explicit', ((5, 'ch'), (5, 'ch'))))),\n    ('5ch circle, blue', ('circle', ('explicit', ((5, 'ch'), (5, 'ch'))))),\n    ('circle 5ch, blue', ('circle', ('explicit', ((5, 'ch'), (5, 'ch'))))),\n    ('10px 50px, blue', (None, ('explicit', ((10, 'px'), (50, 'px'))))),\n    ('10px 50px ellipse, blue', (\n        None, ('explicit', ((10, 'px'), (50, 'px'))))),\n    ('ellipse 10px 50px, blue', (\n        None, ('explicit', ((10, 'px'), (50, 'px'))))),\n    ('at top 10% right, blue', (\n        None, None, ('right', (0, '%'), 'top', (10, '%')))),\n    ('circle at bottom, blue', (\n        'circle', None, ('left', (50, '%'), 'top', (100, '%')))),\n    ('circle at 10px, blue', (\n        'circle', None, ('left', (10, 'px'), 'top', (50, '%')))),\n    ('closest-side circle at right 5em, blue', (\n        'circle', ('keyword', 'closest-side'),\n        ('left', (100, '%'), 'top', (5, 'em')))),\n])\ndef test_radial_gradient(rule, value):\n    shape = get_default_value(value, 0, 'ellipse')\n    size = get_default_value(value, 1, ('keyword', 'farthest-corner'))\n    center = get_default_value(value, 2, ('left', (50, '%'), 'top', (50, '%')))\n    colors = get_default_value(value, 3, ((0, 0, 1, 1),))\n    stop_positions = get_default_value(value, 4, (None,))\n    for repeating, prefix in ((False, ''), (True, 'repeating-')):\n        (type_, image), = get_value(\n            f'background-image: {prefix}radial-gradient({rule})')\n        assert type_ == 'radial-gradient'\n        assert isinstance(image, RadialGradient)\n        assert image.repeating == repeating\n        assert image.shape == shape\n        assert image.size_type == size[0]\n        assert image.size == size[1]\n        assert image.center == center\n        assert image.colors == tuple(colors)\n        assert image.stop_positions == tuple(stop_positions)\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    ' ',\n    '1% blue',\n    'blue 10deg',\n    'blue 4',\n    'soylent-green 4px',\n    'red 4px 2px',\n    'circle',\n    'square, blue',\n    'closest-triangle, blue',\n    'center, blue',\n    'ellipse 5ch',\n    '5ch ellipse',\n    'circle 10px 50px, blue',\n    '10px 50px circle, blue',\n    '10%, blue',\n    '10% circle, blue',\n    'circle 10%, blue',\n    'at appex, blue',\n])\ndef test_radial_gradient_invalid(rule):\n    assert_invalid(f'background-image: radial-gradient({rule})')\n    assert_invalid(f'background-image: repeating-radial-gradient({rule})')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('40px', ((40, 'px'),)),\n    ('2fr', ((2, 'fr'),)),\n    ('18%', ((18, '%'),)),\n    ('auto', ('auto',)),\n    ('min-content', ('min-content',)),\n    ('max-content', ('max-content',)),\n    ('fit-content(20%)', (('fit-content()', (20, '%')),)),\n    ('minmax(20px, 25px)', (('minmax()', (20, 'px'), (25, 'px')),)),\n    ('minmax(min-content, max-content)',\n     (('minmax()', 'min-content', 'max-content'),)),\n    ('min-content max-content', ('min-content', 'max-content')),\n])\ndef test_grid_auto_columns_rows(rule, value):\n    assert get_value(f'grid-auto-columns: {rule}') == value\n    assert get_value(f'grid-auto-rows: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    '40',\n    'coucou',\n    'fit-content',\n    'fit-content(min-content)',\n    'minmax(40px)',\n    'minmax(2fr, 1fr)',\n    '1fr 1fr coucou',\n    'fit-content()',\n    'fit-content(2%, 18%)',\n])\ndef test_grid_auto_columns_rows_invalid(rule):\n    assert_invalid(f'grid-auto-columns: {rule}')\n    assert_invalid(f'grid-auto-rows: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('row', ('row',)),\n    ('column', ('column',)),\n    ('row dense', ('row', 'dense')),\n    ('column dense', ('column', 'dense')),\n    ('dense row', ('dense', 'row')),\n    ('dense column', ('dense', 'column')),\n    ('dense', ('dense', 'row')),\n])\ndef test_grid_auto_flow(rule, value):\n    assert get_value(f'grid-auto-flow: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'row row',\n    'column column',\n    'dense dense',\n    'coucou',\n    'row column',\n    'column row',\n    'row coucou',\n    'column coucou',\n    'coucou row',\n    'coucou column',\n    'row column dense',\n])\ndef test_grid_auto_flow_invalid(rule):\n    assert_invalid(f'grid-auto-flow: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('none', 'none'),\n    ('subgrid', ('subgrid', ())),\n    ('subgrid [a] repeat(auto-fill, [b]) [c]',\n     ('subgrid', (('a',), ('repeat()', 'auto-fill', (('b',),)), ('c',)))),\n    ('subgrid [a] [a] [a] [a] repeat(auto-fill, [b]) [c] [c]',\n     ('subgrid', (('a',), ('a',), ('a',), ('a',),\n      ('repeat()', 'auto-fill', (('b',),)), ('c',), ('c',)))),\n    ('subgrid [] [a]', ('subgrid', ((), ('a',)))),\n    ('subgrid [a] [b] [c] [d] [e] [f]',\n     ('subgrid', (('a',), ('b',), ('c',), ('d',), ('e',), ('f',)))),\n    ('[outer-edge] 20px [main-start] 1fr [center] 1fr max-content [main-end]',\n     (('outer-edge',), (20, 'px'), ('main-start',), (1, 'fr'), ('center',),\n      (1, 'fr'), (), 'max-content', ('main-end',))),\n    ('repeat(auto-fill, minmax(25ch, 1fr))',\n     ((), ('repeat()', 'auto-fill', (\n         (), ('minmax()', (25, 'ch'), (1, 'fr')), ())), ())),\n    ('[a] auto [b] minmax(min-content, 1fr) [b c d] '\n     'repeat(2, [e] 40px) repeat(5, auto)',\n     (('a',), 'auto', ('b',), ('minmax()', 'min-content', (1, 'fr')),\n      ('b', 'c', 'd'), ('repeat()', 2, (('e',), (40, 'px'), ())),\n      (), ('repeat()', 5, ((), 'auto', ())), ())),\n])\ndef test_grid_template_columns_rows(rule, value):\n    assert get_value(f'grid-template-columns: {rule}') == value\n    assert get_value(f'grid-template-rows: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'coucou',\n    'subgrid subgrid',\n    'subgrid coucou',\n    'subgrid [coucou] repeat(0, [wow])',\n    'subgrid [coucou] repeat(auto-fit [wow])',\n    'fit-content(18%) repeat(auto-fill, 15em)',\n    '[coucou] [wow]',\n])\ndef test_grid_template_columns_rows_invalid(rule):\n    assert_invalid(f'grid-template-columns: {rule}')\n    assert_invalid(f'grid-template-rows: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('none', 'none'),\n    ('\"head head\" \"nav main\" \"foot ....\"',\n     (('head', 'head'), ('nav', 'main'), ('foot', None))),\n    ('\"title board\" \"stats board\"',\n     (('title', 'board'), ('stats', 'board'))),\n    ('\". a\" \"b a\" \".a\"',\n     ((None, 'a'), ('b', 'a'), (None, 'a'))),\n    ('\"a b b\" \"c b b\" \"d e f\"',\n     (('a', 'b', 'b'), ('c', 'b', 'b'), ('d', 'e', 'f'))),\n])\ndef test_grid_template_areas(rule, value):\n    assert get_value(f'grid-template-areas: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    '\"head head coucou\" \"nav main\" \"foot ....\"',\n    '\". a\" \"b c\" \". a\"',\n    '\". a\" \"b a\" \"a a\"',\n    '\"a a a a\" \"a b b a\" \"a a a a\"',\n    '\" \"',\n])\ndef test_grid_template_areas_invalid(rule):\n    assert_invalid(f'grid-template-areas: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('auto', 'auto'),\n    ('4', (None, 4, None)),\n    ('C', (None, None, 'C')),\n    ('4 c', (None, 4, 'c')),\n    ('col -4', (None, -4, 'col')),\n    ('span c 4', ('span', 4, 'c')),\n    ('span 4 c', ('span', 4, 'c')),\n    ('4 span C', ('span', 4, 'C')),\n    ('super 4 span', ('span', 4, 'super')),\n])\ndef test_grid_line(rule, value):\n    assert get_value(f'grid-row-start: {rule}') == value\n    assert get_value(f'grid-row-end: {rule}') == value\n    assert get_value(f'grid-column-start: {rule}') == value\n    assert get_value(f'grid-column-end: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'span',\n    '0',\n    '1.1',\n    'span 0',\n    'span -1',\n    'span 2.1',\n    'span auto',\n    'auto auto',\n    '-4 cOL span',\n    'span 1.1 col',\n])\ndef test_grid_line_invalid(rule):\n    assert_invalid(f'grid-row-start: {rule}')\n    assert_invalid(f'grid-row-end: {rule}')\n    assert_invalid(f'grid-column-start: {rule}')\n    assert_invalid(f'grid-column-end: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('normal', ('normal',)),\n    ('baseline', ('first', 'baseline')),\n    ('first baseline', ('first', 'baseline')),\n    ('last baseline', ('last', 'baseline')),\n    ('baseline last', ('baseline', 'last')),\n    ('space-between', ('space-between',)),\n    ('space-around', ('space-around',)),\n    ('space-evenly', ('space-evenly',)),\n    ('stretch', ('stretch',)),\n    ('center', ('center',)),\n    ('start', ('start',)),\n    ('end', ('end',)),\n    ('flex-start', ('flex-start',)),\n    ('flex-end', ('flex-end',)),\n    ('safe center', ('safe', 'center')),\n    ('unsafe start', ('unsafe', 'start')),\n    ('safe end', ('safe', 'end')),\n    ('safe flex-start', ('safe', 'flex-start')),\n    ('unsafe flex-start', ('unsafe', 'flex-start')),\n])\ndef test_align_content(rule, value):\n    assert get_value(f'align-content: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'auto',\n    'none',\n    'auto auto',\n    'first last',\n    'baseline baseline',\n    'start safe',\n    'start end',\n    'safe unsafe',\n    'left',\n    'right',\n])\ndef test_align_content_invalid(rule):\n    assert_invalid(f'align-content: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('normal', ('normal',)),\n    ('stretch', ('stretch',)),\n    ('baseline', ('first', 'baseline')),\n    ('first baseline', ('first', 'baseline')),\n    ('last baseline', ('last', 'baseline')),\n    ('baseline last', ('baseline', 'last')),\n    ('center', ('center',)),\n    ('self-start', ('self-start',)),\n    ('self-end', ('self-end',)),\n    ('start', ('start',)),\n    ('end', ('end',)),\n    ('flex-start', ('flex-start',)),\n    ('flex-end', ('flex-end',)),\n    ('safe center', ('safe', 'center')),\n    ('unsafe start', ('unsafe', 'start')),\n    ('safe end', ('safe', 'end')),\n    ('unsafe self-start', ('unsafe', 'self-start')),\n    ('safe self-end', ('safe', 'self-end')),\n    ('safe flex-start', ('safe', 'flex-start')),\n    ('unsafe flex-start', ('unsafe', 'flex-start')),\n])\ndef test_align_items(rule, value):\n    assert get_value(f'align-items: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'auto',\n    'none',\n    'auto auto',\n    'first last',\n    'baseline baseline',\n    'start safe',\n    'start end',\n    'safe unsafe',\n    'left',\n    'right',\n    'space-between',\n])\ndef test_align_items_invalid(rule):\n    assert_invalid(f'align-items: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('auto', ('auto',)),\n    ('normal', ('normal',)),\n    ('stretch', ('stretch',)),\n    ('baseline', ('first', 'baseline')),\n    ('first baseline', ('first', 'baseline')),\n    ('last baseline', ('last', 'baseline')),\n    ('baseline last', ('baseline', 'last')),\n    ('center', ('center',)),\n    ('self-start', ('self-start',)),\n    ('self-end', ('self-end',)),\n    ('start', ('start',)),\n    ('end', ('end',)),\n    ('flex-start', ('flex-start',)),\n    ('flex-end', ('flex-end',)),\n    ('safe center', ('safe', 'center')),\n    ('unsafe start', ('unsafe', 'start')),\n    ('safe end', ('safe', 'end')),\n    ('unsafe self-start', ('unsafe', 'self-start')),\n    ('safe self-end', ('safe', 'self-end')),\n    ('safe flex-start', ('safe', 'flex-start')),\n    ('unsafe flex-start', ('unsafe', 'flex-start')),\n])\ndef test_align_self(rule, value):\n    assert get_value(f'align-self: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    'auto auto',\n    'first last',\n    'baseline baseline',\n    'start safe',\n    'start end',\n    'safe unsafe',\n    'left',\n    'right',\n    'space-between',\n])\ndef test_align_self_invalid(rule):\n    assert_invalid(f'align-self: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('normal', ('normal',)),\n    ('space-between', ('space-between',)),\n    ('space-around', ('space-around',)),\n    ('space-evenly', ('space-evenly',)),\n    ('stretch', ('stretch',)),\n    ('center', ('center',)),\n    ('left', ('left',)),\n    ('right', ('right',)),\n    ('start', ('start',)),\n    ('end', ('end',)),\n    ('flex-start', ('flex-start',)),\n    ('flex-end', ('flex-end',)),\n    ('safe center', ('safe', 'center')),\n    ('unsafe start', ('unsafe', 'start')),\n    ('safe end', ('safe', 'end')),\n    ('unsafe left', ('unsafe', 'left')),\n    ('safe right', ('safe', 'right')),\n    ('safe flex-start', ('safe', 'flex-start')),\n    ('unsafe flex-start', ('unsafe', 'flex-start')),\n])\ndef test_justify_content(rule, value):\n    assert get_value(f'justify-content: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'auto',\n    'none',\n    'baseline',\n    'auto auto',\n    'first last',\n    'baseline baseline',\n    'start safe',\n    'start end',\n    'safe unsafe',\n])\ndef test_justify_content_invalid(rule):\n    assert_invalid(f'justify-content: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('normal', ('normal',)),\n    ('stretch', ('stretch',)),\n    ('baseline', ('first', 'baseline')),\n    ('first baseline', ('first', 'baseline')),\n    ('last baseline', ('last', 'baseline')),\n    ('baseline last', ('baseline', 'last')),\n    ('center', ('center',)),\n    ('self-start', ('self-start',)),\n    ('self-end', ('self-end',)),\n    ('start', ('start',)),\n    ('end', ('end',)),\n    ('left', ('left',)),\n    ('right', ('right',)),\n    ('flex-start', ('flex-start',)),\n    ('flex-end', ('flex-end',)),\n    ('safe center', ('safe', 'center')),\n    ('unsafe start', ('unsafe', 'start')),\n    ('safe end', ('safe', 'end')),\n    ('unsafe self-start', ('unsafe', 'self-start')),\n    ('safe self-end', ('safe', 'self-end')),\n    ('safe flex-start', ('safe', 'flex-start')),\n    ('unsafe flex-start', ('unsafe', 'flex-start')),\n    ('legacy', ('legacy',)),\n    ('legacy left', ('legacy', 'left')),\n    ('left legacy', ('left', 'legacy')),\n    ('legacy center', ('legacy', 'center')),\n])\ndef test_justify_items(rule, value):\n    assert get_value(f'justify-items: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'auto',\n    'none',\n    'auto auto',\n    'first last',\n    'baseline baseline',\n    'start safe',\n    'start end',\n    'safe unsafe',\n    'space-between',\n])\ndef test_justify_items_invalid(rule):\n    assert_invalid(f'justify-items: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('auto', ('auto',)),\n    ('normal', ('normal',)),\n    ('stretch', ('stretch',)),\n    ('baseline', ('first', 'baseline')),\n    ('first baseline', ('first', 'baseline')),\n    ('last baseline', ('last', 'baseline')),\n    ('baseline last', ('baseline', 'last')),\n    ('center', ('center',)),\n    ('self-start', ('self-start',)),\n    ('self-end', ('self-end',)),\n    ('start', ('start',)),\n    ('end', ('end',)),\n    ('left', ('left',)),\n    ('right', ('right',)),\n    ('flex-start', ('flex-start',)),\n    ('flex-end', ('flex-end',)),\n    ('safe center', ('safe', 'center')),\n    ('unsafe start', ('unsafe', 'start')),\n    ('safe end', ('safe', 'end')),\n    ('unsafe left', ('unsafe', 'left')),\n    ('safe right', ('safe', 'right')),\n    ('unsafe self-start', ('unsafe', 'self-start')),\n    ('safe self-end', ('safe', 'self-end')),\n    ('safe flex-start', ('safe', 'flex-start')),\n    ('unsafe flex-start', ('unsafe', 'flex-start')),\n])\ndef test_justify_self(rule, value):\n    assert get_value(f'justify-self: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'none',\n    'auto auto',\n    'first last',\n    'baseline baseline',\n    'start safe',\n    'start end',\n    'safe unsafe',\n    'space-between',\n])\ndef test_justify_self_invalid(rule):\n    assert_invalid(f'justify-self: {rule}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'color',\n    'background',\n    'background-color',\n    'border-color',\n    'border-left-color',\n    'outline-color',\n])\n@pytest.mark.parametrize('value', [\n    'rgb()',\n    'device-cmyk(0%, 0%, 0%, 30%)',\n    '#abcde',\n])\ndef test_colors_invalid(rule, value):\n    assert_invalid(f'{rule}: {value}')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'value'), [\n    ('normal', 'normal'),\n    ('light', ('light',)),\n    ('dark', ('dark',)),\n    ('light dark', ('light', 'dark')),\n    ('dark light', ('dark', 'light')),\n    ('light only', ('light', 'only')),\n    ('only light', ('light', 'only')),\n    ('dark dark', ('dark', 'dark')),\n    ('light something', ('light', 'something')),\n    ('only dark light', ('dark', 'light', 'only')),\n    ('only something light', ('something', 'light', 'only')),\n])\ndef test_color_scheme(rule, value):\n    assert get_value(f'color-scheme: {rule}') == value\n\n\n@assert_no_logs\n@pytest.mark.parametrize('rule', [\n    'normal only',\n    'normal something',\n    'only',\n    'only only',\n    'light only dark',\n])\ndef test_color_scheme_invalid(rule):\n    assert_invalid(f'color-scheme: {rule}')\n"
  },
  {
    "path": "tests/css/test_variables.py",
    "content": "\"\"\"Test CSS custom properties, also known as CSS variables.\"\"\"\n\nimport pytest\n\nfrom weasyprint.css.properties import KNOWN_PROPERTIES\n\nfrom ..testing_utils import assert_no_logs, capture_logs, render_pages\n\nSIDES = ('top', 'right', 'bottom', 'left')\n\n\n@assert_no_logs\ndef test_variable_simple():\n    page, = render_pages('''\n      <style>\n        p { --var: 10px; width: var(--var) }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 10\n\n\n@assert_no_logs\ndef test_variable_not_computed():\n    page, = render_pages('''\n      <style>\n        p { --var: 1rem; width: var(--var) }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 16\n\n\n@assert_no_logs\ndef test_variable_inherit():\n    page, = render_pages('''\n      <style>\n        html { --var: 10px }\n        p { width: var(--var) }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 10\n\n\n@assert_no_logs\ndef test_variable_inherit_override():\n    page, = render_pages('''\n      <style>\n        html { --var: 20px }\n        p { width: var(--var); --var: 10px }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 10\n\n\n@assert_no_logs\ndef test_variable_default_unknown():\n    page, = render_pages('''\n      <style>\n        p { width: var(--x, 10px) }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 10\n\n\n@assert_no_logs\ndef test_variable_default_var():\n    page, = render_pages('''\n      <style>\n        p { --var: 10px; width: var(--x, var(--var)) }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 10\n\n\n@assert_no_logs\ndef test_variable_case_sensitive():\n    page, = render_pages('''\n      <style>\n        html { --var: 20px }\n        body { --VAR: 10px }\n        p { width: var(--VAR) }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 10\n\n\n@assert_no_logs\ndef test_variable_chain():\n    page, = render_pages('''\n      <style>\n        html { --foo: 10px }\n        body { --var: var(--foo) }\n        p { width: var(--var) }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 10\n\n\n@assert_no_logs\ndef test_variable_double_chain():\n    page, = render_pages('''\n      <style>\n        html { --foo: red }\n        body { --var: var(--foo), var(--foo) }\n        div { background-image: linear-gradient(var(--var)) }\n      </style>\n      <div></dib>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.style['background_image'][0][1].colors[0] == (1, 0, 0, 1)\n    assert div.style['background_image'][0][1].colors[1] == (1, 0, 0, 1)\n\n\n@assert_no_logs\ndef test_variable_chain_root():\n    # Regression test for #1656.\n    page, = render_pages('''\n      <style>\n        html { --var2: 10px; --var1: var(--var2); width: var(--var1) }\n      </style>\n    ''')\n    html, = page.children\n    assert html.width == 10\n\n\ndef test_variable_self():\n    page, = render_pages('''\n      <style>\n        html { --var1: var(--var1) }\n      </style>\n    ''')\n\n\ndef test_variable_loop():\n    page, = render_pages('''\n      <style>\n        html { --var1: var(--var2); --var2: var(--var1); padding: var(--var1) }\n      </style>\n    ''')\n\n\ndef test_variable_chain_root_missing():\n    # Regression test for #1656.\n    page, = render_pages('''\n      <style>\n        html { --var1: var(--var-missing); width: var(--var1) }\n      </style>\n    ''')\n\n\ndef test_variable_chain_root_missing_inherited():\n    # Regression test for #2164.\n    page, = render_pages('''\n      <style>\n        html { --var1: var(--var-missing); font: var(--var1) }\n      </style>a\n    ''')\n\n\n@assert_no_logs\ndef test_variable_shorthand_margin():\n    page, = render_pages('''\n      <style>\n        html { --var: 10px }\n        div { margin: 0 0 0 var(--var) }\n      </style>\n      <div></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.margin_top == 0\n    assert div.margin_right == 0\n    assert div.margin_bottom == 0\n    assert div.margin_left == 10\n\n\n@assert_no_logs\ndef test_variable_shorthand_margin_multiple():\n    page, = render_pages('''\n      <style>\n        html { --var1: 10px; --var2: 20px }\n        div { margin: var(--var2) 0 0 var(--var1) }\n      </style>\n      <div></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.margin_top == 20\n    assert div.margin_right == 0\n    assert div.margin_bottom == 0\n    assert div.margin_left == 10\n\n\n@assert_no_logs\ndef test_variable_shorthand_margin_invalid():\n    with capture_logs() as logs:\n        page, = render_pages('''\n          <style>\n            html { --var: blue }\n            div { margin: 0 0 0 var(--var) }\n          </style>\n          <div></div>\n        ''')\n        log, = logs\n        assert 'invalid value' in log\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.margin_top == 0\n    assert div.margin_right == 0\n    assert div.margin_bottom == 0\n    assert div.margin_left == 0\n\n\n@assert_no_logs\ndef test_variable_shorthand_border():\n    page, = render_pages('''\n      <style>\n        html { --var: 1px solid blue }\n        div { border: var(--var) }\n      </style>\n      <div></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.border_top_width == 1\n    assert div.border_right_width == 1\n    assert div.border_bottom_width == 1\n    assert div.border_left_width == 1\n\n\n@assert_no_logs\ndef test_variable_shorthand_border_side():\n    page, = render_pages('''\n      <style>\n        html { --var: 1px solid blue }\n        div { border-top: var(--var) }\n      </style>\n      <div></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.border_top_width == 1\n    assert div.border_right_width == 0\n    assert div.border_bottom_width == 0\n    assert div.border_left_width == 0\n\n\n@assert_no_logs\ndef test_variable_shorthand_border_mixed():\n    page, = render_pages('''\n      <style>\n        html { --var: 1px solid }\n        div { border: blue var(--var) }\n      </style>\n      <div></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.border_top_width == 1\n    assert div.border_right_width == 1\n    assert div.border_bottom_width == 1\n    assert div.border_left_width == 1\n\n\ndef test_variable_shorthand_border_mixed_invalid():\n    with capture_logs() as logs:\n        page, = render_pages('''\n          <style>\n            html { --var: 1px solid blue }\n            div { border: blue var(--var) }\n          </style>\n          <div></div>\n        ''')\n        # TODO: we should only get one warning here\n        assert 'multiple color values' in logs[0]\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.border_top_width == 0\n    assert div.border_right_width == 0\n    assert div.border_bottom_width == 0\n    assert div.border_left_width == 0\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('var', 'background'), [\n    ('blue', 'var(--v)'),\n    ('padding-box url(pattern.png)', 'var(--v)'),\n    ('padding-box url(pattern.png)', 'white var(--v) center'),\n    ('100%', 'url(pattern.png) var(--v) var(--v) / var(--v) var(--v)'),\n    ('left / 100%', 'url(pattern.png) top var(--v) 100%'),\n])\ndef test_variable_shorthand_background(var, background):\n    page, = render_pages('''\n      <style>\n        html { --v: %s }\n        div { background: %s }\n      </style>\n      <div></div>\n    ''' % (var, background))\n\n\n@pytest.mark.parametrize(('var', 'background'), [\n    ('invalid', 'var(--v)'),\n    ('blue', 'var(--v) var(--v)'),\n    ('100%', 'url(pattern.png) var(--v) var(--v) var(--v)'),\n])\ndef test_variable_shorthand_background_invalid(var, background):\n    with capture_logs() as logs:\n        page, = render_pages('''\n          <style>\n            html { --v: %s }\n            div { background: %s }\n          </style>\n          <div></div>\n        ''' % (var, background))\n        log, = logs\n        assert 'invalid value' in log\n\n\n@assert_no_logs\ndef test_variable_initial():\n    # Regression test for #2075.\n    page, = render_pages('''\n      <style>\n        html { --var: initial }\n        p { width: var(--var) }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == body.width\n\n\n@assert_no_logs\ndef test_variable_initial_default():\n    # Regression test for #2075.\n    page, = render_pages('''\n      <style>\n        p { --var: initial; width: var(--var, 10px) }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == body.width\n\n\n@assert_no_logs\ndef test_variable_initial_default_var():\n    # Regression test for #2075.\n    page, = render_pages('''\n      <style>\n        p { --var: initial; width: var(--var, var(--var)) }\n      </style>\n      <p></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == body.width\n\n\n@pytest.mark.parametrize('prop', sorted(KNOWN_PROPERTIES))\ndef test_variable_fallback(prop):\n    render_pages('''\n      <style>\n        div {\n          --var: improperValue;\n          %s: var(--var);\n        }\n      </style>\n      <div></div>\n    ''' % prop)\n\n\n@assert_no_logs\ndef test_variable_list_content():\n    # Regression test for #1287.\n    page, = render_pages('''\n      <style>\n        :root { --var: \"Page \" counter(page) \"/\" counter(pages) }\n        div::before { content: var(--var) }\n      </style>\n      <div></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line, = div.children\n    before, = line.children\n    text, = before.children\n    assert text.text == 'Page 1/1'\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('var', 'display'), [\n    ('inline', 'var(--var)'),\n    ('inline-block', 'var(--var)'),\n    ('inline flow', 'var(--var)'),\n    ('inline', 'var(--var) flow'),\n    ('flow', 'inline var(--var)'),\n])\ndef test_variable_list_display(var, display):\n    page, = render_pages('''\n      <style>\n        html { --var: %s }\n        div { display: %s }\n      </style>\n      <section><div></div></section>\n    ''' % (var, display))\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    child, = section.children\n    assert type(child).__name__ == 'LineBox'\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('var', 'font'), [\n    ('weasyprint', 'var(--var)'),\n    ('\"weasyprint\"', 'var(--var)'),\n    ('weasyprint', 'var(--var), monospace'),\n    ('weasyprint, monospace', 'var(--var)'),\n    ('monospace', 'weasyprint, var(--var)'),\n])\ndef test_variable_list_font(var, font):\n    page, = render_pages('''\n      <style>\n        html { font-size: 2px; --var: %s }\n        div { font-family: %s }\n      </style>\n      <div>aa</div>\n    ''' % (var, font))\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line, = div.children\n    text, = line.children\n    assert text.width == 4\n\n\n@assert_no_logs\ndef test_variable_in_function():\n    page, = render_pages('''\n      <style>\n        html { --var: title }\n        h1 { counter-increment: var(--var) }\n        div::before { content: counter(var(--var)) }\n      </style>\n      <section>\n        <h1></h1>\n        <div></div>\n        <h1></h1>\n        <div></div>\n      </section>\n    ''')\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    h11, div1, h12, div2 = section.children\n    assert div1.children[0].children[0].children[0].text == '1'\n    assert div2.children[0].children[0].children[0].text == '2'\n\n\n@assert_no_logs\ndef test_same_variable_in_function():\n    page, = render_pages('''\n      <style>\n        body { --var: red }\n        div { background-image: linear-gradient(var(--var), var(--var)) }\n      </style>\n      <div></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.style['background_image'][0][1].colors[0] == (1, 0, 0, 1)\n    assert div.style['background_image'][0][1].colors[1] == (1, 0, 0, 1)\n\n\n@assert_no_logs\ndef test_variable_in_nested_function():\n    page, = render_pages('''\n      <style>\n        body { --var: 255 0 0 }\n        div { background-image: linear-gradient(rgba(var(--var))) }\n      </style>\n      <div></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.style['background_image'][0][1].colors[0] == (1, 0, 0, 1)\n\n\n@assert_no_logs\ndef test_variable_in_function_multiple_values():\n    page, = render_pages('''\n      <style>\n        html { --name: title; --counter: title, upper-roman }\n        h1 { counter-increment: var(--name) }\n        div::before { content: counter(var(--counter)) }\n      </style>\n      <section>\n        <h1></h1>\n        <div></div>\n        <h1></h1>\n        <div></div>\n        <h1></h1>\n        <div style=\"--counter: var(--name), lower-roman\"></div>\n      </section>\n    ''')\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    h11, div1, h12, div2, h13, div3 = section.children\n    assert div1.children[0].children[0].children[0].text == 'I'\n    assert div2.children[0].children[0].children[0].text == 'II'\n    assert div3.children[0].children[0].children[0].text == 'iii'\n\n\n@assert_no_logs\ndef test_variable_in_variable_in_function():\n    page, = render_pages('''\n      <style>\n        html { --name: title; --counter: var(--name), upper-roman }\n        h1 { counter-increment: var(--name) }\n        div::before { content: counter(var(--counter)) }\n      </style>\n      <section>\n        <h1></h1>\n        <div></div>\n        <h1></h1>\n        <div></div>\n        <h1></h1>\n        <div style=\"--counter: var(--name), lower-roman\"></div>\n      </section>\n    ''')\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    h11, div1, h12, div2, h13, div3 = section.children\n    assert div1.children[0].children[0].children[0].text == 'I'\n    assert div2.children[0].children[0].children[0].text == 'II'\n    assert div3.children[0].children[0].children[0].text == 'iii'\n\n\ndef test_variable_in_function_missing():\n    with capture_logs() as logs:\n        page, = render_pages('''\n          <style>\n            h1 { counter-increment: var(--var) }\n            div::before { content: counter(var(--var)) }\n          </style>\n          <section>\n            <h1></h1>\n            <div></div>\n            <h1></h1>\n            <div></div>\n          </section>\n        ''')\n        assert len(logs) == 2\n        assert 'no value' in logs[0]\n        assert 'invalid value' in logs[1]\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    h11, div1, h12, div2 = section.children\n    assert not div1.children\n    assert not div2.children\n\n\n@assert_no_logs\ndef test_variable_in_function_in_variable():\n    page, = render_pages('''\n      <style>\n        html { --name: title; --counter: counter(var(--name), upper-roman) }\n        h1 { counter-increment: var(--name) }\n        div::before { content: var(--counter) }\n      </style>\n      <section>\n        <h1></h1>\n        <div></div>\n        <h1></h1>\n        <div></div>\n        <h1></h1>\n        <div style=\"--counter: counter(var(--name), lower-roman)\"></div>\n      </section>\n    ''')\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    h11, div1, h12, div2, h13, div3 = section.children\n    assert div1.children[0].children[0].children[0].text == 'I'\n    assert div2.children[0].children[0].children[0].text == 'II'\n    assert div3.children[0].children[0].children[0].text == 'iii'\n\n\n@assert_no_logs\ndef test_variable_and_function_in_function():\n    page, = render_pages('''\n      <style>\n        html { --counter-name: page }\n        a::after { content: target-counter(attr(href), var(--counter-name)) }\n      </style>\n      <a id=\"link\" href=\"#link\"></a>\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    a, _ = line.children\n    after, = a.children\n    textbox, = after.children\n    assert textbox.text == '1'\n"
  },
  {
    "path": "tests/draw/__init__.py",
    "content": "\"\"\"Test the final, drawn results and compare PNG images pixel per pixel.\"\"\"\n\nimport io\nfrom itertools import zip_longest\nfrom pathlib import Path\n\nimport pytest\nfrom PIL import Image\n\nfrom ..testing_utils import FakeHTML, resource_path\n\n# NOTE: \"r\" is not half red on purpose. In the pixel strings it has\n# better contrast with \"B\" than does \"R\". eg. \"rBBBrrBrB\" vs \"RBBBRRBRB\".\nPIXELS_BY_CHAR = {\n    '_': (255, 255, 255),  # white\n    'R': (255, 0, 0),  # red\n    'B': (0, 0, 255),  # blue\n    'G': (0, 255, 0),  # lime green\n    'V': (191, 0, 64),  # average of 1*B and 3*R\n    'S': (255, 63, 63),  # R above R above _\n    'C': (0, 255, 255),  # cyan\n    'M': (255, 0, 255),  # magenta\n    'Y': (255, 255, 0),  # yellow\n    'K': (0, 0, 0),  # black\n    'P': (50, 76, 103),  # blue CMYK with ICC\n    'Q': (63, 88, 110),  # blue CMYK without ICC\n    'r': (255, 0, 0),  # red\n    'g': (0, 128, 0),  # half green\n    'b': (0, 0, 128),  # half blue\n    'v': (128, 0, 128),  # average of B and R\n    's': (255, 127, 127),  # R above _\n    't': (127, 255, 127),  # G above _\n    'u': (128, 0, 127),  # r above B above _\n    'h': (64, 0, 64),  # half average of B and R\n    'a': (0, 0, 254),  # R in lossy JPG\n    'p': (192, 0, 63),  # R above R above B above _\n    'z': None,\n}\n\n\ndef parse_pixels(pixels):\n    lines = (line.split('#')[0].strip() for line in pixels.splitlines())\n    lines = tuple(line for line in lines if line)\n    widths = {len(line) for line in lines}\n    assert len(widths) == 1, 'All lines of pixels must have the same width'\n    width = widths.pop()\n    height = len(lines)\n    pixels = tuple(PIXELS_BY_CHAR[char] for line in lines for char in line)\n    return width, height, pixels\n\n\ndef assert_pixels(name, expected_pixels, html):\n    \"\"\"Helper testing the size of the image and the pixels values.\"\"\"\n    expected_width, expected_height, expected_pixels = parse_pixels(\n        expected_pixels)\n    width, height, pixels = html_to_pixels(html)\n    assert (expected_width, expected_height) == (width, height), (\n        'Images do not have the same sizes:\\n'\n        f'- expected: {expected_width} × {expected_height}\\n'\n        f'- result: {width} × {height}')\n    assert_pixels_equal(name, width, height, pixels, expected_pixels)\n\n\ndef assert_same_renderings(name, *documents, tolerance=0):\n    \"\"\"Render HTML documents to PNG and check that they're the same.\"\"\"\n    pixels_list = []\n\n    for html in documents:\n        width, height, pixels = html_to_pixels(html)\n        pixels_list.append(pixels)\n\n    reference = pixels_list[0]\n    for i, pixels in enumerate(pixels_list[1:], start=1):\n        assert_pixels_equal(\n            f'{name}_{i}', width, height, pixels, reference, tolerance)\n\n\ndef assert_different_renderings(name, *documents):\n    \"\"\"Render HTML documents to PNG and check that they’re different.\"\"\"\n    pixels_list = []\n\n    for html in documents:\n        width, height, pixels = html_to_pixels(html)\n        pixels_list.append(pixels)\n\n    for i, pixels_1 in enumerate(pixels_list, start=1):\n        for j, pixels_2 in enumerate(pixels_list[i:], start=i+1):\n            if tuple(pixels_1) == tuple(pixels_2):  # pragma: no cover\n                name_1, name_2 = f'{name}_{i}', f'{name}_{j}'\n                write_png(name_1, pixels_1, width, height)\n                pytest.fail(f'{name_1} and {name_2} are the same')\n\n\ndef assert_pixels_equal(name, width, height, raw, expected_raw, tolerance=0):\n    \"\"\"Take 2 matrices of pixels and assert that they are the same.\"\"\"\n    if raw != expected_raw:  # pragma: no cover\n        pixels = zip_longest(raw, expected_raw, fillvalue=(-1, -1, -1))\n        for i, (value, expected) in enumerate(pixels):\n            if expected is None:\n                continue\n            if any(abs(value - expected) > tolerance\n                   for value, expected in zip(value, expected)):\n                actual_height = len(raw) // width\n                write_png(name, raw, width, actual_height)\n                expected_raw = [\n                    pixel or (255, 255, 255) for pixel in expected_raw]\n                write_png(f'{name}.expected', expected_raw, width, height)\n                x = i % width\n                y = i // width\n                pytest.fail(\n                    f'Pixel ({x}, {y}) in {name}: '\n                    f'expected rgba{expected}, got rgba{value}')\n\n\ndef write_png(name, pixels, width, height):  # pragma: no cover\n    \"\"\"Take a pixel matrix and write a PNG file.\"\"\"\n    directory = Path(__file__).parent / 'results'\n    directory.mkdir(exist_ok=True)\n    image = Image.new('RGB', (width, height))\n    image.putdata(pixels)\n    image.save(directory / f'{name}.png')\n\n\ndef html_to_pixels(html):\n    \"\"\"Render an HTML document to PNG, checks its size and return pixel data.\n\n    Also return the document to aid debugging.\n\n    \"\"\"\n    document = FakeHTML(string=html, base_url=resource_path('<dummy>'))\n    return document_to_pixels(document)\n\n\ndef document_to_pixels(document):\n    \"\"\"Render an HTML document to PNG, check its size and return pixel data.\"\"\"\n    image = Image.open(io.BytesIO(document.write_png()))\n    return image.width, image.height, image.get_flattened_data()\n"
  },
  {
    "path": "tests/draw/svg/__init__.py",
    "content": ""
  },
  {
    "path": "tests/draw/svg/test_bounding_box.py",
    "content": "\"\"\"Test how bounding boxes are defined for SVG tags.\"\"\"\n\nimport pytest\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_bounding_box_rect(assert_pixels):\n    assert_pixels('''\n        BBBBB\n        BBBBR\n        BBBRR\n        BBRRR\n        BRRRR\n    ''', '''\n      <style>\n        @page { size: 5px }\n        svg { display: block }\n      </style>\n      <svg width=\"5px\" height=\"5px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"55%\"></stop>\n            <stop stop-color=\"red\" offset=\"55%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"5\" height=\"5\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_bounding_box_circle(assert_pixels):\n    assert_pixels('''\n        __________\n        __BBBBBB__\n        _BBBBBBBz_\n        _BBBBBBzR_\n        _BBBBBzRR_\n        _BBBBzRRR_\n        _BBBzRRRR_\n        _BBzRRRRR_\n        __zRRRRR__\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"55%\"></stop>\n            <stop stop-color=\"red\" offset=\"55%\"></stop>\n          </linearGradient>\n        </defs>\n        <circle cx=\"5\" cy=\"5\" r=\"4\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_bounding_box_ellipse(assert_pixels):\n    assert_pixels('''\n        __________\n        __BBBBBB__\n        _BBBBBBBz_\n        _BBBBBBzR_\n        _BBBBBzRR_\n        _BBBBzRRR_\n        _BBBzRRRR_\n        _BBzRRRRR_\n        __zRRRRR__\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"55%\"></stop>\n            <stop stop-color=\"red\" offset=\"55%\"></stop>\n          </linearGradient>\n        </defs>\n        <ellipse cx=\"5\" cy=\"5\" rx=\"4\" ry=\"4\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_bounding_box_line(assert_pixels):\n    assert_pixels('''\n        BB___\n        BBB__\n        _BRR_\n        __RRR\n        ___RR\n    ''', '''\n      <style>\n        @page { size: 5px }\n        svg { display: block }\n      </style>\n      <svg width=\"5px\" height=\"5px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"50%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n          </linearGradient>\n        </defs>\n        <line x1=\"0\" y1=\"0\" x2=\"5\" y2=\"5\"\n              stroke-width=\"1\" stroke=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_bounding_box_polygon(assert_pixels):\n    assert_pixels('''\n        BBBBB\n        BBBBR\n        BBBRR\n        BBRRR\n        BRRRR\n    ''', '''\n      <style>\n        @page { size: 5px }\n        svg { display: block }\n      </style>\n      <svg width=\"5px\" height=\"5px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"55%\"></stop>\n            <stop stop-color=\"red\" offset=\"55%\"></stop>\n          </linearGradient>\n        </defs>\n        <polygon points=\"0 0 0 5 5 5 5 0\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_bounding_box_polyline(assert_pixels):\n    assert_pixels('''\n        BBBBB\n        BBBBR\n        BBBRR\n        BBRRR\n        BRRRR\n    ''', '''\n      <style>\n        @page { size: 5px }\n        svg { display: block }\n      </style>\n      <svg width=\"5px\" height=\"5px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"55%\"></stop>\n            <stop stop-color=\"red\" offset=\"55%\"></stop>\n          </linearGradient>\n        </defs>\n        <polyline points=\"0 0 0 5 5 5 5 0\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_bounding_box_text(assert_pixels):\n    assert_pixels('''\n        BB\n        BR\n    ''', '''\n      <style>\n        @page { size: 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"2px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"65%\"></stop>\n            <stop stop-color=\"red\" offset=\"65%\"></stop>\n          </linearGradient>\n        </defs>\n        <text x=\"0\" y=\"2\" font-family=\"weasyprint\" font-size=\"2\" fill=\"url(#grad)\">\n          A\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_bounding_box_path_hv(assert_pixels):\n    assert_pixels('''\n        BBBBB\n        BBBBR\n        BBBRR\n        BBRRR\n        BRRRR\n    ''', '''\n      <style>\n        @page { size: 5px }\n        svg { display: block }\n      </style>\n      <svg width=\"5px\" height=\"5px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"55%\"></stop>\n            <stop stop-color=\"red\" offset=\"55%\"></stop>\n          </linearGradient>\n        </defs>\n        <path d=\"m 5 0 v 5 h -5 V 0 H 5 z\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_bounding_box_path_l(assert_pixels):\n    assert_pixels('''\n        BBBBB\n        BBBBR\n        BBBRR\n        BBRRR\n        BRRRR\n    ''', '''\n      <style>\n        @page { size: 5px }\n        svg { display: block }\n      </style>\n      <svg width=\"5px\" height=\"5px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"55%\"></stop>\n            <stop stop-color=\"red\" offset=\"55%\"></stop>\n          </linearGradient>\n        </defs>\n        <path d=\"M 5 0 l 0 5 l -5 0 L 0 0 z\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_bounding_box_path_c(assert_pixels):\n    assert_pixels('''\n        BBB__\n        BBR__\n        _____\n        BBB__\n        BBR__\n    ''', '''\n      <style>\n        @page { size: 5px }\n        svg { display: block }\n      </style>\n      <svg width=\"5px\" height=\"5px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"55%\"></stop>\n            <stop stop-color=\"red\" offset=\"55%\"></stop>\n          </linearGradient>\n        </defs>\n        <g fill=\"none\" stroke=\"url(#grad)\" stroke-width=\"2\">\n          <path d=\"M 0 1 C 0 1 1 1 3 1\" />\n          <path d=\"M 0 4 c 0 0 1 0 3 0\" />\n        </g>\n      </svg>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_bounding_box_path_s(assert_pixels):\n    assert_pixels('''\n        BBB__\n        BBR__\n        _____\n        BBB__\n        BBR__\n    ''', '''\n      <style>\n        @page { size: 5px }\n        svg { display: block }\n      </style>\n      <svg width=\"5px\" height=\"5px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"55%\"></stop>\n            <stop stop-color=\"red\" offset=\"55%\"></stop>\n          </linearGradient>\n        </defs>\n        <g fill=\"none\" stroke=\"url(#grad)\" stroke-width=\"2\">\n          <path d=\"M 0 1 S 1 1 3 1\" />\n          <path d=\"M 0 4 s 1 0 3 0\" />\n        </g>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_svg_empty_size(assert_pixels):\n    assert_pixels('''\n        BBB__\n        BBB__\n        BBB__\n        BBB__\n        _____\n    ''', '''\n      <style>\n        @page { size: 5px }\n        svg { display: block }\n      </style>\n      <svg xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"0\" y=\"0\" width=\"3\" height=\"4\" fill=\"blue\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_bounding_box_use_opacity(assert_pixels):\n    assert_pixels('''\n        sssss\n        sssss\n        sssss\n        sssss\n        sssss\n    ''', '''\n      <style>\n        @page { size: 5px }\n        svg { display: block }\n      </style>\n      <svg width=\"5px\" height=\"5px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <rect id=\"rect\" x=\"-10\" y=\"-10\" width=\"5\" height=\"5\" fill=\"red\" />\n        </defs>\n        <use href=\"#rect\" opacity=\"0.5\" x=\"10\" y=\"10\" />\n      </svg>\n    ''')\n"
  },
  {
    "path": "tests/draw/svg/test_clip.py",
    "content": "\"\"\"Test clip-path attribute.\"\"\"\n\nimport pytest\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_clip_path(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        __RRRRR__\n        __RBBBR__\n        __RBBBR__\n        __RBBBR__\n        __RRRRR__\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <clipPath id=\"clip\">\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" />\n          </clipPath>\n        </defs>\n        <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n              stroke=\"red\" fill=\"blue\" clip-path=\"url(#clip)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_clip_path_on_group(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        __BBBB___\n        __BRRRR__\n        __BRRRR__\n        __BRRRR__\n        ___RRRR__\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <clipPath id=\"clip\">\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" />\n          </clipPath>\n        </defs>\n        <g clip-path=\"url(#clip)\">\n          <rect x=\"1\" y=\"1\" width=\"5\" height=\"5\" fill=\"blue\" />\n          <rect x=\"3\" y=\"3\" width=\"5\" height=\"5\" fill=\"red\" />\n        </g>\n      </svg>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_clip_path_group_on_group(assert_pixels):\n    assert_pixels(9, 9, '''\n        _________\n        _________\n        __BB_____\n        __BR_____\n        _________\n        _____RR__\n        _____RR__\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <clipPath id=\"clip\">\n            <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" />\n            <rect x=\"3\" y=\"3\" width=\"2\" height=\"2\" />\n          </clipPath>\n        </defs>\n        <g clip-path=\"url(#clip)\">\n          <rect x=\"1\" y=\"1\" width=\"5\" height=\"5\" fill=\"blue\" />\n          <rect x=\"3\" y=\"3\" width=\"5\" height=\"5\" fill=\"red\" />\n        </g>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_clip_path_outside_defs(assert_pixels):\n    # Regression test for #2662.\n    assert_pixels('''\n        _________\n        _________\n        __RRRRR__\n        __RBBBR__\n        __RBBBR__\n        __RBBBR__\n        __RRRRR__\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <clipPath id=\"clip\">\n          <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" />\n        </clipPath>\n        <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n              stroke=\"red\" fill=\"blue\" clip-path=\"url(#clip)\" />\n      </svg>\n    ''')\n"
  },
  {
    "path": "tests/draw/svg/test_defs.py",
    "content": "\"\"\"Test how SVG definitions are drawn.\"\"\"\n\nfrom base64 import b64encode\n\nfrom ...testing_utils import assert_no_logs\n\nSVG = '''\n<svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n  <defs>\n    <rect id=\"rectangle\" width=\"5\" height=\"2\" fill=\"red\" />\n    <symbol id=\"square\">\n      <rect width=\"2\" height=\"2\" fill=\"blue\" />\n    </symbol>\n  </defs>\n  <use href=\"#rectangle\" />\n  <use href=\"#square\" x=\"3\" y=\"3\" />\n  <use href=\"#rectangle\" x=\"5\" y=\"6\" />\n</svg>\n'''\n\nSTYLE = '''\n<style>\n  @page { size: 10px }\n  svg, img { display: block }\n</style>\n'''\n\nRESULT = '''\n  RRRRR_____\n  RRRRR_____\n  __________\n  ___BB_____\n  ___BB_____\n  __________\n  _____RRRRR\n  _____RRRRR\n  __________\n  __________\n'''\n\n\n@assert_no_logs\ndef test_use(assert_pixels):\n    assert_pixels(RESULT, STYLE + SVG)\n\n\n@assert_no_logs\ndef test_use_base64(assert_pixels):\n    base64_svg = b64encode(SVG.encode()).decode()\n    assert_pixels(RESULT, f'{STYLE}<img src=\"data:image/svg+xml;base64,{base64_svg}\"/>')\n\n\n@assert_no_logs\ndef test_use_symbol_color(assert_pixels):\n    # Regression test for #2676.\n    svg = SVG.replace('fill=\"blue\"', '')\n    svg = svg.replace('href=\"#square\"', 'href=\"#square\" fill=\"blue\"')\n    assert_pixels(RESULT, STYLE + svg)\n"
  },
  {
    "path": "tests/draw/svg/test_gradients.py",
    "content": "\"\"\"Test how SVG simple gradients are drawn.\"\"\"\n\nimport pytest\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_linear_gradient(assert_pixels):\n    assert_pixels('''\n        __________\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"50%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"1\" y=\"1\" width=\"8\" height=\"8\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_userspace(assert_pixels):\n    assert_pixels('''\n        __________\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"10\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stop-color=\"blue\" offset=\"50%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"1\" y=\"1\" width=\"8\" height=\"8\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_multicolor(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        GGGGGGGGGG\n        GGGGGGGGGG\n        vvvvvvvvvv\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"10\" height=\"8\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_multicolor_userspace(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        GGGGGGGGGG\n        GGGGGGGGGG\n        vvvvvvvvvv\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"10\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stop-color=\"blue\" offset=\"30%\"></stop>\n            <stop stop-color=\"red\" offset=\"30%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"70%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"70%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"10\" height=\"8\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_transform(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 10px}\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\"\n            gradientTransform=\"matrix(0.5, 0, 0, 0.5, 0, 0.5)\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"10\" height=\"8\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_transform_nested(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 10px}\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\"\n            gradientTransform=\"matrix(0.5, 0, 0, 0.5, 0, 0.5)\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect transform=\"matrix(-2, 0, 0, 1, 10, 100)\"\n              x=\"0\" y=\"-99\" width=\"20\" height=\"8\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_transform_repeat(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        zvvvvvvvvz\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 10px}\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\" spreadMethod=\"repeat\"\n            gradientTransform=\"matrix(0.5, 0, 0, 0.5, 0, 0.5)\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"10\" height=\"8\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_userspace_transform(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 10px}\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"8\"\n            gradientUnits=\"userSpaceOnUse\"\n            gradientTransform=\"matrix(0.5, 0, 0, 0.5, 0, 5)\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"10\" height=\"8\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_userspace_transform_nested(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 10px}\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"8\"\n            gradientUnits=\"userSpaceOnUse\"\n            gradientTransform=\"matrix(0.5, 0, 0, 0.5, 0, 4)\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect transform=\"matrix(-2, 0, 0, 1, 10, 1)\"\n              x=\"0\" y=\"0\" width=\"20\" height=\"8\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_transform_repeat_userspace(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        zvvvvvvvvz\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 10px}\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"8\"\n            gradientUnits=\"userSpaceOnUse\" spreadMethod=\"repeat\"\n            gradientTransform=\"matrix(0.5, 0, 0, 0.5, 0, 5)\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"10\" height=\"8\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_repeat(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        GGGGGGGGGG\n        GGGGGGGGGG\n        vvvvvvvvvv\n        vvvvvvvvvv\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        GGGGGGGGGG\n        GGGGGGGGGG\n        vvvvvvvvvv\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 18px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"18px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"0.5\"\n            gradientUnits=\"objectBoundingBox\" spreadMethod=\"repeat\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"11\" height=\"16\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_repeat_long(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        vvvvvvvvvv\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        vvvvvvvvvv\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        vvvvvvvvvv\n        BBBBBBBBBB\n        RRRRRRRRRR\n        GGGGGGGGGG\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 18px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"18px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"0.25\"\n            gradientUnits=\"objectBoundingBox\" spreadMethod=\"repeat\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"11\" height=\"16\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_repeat_userspace(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        GGGGGGGGGG\n        GGGGGGGGGG\n        vvvvvvvvvv\n        vvvvvvvvvv\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        GGGGGGGGGG\n        GGGGGGGGGG\n        vvvvvvvvvv\n        vvvvvvvvvv\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 18px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"18px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"1\" x2=\"0\" y2=\"9\"\n            gradientUnits=\"userSpaceOnUse\" spreadMethod=\"repeat\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"11\" height=\"16\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_reflect(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        GGGGGGGGGG\n        GGGGGGGGGG\n        vvvvvvvvvv\n        vvvvvvvvvv\n        vvvvvvvvvv\n        vvvvvvvvvv\n        GGGGGGGGGG\n        GGGGGGGGGG\n        RRRRRRRRRR\n        RRRRRRRRRR\n        BBBBBBBBBB\n        BBBBBBBBBB\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 18px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"18px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"0.5\"\n            gradientUnits=\"objectBoundingBox\" spreadMethod=\"reflect\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"11\" height=\"16\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_reflect_userspace(assert_pixels):\n    assert_pixels('''\n        __________\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        GGGGGGGGGG\n        GGGGGGGGGG\n        vvvvvvvvvv\n        vvvvvvvvvv\n        vvvvvvvvvv\n        vvvvvvvvvv\n        GGGGGGGGGG\n        GGGGGGGGGG\n        RRRRRRRRRR\n        RRRRRRRRRR\n        BBBBBBBBBB\n        BBBBBBBBBB\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 18px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"18px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"1\" x2=\"0\" y2=\"9\"\n            gradientUnits=\"userSpaceOnUse\" spreadMethod=\"reflect\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"50%\"></stop>\n            <stop stop-color=\"lime\" offset=\"75%\"></stop>\n            <stop stop-color=\"rgb(128,0,128)\" offset=\"75%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"1\" width=\"11\" height=\"16\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_inherit_attributes(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        RRRRRRRRRR\n        RRRRRRRRRR\n        RRRRRRRRRR\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"parent\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n          </linearGradient>\n          <linearGradient id=\"grad\" href=\"#parent\">\n            <stop stop-color=\"blue\" offset=\"50%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_inherit_children(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        RRRRRRRRRR\n        RRRRRRRRRR\n        RRRRRRRRRR\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"parent\">\n            <stop stop-color=\"blue\" offset=\"50%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n          </linearGradient>\n          <linearGradient id=\"grad\" href=\"#parent\"\n            x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\" gradientUnits=\"objectBoundingBox\">\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_linear_gradient_inherit_no_override(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        RRRRRRRRRR\n        RRRRRRRRRR\n        RRRRRRRRRR\n        RRRRRRRRRR\n        RRRRRRRRRR\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"parent\"\n            x1=\"1\" y1=\"1\" x2=\"1\" y2=\"0\" gradientUnits=\"userSpaceOnUse\">\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n            <stop stop-color=\"blue\" offset=\"50%\"></stop>\n          </linearGradient>\n          <linearGradient id=\"grad\" href=\"#parent\"\n            x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\" gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"50%\"></stop>\n            <stop stop-color=\"red\" offset=\"50%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        _rrrrBBrrrr_\n        _rrrBBBBrrr_\n        _rrBBBBBBrr_\n        _rrBBBBBBrr_\n        _rrrBBBBrrr_\n        _rrrrBBrrrr_\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"grad\" cx=\"0.5\" cy=\"0.5\" r=\"0.5\"\n            fx=\"0.5\" fy=\"0.5\" fr=\"0.2\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_transform(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        _rrrrBBrrrr_\n        _rrrBBBBrrr_\n        _rrBBBBBBrr_\n        _rrBBBBBBrr_\n        _rrrBBBBrrr_\n        _rrrrBBrrrr_\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"grad\" cx=\"-1\" cy=\"-1\" r=\"1\"\n            fx=\"-1\" fy=\"-1\" fr=\"0.4\"\n            gradientTransform=\"matrix(0.5, 0, 0, 0.5, 1, 1)\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_transform_nested(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        _rrrrBBrrrr_\n        _rrrBBBBrrr_\n        _rrBBBBBBrr_\n        _rrBBBBBBrr_\n        _rrrBBBBrrr_\n        _rrrrBBrrrr_\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"grad\" cx=\"-1\" cy=\"-1\" r=\"1\"\n            fx=\"-1\" fy=\"-1\" fr=\"0.4\"\n            gradientTransform=\"matrix(0.5, 0, 0, 0.5, 1, 1)\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect transform=\"matrix(1, 0, 0, 1, 10, 10)\"\n              x=\"-9\" y=\"-9\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_userspace(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        _rrrrBBrrrr_\n        _rrrBBBBrrr_\n        _rrBBBBBBrr_\n        _rrBBBBBBrr_\n        _rrrBBBBrrr_\n        _rrrrBBrrrr_\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"grad\" cx=\"6\" cy=\"6\" r=\"5\" fx=\"6\" fy=\"6\" fr=\"2\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_userspace_transform(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        _rrrrBBrrrr_\n        _rrrBBBBrrr_\n        _rrBBBBBBrr_\n        _rrBBBBBBrr_\n        _rrrBBBBrrr_\n        _rrrrBBrrrr_\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"grad\" cx=\"-8\" cy=\"-8\" r=\"10\" fx=\"-8\" fy=\"-8\" fr=\"4\"\n            gradientTransform=\"matrix(0.5, 0, 0, 0.5, 10, 10)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_userspace_transform_nested(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        _rrrrBBrrrr_\n        _rrrBBBBrrr_\n        _rrBBBBBBrr_\n        _rrBBBBBBrr_\n        _rrrBBBBrrr_\n        _rrrrBBrrrr_\n        _rrrrrrrrrr_\n        _rrrrrrrrrr_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"grad\" cx=\"-28\" cy=\"-28\" r=\"10\" fx=\"-28\" fy=\"-28\" fr=\"4\"\n            gradientTransform=\"matrix(0.5, 0, 0, 0.5, 10, 10)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect transform=\"matrix(1, 0, 0, 1, 10, 10)\"\n              x=\"-9\" y=\"-9\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_multicolor(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rrrrrrrrrr_\n        _rrrGGGGrrr_\n        _rrGGBBGGrr_\n        _rGGBBBBGGr_\n        _rGBBBBBBGr_\n        _rGBBBBBBGr_\n        _rGGBBBBGGr_\n        _rrGGBBGGrr_\n        _rrrGGGGrrr_\n        _rrrrrrrrrr_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"grad\" cx=\"0.5\" cy=\"0.5\" r=\"0.5\"\n            fx=\"0.5\" fy=\"0.5\" fr=\"0.2\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"33%\"></stop>\n            <stop stop-color=\"lime\" offset=\"33%\"></stop>\n            <stop stop-color=\"lime\" offset=\"66%\"></stop>\n            <stop stop-color=\"red\" offset=\"66%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_multicolor_userspace(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rrrrrrrrrr_\n        _rrrGGGGrrr_\n        _rrGGBBGGrr_\n        _rGGBBBBGGr_\n        _rGBBBBBBGr_\n        _rGBBBBBBGr_\n        _rGGBBBBGGr_\n        _rrGGBBGGrr_\n        _rrrGGGGrrr_\n        _rrrrrrrrrr_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"grad\" cx=\"6\" cy=\"6\" r=\"5\"\n            fx=\"6\" fy=\"6\" fr=\"2\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stop-color=\"blue\" offset=\"33%\"></stop>\n            <stop stop-color=\"lime\" offset=\"33%\"></stop>\n            <stop stop-color=\"lime\" offset=\"66%\"></stop>\n            <stop stop-color=\"red\" offset=\"66%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_repeat(assert_pixels):\n    assert_pixels('''\n        ____________\n        _GBBzzzzBBG_\n        _BrrGGGGrrB_\n        _BrGBBBBGrB_\n        _zGBBrrBBGz_\n        _zGBrGGrBGz_\n        _zGBrGGrBGz_\n        _zGBBrrBBGz_\n        _BrGBBBBGrB_\n        _BrrGGGGrrB_\n        _GBBzzzzBBG_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"grad\" cx=\"0.5\" cy=\"0.5\" r=\"0.5\"\n            fx=\"0.5\" fy=\"0.5\" fr=\"0.2\"\n            gradientUnits=\"objectBoundingBox\" spreadMethod=\"repeat\">\n            <stop stop-color=\"blue\" offset=\"33%\"></stop>\n            <stop stop-color=\"lime\" offset=\"33%\"></stop>\n            <stop stop-color=\"lime\" offset=\"66%\"></stop>\n            <stop stop-color=\"red\" offset=\"66%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_reflect(assert_pixels):\n    assert_pixels('''\n        ____________\n        _GrrzzzzrrG_\n        _rrrGGGGrrr_\n        _rrGBBBBGrr_\n        _zGBBBBBBGz_\n        _zGBBGGBBGz_\n        _zGBBGGBBGz_\n        _zGBBBBBBGz_\n        _rrGBBBBGrr_\n        _rrrGGGGrrr_\n        _GrrzzzzrrG_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"grad\" cx=\"0.5\" cy=\"0.5\" r=\"0.5\"\n            fx=\"0.5\" fy=\"0.5\" fr=\"0.2\"\n            gradientUnits=\"objectBoundingBox\" spreadMethod=\"reflect\">\n            <stop stop-color=\"blue\" offset=\"33%\"></stop>\n            <stop stop-color=\"lime\" offset=\"33%\"></stop>\n            <stop stop-color=\"lime\" offset=\"66%\"></stop>\n            <stop stop-color=\"red\" offset=\"66%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_inherit_attributes(assert_pixels):\n    assert_pixels('''\n        rrrrrrrrrr\n        rrrrrrrrrr\n        rrrrBBrrrr\n        rrrBBBBrrr\n        rrBBBBBBrr\n        rrBBBBBBrr\n        rrrBBBBrrr\n        rrrrBBrrrr\n        rrrrrrrrrr\n        rrrrrrrrrr\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"parent\" cx=\"0.5\" cy=\"0.5\" r=\"0.5\"\n            fx=\"0.5\" fy=\"0.5\" fr=\"0.2\"\n            gradientUnits=\"objectBoundingBox\">\n          </radialGradient>\n          <radialGradient id=\"grad\" href=\"#parent\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_inherit_children(assert_pixels):\n    assert_pixels('''\n        rrrrrrrrrr\n        rrrrrrrrrr\n        rrrrBBrrrr\n        rrrBBBBrrr\n        rrBBBBBBrr\n        rrBBBBBBrr\n        rrrBBBBrrr\n        rrrrBBrrrr\n        rrrrrrrrrr\n        rrrrrrrrrr\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"parent\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n          </radialGradient>\n          <radialGradient id=\"grad\" href=\"#parent\"\n            cx=\"0.5\" cy=\"0.5\" r=\"0.5\" fx=\"0.5\" fy=\"0.5\" fr=\"0.2\"\n            gradientUnits=\"objectBoundingBox\">\n          </radialGradient>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_radial_gradient_inherit_no_override(assert_pixels):\n    assert_pixels('''\n        rrrrrrrrrr\n        rrrrrrrrrr\n        rrrrBBrrrr\n        rrrBBBBrrr\n        rrBBBBBBrr\n        rrBBBBBBrr\n        rrrBBBBrrr\n        rrrrBBrrrr\n        rrrrrrrrrr\n        rrrrrrrrrr\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <radialGradient id=\"parent\" cx=\"5\" cy=\"5\" r=\"5\" fx=\"5\" fy=\"5\" fr=\"2\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n          </radialGradient>\n          <radialGradient id=\"grad\" href=\"#parent\" cx=\"0.5\" cy=\"0.5\" r=\"0.5\"\n            fx=\"0.5\" fy=\"0.5\" fr=\"0.2\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"25%\"></stop>\n            <stop stop-color=\"red\" offset=\"25%\"></stop>\n          </radialGradient>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_gradient_opacity(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        ssssssssss\n        ssssssssss\n        ssssssssss\n        ssssssssss\n        ssssssssss\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\" offset=\"50%\"></stop>\n            <stop stop-color=\"red\" stop-opacity=\"0.502\" offset=\"50%\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"url(#grad)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\n@pytest.mark.parametrize('url', [\"#grad'\", \"'#gra\", '!', '#'])\ndef test_gradient_bad_url(assert_pixels, url):\n    assert_pixels('''\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"\n            gradientUnits=\"objectBoundingBox\">\n            <stop stop-color=\"blue\"></stop>\n          </linearGradient>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"url(%s)\" />\n      </svg>\n    ''' % url)\n"
  },
  {
    "path": "tests/draw/svg/test_images.py",
    "content": "\"\"\"Test how images are drawn in SVG.\"\"\"\n\nimport pytest\n\nfrom weasyprint.urls import path2url\n\nfrom ...testing_utils import assert_no_logs, resource_path\n\n\n@assert_no_logs\ndef test_image_svg(assert_pixels):\n    assert_pixels('''\n        ____\n        ____\n        __B_\n        ____\n    ''', '''\n      <style>\n        @page { size: 4px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"4px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <svg x=\"1\" y=\"1\" width=\"2\" height=\"2\" viewBox=\"0 0 10 10\">\n          <rect x=\"5\" y=\"5\" width=\"5\" height=\"5\" fill=\"blue\" />\n        </svg>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_svg_viewbox(assert_pixels):\n    assert_pixels('''\n        ____\n        ____\n        __B_\n        ____\n    ''', '''\n      <style>\n        @page { size: 4px 4px }\n        svg { display: block }\n      </style>\n      <svg viewBox=\"0 0 4 4\" xmlns=\"http://www.w3.org/2000/svg\">\n        <svg x=\"1\" y=\"1\" width=\"2\" height=\"2\" viewBox=\"10 10 10 10\">\n          <rect x=\"15\" y=\"15\" width=\"5\" height=\"5\" fill=\"blue\" />\n        </svg>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_svg_align_default(assert_pixels):\n    assert_pixels('''\n        __BRRR__\n        __BRRR__\n        __RRRG__\n        __RRRG__\n        ________\n        ________\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"4px\" viewBox=\"0 0 4 4\"\n           xmlns=\"http://www.w3.org/2000/svg\">\n        <rect width=\"4\" height=\"4\" fill=\"red\" />\n        <rect width=\"1\" height=\"2\" fill=\"blue\" />\n        <rect x=\"3\" y=\"2\" width=\"1\" height=\"2\" fill=\"lime\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_svg_align_none(assert_pixels):\n    assert_pixels('''\n        BBRRRRRR\n        BBRRRRRR\n        RRRRRRGG\n        RRRRRRGG\n        ________\n        ________\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"4px\" viewBox=\"0 0 4 4\"\n           preserveAspectRatio=\"none\"\n           xmlns=\"http://www.w3.org/2000/svg\">\n        <rect width=\"4\" height=\"4\" fill=\"red\" />\n        <rect width=\"1\" height=\"2\" fill=\"blue\" />\n        <rect x=\"3\" y=\"2\" width=\"1\" height=\"2\" fill=\"lime\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_svg_align_no_viewbox(assert_pixels):\n    assert_pixels('''\n        BBRRRRRR\n        BBRRRRRR\n        RRRRRRGG\n        RRRRRRGG\n        ________\n        ________\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px 8px }\n        svg { display: block; height: 4px; width: 8px }\n      </style>\n      <svg width=\"4px\" height=\"4px\"\n           xmlns=\"http://www.w3.org/2000/svg\">\n        <rect width=\"4\" height=\"4\" fill=\"red\" />\n        <rect width=\"1\" height=\"2\" fill=\"blue\" />\n        <rect x=\"3\" y=\"2\" width=\"1\" height=\"2\" fill=\"lime\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_svg_align_meet_x(assert_pixels):\n    assert_pixels('''\n        ____BRRR\n        ____BRRR\n        ____RRRG\n        ____RRRG\n        ________\n        ________\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"4px\" viewBox=\"0 0 4 4\"\n           preserveAspectRatio=\"xMaxYMax meet\"\n           xmlns=\"http://www.w3.org/2000/svg\">\n        <rect width=\"4\" height=\"4\" fill=\"red\" />\n        <rect width=\"1\" height=\"2\" fill=\"blue\" />\n        <rect x=\"3\" y=\"2\" width=\"1\" height=\"2\" fill=\"lime\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_svg_align_meet_y(assert_pixels):\n    assert_pixels('''\n        ________\n        ________\n        ________\n        ________\n        BRRR____\n        BRRR____\n        RRRG____\n        RRRG____\n    ''', '''\n      <style>\n        @page { size: 8px 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"4px\" height=\"8px\" viewBox=\"0 0 4 4\"\n           preserveAspectRatio=\"xMaxYMax meet\"\n           xmlns=\"http://www.w3.org/2000/svg\">\n        <rect width=\"4\" height=\"4\" fill=\"red\" />\n        <rect width=\"1\" height=\"2\" fill=\"blue\" />\n        <rect x=\"3\" y=\"2\" width=\"1\" height=\"2\" fill=\"lime\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_svg_align_slice_x(assert_pixels):\n    assert_pixels('''\n        BBRRRRRR\n        BBRRRRRR\n        BBRRRRRR\n        BBRRRRRR\n        ________\n        ________\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px 8px }\n        svg { display: block; overflow: hidden }\n      </style>\n      <svg width=\"8px\" height=\"4px\" viewBox=\"0 0 4 4\"\n           preserveAspectRatio=\"xMinYMin slice\"\n           xmlns=\"http://www.w3.org/2000/svg\">\n        <rect width=\"4\" height=\"4\" fill=\"red\" />\n        <rect width=\"1\" height=\"2\" fill=\"blue\" />\n        <rect x=\"3\" y=\"2\" width=\"1\" height=\"2\" fill=\"lime\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_svg_align_slice_y(assert_pixels):\n    assert_pixels('''\n        BBRR____\n        BBRR____\n        BBRR____\n        BBRR____\n        RRRR____\n        RRRR____\n        RRRR____\n        RRRR____\n    ''', '''\n      <style>\n        @page { size: 8px 8px }\n        svg { display: block; overflow: hidden }\n      </style>\n      <svg width=\"4px\" height=\"8px\" viewBox=\"0 0 4 4\"\n           preserveAspectRatio=\"xMinYMin slice\"\n           xmlns=\"http://www.w3.org/2000/svg\">\n        <rect width=\"4\" height=\"4\" fill=\"red\" />\n        <rect width=\"1\" height=\"2\" fill=\"blue\" />\n        <rect x=\"3\" y=\"2\" width=\"1\" height=\"2\" fill=\"lime\" />\n      </svg>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_image_svg_percentage(assert_pixels):\n    assert_pixels('''\n        ____\n        ____\n        __B_\n        ____\n    ''', '''\n      <style>\n        @page { size: 4px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"100%\" height=\"100%\" xmlns=\"http://www.w3.org/2000/svg\">\n        <svg x=\"1\" y=\"1\" width=\"50%\" height=\"50%\" viewBox=\"0 0 10 10\">\n          <rect x=\"5\" y=\"5\" width=\"5\" height=\"5\" fill=\"blue\" />\n        </svg>\n      </svg>\n    ''')\n\n\ndef test_image_svg_wrong(assert_pixels):\n    assert_pixels('''\n        ____\n        ____\n        ____\n        ____\n    ''', '''\n      <style>\n        @page { size: 4px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"4px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <That’s bad!\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_image(assert_pixels):\n    assert_pixels('''\n        rBBB\n        BBBB\n        BBBB\n        BBBB\n    ''', '''\n      <style>\n        @page { size: 4px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"4px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <image xlink:href=\"%s\" />\n      </svg>\n    ''' % path2url(resource_path('pattern.png')))\n\n\n@assert_no_logs\ndef test_image_image_rendering(assert_pixels):\n    assert_pixels('''\n        rrBBBBBB\n        rrBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n    ''', '''\n      <style>\n        @page { size: 8px 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"8px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <image width=\"8px\" height=\"8px\" xlink:href=\"%s\"\n            style=\"image-rendering:pixelated\"/>\n      </svg>\n    ''' % path2url(resource_path('pattern.png')))\n\n\ndef test_image_image_wrong(assert_pixels):\n    assert_pixels('''\n        ____\n        ____\n        ____\n        ____\n    ''', '''\n      <style>\n        @page { size: 4px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"4px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <image xlink:href=\"it doesn’t exist, mouhahahaha\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_in_g_height_only(assert_pixels):\n    \"\"\"Test that image inside g with only height set preserves aspect ratio.\"\"\"\n    assert_pixels('''\n        rrBBBBBB\n        rrBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n    ''', '''\n      <style>\n        @page { size: 8px 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"8px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g>\n          <image image-rendering=\"pixelated\" href=\"%s\" height=\"8px\"/>\n        </g>\n      </svg>\n    ''' % path2url(resource_path('pattern.png')))\n\n\n@assert_no_logs\ndef test_image_in_g_width_only(assert_pixels):\n    \"\"\"Test that image inside g with only width set preserves aspect ratio.\"\"\"\n    assert_pixels('''\n        rrBBBBBB\n        rrBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n        BBBBBBBB\n    ''', '''\n      <style>\n        @page { size: 8px 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"8px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g>\n          <image image-rendering=\"pixelated\" href=\"%s\" width=\"8px\"/>\n        </g>\n      </svg>\n    ''' % path2url(resource_path('pattern.png')))\n"
  },
  {
    "path": "tests/draw/svg/test_markers.py",
    "content": "\"\"\"Test how SVG markers are drawn.\"\"\"\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_markers(assert_pixels):\n    assert_pixels('''\n        ___________\n        ___________\n        _____RRR___\n        _____RRR___\n        _____RRR___\n        ___________\n        _____RRR___\n        _____RRR___\n        _____RRR___\n        ___________\n        _____RRR___\n        _____RRR___\n        _____RRR___\n    ''', '''\n      <style>\n        @page { size: 11px 13px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"13px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <marker id=\"rectangle\">\n            <rect width=\"3\" height=\"3\" fill=\"red\" />\n          </marker>\n        </defs>\n        <path\n          d=\"M 5 2 v 4 v 4\"\n          marker-start=\"url(#rectangle)\"\n          marker-mid=\"url(#rectangle)\"\n          marker-end=\"url(#rectangle)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_markers_viewbox(assert_pixels):\n    assert_pixels('''\n        ___________\n        ____RRR____\n        ____RRR____\n        ____RRR____\n        ___________\n        ____RRR____\n        ____RRR____\n        ____RRR____\n        ___________\n        ____RRR____\n        ____RRR____\n        ____RRR____\n        ___________\n    ''', '''\n      <style>\n        @page { size: 11px 13px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"13px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <marker id=\"rectangle\" viewBox=\"-1 -1 3 3\">\n            <rect x=\"-10\" y=\"-10\" width=\"20\" height=\"20\" fill=\"red\" />\n          </marker>\n        </defs>\n        <path\n          d=\"M 5 2 v 4 v 4\"\n          marker-start=\"url(#rectangle)\"\n          marker-mid=\"url(#rectangle)\"\n          marker-end=\"url(#rectangle)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_markers_size(assert_pixels):\n    assert_pixels('''\n        ___________\n        ____BBR____\n        ____BBR____\n        ____RRR____\n        ___________\n        ____BBR____\n        ____BBR____\n        ____RRR____\n        ___________\n        ____BBR____\n        ____BBR____\n        ____RRR____\n        ___________\n    ''', '''\n      <style>\n        @page { size: 11px 13px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"13px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <marker id=\"rectangle\"\n                  refX=\"1\" refY=\"1\" markerWidth=\"3\" markerHeight=\"3\">\n            <rect width=\"6\" height=\"6\" fill=\"red\" />\n            <rect width=\"2\" height=\"2\" fill=\"blue\" />\n          </marker>\n        </defs>\n        <path\n          d=\"M 5 2 v 4 v 4\"\n          marker-start=\"url(#rectangle)\"\n          marker-mid=\"url(#rectangle)\"\n          marker-end=\"url(#rectangle)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_markers_viewbox_size(assert_pixels):\n    assert_pixels('''\n        ___________\n        ____RRR____\n        ____RRR____\n        ____RRR____\n        ___________\n        ____RRR____\n        ____RRR____\n        ____RRR____\n        ___________\n        ____RRR____\n        ____RRR____\n        ____RRR____\n        ___________\n    ''', '''\n      <style>\n        @page { size: 11px 13px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"13px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <marker id=\"rectangle\" viewBox=\"-10 -10 6 6\"\n                  refX=\"-8\" refY=\"-8\" markerWidth=\"3\" markerHeight=\"3\">\n            <rect x=\"-10\" y=\"-10\" width=\"6\" height=\"6\" fill=\"red\" />\n          </marker>\n        </defs>\n        <path\n          d=\"M 5 2 v 4 v 4\"\n          marker-start=\"url(#rectangle)\"\n          marker-mid=\"url(#rectangle)\"\n          marker-end=\"url(#rectangle)\" />\n      </svg>\n    ''')\n\n\ndef test_markers_overflow(assert_pixels):\n    assert_pixels('''\n        ___________\n        ____BBRR___\n        ____BBRR___\n        ____RRRR___\n        ____RRRR___\n        ____BBRR___\n        ____BBRR___\n        ____RRRR___\n        ____RRRR___\n        ____BBRR___\n        ____BBRR___\n        ____RRRR___\n        ____RRRR___\n    ''', '''\n      <style>\n        @page { size: 11px 13px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"13px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <marker id=\"rectangle\" overflow=\"visible\"\n                  refX=\"1\" refY=\"1\" markerWidth=\"3\" markerHeight=\"3\">\n            <rect width=\"4\" height=\"4\" fill=\"red\" />\n            <rect width=\"2\" height=\"2\" fill=\"blue\" />\n          </marker>\n        </defs>\n        <path\n          d=\"M 5 2 v 4 v 4\"\n          marker-start=\"url(#rectangle)\"\n          marker-mid=\"url(#rectangle)\"\n          marker-end=\"url(#rectangle)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_markers_userspace(assert_pixels):\n    assert_pixels('''\n        ___________\n        ___________\n        _____R_____\n        ___________\n        ___________\n        ___________\n        _____R_____\n        ___________\n        ___________\n        ___________\n        _____R_____\n        ___________\n        ___________\n    ''', '''\n      <style>\n        @page { size: 11px 13px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"13px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <marker id=\"rectangle\" markerUnits=\"userSpaceOnUse\">\n            <rect width=\"1\" height=\"1\" fill=\"red\" />\n          </marker>\n        </defs>\n        <path\n          d=\"M 5 2 v 4 v 4\"\n          stroke-width=\"10\"\n          marker-start=\"url(#rectangle)\"\n          marker-mid=\"url(#rectangle)\"\n          marker-end=\"url(#rectangle)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_markers_stroke_width(assert_pixels):\n    assert_pixels('''\n        ___________\n        ___________\n        _____RRR___\n        _____RRR___\n        _____RRR___\n        ___________\n        _____RRR___\n        _____RRR___\n        _____RRR___\n        ___________\n        _____RRR___\n        _____RRR___\n        _____RRR___\n    ''', '''\n      <style>\n        @page { size: 11px 13px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"13px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <marker id=\"rectangle\">\n            <rect width=\"1\" height=\"1\" fill=\"red\" />\n          </marker>\n        </defs>\n        <path\n          d=\"M 5 2 v 4 v 4\"\n          stroke-width=\"3\"\n          marker-start=\"url(#rectangle)\"\n          marker-mid=\"url(#rectangle)\"\n          marker-end=\"url(#rectangle)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_markers_viewbox_stroke_width(assert_pixels):\n    assert_pixels('''\n        ___________\n        ____BRR____\n        ____RRR____\n        ____RRR____\n        ___________\n        ____BRR____\n        ____RRR____\n        ____RRR____\n        ___________\n        ____BRR____\n        ____RRR____\n        ____RRR____\n        ___________\n    ''', '''\n      <style>\n        @page { size: 11px 13px }\n        svg { display: block }\n      </style>\n      <svg width=\"11px\" height=\"13px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <marker id=\"rectangle\" viewBox=\"-1 -1 3 3\"\n                  markerWidth=\"1.5\" markerHeight=\"1.5\">\n            <rect x=\"-10\" y=\"-10\" width=\"20\" height=\"20\" fill=\"red\" />\n            <rect x=\"-1\" y=\"-1\" width=\"1\" height=\"1\" fill=\"blue\" />\n          </marker>\n        </defs>\n        <path\n          d=\"M 5 2 v 4 v 4\"\n          stroke-width=\"2\"\n          marker-start=\"url(#rectangle)\"\n          marker-mid=\"url(#rectangle)\"\n          marker-end=\"url(#rectangle)\" />\n      </svg>\n    ''')\n"
  },
  {
    "path": "tests/draw/svg/test_opacity.py",
    "content": "\"\"\"Test how opacity is handled for SVG.\"\"\"\n\nimport pytest\n\nfrom ...testing_utils import assert_no_logs\n\nopacity_source = '''\n  <style>\n    @page { size: 9px }\n    svg { display: block }\n  </style>\n  <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">%s</svg>'''\n\n\n@assert_no_logs\ndef test_opacity(assert_same_renderings):\n    assert_same_renderings(\n        opacity_source % '''\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n                  stroke=\"rgb(127, 255, 127)\" fill=\"rgb(127, 127, 255)\" />\n        ''',\n        opacity_source % '''\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n                  stroke=\"lime\" fill=\"blue\" opacity=\"0.5\" />\n        ''',\n    )\n\n\n@assert_no_logs\ndef test_fill_opacity(assert_same_renderings):\n    assert_same_renderings(\n        opacity_source % '''\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\"\n                  fill=\"blue\" opacity=\"50%\" />\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n                  stroke=\"lime\" fill=\"transparent\" />\n        ''',\n        opacity_source % '''\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n                  stroke=\"lime\" fill=\"blue\" fill-opacity=\"0.5\" />\n        ''',\n    )\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_stroke_opacity(assert_same_renderings):\n    # TODO: This test (and the other ones) fail because of a difference between\n    # the PDF and the SVG specifications: transparent borders have to be drawn\n    # on top of the shape filling in SVG but not in PDF. See:\n    # - PDF-1.7 11.7.4.4 Note 2\n    # - https://www.w3.org/TR/SVG2/render.html#PaintingShapesAndText\n    assert_same_renderings(\n        '''\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\"\n                  fill=\"blue\" />\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n                  stroke=\"lime\" fill=\"transparent\" opacity=\"0.5\" />\n        ''',\n        opacity_source % '''\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n                  stroke=\"lime\" fill=\"blue\" stroke-opacity=\"50%\" />\n        ''',\n    )\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_stroke_fill_opacity(assert_same_renderings):\n    assert_same_renderings(\n        opacity_source % '''\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\"\n                  fill=\"blue\" opacity=\"0.5\" />\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n                  stroke=\"lime\" fill=\"transparent\" opacity=\"0.5\" />\n        ''',\n        opacity_source % '''\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n                  stroke=\"lime\" fill=\"blue\"\n                  stroke-opacity=\"0.5\" fill-opacity=\"0.5\" />\n        ''',\n    )\n\n\n@assert_no_logs\ndef test_pattern_gradient_stroke_fill_opacity(assert_same_renderings):\n    assert_same_renderings(\n        opacity_source % '''\n            <defs>\n              <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"\n                              gradientUnits=\"objectBoundingBox\">\n                <stop stop-color=\"black\" offset=\"42.86%\"></stop>\n                <stop stop-color=\"green\" offset=\"42.86%\"></stop>\n              </linearGradient>\n              <pattern id=\"pat\" x=\"0\" y=\"0\" width=\"2\" height=\"2\"\n                       patternUnits=\"userSpaceOnUse\"\n                       patternContentUnits=\"userSpaceOnUse\">\n                <rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" fill=\"blue\" />\n                <rect x=\"0\" y=\"1\" width=\"1\" height=\"1\" fill=\"red\" />\n                <rect x=\"1\" y=\"0\" width=\"1\" height=\"1\" fill=\"red\" />\n                <rect x=\"1\" y=\"1\" width=\"1\" height=\"1\" fill=\"blue\" />\n              </pattern>\n            </defs>\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\"\n                  fill=\"url(#pat)\" opacity=\"0.5\" />\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n                  stroke=\"url(#grad)\" fill=\"transparent\" opacity=\"0.5\" />\n        ''',\n        opacity_source % '''\n            <defs>\n              <linearGradient id=\"grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"\n                              gradientUnits=\"objectBoundingBox\">\n                <stop stop-color=\"black\" offset=\"42.86%\"></stop>\n                <stop stop-color=\"green\" offset=\"42.86%\"></stop>\n              </linearGradient>\n              <pattern id=\"pat\" x=\"0\" y=\"0\" width=\"2\" height=\"2\"\n                       patternUnits=\"userSpaceOnUse\"\n                       patternContentUnits=\"userSpaceOnUse\">\n                <rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" fill=\"blue\" />\n                <rect x=\"0\" y=\"1\" width=\"1\" height=\"1\" fill=\"red\" />\n                <rect x=\"1\" y=\"0\" width=\"1\" height=\"1\" fill=\"red\" />\n                <rect x=\"1\" y=\"1\" width=\"1\" height=\"1\" fill=\"blue\" />\n              </pattern>\n            </defs>\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" stroke-width=\"2\"\n                  stroke=\"url(#grad)\" fill=\"url(#pat)\"\n                  stroke-opacity=\"0.5\" fill-opacity=\"0.5\" />\n        ''',\n        tolerance=1,\n    )\n\n\n@assert_no_logs\ndef test_translate_opacity(assert_same_renderings):\n    # Regression test for #1976.\n    assert_same_renderings(\n        opacity_source % '''\n            <rect transform=\"translate(2, 2)\" width=\"5\" height=\"5\"\n                  fill=\"blue\" opacity=\"0.5\" />\n        ''',\n        opacity_source % '''\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\"\n                  fill=\"blue\" opacity=\"50%\" />\n        ''',\n    )\n\n\n@assert_no_logs\ndef test_translate_use_opacity(assert_same_renderings):\n    # Regression test for #1976.\n    assert_same_renderings(\n        opacity_source % '''\n            <defs>\n              <rect id=\"rect\" x=\"-10\" y=\"-10\" width=\"5\" height=\"5\" fill=\"blue\" />\n            </defs>\n            <use href=\"#rect\" transform=\"translate(12, 12)\" opacity=\"0.5\" />\n        ''',\n        opacity_source % '''\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"blue\" opacity=\"50%\" />\n        ''',\n    )\n"
  },
  {
    "path": "tests/draw/svg/test_paths.py",
    "content": "\"\"\"Test how SVG simple paths are drawn.\"\"\"\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_path_h(assert_pixels):\n    assert_pixels('''\n        BBBBBBBB__\n        BBBBBBBB__\n        __________\n        RRRRRRRR__\n        RRRRRRRR__\n        __________\n        GGGGGGGG__\n        GGGGGGGG__\n        BBBBBBBB__\n        BBBBBBBB__\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 0 1 H 8 H 1\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 0 4 H 8 4\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 0 7 h 8 h 0\"\n          stroke=\"lime\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 0 9 h 8 0\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_v(assert_pixels):\n    assert_pixels('''\n        BB____GG__\n        BB____GG__\n        BB____GG__\n        BB____GG__\n        ___RR_____\n        ___RR_____\n        ___RR___BB\n        ___RR___BB\n        ___RR___BB\n        ___RR___BB\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 1 0 V 1 V 4\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 4 6 V 4 10\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 7 0 v 0 v 4\"\n          stroke=\"lime\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 9 6 v 0 4\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_l(assert_pixels):\n    assert_pixels('''\n        ______RR__\n        ______RR__\n        ______RR__\n        ___BB_RR__\n        ___BB_RR__\n        ___BB_RR__\n        ___BB_____\n        ___BB_____\n        ___BB_____\n        ___BB_____\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 4 3 L 4 10\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 7 0 l 0 6\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_z(assert_pixels):\n    assert_pixels('''\n        BBBBBBB___\n        BBBBBBB___\n        BB___BB___\n        BB___BB___\n        BBBBBBB___\n        BBBBBBB___\n        ____RRRRRR\n        ____RRRRRR\n        ____RR__RR\n        ____RRRRRR\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 1 1 H 6 V 5 H 1 Z\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 9 10 V 7 H 5 V 10 z\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_z_fill(assert_pixels):\n    assert_pixels('''\n        BBBBBBB___\n        BBBBBBB___\n        BBGGGBB___\n        BBGGGBB___\n        BBBBBBB___\n        BBBBBBB___\n        ____RRRRRR\n        ____RRRRRR\n        ____RRGGRR\n        ____RRRRRR\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 1 1 H 6 V 5 H 1 Z\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"lime\"/>\n        <path d=\"M 9 10 V 7 H 5 V 10 z\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"lime\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_c(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        __________\n        __________\n        __BBB_____\n        __BBB_____\n        __________\n        __RRR_____\n        __RRR_____\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 2 5 C 2 5 3 5 5 5\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 2 8 c 0 0 1 0 3 0\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_s(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        __________\n        __________\n        __BBB_____\n        __BBB_____\n        __________\n        __RRR_____\n        __RRR_____\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 2 5 S 3 5 5 5\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 2 8 s 1 0 3 0\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_cs(assert_pixels):\n    assert_pixels('''\n        __BBBBBB__\n        __BBBBBBB_\n        _____BBBB_\n        __RRRRRR__\n        __RRRRRRR_\n        _____RRRR_\n        __GGGGGG__\n        __GGGGGGG_\n        _____GGGG_\n        __BBBBBB__\n        __BBBBBBB_\n        _____BBBB_\n    ''', '''\n      <style>\n        @page { size: 10px 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 2 1 C 2 1 3 1 5 1 S 8 3 8 1\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 2 4 C 2 4 3 4 5 4 s 3 2 1 0\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 2 7 c 0 0 1 0 3 0 S 8 9 8 7\"\n          stroke=\"lime\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 2 10 c 0 0 1 0 3 0 s 3 2 1 0\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_q(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        __________\n        __________\n        __BBBB____\n        __BBBB____\n        __________\n        __RRRR____\n        __RRRR____\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 2 5 Q 4 5 6 5\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 2 8 q 2 0 4 0\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_t(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        __________\n        __________\n        __BBBB____\n        __BBBB____\n        __________\n        __RRRR____\n        __RRRR____\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 2 5 T 6 5\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 2 8 t 4 0\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_qt(assert_pixels):\n    assert_pixels('''\n        _BBBB_______\n        BBBBBBB_____\n        BBBBBBBB__BB\n        BB__BBBBBBBB\n        _____BBBBBBB\n        _______BBBB_\n        _RRRR_______\n        RRRRRRR_____\n        RRRRRRRR__RR\n        RR__RRRRRRRR\n        _____RRRRRRR\n        _______RRRR_\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 0 3 Q 3 0 6 3 T 12 3\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 0 9 Q 3 6 6 9 t 6 0\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_qt2(assert_pixels):\n    assert_pixels('''\n        _BBBB_______\n        BBBBBBB_____\n        BBBBBBBB__BB\n        BB__BBBBBBBB\n        _____BBBBBBB\n        _______BBBB_\n        _RRRR_______\n        RRRRRRR_____\n        RRRRRRRR__RR\n        RR__RRRRRRRR\n        _____RRRRRRR\n        _______RRRR_\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 0 3 q 3 -3 6 0 T 12 3\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 0 9 q 3 -3 6 0 t 6 0\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_a(assert_pixels):\n    assert_pixels('''\n        __BBBB______\n        _BBBBB______\n        BBBBBB______\n        BBBB________\n        BBB_________\n        BBB____RRRR_\n        ______RRRRR_\n        _____RRRRRR_\n        _____RRRR___\n        _____RRR____\n        _____RRR____\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 1 6 A 5 5 0 0 1 6 1\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 6 11 a 5 5 0 0 1 5 -5\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_a2(assert_pixels):\n    assert_pixels('''\n        ______GGGG__\n        ______GGGGG_\n        ______GGGGGG\n        ________GGGG\n        _________GGG\n        _________GGG\n        GGG______GGG\n        GGG______GGG\n        GGGG____GGGG\n        GGGGGGGGGGGG\n        _GGGGGGGGGG_\n        __GGGGGGGG__\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 1 6 A 5 5 0 1 0 6 1\"\n          stroke=\"lime\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_a3(assert_pixels):\n    assert_pixels('''\n        ______GGGG__\n        ______GGGGG_\n        ______GGGGGG\n        ________GGGG\n        _________GGG\n        _________GGG\n        GGG______GGG\n        GGG______GGG\n        GGGG____GGGG\n        GGGGGGGGGGGG\n        _GGGGGGGGGG_\n        __GGGGGGGG__\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 1 6 a 5 5 0 1 0 5 -5\"\n          stroke=\"lime\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_a4(assert_pixels):\n    assert_pixels('''\n        ____________\n        ____BBB_____\n        ____BBB_____\n        ___BBBB_____\n        _BBBBBB_____\n        _BBBBB______\n        _BBBB____RRR\n        _________RRR\n        ________RRRR\n        ______RRRRRR\n        ______RRRRR_\n        ______RRRR__\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 1 6 A 5 5 0 0 0 6 1\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n        <path d=\"M 6 11 a 5 5 0 0 0 5 -5\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_a5(assert_pixels):\n    assert_pixels('''\n        __BBBBBBBB__\n        _BBBBBBBBBB_\n        BBBBBBBBBBBB\n        BBBB____BBBB\n        BBB______BBB\n        BBB______BBB\n        BBB_________\n        BBB_________\n        BBBB________\n        BBBBBB______\n        _BBBBB______\n        __BBBB______\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 6 11 A 5 5 0 1 1 11 6\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_a6(assert_pixels):\n    assert_pixels('''\n        __BBBBBBBB__\n        _BBBBBBBBBB_\n        BBBBBBBBBBBB\n        BBBB____BBBB\n        BBB______BBB\n        BBB______BBB\n        BBB_________\n        BBB_________\n        BBBB________\n        BBBBBB______\n        _BBBBB______\n        __BBBB______\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 6 11 a 5 5 0 1 1 5 -5\"\n          stroke=\"blue\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_a7(assert_pixels):\n    assert_pixels('''\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        GGG______GGG\n        GGG______GGG\n        GGGG____GGGG\n        GGGGGGGGGGGG\n        _GGGGGGGGGG_\n        __GGGGGGGG__\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 1 6 A 5 5 0 0 0 11 6\"\n          stroke=\"lime\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_wrong_point(assert_pixels):\n    assert_pixels('''\n        ____________\n        GG__________\n        GG__________\n        GG__________\n        GG__________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M 1 1 L 1 5 L\"\n          stroke=\"lime\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_markers_l(assert_pixels):\n    assert_pixels('''\n        _________zz_\n        _RR_____zzRz\n        _RRGGGGzzRzz\n        _RRGGGzzRzz_\n        _RR___zRzz__\n        ________zG__\n        _______RRRR_\n        _______RRRR_\n        ________GG__\n        _______RRRR_\n        _______RRRR_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <marker id=\"line\"\n          viewBox=\"0 0 1 2\" refX=\"0.5\" refY=\"1\"\n          markerUnits=\"strokeWidth\"\n          markerWidth=\"1\" markerHeight=\"2\"\n          orient=\"auto\">\n          <rect x=\"0\" y=\"0\" width=\"1\" height=\"2\" fill=\"red\" />\n        </marker>\n        <path d=\"M 2 3 l 7 0 l 0 4 l 0 3\"\n          stroke=\"lime\" stroke-width=\"2\" fill=\"none\" marker=\"url('#line')\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_path_markers_hv(assert_pixels):\n    assert_pixels('''\n        _________zz_\n        _RR_____zzRz\n        _RRGGGGzzRzz\n        _RRGGGzzRzz_\n        _RR___zRzz__\n        ________zG__\n        _______RRRR_\n        _______RRRR_\n        ________GG__\n        _______RRRR_\n        _______RRRR_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        svg { display: block }\n      </style>\n      <svg width=\"12px\" height=\"12px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <marker id=\"line\"\n          viewBox=\"0 0 1 2\" refX=\"0.5\" refY=\"1\"\n          markerUnits=\"strokeWidth\"\n          markerWidth=\"1\" markerHeight=\"2\"\n          orient=\"auto\">\n          <rect x=\"0\" y=\"0\" width=\"1\" height=\"2\" fill=\"red\" />\n        </marker>\n        <path d=\"M 2 3 h 7 v 4 v 3\"\n          stroke=\"lime\" stroke-width=\"2\" fill=\"none\" marker=\"url(#line)\"/>\n      </svg>\n    ''')\n"
  },
  {
    "path": "tests/draw/svg/test_patterns.py",
    "content": "\"\"\"Test how SVG simple patterns are drawn.\"\"\"\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_pattern(assert_pixels):\n    assert_pixels('''\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n    ''', '''\n      <style>\n        @page { size: 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"8px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <pattern id=\"pat\" x=\"0\" y=\"0\" width=\"4\" height=\"4\"\n            patternUnits=\"userSpaceOnUse\"\n            patternContentUnits=\"userSpaceOnUse\">\n            <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"blue\" />\n            <rect x=\"0\" y=\"2\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"0\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"blue\" />\n          </pattern>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" fill=\"url(#pat)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_pattern_2(assert_pixels):\n    assert_pixels('''\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n    ''', '''\n      <style>\n        @page { size: 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"8px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <pattern id=\"pat\" x=\"0\" y=\"0\" width=\"50%\" height=\"50%\"\n            patternUnits=\"objectBoundingBox\"\n            patternContentUnits=\"userSpaceOnUse\">\n            <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"blue\" />\n            <rect x=\"0\" y=\"2\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"0\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"blue\" />\n          </pattern>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" fill=\"url(#pat)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_pattern_3(assert_pixels):\n    assert_pixels('''\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n    ''', '''\n      <style>\n        @page { size: 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"8px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <pattern id=\"pat\" x=\"0\" y=\"0\" width=\"4\" height=\"4\"\n            patternUnits=\"userSpaceOnUse\"\n            patternContentUnits=\"userSpaceOnUse\">\n            <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"blue\" />\n            <rect x=\"0\" y=\"2\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"0\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"blue\" />\n          </pattern>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" fill=\"url(#pat)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_pattern_4(assert_pixels):\n    assert_pixels('''\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n    ''', '''\n      <style>\n        @page { size: 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"8px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <pattern id=\"pat\" x=\"0\" y=\"0\" width=\"4\" height=\"4\"\n            patternUnits=\"userSpaceOnUse\"\n            patternContentUnits=\"objectBoundingBox\">\n            <rect x=\"0\" y=\"0\" width=\"50%\" height=\"50%\" fill=\"blue\" />\n            <rect x=\"0\" y=\"50%\" width=\"50%\" height=\"50%\" fill=\"red\" />\n            <rect x=\"50%\" y=\"0\" width=\"50%\" height=\"50%\" fill=\"red\" />\n            <rect x=\"50%\" y=\"50%\" width=\"50%\" height=\"50%\" fill=\"blue\" />\n          </pattern>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" fill=\"url(#pat)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_pattern_inherit_attributes(assert_pixels):\n    assert_pixels('''\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n    ''', '''\n      <style>\n        @page { size: 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"8px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <pattern id=\"parent\" x=\"0\" y=\"0\" width=\"4\" height=\"4\"\n            patternUnits=\"userSpaceOnUse\"\n            patternContentUnits=\"userSpaceOnUse\">\n          </pattern>\n          <pattern id=\"pat\" href=\"#parent\">\n            <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"blue\" />\n            <rect x=\"0\" y=\"2\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"0\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"blue\" />\n          </pattern>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" fill=\"url(#pat)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_pattern_inherit_children(assert_pixels):\n    assert_pixels('''\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n    ''', '''\n      <style>\n        @page { size: 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"8px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <pattern id=\"parent\">\n            <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"blue\" />\n            <rect x=\"0\" y=\"2\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"0\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"blue\" />\n          </pattern>\n          <pattern id=\"pat\" href=\"#parent\" x=\"0\" y=\"0\" width=\"4\" height=\"4\"\n            patternUnits=\"userSpaceOnUse\" patternContentUnits=\"userSpaceOnUse\">\n          </pattern>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" fill=\"url(#pat)\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_pattern_inherit_no_override(assert_pixels):\n    assert_pixels('''\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n        BBrrBBrr\n        BBrrBBrr\n        rrBBrrBB\n        rrBBrrBB\n    ''', '''\n      <style>\n        @page { size: 8px }\n        svg { display: block }\n      </style>\n      <svg width=\"8px\" height=\"8px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <pattern id=\"parent\" x=\"1\" y=\"1\" width=\"3\" height=\"3\"\n            patternUnits=\"objectBoundingBox\"\n            patternContentUnits=\"objectBoundingBox\">\n            <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"green\" />\n            <rect x=\"0\" y=\"2\" width=\"2\" height=\"2\" fill=\"green\" />\n            <rect x=\"2\" y=\"0\" width=\"2\" height=\"2\" fill=\"yellow\" />\n            <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"yellow\" />\n          </pattern>\n          <pattern id=\"pat\" href=\"#parent\" x=\"0\" y=\"0\" width=\"4\" height=\"4\"\n            patternUnits=\"userSpaceOnUse\" patternContentUnits=\"userSpaceOnUse\">\n            <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"blue\" />\n            <rect x=\"0\" y=\"2\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"0\" width=\"2\" height=\"2\" fill=\"red\" />\n            <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"blue\" />\n          </pattern>\n        </defs>\n        <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" fill=\"url(#pat)\" />\n      </svg>\n    ''')\n"
  },
  {
    "path": "tests/draw/svg/test_shapes.py",
    "content": "\"\"\"Test how SVG simple shapes are drawn.\"\"\"\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_rect_stroke(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_rect_fill(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_rect_stroke_fill(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRBBBRR_\n        _RRBBBRR_\n        _RRBBBRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"blue\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_rect_round(assert_pixels):\n    assert_pixels('''\n        _zzzzzzz_\n        zzzzzzzzz\n        zzRRRRRzz\n        zzRRRRRzz\n        zzRRRRRzz\n        zzRRRRRzz\n        zzRRRRRzz\n        zzzzzzzzz\n        _zzzzzzz_\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect width=\"9\" height=\"9\" fill=\"red\" rx=\"4\" ry=\"4\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_rect_round_zero(assert_pixels):\n    assert_pixels('''\n        RRRRRRRRR\n        RRRRRRRRR\n        RRRRRRRRR\n        RRRRRRRRR\n        RRRRRRRRR\n        RRRRRRRRR\n        RRRRRRRRR\n        RRRRRRRRR\n        RRRRRRRRR\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect width=\"9\" height=\"9\" fill=\"red\" rx=\"0\" ry=\"4\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_line(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        _________\n        _________\n        RRRRRR___\n        RRRRRR___\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <line x1=\"0\" y1=\"5\" x2=\"6\" y2=\"5\"\n          stroke=\"red\" stroke-width=\"2\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_polyline(assert_pixels):\n    assert_pixels('''\n        _________\n        RRRRRR___\n        RRRRRR___\n        RR__RR___\n        RR__RR___\n        RR__RR___\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <polyline points=\"1,6, 1,2, 5,2, 5,6\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_polyline_fill(assert_pixels):\n    assert_pixels('''\n        _________\n        RRRRRR___\n        RRRRRR___\n        RRBBRR___\n        RRBBRR___\n        RRBBRR___\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <polyline points=\"1,6, 1,2, 5,2, 5,6\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"blue\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_polygon(assert_pixels):\n    assert_pixels('''\n        _________\n        RRRRRR___\n        RRRRRR___\n        RR__RR___\n        RR__RR___\n        RRRRRR___\n        RRRRRR___\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <polygon points=\"1,6, 1,2, 5,2, 5,6\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_polygon_fill(assert_pixels):\n    assert_pixels('''\n        _________\n        RRRRRR___\n        RRRRRR___\n        RRBBRR___\n        RRBBRR___\n        RRRRRR___\n        RRRRRR___\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <polygon points=\"1,6, 1,2, 5,2, 5,6\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"blue\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_circle_stroke(assert_pixels):\n    assert_pixels('''\n        __________\n        __RRRRRR__\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _RRR__RRR_\n        _RRR__RRR_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        __RRRRRR__\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <circle cx=\"5\" cy=\"5\" r=\"3\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_circle_fill(assert_pixels):\n    assert_pixels('''\n        __________\n        __RRRRRR__\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _RRRBBRRR_\n        _RRRBBRRR_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        __RRRRRR__\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <circle cx=\"5\" cy=\"5\" r=\"3\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"blue\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_ellipse_stroke(assert_pixels):\n    assert_pixels('''\n        __________\n        __RRRRRR__\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _RRR__RRR_\n        _RRR__RRR_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        __RRRRRR__\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <ellipse cx=\"5\" cy=\"5\" rx=\"3\" ry=\"3\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"none\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_ellipse_fill(assert_pixels):\n    assert_pixels('''\n        __________\n        __RRRRRR__\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _RRRBBRRR_\n        _RRRBBRRR_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        __RRRRRR__\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"10px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <ellipse cx=\"5\" cy=\"5\" rx=\"3\" ry=\"3\"\n          stroke=\"red\" stroke-width=\"2\" fill=\"blue\"/>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_rect_in_g(assert_pixels):\n    assert_pixels('''\n        RRRRR____\n        RRRRR____\n        RRRRR____\n        RRRRR____\n        RRRRR____\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g x=\"5\" y=\"5\">\n          <rect width=\"5\" height=\"5\" fill=\"red\" />\n        </g>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_rect_x_y_in_g(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g x=\"5\" y=\"5\">\n          <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n        </g>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_rect_stroke_zero(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\"\n              stroke-width=\"0\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_rect_width_height_zero(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n        <style>\n            @page { size: 9px }\n            svg { display: block }\n        </style>\n        <svg width=\"0\" height=\"0\" xmlns=\"http://www.w3.org/2000/svg\">\n            <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n        </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_rect_fill_inherit(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        __KKKKK__\n        __KKKKK__\n        __KKKKK__\n        __KKKKK__\n        __KKKKK__\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"inherit\" />\n      </svg>\n    ''')\n"
  },
  {
    "path": "tests/draw/svg/test_text.py",
    "content": "\"\"\"Test how SVG text is drawn.\"\"\"\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_text_fill(assert_pixels):\n    assert_pixels('''\n        BBBBBB__BBBBBB______\n        BBBBBB__BBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"0\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\">\n          ABC DEF\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_stroke(assert_pixels):\n    assert_pixels('''\n        _BBBBBBBBBBBB_______\n        _BBBBBBBBBBBB_______\n        _BBBBBBBBBBBB_______\n        _BBBBBBBBBBBB_______\n    ''', '''\n      <style>\n        @page { font-size: 1px; size: 20em 4em }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"2\" y=\"2.5\" font-family=\"weasyprint\" font-size=\"2\"\n              fill=\"transparent\" stroke=\"blue\" stroke-width=\"1ex\">\n          A B C\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_x(assert_pixels):\n    assert_pixels('''\n        BB__BB_BBBB_________\n        BB__BB_BBBB_________\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"0 4 7\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\"\n              fill=\"blue\">\n          ABCD\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_y(assert_pixels):\n    assert_pixels('''\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        BBBBBBBBBB_____BBBBB__________\n        BBBBBBBBBB_____BBBBB__________\n        BBBBBBBBBB_____BBBBB__________\n        BBBBBBBBBB_____BBBBB__________\n        BBBBBBBBBB_____BBBBB__________\n    ''', '''\n      <style>\n        @page { size: 30px 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"30px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"0\" y=\"9 9 4 9 4\" font-family=\"weasyprint\" font-size=\"5\"\n              fill=\"blue\">\n          ABCDEF\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_xy(assert_pixels):\n    assert_pixels('''\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        BBBBB__________BBBBB__________\n        BBBBB__________BBBBB__________\n        BBBBB__________BBBBB__________\n        BBBBB__________BBBBB__________\n        BBBBB__________BBBBB__________\n    ''', '''\n      <style>\n        @page { size: 30px 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"30px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"0 10\" y=\"9 4 9 4\" font-family=\"weasyprint\" font-size=\"5\"\n              fill=\"blue\">\n          ABCDE\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_dx(assert_pixels):\n    assert_pixels('''\n        BB__BB_BBBB_________\n        BB__BB_BBBB_________\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text dx=\"0 2 1\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\"\n              fill=\"blue\">\n          ABCD\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_dy(assert_pixels):\n    assert_pixels('''\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        BBBBBBBBBB_____BBBBB__________\n        BBBBBBBBBB_____BBBBB__________\n        BBBBBBBBBB_____BBBBB__________\n        BBBBBBBBBB_____BBBBB__________\n        BBBBBBBBBB_____BBBBB__________\n    ''', '''\n      <style>\n        @page { size: 30px 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"30px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"0\" dy=\"9 0 -5 5 -5\" font-family=\"weasyprint\" font-size=\"5\"\n              fill=\"blue\">\n          ABCDEF\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_dx_dy(assert_pixels):\n    assert_pixels('''\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        __________BBBBB_____BBBBBBBBBB\n        BBBBB__________BBBBB__________\n        BBBBB__________BBBBB__________\n        BBBBB__________BBBBB__________\n        BBBBB__________BBBBB__________\n        BBBBB__________BBBBB__________\n    ''', '''\n      <style>\n        @page { size: 30px 10px }\n        svg { display: block }\n      </style>\n      <svg width=\"30px\" height=\"10px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text dx=\"0 5\" dy=\"9 -5 5 -5\" font-family=\"weasyprint\" font-size=\"5\"\n              fill=\"blue\">\n          ABCDE\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_anchor_start(assert_pixels):\n    assert_pixels('''\n        __BBBBBB____________\n        __BBBBBB____________\n        ____BBBBBB__________\n        ____BBBBBB__________\n    ''', '''\n      <style>\n        @page { size: 20px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"2\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\"\n              fill=\"blue\">\n          ABC\n        </text>\n        <text x=\"4\" y=\"3.5\" font-family=\"weasyprint\" font-size=\"2\"\n              fill=\"blue\" text-anchor=\"start\">\n          ABC\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_anchor_middle(assert_pixels):\n    assert_pixels('''\n        _______BBBBBB_______\n        _______BBBBBB_______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"10\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\"\n              fill=\"blue\" text-anchor=\"middle\">\n          ABC\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_anchor_end(assert_pixels):\n    assert_pixels('''\n        ____________BBBBBB__\n        ____________BBBBBB__\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"18\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\"\n              fill=\"blue\" text-anchor=\"end\">\n          ABC\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_tspan(assert_pixels):\n    assert_pixels('''\n        BBBBBB__BBBBBB______\n        BBBBBB__BBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"10\" y=\"10\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\">\n          <tspan x=\"0\" y=\"1.5\">ABC DEF</tspan>\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_tspan_anchor_middle(assert_pixels):\n    assert_pixels('''\n        _______BBBBBB_______\n        _______BBBBBB_______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"10\" y=\"10\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\">\n          <tspan x=\"10\" y=\"1.5\" text-anchor=\"middle\">ABC</tspan>\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_tspan_anchor_end(assert_pixels):\n    assert_pixels('''\n        ____________BBBBBB__\n        ____________BBBBBB__\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"10\" y=\"10\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\">\n          <tspan x=\"18\" y=\"1.5\" text-anchor=\"end\">ABC</tspan>\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_anchor_middle_tspan(assert_pixels):\n    assert_pixels('''\n        _______BBBBBB_______\n        _______BBBBBB_______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"10\" y=\"10\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\"\n              text-anchor=\"middle\">\n          <tspan x=\"10\" y=\"1.5\">ABC</tspan>\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_anchor_end_tspan(assert_pixels):\n    assert_pixels('''\n        ____________BBBBBB__\n        ____________BBBBBB__\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"10\" y=\"10\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\"\n              text-anchor=\"end\">\n          <tspan x=\"18\" y=\"1.5\">ABC</tspan>\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_anchor_middle_tspan_head_tail(assert_pixels):\n    assert_pixels('''\n        ____BBBBRRRRRRBB____\n        ____BBBBRRRRRRBB____\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"10\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\"\n              text-anchor=\"middle\">\n          AA<tspan fill=\"red\">ABC</tspan>A\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_anchor_end_tspan_head_tail(assert_pixels):\n    assert_pixels('''\n        ______BBBBRRRRRRBB__\n        ______BBBBRRRRRRBB__\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"18\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\"\n              text-anchor=\"end\">\n          AA<tspan fill=\"red\">ABC</tspan>A\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_anchor_middle_end_tspan(assert_pixels):\n    assert_pixels('''\n        _______BBBBBB_______\n        _______BBBBBB_______\n        ____________BBBBBB__\n        ____________BBBBBB__\n    ''', '''\n      <style>\n        @page { size: 20px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"10\" y=\"10\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\">\n          <tspan x=\"10\" y=\"1.5\" text-anchor=\"middle\">ABC</tspan>\n          <tspan x=\"18\" y=\"3.5\" text-anchor=\"end\">ABC</tspan>\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_tspan_anchor_non_text(assert_pixels):\n    # Regression test for #2375.\n    assert_pixels('''\n        _______BBBBBB_______\n        _______BBBBBB_______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" text-anchor=\"end\"\n           xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"10\" y=\"10\" font-family=\"weasyprint\" font-size=\"2\" text-anchor=\"start\">\n          <tspan x=\"10\" y=\"1.5\" text-anchor=\"middle\" fill=\"blue\">ABC</tspan>\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_rotate(assert_pixels):\n    assert_pixels('''\n        __RR__RR__RR________\n        __RR__RR__RR________\n        BB__BB__BB__________\n        BB__BB__BB__________\n    ''', '''\n      <style>\n        @page { size: 20px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"2\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"red\"\n          letter-spacing=\"2\">abc</text>\n        <text x=\"2\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\"\n          rotate=\"180\" letter-spacing=\"2\">abc</text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_text_length(assert_pixels):\n    assert_pixels('''\n        __RRRRRR____________\n        __RRRRRR____________\n        __BB__BB__BB________\n        __BB__BB__BB________\n    ''', '''\n      <style>\n        @page { size: 20px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"2\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"red\">\n          abc\n        </text>\n        <text x=\"2\" y=\"3.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\"\n          textLength=\"10\">abc</text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_length_adjust_glyphs_only(assert_pixels):\n    assert_pixels('''\n        __RRRRRR____________\n        __RRRRRR____________\n        __BBBBBBBBBBBB______\n        __BBBBBBBBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"2\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"red\">\n          abc\n        </text>\n        <text x=\"2\" y=\"3.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\"\n          textLength=\"12\" lengthAdjust=\"spacingAndGlyphs\">abc</text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_font_face(assert_pixels):\n    assert_pixels('''\n        BBBBBB__BBBBBB______\n        BBBBBB__BBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <style>\n            @font-face {\n              font-family: \"SVGFont\";\n              src: url(weasyprint.otf);\n            }\n          </style>\n        </defs>\n        <text x=\"0\" y=\"1.5\" font-family=\"SVGFont\" font-size=\"2\" fill=\"blue\">\n          ABC DEF\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_font_face_css(assert_pixels):\n    assert_pixels('''\n        BBBBBB__BBBBBB______\n        BBBBBB__BBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n          <style>\n            @font-face {\n              font-family: \"SVGFont\";\n              src: url(weasyprint.otf);\n            }\n            text { font-family: \"SVGFont\" }\n          </style>\n        </defs>\n        <text x=\"0\" y=\"1.5\" font-size=\"2\" fill=\"blue\">\n          ABC DEF\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_text_length_adjust_spacing_and_glyphs(assert_pixels):\n    assert_pixels('''\n        __RR_RR_RR__________\n        __RR_RR_RR__________\n        __BBBB__BBBB__BBBB__\n        __BBBB__BBBB__BBBB__\n    ''', '''\n      <style>\n        @page { size: 20px 4px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"4px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"2\" y=\"1.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"red\"\n          letter-spacing=\"1\">abc</text>\n        <text x=\"2\" y=\"3.5\" font-family=\"weasyprint\" font-size=\"2\" fill=\"blue\"\n          letter-spacing=\"1\" textLength=\"16\" lengthAdjust=\"spacingAndGlyphs\">\n          abc\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_font_shorthand(assert_pixels):\n    assert_pixels('''\n        BBBBBB__BBBBBB______\n        BBBBBB__BBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"0\" y=\"1.5\" style=\"font: 2px 'weasyprint'\" fill=\"blue\">\n          ABC DEF\n        </text>\n      </svg>\n    ''',\n    )\n\n\n@assert_no_logs\ndef test_font_shorthand_inheritance_from_parent(assert_pixels):\n    assert_pixels('''\n        BBBBBB__BBBBBB______\n        BBBBBB__BBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g style=\"font: 2px weasyprint\">\n          <text x=\"0\" y=\"1.5\" fill=\"blue\" font=\"bad\">\n            <tspan>ABC DEF</tspan>\n          </text>\n        </g>\n      </svg>\n    ''',\n    )\n\n\n@assert_no_logs\ndef test_explicit_properties_override_parent_shorthand(assert_pixels):\n    assert_pixels('''\n        BBBBBB__BBBBBB______\n        BBBBBB__BBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g font=\"  28px Times New Roman  \">\n          <text x=\"0\" y=\"1.5\" font-size=\"2px\" font-family=\"weasyprint\" fill=\"blue\">\n            ABC DEF\n          </text>\n        </g>\n      </svg>\n    ''',\n    )\n\n\n@assert_no_logs\ndef test_font_shorthand_overrides_explicit_parent_properties(assert_pixels):\n    assert_pixels('''\n        BBBBBB__BBBBBB______\n        BBBBBB__BBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g font-size=\"18px\" font-family=\"weasyprint\">\n          <text x=\"0\" y=\"1.5\" style=\"font: 2px weasyprint\" fill=\"blue\">\n            ABC DEF\n          </text>\n        </g>\n      </svg>\n    ''',\n    )\n\n\n@assert_no_logs\ndef test_child_font_shorthand_overrides_parent_shorthand(assert_pixels):\n    assert_pixels('''\n        BBBBBB__BBBBBB______\n        BBBBBB__BBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g font=\"   34px    sans   \">\n          <text x=\"0\" y=\"1.5\" style=\"font: 2px    weasyprint\" fill=\"blue\">\n            ABC DEF\n          </text>\n        </g>\n      </svg>\n    ''',\n    )\n\n\n@assert_no_logs\ndef test_mixed_explicit_and_shorthand_across_levels(assert_pixels):\n    assert_pixels('''\n        BBBBBB__BBBBBB______\n        BBBBBB__BBBBBB______\n    ''', '''\n      <style>\n        @page { size: 20px 2px }\n        svg { display: block }\n      </style>\n      <svg width=\"20px\" height=\"2px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g font-size=\"40px\" font-family=\"sans-serif\">\n          <g style=\"font: 30px sans\">\n            <text x=\"0\" y=\"1.5\" font-size=\"2px\" font-family=\"weasyprint\" fill=\"blue\">\n              ABC DEF\n            </text>\n          </g>\n        </g>\n      </svg>\n    ''',\n    )\n\n\n@assert_no_logs\ndef test_text_fill_opacity(assert_pixels):\n    # Regression text for #2665.\n    assert_pixels('''\n        ______\n        _ssss_\n        _ssss_\n        _ssss_\n        _ssss_\n        ______\n    ''', '''\n      <style>\n        @page { size: 6px 6px }\n        svg { display: block }\n      </style>\n      <svg width=\"6px\" height=\"6px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <text x=\"1\" y=\"4\" font=\"4px weasyprint\" fill=\"red\" opacity=\"0.5\">\n          A\n        </text>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_emoji_text_svg(assert_pixels):\n    # Regression text for #2683.\n    assert_pixels('''\n        zzzzz\n        zzzzz\n        zzzzz\n        zzzzz\n        zzzzz\n    ''', '''\n      <style>\n        @page { size: 5px 5px }\n        svg { display: block }\n      </style>\n      <svg viewBox=\"0 0 5 5\">\n        <text>🚀</text>\n      </svg>\n    ''')\n"
  },
  {
    "path": "tests/draw/svg/test_transform.py",
    "content": "\"\"\"Test how SVG shapes are transformed.\"\"\"\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_transform_translate(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"0\" y=\"4\" width=\"5\" height=\"5\" transform=\"translate(2, -2)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_translate_one(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"0\" y=\"2\" width=\"5\" height=\"5\" transform=\"translate(2)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_translatex(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"0\" y=\"2\" width=\"5\" height=\"5\" transform=\"translateX(2)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_translatey(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"0\" width=\"5\" height=\"5\" transform=\"translateY(2)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_rotate(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"-7\" width=\"4\" height=\"5\" transform=\"rotate(90)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_rotate_cx_cy(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"7\" y=\"2\" width=\"4\" height=\"5\" transform=\"rotate(90 7 2)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_skew(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRR_____\n        _RRRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRRR_\n        ____RRRR_\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" transform=\"skew(20 20)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_skew_one(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRR___\n        _RRRRRR__\n        __RRRRR__\n        __RRRRR__\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" transform=\"skew(20)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_skewx(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRR___\n        _RRRRRR__\n        __RRRRR__\n        __RRRRR__\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" transform=\"skewX(20)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_skewy(assert_pixels):\n    assert_pixels('''\n        _________\n        _RR______\n        _RRRR____\n        _RRRR____\n        _RRRR____\n        _RRRR____\n        __RRR____\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" transform=\"skewY(20)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_scale(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" transform=\"scale(1.5)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_scale_2(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" transform=\"scale(1.5 1.5)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_scalex(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" transform=\"scaleX(1.5)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_scaley(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRR____\n        _RRRR____\n        _RRRR____\n        _RRRR____\n        _RRRR____\n        _RRRR____\n        _RRRR____\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" transform=\"scaleY(1.5)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_matrix(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\"\n              transform=\"matrix(1.5 0 0 1.5 3 3)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_multiple(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"0\" y=\"0\" width=\"4\" height=\"5\"\n              transform=\"rotate(90) translateY(-7) translateX(2)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_unknown(assert_pixels):\n    assert_pixels('''\n        RRRRR____\n        R__RR____\n        R__RR____\n        R__RR____\n        RRRRR____\n        RRRRR____\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"0\" y=\"0\" width=\"4\" height=\"5\" transform=\"unknown(2)\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_transform_origin(assert_pixels):\n    assert_pixels('''\n        ___RRRRRR\n        ___RRRRRR\n        ___RR___R\n        ___RR___R\n        ___RRRRRR\n        ___RRRRRR\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"0\" y=\"0\" width=\"4\" height=\"5\"\n              transform=\"rotate(90)\" transform-origin=\"4 5\"\n              stroke-width=\"2\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n"
  },
  {
    "path": "tests/draw/svg/test_units.py",
    "content": "\"\"\"Test SVG units.\"\"\"\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_units_px(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2px\" y=\"2px\" width=\"5px\" height=\"5px\"\n              stroke-width=\"2px\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_units_em(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" font-size=\"1px\"\n           xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2em\" y=\"2em\" width=\"5em\" height=\"5em\"\n              stroke-width=\"2em\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_units_ex(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" font-size=\"1px\"\n           xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"4ex\" y=\"4ex\" width=\"10ex\" height=\"10ex\"\n              stroke-width=\"4ex\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_units_unknown(assert_pixels):\n    assert_pixels('''\n        _RRRRRRR_\n        _RR___RR_\n        _RR___RR_\n        _RR___RR_\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect x=\"2px\" y=\"2unk\" width=\"5px\" height=\"5px\"\n              stroke-width=\"2px\" stroke=\"red\" fill=\"none\" />\n      </svg>\n    ''')\n"
  },
  {
    "path": "tests/draw/svg/test_visibility.py",
    "content": "\"\"\"Test how the visibility is controlled with \"visibility\" and \"display\".\"\"\"\n\nfrom ...testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_visibility_visible(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect visibility=\"visible\"\n              x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_visibility_hidden(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect visibility=\"hidden\"\n              x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_visibility_inherit_hidden(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g visibility=\"hidden\">\n          <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n        </g>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_visibility_inherit_visible(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g visibility=\"hidden\">\n          <rect visibility=\"visible\"\n                x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n        </g>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_display_inline(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        __RRRRR__\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect display=\"inline\"\n              x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_display_none(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <rect display=\"none\"\n              x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_display_inherit_none(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g display=\"none\">\n          <rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n        </g>\n      </svg>\n    ''')\n\n\n@assert_no_logs\ndef test_display_inherit_inline(assert_pixels):\n    assert_pixels('''\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        svg { display: block }\n      </style>\n      <svg width=\"9px\" height=\"9px\" xmlns=\"http://www.w3.org/2000/svg\">\n        <g display=\"none\">\n          <rect display=\"inline\"\n                x=\"2\" y=\"2\" width=\"5\" height=\"5\" fill=\"red\" />\n        </g>\n      </svg>\n    ''')\n"
  },
  {
    "path": "tests/draw/test_absolute.py",
    "content": "\"\"\"Test how absolutes are drawn.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_absolute_split_1(assert_pixels):\n    assert_pixels('''\n        BBBBRRRRRRRR____\n        BBBBRRRRRRRR____\n        BBBBRR__________\n        BBBBRR__________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2Px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                left: 0;\n                position: absolute;\n                top: 0;\n                width: 4px;\n            }\n        </style>\n        <div class=\"split\">aa aa</div>\n        <div>bbbbbb bbb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_2(assert_pixels):\n    assert_pixels('''\n        RRRRRRRRRRRRBBBB\n        RRRRRRRRRRRRBBBB\n        RRRR________BBBB\n        RRRR________BBBB\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                position: absolute;\n                top: 0;\n                right: 0;\n                width: 4px;\n            }\n        </style>\n        <div class=\"split\">aa aa</div>\n        <div>bbbbbb bb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_3(assert_pixels):\n    assert_pixels('''\n        BBBBRRRRRRRR____\n        BBBBRRRRRRRR____\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2PX;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                position: absolute;\n                top: 0;\n                left: 0;\n                width: 4px;\n            }\n        </style>\n        <div class=\"split\">aa</div>\n        <div>bbbbbb bbbbb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_4(assert_pixels):\n    assert_pixels('''\n        RRRRRRRRRRRRBBBB\n        RRRRRRRRRRRRBBBB\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                position: absolute;\n                top: 0;\n                right: 0;\n                width: 4px;\n            }\n        </style>\n        <div class=\"split\">aa</div>\n        <div>bbbbbb bbbbb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_5(assert_pixels):\n    assert_pixels('''\n        BBBBRRRR____gggg\n        BBBBRRRR____gggg\n        BBBBRRRRRR__gggg\n        BBBBRRRRRR__gggg\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                position: absolute;\n                top: 0;\n                left: 0;\n                width: 4px;\n            }\n            div.split2 {\n                color: green;\n                position: absolute;\n                top: 0;\n                right: 0;\n                width: 4px;\n        </style>\n        <div class=\"split\">aa aa</div>\n        <div class=\"split2\">cc cc</div>\n        <div>bbbb bbbbb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_6(assert_pixels):\n    assert_pixels('''\n        BBBBRRRR____gggg\n        BBBBRRRR____gggg\n        BBBBRRRRRR______\n        BBBBRRRRRR______\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                position: absolute;\n                width: 4px;\n            }\n            div.split2 {\n                color: green;\n                position: absolute;\n                top: 0;\n                right: 0;\n                width: 4px;\n        </style>\n        <div class=\"split\">aa aa</div>\n        <div class=\"split2\">cc</div>\n        <div>bbbb bbbbb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_7(assert_pixels):\n    assert_pixels('''\n        BBBBRRRRRRRRgggg\n        BBBBRRRRRRRRgggg\n        ____RRRR____gggg\n        ____RRRR____gggg\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                position: absolute;\n                width: 4px;\n            }\n            div.split2 {\n                color: green;\n                position: absolute;\n                top: 0;\n                right: 0;\n                width: 4px;\n            }\n            div.push {\n                margin-left: 4px;\n            }\n        </style>\n        <div class=\"split\">aa</div>\n        <div class=\"split2\">cc cc</div>\n        <div class=\"push\">bbbb bb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_8(assert_pixels):\n    assert_pixels('''\n        ______\n        ______\n        ______\n        ______\n        __RR__\n        __RR__\n        ______\n        ______\n    ''', '''\n        <style>\n            @page {\n                margin: 2px 0;\n                size: 6px 8px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div {\n                position: absolute;\n                left: 2px;\n                top: 2px;\n                width: 2px;\n            }\n        </style>\n        <div>a a a a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_9(assert_pixels):\n    assert_pixels('''\n        ______\n        ______\n        BBRRBB\n        BBRRBB\n        BBRR__\n        BBRR__\n        ______\n        ______\n    ''', '''\n        <style>\n            @page {\n                margin: 2px 0;\n                size: 6px 8px;\n            }\n            body {\n                color: blue;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div {\n                color: red;\n                position: absolute;\n                left: 2px;\n                top: 0;\n                width: 2px;\n            }\n        </style>\n        aaa a<div>a a a a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_10(assert_pixels):\n    assert_pixels('''\n        BB____\n        BB____\n        __RR__\n        __RR__\n        __RR__\n        __RR__\n\n        BBRR__\n        BBRR__\n        __RR__\n        __RR__\n        ______\n        ______\n    ''', '''\n        <style>\n            @page {\n                size: 6px;\n            }\n            body {\n                color: blue;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div {\n                color: red;\n                position: absolute;\n                left: 2px;\n                top: 2px;\n                width: 2px;\n            }\n            div + article {\n                break-before: page;\n            }\n        </style>\n        <article>a</article>\n        <div>a a a a</div>\n        <article>a</article>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_11(assert_pixels):\n    assert_pixels('''\n        BBBBBB\n        BBBBBB\n        BBRRBB\n        BBRRBB\n        __RR__\n        __RR__\n    ''', '''\n        <style>\n            @page {\n                size: 6px;\n            }\n            body {\n                color: blue;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div {\n                bottom: 0;\n                color: red;\n                position: absolute;\n                left: 2px;\n                width: 2px;\n            }\n        </style>\n        aaa aaa<div>a a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_split_12(assert_pixels):\n    assert_pixels('''\n        BBBBBB__\n        BBBBBB__\n        ________\n        ________\n        ________\n        ________\n        ________\n        ________\n        BB______\n        BB______\n        BBRR____\n        BBRR____\n        BBRRRR__\n        BBRRRR__\n        BBRRRRRR\n        BBRRRRRR\n    ''', '''\n        <style>\n            @page {\n                size: 8px;\n            }\n            body {\n                color: blue;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div {\n                break-inside: avoid;\n            }\n            section {\n                left: 2px;\n                position: absolute;\n                color: red;\n            }\n        </style>\n        aaa\n        <div>\n          a\n          <section>x<br>xx<br>xxx</section>\n          <br>\n          a<br>\n          a<br>\n          a\n        </div>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_absolute_next_page(assert_pixels):\n    # TODO: currently, the layout of absolute boxes forces to render a box,\n    # even when it doesn’t fit in the page. This workaround avoids placeholders\n    # with no box. Instead, we should remove these placeholders, or avoid\n    # crashes when they’re rendered.\n    assert_pixels('''\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n        BBBBBBRRRR______\n        BBBBBBRRRR______\n        BBBBBB__________\n        ________________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 4px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                position: absolute;\n                font-size: 3px;\n            }\n        </style>\n        aaaaa aaaaa\n        <div class=\"split\">bb</div>\n        aaaaa\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_rtl_1(assert_pixels):\n    assert_pixels('''\n        __________RRRRRR\n        __________RRRRRR\n        ________________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 3px;\n            }\n            body {\n                direction: rtl;\n            }\n            div {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n                position: absolute;\n            }\n        </style>\n        <div>bbb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_rtl_2(assert_pixels):\n    assert_pixels('''\n        ________________\n        _________RRRRRR_\n        _________RRRRRR_\n    ''', '''\n        <style>\n            @page {\n                size: 16px 3px;\n            }\n            body {\n                direction: rtl;\n            }\n            div {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n                padding: 1px;\n                position: absolute;\n            }\n        </style>\n        <div>bbb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_rtl_3(assert_pixels):\n    assert_pixels('''\n        ________________\n        RRRRRR__________\n        RRRRRR__________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 3px;\n            }\n            body {\n                direction: rtl;\n            }\n            div {\n                bottom: 0;\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                left: 0;\n                line-height: 1;\n                position: absolute;\n            }\n        </style>\n        <div>bbb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_rtl_4(assert_pixels):\n    assert_pixels('''\n        ________________\n        _________RRRRRR_\n        _________RRRRRR_\n    ''', '''\n        <style>\n            @page {\n                size: 16px 3px;\n            }\n            body {\n                direction: rtl;\n            }\n            div {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n                position: absolute;\n                right: 1px;\n                top: 1px;\n            }\n        </style>\n        <div>bbb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_rtl_5(assert_pixels):\n    assert_pixels('''\n        RRRRRR__________\n        RRRRRR__________\n        ________________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 3px;\n            }\n            div {\n                color: red;\n                direction: rtl;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n                position: absolute;\n            }\n        </style>\n        <div>bbb</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_pages_counter(assert_pixels):\n    assert_pixels('''\n        ______\n        _RR___\n        _RR___\n        _RR___\n        _RR___\n        _____B\n        ______\n        _RR___\n        _RR___\n        _BB___\n        _BB___\n        _____B\n    ''', '''\n        <style>\n            @page {\n                font-family: weasyprint;\n                margin: 1px;\n                size: 6px 6px;\n                @bottom-right-corner {\n                    color: blue;\n                    content: counter(pages);\n                    font-size: 1px;\n                }\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div {\n                color: blue;\n                position: absolute;\n            }\n        </style>\n        a a a <div>a a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_pages_counter_orphans(assert_pixels):\n    assert_pixels('''\n        ______\n        _RR___\n        _RR___\n        _RR___\n        _RR___\n        ______\n        ______\n        ______\n        _____B\n        ______\n        _RR___\n        _RR___\n        _BB___\n        _BB___\n        _GG___\n        _GG___\n        ______\n        _____B\n    ''', '''\n        <style>\n            @page {\n                font-family: weasyprint;\n                margin: 1px;\n                size: 6px 9px;\n                @bottom-right-corner {\n                    color: blue;\n                    content: counter(pages);\n                    font-size: 1PX;\n                }\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n                orphans: 2;\n                widows: 2;\n            }\n            div {\n                color: blue;\n                position: absolute;\n            }\n            div ~ div {\n                color: lime;\n            }\n        </style>\n        a a a <div>a a a</div> a <div>a a a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_in_inline(assert_pixels):\n    assert_pixels('''\n        ______\n        _GG___\n        _GG___\n        _GG___\n        _GG___\n        ______\n        ______\n        ______\n        ______\n\n        ______\n        _RR___\n        _RR___\n        _RR___\n        _RR___\n        _BB___\n        _BB___\n        ______\n        ______\n    ''', '''\n        <style>\n            @page {\n                margin: 1px;\n                size: 6px 9px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n                orphans: 2;\n                widows: 2;\n            }\n            p {\n                color: lime;\n            }\n            div {\n                color: blue;\n                position: absolute;\n            }\n        </style>\n        <p>a a</p> a a <div>a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_fixed_in_inline(assert_pixels):\n    assert_pixels('''\n        ______\n        _GG___\n        _GG___\n        _GG___\n        _GG___\n        _BB___\n        _BB___\n        ______\n        ______\n\n        ______\n        _RR___\n        _RR___\n        _RR___\n        _RR___\n        _BB___\n        _BB___\n        ______\n        ______\n    ''', '''\n        <style>\n            @page {\n                margin: 1px;\n                size: 6px 9px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2PX;\n                line-height: 1;\n                orphans: 2;\n                widows: 2;\n            }\n            p {\n                color: lime;\n            }\n            div {\n                color: blue;\n                position: fixed;\n            }\n        </style>\n        <p>a a</p> a a <div>a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_image_background(assert_pixels):\n    assert_pixels('''\n        ____\n        _RBB\n        _BBB\n        _BBB\n    ''', '''\n        <style>\n          @page {\n            size: 4px;\n          }\n          img {\n            background: blue;\n            position: absolute;\n            top: 1px;\n            left: 1px;\n          }\n        </style>\n        <img src=\"pattern-transparent.svg\" />\n    ''')\n\n\n@assert_no_logs\ndef test_absolute_in_absolute_break(assert_pixels):\n    # Regression test for #2134.\n    assert_pixels('''\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n\n        BBBB\n        BBBB\n        RRRR\n        RRRR\n        RRRR\n\n        RRRR\n        RRRR\n        ____\n        ____\n        ____\n    ''', '''\n        <style>\n          @page {\n            size: 4px 5px;\n          }\n          body {\n            font-size: 2px;\n            line-height: 1;\n          }\n          div {\n            position: absolute;\n            width: 100%;\n          }\n        </style>\n        <div style=\"background: blue\">\n          <br><br><br><br><br>\n          <div style=\"background: red\">\n            <br><br>\n          </div>\n        </div>\n        <br><br><br><br><br><br><br>\n    ''')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('position', 'display'), [\n    ('absolute', 'grid'),\n    ('absolute', 'flex'),\n    ('relative', 'grid'),\n    ('relative', 'flex'),\n])\ndef test_absolute_alternative_layout(assert_pixels, position, display):\n    # Regression test for #2450.\n    assert_pixels('''\n        ______\n        _RRRR_\n        _RBBR_\n        _RBBR_\n        _RRRR_\n        ______\n    ''', '''\n        <style>\n          @page {\n            size: 6px;\n          }\n          div {\n            background: blue;\n            position: %s;\n            display: %s;\n            top: 1px;\n            left: 1px;\n            border: 1px solid red;\n            font: 2px weasyprint;\n            color: transparent;\n            width: 2px;\n          }\n        </style>\n        <div>a</div>\n    ''' % (position, display))\n"
  },
  {
    "path": "tests/draw/test_background.py",
    "content": "\"\"\"Test how backgrounds are drawn.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\n@pytest.mark.parametrize(\n    ('expected_pixels', 'html'), [\n        ((10 * (10 * 'B' + '\\n')), '''\n           <style>\n             @page { size: 10px }\n             /* body’s background propagates to the whole canvas */\n             body { margin: 2px; background: #00f; height: 5Px }\n           </style>\n         <body>'''),\n        ('''\n             rrrrrrrrrr\n             rrrrrrrrrr\n             rrBBBBBBrr\n             rrBBBBBBrr\n             rrBBBBBBrr\n             rrBBBBBBrr\n             rrBBBBBBrr\n             rrrrrrrrrr\n             rrrrrrrrrr\n             rrrrrrrrrr\n         ''', '''\n           <style>\n             @page { size: 10px }\n             /* html’s background propagates to the whole canvas */\n             html { padding: 1px; background: #f00 }\n             /* html has a background, so body’s does not propagate */\n             body { margin: 1px; background: #00f; height: 5px }\n          </style>\n          <body>'''),\n    ])\ndef test_canvas_background(assert_pixels, expected_pixels, html):\n    assert_pixels(expected_pixels, html)\n\n\ndef test_canvas_background_size(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        __RRRRRR__\n        __RGGGGR__\n        __zzzzzz__\n        __zzzzzz__\n        __BBBBBB__\n        __BBBBBB__\n        __________\n        __________\n    ''', '''\n      <style>\n         @page { size: 10px; margin: 2px }\n         /* html’s background propagates to the whole canvas */\n         html { background: linear-gradient(\n           red 0, red 50%, blue 50%, blue 100%); }\n         /* html has a background, so body’s does not propagate */\n         body { margin: 1px; background: lime; height: 1px }\n      </style>\n      <body>\n    ''')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('css', 'pixels'), [\n    ('url(pattern.png)', '''\n        ______________\n        ______________\n        __rBBBrBBBrB__\n        __BBBBBBBBBB__\n        __BBBBBBBBBB__\n        __BBBBBBBBBB__\n        __rBBBrBBBrB__\n        __BBBBBBBBBB__\n        __BBBBBBBBBB__\n        __BBBBBBBBBB__\n        __rBBBrBBBrB__\n        __BBBBBBBBBB__\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) repeat-x', '''\n        ______________\n        ______________\n        __rBBBrBBBrB__\n        __BBBBBBBBBB__\n        __BBBBBBBBBB__\n        __BBBBBBBBBB__\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) repeat-y', '''\n        ______________\n        ______________\n        __rBBB________\n        __BBBB________\n        __BBBB________\n        __BBBB________\n        __rBBB________\n        __BBBB________\n        __BBBB________\n        __BBBB________\n        __rBBB________\n        __BBBB________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n\n    ('url(pattern.png) no-repeat 0 0%', '''\n        ______________\n        ______________\n        __rBBB________\n        __BBBB________\n        __BBBB________\n        __BBBB________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat 50% 0px', '''\n        ______________\n        ______________\n        _____rBBB_____\n        _____BBBB_____\n        _____BBBB_____\n        _____BBBB_____\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat 6px top', '''\n        ______________\n        ______________\n        ________rBBB__\n        ________BBBB__\n        ________BBBB__\n        ________BBBB__\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat bottom 6PX right 0', '''\n        ______________\n        ______________\n        ________rBBB__\n        ________BBBB__\n        ________BBBB__\n        ________BBBB__\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat left center', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        __rBBB________\n        __BBBB________\n        __BBBB________\n        __BBBB________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat center left', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        __rBBB________\n        __BBBB________\n        __BBBB________\n        __BBBB________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat 3px 3px', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        _____rBBB_____\n        _____BBBB_____\n        _____BBBB_____\n        _____BBBB_____\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat 100% 50%', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ________rBBB__\n        ________BBBB__\n        ________BBBB__\n        ________BBBB__\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n\n    ('url(pattern.png) no-repeat 0% bottom', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        __rBBB________\n        __BBBB________\n        __BBBB________\n        __BBBB________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat center 6px', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        _____rBBB_____\n        _____BBBB_____\n        _____BBBB_____\n        _____BBBB_____\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat bottom center', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        _____rBBB_____\n        _____BBBB_____\n        _____BBBB_____\n        _____BBBB_____\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat 6px 100%', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ________rBBB__\n        ________BBBB__\n        ________BBBB__\n        ________BBBB__\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) repeat-x 1px 2px', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        __BrBBBrBBBr__\n        __BBBBBBBBBB__\n        __BBBBBBBBBB__\n        __BBBBBBBBBB__\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) repeat-y local 2pX 1px', '''\n        ______________\n        ______________\n        ____BBBB______\n        ____rBBB______\n        ____BBBB______\n        ____BBBB______\n        ____BBBB______\n        ____rBBB______\n        ____BBBB______\n        ____BBBB______\n        ____BBBB______\n        ____rBBB______\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat fixed', '''\n        # The image is actually here:\n        #######\n        ______________\n        ______________\n        __BB__________\n        __BB__________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat fixed right 3px', '''\n        #                   x x x x\n        ______________\n        ______________\n        ______________\n        __________rB__   #\n        __________BB__   #\n        __________BB__   #\n        __________BB__   #\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png)no-repeat fixed 50%center', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        _____rBBB_____\n        _____BBBB_____\n        _____BBBB_____\n        _____BBBB_____\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat, url(pattern.png) no-repeat 2px 1px', '''\n        ______________\n        ______________\n        __rBBB________\n        __BBBBBB______\n        __BBBBBB______\n        __BBBBBB______\n        ____BBBB______\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    ('url(pattern.png) no-repeat 2px 1px, url(pattern.png) no-repeat', '''\n        ______________\n        ______________\n        __rBBB________\n        __BBrBBB______\n        __BBBBBB______\n        __BBBBBB______\n        ____BBBB______\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n])\ndef test_background_image(assert_pixels, css, pixels):\n    assert_pixels(pixels, '''\n      <style>\n        @page { size: 14px 16px }\n        html { background: #fff }\n        body { margin: 2px; height: 10px; background: %s }\n        p { background: none }\n      </style>\n      <body>\n      <p>&nbsp;\n    ''' % css)\n\n\n@assert_no_logs\ndef test_background_image_zero_size_background(assert_pixels):\n    # Regression test for #217.\n    assert_pixels('''\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        html { background: #fff }\n        body { background: url(pattern.png);\n               background-size: cover;\n               display: inline-block }\n      </style>\n      <body>\n    ''')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('css', 'pixels'), [\n    ('border-box', '''\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        _______rBBB_\n        _______BBBB_\n        _______BBBB_\n        _______BBBB_\n        ____________\n    '''),\n    ('padding-box', '''\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ______rBBB__\n        ______BBBB__\n        ______BBBB__\n        ______BBBB__\n        ____________\n        ____________\n    '''),\n    ('content-box', '''\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        _____rBBB___\n        _____BBBB___\n        _____BBBB___\n        _____BBBB___\n        ____________\n        ____________\n        ____________\n    '''),\n    ('border-box; background-clip: content-box', '''\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        _______rB___\n        _______BB___\n        ____________\n        ____________\n        ____________\n    ''')\n])\ndef test_background_origin(assert_pixels, css, pixels):\n    \"\"\"Test the background-origin property.\"\"\"\n    assert_pixels(pixels, '''\n      <style>\n        @page { size: 12px }\n        html { background: #fff }\n        body { margin: 1px; padding: 1px; height: 6px;\n               border: 1px solid transparent;\n               background: url(pattern.png) bottom right no-repeat;\n               background-origin: %s }\n      </style>\n      <body>''' % css)\n\n\n@assert_no_logs\ndef test_background_transform(assert_pixels):\n    # Regression test for #1809.\n    assert_pixels('''\n        _______\n        _RRRRR_\n        _RRRRR_\n        _RRRRR_\n        _RRRRR_\n        _RRRRR_\n        _______\n    ''', '''\n      <style>\n        @page { size: 7px 7px }\n        html { background: #fff }\n        body { position: absolute;\n               width: 5px; height: 5px;\n               top: -5px; left: -5px;\n               transform: translate(6px,6px);\n               background: red }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_background_repeat_space_1(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rBBB__rBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        ____________\n        _rBBB__rBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        ____________\n        _rBBB__rBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px 16px }\n        html { background: #fff }\n        body { margin: 1px; height: 14px;\n               background: url(pattern.png) space }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_background_repeat_space_2(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rBBB__rBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        _rBBB__rBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        _rBBB__rBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        _BBBB__BBBB_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px 14px }\n        html { background: #fff }\n        body { margin: 1px; height: 12px;\n               background: url(pattern.png) space }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_background_repeat_space_3(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rBBBrBBBrB_\n        _BBBBBBBBBB_\n        _BBBBBBBBBB_\n        _BBBBBBBBBB_\n        ____________\n        ____________\n        ____________\n        _rBBBrBBBrB_\n        _BBBBBBBBBB_\n        _BBBBBBBBBB_\n        _BBBBBBBBBB_\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px 13px }\n        html { background: #fff }\n        body { margin: 1px; height: 11px;\n               background: url(pattern.png) repeat space }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_background_repeat_space_4(assert_pixels):\n    assert_pixels('''\n        ________\n        _rBBBGG_\n        _BBBBGG_\n        _BBBBGG_\n        _BBBBGG_\n        _GGGGGG_\n        _GGGGGG_\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px }\n        html { background: #fff }\n        body { margin: 1px; height: 6Px;\n               background: url(pattern.png) space lime }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_background_repeat_round_1(assert_pixels):\n    assert_pixels('''\n        __________\n        _rrBBBBBB_\n        _rrBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _rrBBBBBB_\n        _rrBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 14px }\n        html { background: #fff }\n        body { margin: 1px; height: 12px;\n               image-rendering: pixelated;\n               background: url(pattern.png) top/6px round repeat; }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_background_repeat_round_2(assert_pixels):\n    assert_pixels('''\n        __________\n        _rrBBBBBB_\n        _rrBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _rrBBBBBB_\n        _rrBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 18px }\n        html { background: #fff }\n        body { margin: 1px; height: 16px;\n               image-rendering: pixelated;\n               background: url(pattern.png) center/auto 8px repeat round; }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_background_repeat_round_3(assert_pixels):\n    assert_pixels('''\n        __________\n        _rrBBBBBB_\n        _rrBBBBBB_\n        _rrBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 14px }\n        html { background: #fff }\n        body { margin: 1px; height: 12px;\n               image-rendering: pixelated;\n               background: url(pattern.png) center/6px 9px round; }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_background_repeat_round_4(assert_pixels):\n    assert_pixels('''\n        __________\n        _rBBBrBBB_\n        _rBBBrBBB_\n        _rBBBrBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 14px }\n        html { background: #fff }\n        body { margin: 1px; height: 12px;\n               image-rendering: pixelated;\n               background: url(pattern.png) center/5px 9px round; }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('css', 'pixels'), [\n    ('#00f border-box', '''\n        ________\n        _BBBBBB_\n        _BBBBBB_\n        _BBBBBB_\n        _BBBBBB_\n        _BBBBBB_\n        _BBBBBB_\n        ________\n    '''),\n    ('#00f padding-box', '''\n        ________\n        ________\n        __BBBB__\n        __BBBB__\n        __BBBB__\n        __BBBB__\n        ________\n        ________\n    '''),\n    ('#00f content-box', '''\n        ________\n        ________\n        ________\n        ___BB___\n        ___BB___\n        ________\n        ________\n        ________\n    '''),\n    ('url(pattern.png) padding-box, #0f0', '''\n        ________\n        _GGGGGG_\n        _GrBBBG_\n        _GBBBBG_\n        _GBBBBG_\n        _GBBBBG_\n        _GGGGGG_\n        ________\n    '''),\n])\ndef test_background_clip(assert_pixels, css, pixels):\n    assert_pixels(pixels, '''\n      <style>\n        @page { size: 8px }\n        html { background: #fff }\n        body { margin: 1px; padding: 1px; height: 2px;\n               border: 1px solid transparent;\n               background: %s }\n      </style>\n      <body>''' % css)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('expected_pixels', 'html'), [\n    ('''\n         ____________\n         ____________\n         ____________\n         ___rrBBBBBB_\n         ___rrBBBBBB_\n         ___BBBBBBBB_\n         ___BBBBBBBB_\n         ___BBBBBBBB_\n         ___BBBBBBBB_\n         ___BBBBBBBB_\n         ___BBBBBBBB_\n         ____________\n     ''', '''\n       <style>\n         @page { size: 12px }\n         html { background: #fff }\n         body { margin: 1px; height: 10px;\n                /* Use nearest neighbor algorithm for image resizing: */\n                image-rendering: pixelated;\n                background: url(pattern.png) no-repeat\n                            bottom right / 80% 8px; }\n       </style>\n       <body>'''),\n    ('''\n         ____________\n         ____________\n         ____________\n         ____________\n         ____________\n         ____________\n         ____________\n         _______rBBB_\n         _______BBBB_\n         _______BBBB_\n         _______BBBB_\n         ____________\n     ''', '''\n       <style>\n         @page { size: 12px }\n         html { background: #fff }\n         body { margin: 1px; height: 10px;\n                /* Use nearest neighbor algorithm for image resizing: */\n                image-rendering: pixelated;\n                background: url(pattern.png) bottom right/auto no-repeat }\n       </style>\n       <body>'''),\n    ('''\n         ______________\n         _rrBBBBBB_____\n         _rrBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         ______________\n     ''', '''\n       <style>\n         @page { size: 14px 10px }\n         html { background: #fff }\n         body { margin: 1px; height: 8px;\n                /* Use nearest neighbor algorithm for image resizing: */\n                image-rendering: pixelated;\n                background: url(pattern.png) no-repeat;\n                background-size: contain }\n       </style>\n       <body>'''),\n\n    ('''\n         ______________\n         _rrBBBBBB_____\n         _rrBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         ______________\n     ''', '''\n       <style>\n         @page { size: 14px 10px }\n         html { background: #fff }\n         body { margin: 1px; height: 8px;\n                /* Use nearest neighbor algorithm for image resizing: */\n                image-rendering: pixelated;\n                background: url(pattern.png) no-repeat left / auto 8px;\n                clip: auto; /* no-op to cover more validation */ }\n       </style>\n       <body>'''),\n    ('''\n         ______________\n         _rrBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         _BBBBBBBB_____\n         ______________\n         ______________\n         ______________\n         ______________\n         ______________\n     ''', '''\n       <style>\n         @page { size: 14px 10px }\n         html { background: #fff }\n         body { margin: 1px; height: 8px;\n                /* Use nearest neighbor algorithm for image resizing: */\n                image-rendering: pixelated;\n                background: url(pattern.png) no-repeat 0 0 / 8px 4px;\n                clip: auto; /* no-op to cover more validation */ }\n       </style>\n       <body>'''),\n    ('''\n         ______________\n         _rrrBBBBBBBBB_\n         _rrrBBBBBBBBB_\n         _rrrBBBBBBBBB_\n         _BBBBBBBBBBBB_\n         _BBBBBBBBBBBB_\n         _BBBBBBBBBBBB_\n         _BBBBBBBBBBBB_\n         _BBBBBBBBBBBB_\n         ______________\n     ''', '''\n       <style>\n         @page { size: 14px 10px }\n         html { background: #fff }\n         body { margin: 1px; height: 8px;\n                /* Use nearest neighbor algorithm for image resizing: */\n                image-rendering: pixelated;\n                background: url(pattern.png) no-repeat right 0/cover }\n       </style>\n       <body>'''),\n    ]\n)\ndef test_background_size(assert_pixels, expected_pixels, html):\n    assert_pixels(expected_pixels, html)\n\n\n@assert_no_logs\ndef test_bleed_background_size(assert_pixels):\n    assert_pixels('''\n        RRRR\n        RRRR\n        RRRR\n        RRRR\n    ''', '''\n      <style>\n         @page { size: 2px; bleed: 1px; background: red }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_background_size_clip(assert_pixels):\n    assert_pixels('''\n        BBBB\n        BRBB\n        BBBB\n        BBBB\n    ''', '''\n      <style>\n         @page { size: 4px; margin: 1px;\n                 background: url(pattern.png) red;\n                 background-clip: content-box }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_page_background_fixed(assert_pixels):\n    # Regression test for #1993.\n    assert_pixels('''\n        RBBB\n        BBBB\n        BBBB\n        BBBB\n    ''', '''\n      <style>\n         @page { size: 4px; margin: 1px;\n                 background: url(pattern.png) red;\n                 background-attachment: fixed; }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_page_background_fixed_bleed(assert_pixels):\n    # Regression test for #1993.\n    assert_pixels('''\n        RRRRRR\n        RRBBBR\n        RBBBBR\n        RBBBBR\n        RBBBBR\n        RRRRRR\n    ''', '''\n      <style>\n         @page { size: 4px; margin: 1px; bleed: 1px;\n                 background: url(pattern.png) no-repeat red;\n                 background-attachment: fixed; }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_bleed_background_size_clip(assert_pixels):\n    # Regression test for #1943.\n    assert_pixels('''\n        BBBBBB\n        BBBBBB\n        BBRBBB\n        BBBBBB\n        BBBBBB\n        BBBBBB\n    ''', '''\n      <style>\n         @page { size: 4px; bleed: 1px; margin: 1px;\n                 background: url(pattern.png) red;\n                 background-clip: content-box }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_marks_crop(assert_pixels):\n    assert_pixels('''\n        KK__KK\n        K____K\n        ______\n        ______\n        K____K\n        KK__KK\n    ''', '''\n      <style>\n         @page { size: 4px; bleed: 1px; margin: 1px; marks: crop }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_marks_cross(assert_pixels):\n    assert_pixels('''\n        __KK__\n        ______\n        K____K\n        K____K\n        ______\n        __KK__\n    ''', '''\n      <style>\n         @page { size: 4px; bleed: 1px; margin: 1px; marks: cross }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_marks_crop_cross(assert_pixels):\n    assert_pixels('''\n        KKKKKK\n        K____K\n        K____K\n        K____K\n        K____K\n        KKKKKK\n    ''', '''\n      <style>\n         @page { size: 4px; bleed: 1px; margin: 1px; marks: crop cross }\n      </style>\n      <body>''')\n"
  },
  {
    "path": "tests/draw/test_before_after.py",
    "content": "\"\"\"Test how before and after pseudo elements are drawn.\"\"\"\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_before_after_1(assert_same_renderings):\n    assert_same_renderings(\n        '''\n            <style>\n                @page { size: 300px 30px }\n                body { margin: 0 }\n                a[href]:before { content: '[' attr(href) '] ' }\n            </style>\n            <p><a href=\"some url\">some content</a></p>\n        ''',\n        '''\n            <style>\n                @page { size: 300px 30PX }\n                body { margin: 0 }\n            </style>\n            <p><a href=\"another url\"><span>[some url] </span>some content</p>\n        ''', tolerance=10)\n\n\n@assert_no_logs\ndef test_before_after_2(assert_same_renderings):\n    assert_same_renderings(\n        '''\n            <style>\n                @page { size: 500px 30px }\n                body { margin: 0; quotes: '«' '»' '“' '”' }\n                q:before { content: open-quote ' '}\n                q:after { content: ' ' close-quote }\n            </style>\n            <p><q>Lorem ipsum <q>dolor</q> sit amet</q></p>\n        ''',\n        '''\n            <style>\n                @page { size: 500px 30Px }\n                body { margin: 0 }\n                q:before, q:after { content: none }\n            </style>\n            <p><span><span>« </span>Lorem ipsum\n                <span><span>“ </span>dolor<span> ”</span></span>\n                sit amet<span> »</span></span></p>\n        ''', tolerance=10)\n\n\n@assert_no_logs\ndef test_before_after_3(assert_same_renderings):\n    assert_same_renderings(\n        '''\n            <style>\n                @page { size: 100px 30px }\n                body { margin: 0; }\n                p:before { content: 'a' url(pattern.png) 'b'}\n            </style>\n            <p>c</p>\n        ''',\n        '''\n            <style>\n                @page { size: 100PX 30px }\n                body { margin: 0 }\n            </style>\n            <p><span>a<img src=\"pattern.png\" alt=\"Missing image\">b</span>c</p>\n        ''', tolerance=10)\n"
  },
  {
    "path": "tests/draw/test_box.py",
    "content": "\"\"\"Test how boxes, borders, outlines are drawn.\"\"\"\n\nimport itertools\n\nimport pytest\n\nfrom weasyprint import HTML\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('margin', 'prop'), [\n    ('10px', 'border'),\n    ('20px', 'outline'),\n])\ndef test_outlines_and_borders(assert_pixels, assert_different_renderings, margin, prop):\n    \"\"\"Test the rendering of outlines and borders.\"\"\"\n    source = '''\n      <style>\n        @page { size: 140px 110px }\n        body { width: 100px; height: 70px; margin: %s; %s: 10PX %s blue }\n      </style>\n      <body>'''\n\n    # Do not test the exact rendering of earch border style but at least\n    # check that they do not do the same.\n    documents = (\n        source % (margin, prop, border_style)\n        for border_style in (\n            'none', 'solid', 'dashed', 'dotted', 'double',\n            'inset', 'outset', 'groove', 'ridge'))\n    assert_different_renderings(*documents)\n\n    css_margin = margin\n    width = 140\n    height = 110\n    margin = 10\n    border = 10\n    solid_pixels = [['_'] * width for _ in range(height)]\n    for x in range(margin, width - margin):\n        for y in itertools.chain(\n                range(margin, margin + border),\n                range(height - margin - border, height - margin)):\n            solid_pixels[y][x] = 'B'\n    for y in range(margin, height - margin):\n        for x in itertools.chain(\n                range(margin, margin + border),\n                range(width - margin - border, width - margin)):\n            solid_pixels[y][x] = 'B'\n    pixels = '\\n'.join(''.join(chars) for chars in solid_pixels)\n    html = source % (css_margin, prop, 'solid')\n    assert_pixels(pixels, html)\n\n\n@assert_no_logs\ndef test_borders_table_collapse(assert_different_renderings):\n    \"\"\"Test the rendering of collapsing borders.\"\"\"\n    source = '''\n      <style>\n        @page { size: 140px 110px }\n        table { width: 100px; height: 70px; margin: 10px;\n                border-collapse: collapse; border: 10px %s blue }\n      </style>\n      <table><td>abc</td>'''\n\n    # Do not test the exact rendering of earch border style but at least\n    # check that they do not do the same.\n    documents = (\n        source % border_style\n        for border_style in (\n            'none', 'solid', 'dashed', 'dotted', 'double',\n            'inset', 'groove'))\n    assert_different_renderings(*documents)\n\n\n@assert_no_logs\n@pytest.mark.parametrize('styles', [\n    ('groove', 'outset'),\n    ('ridge', 'inset'),\n])\ndef test_borders_table_collapse_equivalent(assert_same_renderings, styles):\n    \"\"\"Test the rendering of equivalent collapsing borders.\"\"\"\n    source = '''\n      <style>\n        @page { size: 140px 110px }\n        table { width: 100px; height: 70px; margin: 10px;\n                border-collapse: collapse; border: 10pX %s blue }\n      </style>\n      <table><td>abc</td>'''\n\n    documents = (source % border_style for border_style in styles)\n    assert_same_renderings(*documents)\n\n\n@assert_no_logs\n@pytest.mark.parametrize('border_style', ['none', 'solid', 'dashed', 'dotted'])\ndef test_small_borders_1(border_style):\n    # Regression test for #49.\n    html = '''\n      <style>\n        @page { size: 50px 50px }\n        body { margin: 5px; height: 0; border: 10px %s blue }\n      </style>\n      <body>''' % border_style\n    HTML(string=html).write_pdf()\n\n\n@assert_no_logs\n@pytest.mark.parametrize('border_style', ['none', 'solid', 'dashed', 'dotted'])\ndef test_small_borders_2(border_style):\n    # Regression test for #146.\n    html = '''\n      <style>\n        @page { size: 50Px 50px }\n        body { height: 0; width: 0; border-width: 1px 0; border-style: %s }\n      </style>\n      <body>''' % border_style\n    HTML(string=html).write_pdf()\n\n\n@assert_no_logs\ndef test_em_borders():\n    # Regression test for #1378.\n    html = '<body style=\"border: 1em solid\">'\n    HTML(string=html).write_pdf()\n\n\n@assert_no_logs\ndef test_borders_box_sizing(assert_pixels):\n    assert_pixels('''\n        ________\n        _RRRRRR_\n        _R____R_\n        _RRRRRR_\n        ________\n    ''', '''\n      <style>\n        @page {\n          size: 8px 5px;\n        }\n        div {\n          border: 1px solid red;\n          box-sizing: border-box;\n          height: 3px;\n          margin: 1px;\n          min-height: auto;\n          min-width: auto;\n          width: 6px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_margin_boxes(assert_pixels):\n    assert_pixels('''\n        _______________\n        _GGG______BBBB_\n        _GGG______BBBB_\n        _______________\n        _____RRRR______\n        _____RRRR______\n        _____RRRR______\n        _____RRRR______\n        _______________\n        _bbb______gggg_\n        _bbb______gggg_\n        _bbb______gggg_\n        _bbb______gggg_\n        _bbb______gggg_\n        _______________\n    ''', '''\n      <style>\n        html { height: 100% }\n        body { background: #f00; height: 100% }\n        @page {\n          size: 15px;\n          margin: 4px 6Px 7px 5px;\n\n          @top-left-corner {\n            margin: 1px;\n            content: \" \";\n            background: #0f0;\n          }\n          @top-right-corner {\n            margin: 1px;\n            content: \" \";\n            background: #00f;\n          }\n          @bottom-right-corner {\n            margin: 1px;\n            content: \" \";\n            background: #008000;\n          }\n          @bottom-left-corner {\n            margin: 1px;\n            content: \" \";\n            background: #000080;\n          }\n        }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_display_inline_block_twice():\n    # Regression test for #880.\n    html = '<div style=\"background: red; display: inline-block\">'\n    document = HTML(string=html).render()\n    assert document.write_pdf() == document.write_pdf()\n\n\n@assert_no_logs\ndef test_draw_border_radius(assert_pixels):\n    assert_pixels('''\n        ___zzzzz\n        __zzzzzz\n        _zzzzzzz\n        zzzzzzzz\n        zzzzzzzz\n        zzzzzzzR\n        zzzzzzRR\n        zzzzzRRR\n    ''', '''\n      <style>\n        @page {\n          size: 8px 8px;\n        }\n        div {\n          background: Red;\n          border-radius: 50% 0 0 0;\n          height: 16px;\n          width: 16PX;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_draw_split_border_radius(assert_pixels):\n    assert_pixels('''\n        ___zzzzz\n        __zzzzzz\n        _zzzzzzz\n        zzzzzzzz\n        zzzzzzzz\n        zzzzzzzz\n        zzzzzzRR\n        zzzzzRRR\n\n        RRRRRRRR\n        RRRRRRRR\n        RRRRRRRR\n        RRRRRRRR\n        RRRRRRRR\n        RRRRRRRR\n        RRRRRRRR\n        RRRRRRRR\n\n        zzzzzzRR\n        zzzzzzzR\n        zzzzzzzz\n        zzzzzzzz\n        zzzzzzzz\n        zzzzzzzz\n        _zzzzzzz\n        __zzzzzz\n    ''', '''\n      <style>\n        @page {\n          size: 8px 8px;\n        }\n        div {\n          background: red;\n          color: transparent;\n          border-radius: 8px;\n          line-height: 9px;\n          width: 16px;\n        }\n      </style>\n      <div>a b c</div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_stretch(assert_pixels):\n    assert_pixels('''\n        __________\n        _RYYYMMMG_\n        _M______C_\n        _M______C_\n        _Y______Y_\n        _Y______Y_\n        _BYYYCCCK_\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 8px;\n        }\n        div {\n          border: 1px solid black;\n          border-image-source: url(border.svg);\n          border-image-slice: 25%;\n          height: 4px;\n          margin: 1px;\n          width: 6px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_fill(assert_pixels):\n    assert_pixels('''\n        __________\n        _RYYYMMMG_\n        _MbbbgggC_\n        _MbbbgggC_\n        _YgggbbbY_\n        _YgggbbbY_\n        _BYYYCCCK_\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 8px;\n        }\n        div {\n          border: 1px solid black;\n          border-image-source: url(border.svg);\n          border-image-slice: 25% fill;\n          height: 4px;\n          margin: 1px;\n          width: 6px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_default_slice(assert_pixels):\n    assert_pixels('''\n        _____________\n        _RYMG___RYMG_\n        _MbgC___MbgC_\n        _YgbY___YgbY_\n        _BYCK___BYCK_\n        _____________\n        _____________\n        _RYMG___RYMG_\n        _MbgC___MbgC_\n        _YgbY___YgbY_\n        _BYCK___BYCK_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 12px;\n        }\n        div {\n          border: 4px solid black;\n          border-image-source: url(border.svg);\n          height: 2px;\n          margin: 1px;\n          width: 3px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_uneven_width(assert_pixels):\n    assert_pixels('''\n        ____________\n        _RRRYYYMMMG_\n        _MMM______C_\n        _MMM______C_\n        _YYY______Y_\n        _YYY______Y_\n        _BBBYYYCCCK_\n        ____________\n    ''', '''\n      <style>\n        @page {\n          size: 12px 8px;\n        }\n        div {\n          border: 1px solid black;\n          border-left-width: 3px;\n          border-image-source: url(border.svg);\n          border-image-slice: 25%;\n          height: 4px;\n          margin: 1px;\n          width: 6px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_not_percent(assert_pixels):\n    assert_pixels('''\n        __________\n        _RYYYMMMG_\n        _M______C_\n        _M______C_\n        _Y______Y_\n        _Y______Y_\n        _BYYYCCCK_\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 8px;\n        }\n        div {\n          border: 1px solid black;\n          border-image-source: url(border.svg);\n          border-image-slice: 1;\n          height: 4px;\n          margin: 1px;\n          width: 6px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_repeat(assert_pixels):\n    assert_pixels('''\n        ___________\n        _RYMYMYMYG_\n        _M_______C_\n        _Y_______Y_\n        _M_______C_\n        _Y_______Y_\n        _BYCYCYCYK_\n        ___________\n    ''', '''\n      <style>\n        @page {\n          size: 11px 8px;\n        }\n        div {\n          border: 1px solid black;\n          border-image-source: url(border.svg);\n          border-image-slice: 25%;\n          border-image-repeat: repeat;\n          height: 4px;\n          margin: 1px;\n          width: 7px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_space(assert_pixels):\n    assert_pixels('''\n        _________\n        _R_YMC_G_\n        _________\n        _M_____C_\n        _Y_____Y_\n        _C_____M_\n        _________\n        _B_YCM_K_\n        _________\n    ''', '''\n      <style>\n        @page {\n          size: 9px 9px;\n        }\n        div {\n          border: 1px solid black;\n          border-image-source: url(border2.svg);\n          border-image-slice: 20%;\n          border-image-repeat: space;\n          height: 5px;\n          margin: 1px;\n          width: 5px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_outset(assert_pixels):\n    assert_pixels('''\n        ____________\n        _RYYYYMMMMG_\n        _M________C_\n        _M_bbbbbb_C_\n        _M_bbbbbb_C_\n        _Y_bbbbbb_Y_\n        _Y_bbbbbb_Y_\n        _Y________Y_\n        _BYYYYCCCCK_\n        ____________\n    ''', '''\n      <style>\n        @page {\n          size: 12px 10px;\n        }\n        div {\n          border: 1px solid black;\n          border-image-source: url(border.svg);\n          border-image-slice: 25%;\n          border-image-outset: 2px;\n          height: 2px;\n          margin: 3px;\n          width: 4px;\n          background: #000080\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_width(assert_pixels):\n    assert_pixels('''\n        __________\n        _RRYYMMGG_\n        _RRYYMMGG_\n        _MM____CC_\n        _YY____YY_\n        _BBYYCCKK_\n        _BBYYCCKK_\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 8px;\n        }\n        div {\n          border: 1px solid black;\n          border-image-source: url(border.svg);\n          border-image-slice: 25%;\n          border-image-width: 2;\n          height: 4px;\n          margin: 1PX;\n          width: 6px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_gradient(assert_pixels):\n    assert_pixels('''\n        __________\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _RR____RR_\n        _BB____BB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 8px;\n        }\n        div {\n          border: 1px solid black;\n          border-image-source: linear-gradient(to bottom, red, red 50%, blue 50%, blue);\n          border-image-slice: 25%;\n          border-image-width: 2;\n          height: 4px;\n          margin: 1px;\n          width: 6px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_border_image_gradient_units(assert_pixels):\n    # Regression test for #2690.\n    assert_pixels('''\n        __________\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _RR____RR_\n        _BB____BB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 8px;\n        }\n        div {\n          border: 1px solid black;\n          border-image-source: linear-gradient(\n            to bottom, red 0, red 50%, blue 0.5em, blue);\n          border-image-slice: 25%;\n          border-image-width: 2;\n          font-size: 6px;\n          height: 4px;\n          margin: 1px;\n          width: 6px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_mask_border(assert_pixels):\n    assert_pixels('''\n        __________\n        __RR__RRR_\n        _R______R_\n        _R______R_\n        _s______R_\n        _s______R_\n        _R______R_\n        _R______R_\n        __RRRRRRR_\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 10px;\n        }\n        div {\n          background: red;\n          mask-border-source: url(mask.svg);\n          mask-border-slice: 20%;\n          height: 8px;\n          width: 8px;\n          margin: 1px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_mask_border_fill(assert_pixels):\n    assert_pixels('''\n        __________\n        __RR__RRR_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        _sRR__RRR_\n        _sRR__RRR_\n        _RRRRRRRR_\n        _RRRRRRRR_\n        __RRRRRRR_\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 10px;\n        }\n        div {\n          background: red;\n          mask-border-source: url(mask.svg);\n          mask-border-slice: 20% fill;\n          height: 8px;\n          width: 8px;\n          margin: 1px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_outline_and_border(assert_pixels):\n    assert_pixels('''\n        __________\n        _BBBBBBBB_\n        _BRRRRRRB_\n        _BR____RB_\n        _BRRRRRRB_\n        _BBBBBBBB_\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 7px;\n        }\n        div {\n          border: 1px solid red;\n          outline: 1px solid blue;\n          height: 1px;\n          margin: 2px;\n          min-height: auto;\n          min-width: auto;\n          width: 4px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_outline_offset(assert_pixels):\n    assert_pixels('''\n        ____________\n        _BBBBBBBBBB_\n        _B________B_\n        _B_RRRRRR_B_\n        _B_R____R_B_\n        _B_RRRRRR_B_\n        _B________B_\n        _BBBBBBBBBB_\n        ____________\n    ''', '''\n      <style>\n        @page {\n          size: 12px 9px;\n        }\n        div {\n          border: 1px solid red;\n          outline: 1px solid blue;\n          outline-offset: 1px;\n          height: 1px;\n          margin: 3px;\n          min-height: auto;\n          min-width: auto;\n          width: 4px;\n        }\n      </style>\n      <div></div>\n    ''')\n\n\n@assert_no_logs\ndef test_infinite_border_radius(assert_pixels):\n    # Regression test for #2706.\n    assert_pixels('''\n        __________\n        __zzzzzz__\n        _zzzzzzzz_\n        _zzzRRzzz_\n        _zzRRRRzz_\n        _zzRRRRzz_\n        _zzzRRzzz_\n        _zzzzzzzz_\n        __zzzzzz__\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px;\n        }\n        div {\n          background-color: red;\n          border-radius: calc(infinity * 1px);\n          height: 8px;\n          margin: 1px;\n          width: 8px;\n        }\n      </style>\n      <div></div>\n    ''')\n"
  },
  {
    "path": "tests/draw/test_cmyk_color_profiles.py",
    "content": "\"\"\"Test CMYK and Color Profiles.\"\"\"\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_device_cmyk_with_icc(assert_pixels):\n    assert_pixels('PP\\nPP', '''\n      <style>\n        @color-profile device-cmyk {\n          src: url(cmyk.icc);\n          components: C, M, Y, K;\n        }\n        @page { size: 2px }\n        html, body { height: 100%; margin: 0 }\n        html { background: device-cmyk(0.8 0.6 0.4 0.2) }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_device_cmyk_without_icc(assert_pixels):\n    assert_pixels('QQ\\nQQ', '''\n      <style>\n        @page { size: 2px }\n        html, body { height: 100%; margin: 0 }\n        html { background: device-cmyk(0.8 0.6 0.4 0.2) }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_custom_cmyk_with_icc(assert_pixels):\n    assert_pixels('PP\\nPP', '''\n      <style>\n        @color-profile --custom-cmyk {\n          src: url(cmyk.icc);\n          components: C, M, Y, K;\n        }\n        @page { size: 2px }\n        html, body { height: 100%; margin: 0 }\n        html { background: color(--custom-cmyk 0.8 0.6 0.4 0.2) }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_image_cmyk_without_icc(assert_pixels):\n    assert_pixels('QQ\\nQQ', '''\n      <style>\n        @page { size: 2px }\n        img { display: block; }\n      </style>\n      <img src=\"cmyk_without_icc.jpg\">''')\n\n\n@assert_no_logs\ndef test_image_cmyk_with_external_icc(assert_pixels):\n    assert_pixels('PP\\nPP', '''\n      <style>\n        @color-profile device-cmyk {\n          src: url(cmyk.icc);\n          components: C, M, Y, K;\n        }\n        @page { size: 2px }\n        img { display: block; }\n      </style>\n      <img src=\"cmyk_without_icc.jpg\">''')\n\n\n@assert_no_logs\ndef test_image_cmyk_with_icc(assert_pixels):\n    assert_pixels('QQ\\nQQ', '''\n      <style>\n        @page { size: 2px }\n        img { display: block; }\n      </style>\n      <img src=\"cmyk_with_icc.jpg\">''')\n"
  },
  {
    "path": "tests/draw/test_column.py",
    "content": "\"\"\"Test how columns are drawn.\"\"\"\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_column_rule_1(assert_pixels):\n    assert_pixels('''\n        a_r_a\n        a_r_a\n        _____\n    ''', '''\n      <style>\n        img { display: inline-block; width: 1px; height: 1px }\n        div { columns: 2; column-rule-style: solid;\n              column-rule-width: 1px; column-gap: 3px;\n              column-rule-color: red }\n        body { margin: 0; font-size: 0 }\n        @page { margin: 0; size: 5px 3Px }\n      </style>\n      <div>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n      </div>''')\n\n\n@assert_no_logs\ndef test_column_rule_2(assert_pixels):\n    assert_pixels('''\n        a_r_a\n        a___a\n        a_r_a\n    ''', '''\n      <style>\n        img { display: inline-block; width: 1px; height: 1px }\n        div { columns: 2; column-rule-style: dotted;\n              column-rule-width: 1px; column-gap: 3px;\n              column-rule-color: red }\n        body { margin: 0; font-size: 0 }\n        @page { margin: 0; size: 5px 3px }\n      </style>\n      <div>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n      </div>''')\n\n\n@assert_no_logs\ndef test_column_rule_span(assert_pixels):\n    assert_pixels('''\n        ___________\n        ___________\n        ___________\n        ___a_______\n        ___a_r_a___\n        ___a_r_a___\n        ___________\n        ___________\n        ___________\n    ''', '''\n      <style>\n        img { display: inline-block; width: 1px; height: 1px }\n        div { columns: 2; column-rule: 1px solid red; column-gap: 3px }\n        article { column-span: all }\n        body { margin: 0; font-size: 0 }\n        @page { margin: 3px; size: 11px 9px }\n      </style>\n      <div>\n        <article>\n          <img src=blue.jpg>\n        </article>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n      </div>''')\n\n\n@assert_no_logs\ndef test_column_rule_normal(assert_pixels):\n    # Regression test for #2217.\n    assert_pixels('''\n        a___a\n        a___a\n        _____\n    ''', '''\n      <style>\n        img { display: inline-block; width: 1px; height: 1px }\n        div { columns: 2; column-gap: normal }\n        body { margin: 0; font: 3px/0 weasyprint }\n        @page { margin: 0; size: 5PX 3px }\n      </style>\n      <div>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n        <img src=blue.jpg>\n      </div>''')\n"
  },
  {
    "path": "tests/draw/test_current_color.py",
    "content": "\"\"\"Test the currentColor value.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_current_color_1(assert_pixels):\n    assert_pixels('GG\\nGG', '''\n      <style>\n        @page { size: 2px }\n        html, body { height: 100%; margin: 0 }\n        html { color: red; background: currentColor }\n        body { color: lime; background: inherit }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_current_color_2(assert_pixels):\n    assert_pixels('GG\\nGG', '''\n      <style>\n        @page { size: 2px }\n        html { color: red; border-color: currentColor }\n        body { color: lime; border: 1Px solid; border-color: inherit;\n               margin: 0 }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_current_color_3(assert_pixels):\n    assert_pixels('GG\\nGG', '''\n      <style>\n        @page { size: 2px }\n        html { color: red; outline-color: currentColor }\n        body { color: lime; outline: 1px solid; outline-color: inherit;\n               margin: 1px }\n      </style>\n      <body>''')\n\n\n@assert_no_logs\ndef test_current_color_4(assert_pixels):\n    assert_pixels('GG\\nGG', '''\n      <style>\n        @page { size: 2px }\n        html { color: red; border-color: currentColor; }\n        body { margin: 0 }\n        table { border-collapse: collapse;\n                color: lime; border: 1PX solid; border-color: inherit }\n      </style>\n      <table><td>''')\n\n\n@assert_no_logs\ndef test_current_color_svg_1(assert_pixels):\n    assert_pixels('KK\\nKK', '''\n      <style>\n        @page { size: 2px }\n        svg { display: block }\n      </style>\n      <svg xmlns=\"http://www.w3.org/2000/svg\"\n           width=\"2\" height=\"2\" fill=\"currentColor\">\n        <rect width=\"2\" height=\"2\"></rect>\n      </svg>''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_current_color_svg_2(assert_pixels):\n    assert_pixels('GG\\nGG', '''\n      <style>\n        @page { size: 2px }\n        svg { display: block }\n        body { color: lime }\n      </style>\n      <svg xmlns=\"http://www.w3.org/2000/svg\"\n           width=\"2\" height=\"2\">\n        <rect width=\"2\" height=\"2\" fill=\"currentColor\"></rect>\n      </svg>''')\n\n\n@assert_no_logs\ndef test_current_color_variable(assert_pixels):\n    # Regression test for #2010.\n    assert_pixels('GG\\nGG', '''\n      <style>\n        @page { size: 2px }\n        html { color: lime; font-family: weasyprint; --var: currentColor }\n        div { color: var(--var); font-size: 2px; line-height: 1 }\n      </style>\n      <div>aa''')\n\n\n@assert_no_logs\ndef test_current_color_variable_border(assert_pixels):\n    # Regression test for #2010.\n    assert_pixels('GG\\nGG', '''\n      <style>\n        @page { size: 2px }\n        html { color: lime; --var: currentColor }\n        div { color: var(--var); width: 0; height: 0; border: 1px solid }\n      </style>\n      <div>''')\n"
  },
  {
    "path": "tests/draw/test_float.py",
    "content": "\"\"\"Test how floats are drawn.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_float(assert_pixels):\n    assert_pixels('''\n        rBBB__aaaa\n        BBBB__aaaa\n        BBBB__aaaa\n        BBBB__aaaa\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 5px }\n      </style>\n      <div>\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"blue.jpg\">\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_rtl(assert_pixels):\n    assert_pixels('''\n        rBBB__aaaa\n        BBBB__aaaa\n        BBBB__aaaa\n        BBBB__aaaa\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px 5Px }\n      </style>\n      <div style=\"direction: rtl\">\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"blue.jpg\">\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_inline(assert_pixels):\n    assert_pixels('''\n        rBBBGG_____aaaa\n        BBBBGG_____aaaa\n        BBBB_______aaaa\n        BBBB_______aaaa\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n        body { font-family: weasyprint; font-size: 2px; line-height: 1;\n               color: lime }\n      </style>\n      <div>\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"blue.jpg\">\n        <span>a</span>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_inline_rtl(assert_pixels):\n    assert_pixels('''\n        rBBB_____GGaaaa\n        BBBB_____GGaaaa\n        BBBB_______aaaa\n        BBBB_______aaaa\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n        body { font-family: weasyprint; font-size: 2px; line-height: 1;\n               color: lime }\n      </style>\n      <div style=\"direction: rtl\">\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"blue.jpg\">\n        <span>a</span>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_inline_block(assert_pixels):\n    assert_pixels('''\n        rBBBGG_____aaaa\n        BBBBGG_____aaaa\n        BBBB_______aaaa\n        BBBB_______aaaa\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n        body { font-family: weasyprint; font-size: 2px; line-height: 1;\n               color: lime }\n      </style>\n      <div>\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"blue.jpg\">\n        <span style=\"display: inline-block\">a</span>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_inline_block_rtl(assert_pixels):\n    assert_pixels('''\n        rBBB_____GGaaaa\n        BBBB_____GGaaaa\n        BBBB_______aaaa\n        BBBB_______aaaa\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15Px 5px }\n        body { font-family: weasyprint; font-size: 2px; line-height: 1;\n               color: lime }\n      </style>\n      <div style=\"direction: rtl\">\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"blue.jpg\">\n        <span style=\"display: inline-block\">a</span>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_table(assert_pixels):\n    assert_pixels('''\n        rBBBGG_____aaaa\n        BBBBGG_____aaaa\n        BBBB_______aaaa\n        BBBB_______aaaa\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n        body { font-family: weasyprint; font-size: 2px; line-height: 1;\n               color: lime }\n      </style>\n      <div>\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"blue.jpg\">\n        <table><tbody><tr><td>a</td></tr></tbody></table>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_table_rtl(assert_pixels):\n    assert_pixels('''\n        rBBB_____GGaaaa\n        BBBB_____GGaaaa\n        BBBB_______aaaa\n        BBBB_______aaaa\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n        body { font-family: weasyprint; font-size: 2px; line-height: 1;\n               color: lime }\n      </style>\n      <div style=\"direction: rtl\">\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"blue.jpg\">\n        <table><tbody><tr><td>a</td></tr></tbody></table>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_inline_table(assert_pixels):\n    assert_pixels('''\n        rBBBGG_____aaaa\n        BBBBGG_____aaaa\n        BBBB_______aaaa\n        BBBB_______aaaa\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15PX 5px }\n        table { display: inline-table }\n        body { font-family: weasyprint; font-size: 2px; line-height: 1;\n               color: lime }\n      </style>\n      <div>\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"blue.jpg\">\n        <table><tbody><tr><td>a</td></tr></tbody></table>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_inline_table_rtl(assert_pixels):\n    assert_pixels('''\n        rBBB_____GGaaaa\n        BBBB_____GGaaaa\n        BBBB_______aaaa\n        BBBB_______aaaa\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n        table { display: inline-table }\n        body { font-family: weasyprint; font-size: 2px; line-height: 1;\n               color: lime }\n      </style>\n      <div style=\"direction: rtl\">\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"blue.jpg\">\n        <table><tbody><tr><td>a</td></tr></tbody></table>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_replaced_block(assert_pixels):\n    assert_pixels('''\n        rBBBaaaa___rBBB\n        BBBBaaaa___BBBB\n        BBBBaaaa___BBBB\n        BBBBaaaa___BBBB\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n      </style>\n      <div>\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"pattern.png\">\n        <img style=\"display: block\" src=\"blue.jpg\">\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_replaced_block_rtl(assert_pixels):\n    assert_pixels('''\n        rBBB___aaaarBBB\n        BBBB___aaaaBBBB\n        BBBB___aaaaBBBB\n        BBBB___aaaaBBBB\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n      </style>\n      <div style=\"direction: rtl\">\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"pattern.png\">\n        <img style=\"display: block\" src=\"blue.jpg\">\n      </div>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_float_replaced_inline(assert_pixels):\n    assert_pixels('''\n        rBBBaaaa___rBBB\n        BBBBaaaa___BBBB\n        BBBBaaaa___BBBB\n        BBBBaaaa___BBBB\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n        body { line-height: 1px }\n      </style>\n      <div>\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"pattern.png\">\n        <img src=\"blue.jpg\">\n      </div>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_float_replaced_inline_rtl(assert_pixels):\n    assert_pixels('''\n        rBBB___aaaarBBB\n        BBBB___aaaaBBBB\n        BBBB___aaaaBBBB\n        BBBB___aaaaBBBB\n        _______________\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n        body { line-height: 1px }\n      </style>\n      <div style=\"direction: rtl\">\n        <img style=\"float: left\" src=\"pattern.png\">\n        <img style=\"float: right\" src=\"pattern.png\">\n        <img src=\"blue.jpg\">\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_float_margin(assert_pixels):\n    assert_pixels('''\n        BBBBRRRRRRRRRR__\n        BBBBRRRRRRRRRR__\n        __RRRRRRRRRR____\n        __RRRRRRRRRR____\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: left;\n                width: 4px;\n            }\n            div.pushed {\n                margin-left: 2px;\n            }\n        </style>\n        <div class=\"split\">aa</div>\n        <div class=\"pushed\">bbbbb bbbbb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_1(assert_pixels):\n    assert_pixels('''\n        BBBBRRRRRRRRRRRR\n        BBBBRRRRRRRRRRRR\n        BBBBRRRR________\n        BBBBRRRR________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: left;\n                width: 4px;\n            }\n        </style>\n        <div class=\"split\">aa aa</div>\n        <div>bbbbbb bb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_2(assert_pixels):\n    assert_pixels('''\n        RRRRRRRRRRRRBBBB\n        RRRRRRRRRRRRBBBB\n        RRRR________BBBB\n        RRRR________BBBB\n    ''', '''\n        <style>\n          @page {\n            size: 16px 2px;\n          }\n          body {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n          }\n          div.split {\n            color: blue;\n            float: right;\n            width: 4px;\n          }\n        </style>\n        <div class=\"split\">aa aa</div>\n        <div>bbbbbb bb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_3(assert_pixels):\n    assert_pixels('''\n        BBBBRRRRRRRRRRRR\n        BBBBRRRRRRRRRRRR\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: left;\n                width: 4px;\n            }\n        </style>\n        <div class=\"split\">aa</div>\n        <div>bbbbbb bbbbb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_4(assert_pixels):\n    assert_pixels('''\n        RRRRRRRRRRRRBBBB\n        RRRRRRRRRRRRBBBB\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: right;\n                width: 4px;\n            }\n        </style>\n        <div class=\"split\">aa</div>\n        <div>bbbbbb bbbbb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_5(assert_pixels):\n    assert_pixels('''\n        BBBBRRRRRRRRgggg\n        BBBBRRRRRRRRgggg\n        BBBBRRRR____gggg\n        BBBBRRRR____gggg\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: left;\n                width: 4px;\n            }\n            div.split2 {\n                color: green;\n                float: right;\n                width: 4px;\n        </style>\n        <div class=\"split\">aa aa</div>\n        <div class=\"split2\">cc cc</div>\n        <div>bbbb bb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_6(assert_pixels):\n    assert_pixels('''\n        BBBBRRRRRRRRgggg\n        BBBBRRRRRRRRgggg\n        BBBBRRRR________\n        BBBBRRRR________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: left;\n                width: 4px;\n            }\n            div.split2 {\n                color: green;\n                float: right;\n                width: 4px;\n        </style>\n        <div class=\"split\">aa aa</div>\n        <div class=\"split2\">cc</div>\n        <div>bbbb bb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_7(assert_pixels):\n    assert_pixels('''\n        BBBBRRRRRRRRgggg\n        BBBBRRRRRRRRgggg\n        RRRR________gggg\n        RRRR________gggg\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: left;\n                width: 4px;\n            }\n            div.split2 {\n                color: green;\n                float: right;\n                width: 4px;\n        </style>\n        <div class=\"split\">aa</div>\n        <div class=\"split2\">cc cc</div>\n        <div>bbbb bb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_8(assert_pixels):\n    assert_pixels('''\n        BBBB__RRRRRRRRRR\n        BBBB__RRRRRRRRRR\n        BBBB__RRRR______\n        BBBB__RRRR______\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: left;\n                margin-right: 2px;\n                width: 4px;\n            }\n        </style>\n        <div class=\"split\">aa aa</div>\n        <div>bbbbb bb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_9(assert_pixels):\n    assert_pixels('''\n        RRRRRRRRRRBBBB__\n        RRRRRRRRRRBBBB__\n        RRRR______BBBB__\n        RRRR______BBBB__\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: right;\n                margin-right: 2px;\n                width: 4px;\n            }\n        </style>\n        <div class=\"split\">aa aa</div>\n        <div>bbbbb bb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_10(assert_pixels):\n    assert_pixels('''\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n        ________________\n        RRRRRR____BBBB__\n        RRRRRR____BBBB__\n        RRRRRR____BBBB__\n        RRRRRR____BBBB__\n        ________________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 5px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: right;\n                margin-right: 2px;\n                width: 4px;\n            }\n        </style>\n        <div>bbbbb bbbbb</div>\n        <div class=\"split\">aa aa</div>\n        <div>bbb bbb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_11(assert_pixels):\n    assert_pixels('''\n        ________________\n        _BBBBBBBBBB_____\n        _BBBBBBBBBB_____\n        _BBBBBBBBBB_____\n        _BBBBBBBBBB_____\n        ________________\n        ________________\n        ________________\n        _BBBB___________\n        _BBBB___________\n        _rrrrrrrrrrrrrr_\n        _rrrrrrrrrrrrrr_\n        ________________\n        ________________\n    ''', '''\n        <style>\n            @page {\n                margin: 1px;\n                size: 16px 7px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div.split {\n                color: blue;\n                float: right;\n            }\n        </style>\n        <div class=\"split\">aaaaa aaaaa aa</div>\n        bbbbbbb''')\n\n\n@assert_no_logs\ndef test_float_split_12(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBGG______BBBB\n        BBBBGG______BBBB\n        BBBB________BBBB\n        BBBB________BBBB\n        ________________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 5px;\n            }\n            body {\n                color: lime;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            article {\n                background: blue;\n                height: 5px;\n            }\n            div {\n                background: red;\n                color: blue;\n            }\n        </style>\n        <article></article>\n        <section>\n          a\n          <div style=\"float: left\"><p>aa<p>aa</div>\n          <div style=\"float: right\"><p>bb<p>bb</div>''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_float_split_13(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBGG______BBBB\n        BBBBGG______BBBB\n        BBBB________BBBB\n        BBBB________BBBB\n        ________________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 5px;\n            }\n            body {\n                color: lime;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            article {\n                background: blue;\n                height: 5px;\n            }\n            div {\n                background: red;\n                color: blue;\n            }\n        </style>\n        <article></article>\n        <section>\n          <div style=\"float: left\"><p>a<p>aa</div>\n          a\n          <div style=\"float: right\"><p>bb<p>bb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_14(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBBBBBBBBBBBBB\n        BBBBGG______BBBB\n        BBBBGG______BBBB\n        BBBB________BBBB\n        BBBB________BBBB\n        ________________\n    ''', '''\n        <style>\n            @page {\n                size: 16px 5px;\n            }\n            body {\n                color: lime;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            article {\n                background: blue;\n                height: 5px;\n            }\n            div {\n                background: red;\n                color: blue;\n            }\n        </style>\n        <article></article>\n        a\n        <div style=\"float: left\"><p>aa<p>aa</div>\n        <div style=\"float: right\"><p>bb<p>bb</div>''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_float_split_15(assert_pixels):\n    assert_pixels('''\n        BB__RRRRRRRRRR__\n        BB__RRRRRRRRRR__\n        BB__RRRRRRRRRR__\n        BB__RRRRRRRRRR__\n        GGBBRRRRRRRRRR__\n        GGBBRRRRRRRRRR__\n        GGBBRRRRRRRRRR__\n        GGBBRRRRRRRRRR__\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n    ''', '''\n        <style>\n            @page {\n                size: 16px 2px;\n            }\n            body {\n                color: red;\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n        </style>\n        <div style=\"float: left; position: relative; color: blue; width: 4px\">\n          a a a\n          <div style=\"float: left; color: lime; width: 2px\">\n            a a\n          </div>\n          a a\n        </div>\n        <div>bbbbb bbbbb bbbbb bbbbb bbbbb</div>''')\n\n\n@assert_no_logs\ndef test_float_split_clear(assert_pixels):\n    assert_pixels('''\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        rrrr\n        rrrr\n    ''', '''\n        <style>\n            @page {\n                size: 4px;\n            }\n            body {\n                color: red;\n                font: 2px / 1 weasyprint;\n            }\n            div {\n                color: blue;\n                float: left;\n            }\n            article {\n                clear: left;\n            }\n        </style>\n        <div>bb bb bb bb bb</div>\n        <article>aa</article>''')\n\n\n@assert_no_logs\ndef test_float_split_clear_empty(assert_pixels):\n    assert_pixels('''\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        ____\n        ____\n    ''', '''\n        <style>\n            @page {\n                size: 4px;\n            }\n            body {\n                color: red;\n                font: 2px / 1 weasyprint;\n            }\n            div {\n                color: blue;\n                float: left;\n            }\n            article {\n                clear: left;\n            }\n        </style>\n        <div>bb bb bb bb bb</div>\n        <article></article>''')\n\n\n@assert_no_logs\ndef test_float_split_large_parent(assert_pixels):\n    assert_pixels('''\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        GGGG\n\n        BBBB\n        BBBB\n        BBBB\n        BBBB\n        GGGG\n\n        BBBB\n        BBBB\n        GGGG\n        GGGG\n        GGGG\n\n        rrrr\n        rrrr\n        GGGG\n        GGGG\n        GGGG\n    ''', '''\n        <style>\n            @page {\n                size: 4px 5px;\n            }\n            body {\n                color: red;\n                font: 2px / 1 weasyprint;\n            }\n            section {\n                background: lime;\n                height: 100px;\n            }\n            div {\n                color: blue;\n                float: left;\n            }\n            article {\n                clear: left;\n            }\n        </style>\n        <section>\n        <div>bb bb bb bb bb</div>\n        <article>aa</article>''')\n\n\n@assert_no_logs\ndef test_float_split_in_stacking_context(assert_pixels):\n    assert_pixels('''\n        RRRR\n        RRRR\n        BBBB\n        BBBB\n        ____\n\n        BBBB\n        BBBB\n        RRRR\n        RRRR\n        ____\n\n        RRRR\n        RRRR\n        ____\n        ____\n        ____\n    ''', '''\n        <style>\n            @page {\n                size: 4px 5px;\n            }\n            body {\n                color: red;\n                font: 2px / 1 weasyprint;\n            }\n            section {\n                display: flow-root;\n            }\n            div {\n                color: blue;\n                float: left;\n            }\n            article {\n                clear: left;\n            }\n        </style>\n        <section>\n        aa\n        <div>bb bb</div>\n        cc cc''')\n"
  },
  {
    "path": "tests/draw/test_footnote.py",
    "content": "\"\"\"Test how footnotes are drawn.\"\"\"\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_inline_footnote(assert_pixels):\n    assert_pixels('''\n        RRRRRRRR_\n        RRRRRRRR_\n        _________\n        _________\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n    ''', '''\n    <style>\n        @page {\n            size: 9px 7px;\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        span {\n            float: footnote;\n        }\n    </style>\n    <div>abc<span>de</span></div>''')\n\n\n@assert_no_logs\ndef test_block_footnote(assert_pixels):\n    assert_pixels('''\n        RRRRRRRR_\n        RRRRRRRR_\n        _________\n        _________\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n    ''', '''\n    <style>\n        @page {\n            size: 9Px 7px;\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        div.footnote {\n            float: footnote;\n        }\n    </style>\n    <div>abc<div class=\"footnote\">de</div></div>''')\n\n\n@assert_no_logs\ndef test_long_footnote(assert_pixels):\n    assert_pixels('''\n        RRRRRRRR_\n        RRRRRRRR_\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n        RR_______\n        RR_______\n    ''', '''\n    <style>\n        @page {\n            size: 9px 7px;\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2PX;\n            line-height: 1;\n        }\n        span {\n            float: footnote;\n        }\n    </style>\n    <div>abc<span>de f</span></div>''')\n\n\n@assert_no_logs\ndef test_footnote_margin(assert_pixels):\n    assert_pixels('''\n        RRRRRRRR_\n        RRRRRRRR_\n        _________\n        _________\n        _RRRRRR__\n        _RRRRRR__\n        _________\n    ''', '''\n    <style>\n        @page {\n            size: 9px 7px;\n\n            @footnote {\n                margin: 1px;\n            }\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        span {\n            float: footnote;\n        }\n    </style>\n    <div>abc<span>d</span></div>''')\n\n\n@assert_no_logs\ndef test_footnote_multiple_margin(assert_pixels):\n    assert_pixels('''\n        RRRR___\n        RRRR___\n        RRRR___\n        RRRR___\n        RRRR___\n        RRRR___\n        RRRRRR_\n        RRRRRR_\n        _______\n        _______\n\n        RRRR___\n        RRRR___\n        _______\n        _______\n        _______\n        _______\n        RRRRRR_\n        RRRRRR_\n        RRRRRR_\n        RRRRRR_\n    ''', '''\n    <style>\n        @page {\n            size: 7px 10Px;\n\n            @footnote {\n                margin-top: 1px;\n            }\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        span {\n            float: footnote;\n        }\n    </style>\n    <div>ab</div>\n    <div>ab</div>\n    <div>ab</div>\n    <div>a<span>d</span><span>e</span></div>\n    <div>ab</div>''')\n\n\n@assert_no_logs\ndef test_footnote_with_absolute(assert_pixels):\n    assert_pixels('''\n        _RRRR____\n        _RRRR____\n        _________\n        _RRRR____\n        _RRRR____\n        BB_______\n        BB_______\n    ''', '''\n    <style>\n        @page {\n            size: 9px 7px;\n            margin: 0 1px 2px;\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        span {\n            float: footnote;\n        }\n        mark {\n            display: block;\n            position: absolute;\n            left: -1px;\n            color: blue;\n        }\n    </style>\n    <div>a<span><mark>d</mark></span></div>''')\n\n\n@assert_no_logs\ndef test_footnote_max_height_1(assert_pixels):\n    assert_pixels('''\n        RRRRKKKK_\n        RRRRKKKK_\n        RRRR_____\n        RRRR_____\n        _GGGGBB__\n        _GGGGBB__\n        _________\n        _________\n        _________\n        _________\n        _GGGGBB__\n        _GGGGBB__\n    ''', '''\n    <style>\n        @page {\n            size: 9px 6px;\n\n            @footnote {\n                margin-left: 1px;\n                max-height: 3px;\n            }\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        div.footnote {\n            float: footnote;\n            color: blue;\n            &::footnote-call { color: black }\n            &::footnote-marker { color: lime }\n        }\n    </style>\n    <div>ab<div class=\"footnote\">c</div><div class=\"footnote\">d</div></div>\n    <div>ef</div>''')\n\n\n@assert_no_logs\ndef test_footnote_max_height_2(assert_pixels):\n    assert_pixels('''\n        RRRRRRRR_\n        RRRRRRRR_\n        _________\n        _________\n        _BBBBBB__\n        _BBBBBB__\n        _________\n        _________\n        _________\n        _________\n        _BBBBBB__\n        _BBBBBB__\n    ''', '''\n    <style>\n        @page {\n            size: 9px 6px;\n\n            @footnote {\n                margin-left: 1px;\n                max-height: 3px;\n            }\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        div.footnote {\n            float: footnote;\n            color: blue;\n            &::footnote-call { color: red }\n        }\n    </style>\n    <div>ab<div class=\"footnote\">c</div><div class=\"footnote\">d</div></div>''')\n\n\n@assert_no_logs\ndef test_footnote_max_height_3(assert_pixels):\n    # This case is crazy and the rendering is not really defined, but this test\n    # is useful to check that there’s no endless loop.\n    assert_pixels('''\n        RRRRRRRR_\n        RRRRRRRR_\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _BBBBBB__\n        _________\n        _________\n        _________\n        _________\n        _________\n        _BBBBBB__\n    ''', '''\n    <style>\n        @page {\n            size: 9px 6px;\n\n            @footnote {\n                margin-left: 1px;\n                max-height: 1px;\n            }\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        div.footnote {\n            float: footnote;\n            color: blue;\n            &::footnote-call { color: red }\n        }\n    </style>\n    <div>ab<div class=\"footnote\">c</div><div class=\"footnote\">d</div></div>''')\n\n\n@assert_no_logs\ndef test_footnote_max_height_4(assert_pixels):\n    assert_pixels('''\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRR_____\n        RRRR_____\n        _BBBBBB__\n        _BBBBBB__\n        RRRR_____\n        RRRR_____\n        _________\n        _________\n        _BBBBBB__\n        _BBBBBB__\n    ''', '''\n    <style>\n        @page {\n            size: 9px 6px;\n\n            @footnote {\n                margin-left: 1px;\n                max-height: 3px;\n            }\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        div.footnote {\n            float: footnote;\n            color: blue;\n            &::footnote-call { color: red }\n        }\n    </style>\n    <div>ab<div class=\"footnote\">c</div><div class=\"footnote\">d</div></div>\n    <div>ef</div>\n    <div>gh</div>''')\n\n\n@assert_no_logs\ndef test_footnote_max_height_5(assert_pixels):\n    assert_pixels('''\n        RRRRRRRR__RR\n        RRRRRRRR__RR\n        _BBBBBB_____\n        _BBBBBB_____\n        _BBBBBB_____\n        _BBBBBB_____\n        RRRR________\n        RRRR________\n        ____________\n        ____________\n        _BBBBBB_____\n        _BBBBBB_____\n    ''', '''\n    <style>\n        @page {\n            size: 12px 6px;\n\n            @footnote {\n                margin-left: 1px;\n                max-height: 4px;\n            }\n        }\n        div {\n            color: red;\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        div.footnote {\n            float: footnote;\n            color: blue;\n            &::footnote-call { color: red }\n        }\n    </style>\n    <div>ab<div class=\"footnote\">c</div><div class=\"footnote\">d</div>\n    <div class=\"footnote\">e</div></div>\n    <div>fg</div>''')\n"
  },
  {
    "path": "tests/draw/test_footnote_column.py",
    "content": "\"\"\"Test how footnotes in columns are drawn.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_footnote_column_margin_top(assert_pixels):\n    assert_pixels('''\n        RRRR_RRRR\n        RRRR_RRRR\n        _________\n        _________\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_____\n        RRRR_____\n        _________\n    ''', '''\n    <style>\n      @page {\n        size: 9px 7px;\n        @footnote {\n          margin-top: 2Px;\n        }\n      }\n      div {\n        color: red;\n        columns: 2;\n        column-gap: 1px;\n        font-family: weasyprint;\n        font-size: 2px;\n        line-height: 1;\n      }\n      span {\n        float: footnote;\n      }\n    </style>\n    <div>a<span>de</span> ab ab ab ab ab ab</div>''')\n\n\n@assert_no_logs\ndef test_footnote_column_fill_auto(assert_pixels):\n    assert_pixels('''\n        RRRR_____\n        RRRR_____\n        RRRR_____\n        RRRR_____\n        RRRR_____\n        RRRR_____\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n    ''', '''\n    <style>\n      @page {\n        size: 9px 13px;\n      }\n      div {\n        color: red;\n        columns: 2;\n        column-fill: auto;\n        column-gap: 1px;\n        font-family: weasyprint;\n        font-size: 2px;\n        line-height: 1;\n      }\n      span {\n        float: footnote;\n      }\n    </style>\n    <div>a<span>de</span> a<span>de</span> a<span>de</span></div>''')\n\n\n@assert_no_logs\ndef test_footnote_column_fill_auto_break_inside_avoid(assert_pixels):\n    assert_pixels('''\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n    ''', '''\n    <style>\n      @page {\n        size: 9px 13px;\n      }\n      div {\n        color: red;\n        columns: 2;\n        column-fill: auto;\n        column-gap: 1px;\n        font-family: weasyprint;\n        font-size: 2pX;\n        line-height: 1;\n      }\n      article {\n        break-inside: avoid;\n      }\n      span {\n        float: footnote;\n      }\n    </style>\n    <div>\n      <article>a<span>de</span> a<span>de</span></article>\n      <article>ab</article>\n      <article>a<span>de</span> ab</article>\n      <article>ab</article>\n    </div>''')\n\n\n@assert_no_logs\ndef test_footnote_column_p_after(assert_pixels):\n    assert_pixels('''\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        KK__KK___\n        KK__KK___\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        KK__KK___\n        KK__KK___\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n    <style>\n      @page {\n        size: 9px 11px;\n      }\n      body {\n        font-family: weasyprint;\n        font-size: 2px;\n        line-height: 1;\n      }\n      div {\n        color: red;\n        columns: 2;\n        column-gap: 1px;\n      }\n      span {\n        float: footnote;\n      }\n    </style>\n    <div>a<span>de</span> a<span>de</span> ab ab</div>\n    <p>a a a a</p>''')\n\n\n@assert_no_logs\ndef test_footnote_column_p_before(assert_pixels):\n    assert_pixels('''\n        KKKK_____\n        KKKK_____\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RR__\n        RRRR_RR__\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRR_RR__\n        RRRR_RR__\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n    <style>\n      @page {\n        size: 9px 13px;\n      }\n      body {\n        font-family: weasyprint;\n        font-size: 2px;\n        line-height: 1;\n      }\n      div {\n        color: red;\n        columns: 2;\n        column-gap: 1px;\n      }\n      span {\n        float: footnote;\n      }\n    </style>\n    <p>ab</p>\n    <div>\n    a<span>de</span> a<span>de</span>\n    a<span>de</span> a ab a </div>''')\n\n\n@assert_no_logs\ndef test_footnote_column_3(assert_pixels):\n    assert_pixels('''\n        RRRR_RRRR_RRRR\n        RRRR_RRRR_RRRR\n        ______________\n        RRRRRRRR______\n        RRRRRRRR______\n        RRRR_RRRR_____\n        RRRR_RRRR_____\n        ______________\n        ______________\n        ______________\n    ''', '''\n    <style>\n      @page {\n        size: 14px 5px;\n      }\n      body {\n        font-family: weasyprint;\n        font-size: 2px;\n        line-height: 1;\n      }\n      div {\n        color: red;\n        columns: 3;\n        column-gap: 1Px;\n      }\n      span {\n        float: footnote;\n      }\n    </style>\n    <div>ab ab a<span>de</span> ab ab </div>''')\n\n\n@assert_no_logs\ndef test_footnote_column_3_p_before(assert_pixels):\n    assert_pixels('''\n        KKKK__________\n        KKKK__________\n        RRRR_RRRR_RRRR\n        RRRR_RRRR_RRRR\n        RRRR_RRRR_RRRR\n        RRRR_RRRR_RRRR\n        ______________\n        RRRRRRRR______\n        RRRRRRRR______\n        RRRR_RRRR_____\n        RRRR_RRRR_____\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        RRRRRRRR______\n        RRRRRRRR______\n    ''', '''\n    <style>\n      @page {\n        size: 14px 9px;\n      }\n      body {\n        font-family: weasyprint;\n        font-size: 2px;\n        line-height: 1;\n      }\n      div {\n        color: red;\n        columns: 3;\n        column-gap: 1px;\n      }\n      span {\n        float: footnote;\n      }\n    </style>\n    <p>ab</p>\n    <div>ab ab a<span>de</span> ab ab ab a<span>de</span> ab </div>''')\n\n\n@assert_no_logs\ndef test_footnote_column_clone_decoration(assert_pixels):\n    assert_pixels('''\n        _________\n        RRRR_RRRR\n        RRRR_RRRR\n        _________\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n        _________\n        RRRR_RRRR\n        RRRR_RRRR\n        _________\n        _________\n        _________\n        _________\n    ''', '''\n    <style>\n      @page {\n        size: 9px 7px;\n      }\n      div {\n        box-decoration-break: clone;\n        color: red;\n        columns: 2;\n        column-gap: 1px;\n        font-family: weasyprint;\n        font-size: 2px;\n        line-height: 1;\n        padding: 1px 0;\n      }\n      span {\n        float: footnote;\n      }\n    </style>\n    <div>a<span>de</span> ab ab ab</div>''')\n\n\n@assert_no_logs\ndef test_footnote_column_max_height(assert_pixels):\n    assert_pixels('''\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRR_RRRR\n        RRRR_RRRR\n        _________\n        _________\n        _________\n        _________\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n    ''', '''\n    <style>\n      @page {\n        size: 9px 9px;\n        @footnote {\n          max-height: 2em;\n        }\n      }\n      div {\n        color: red;\n        columns: 2;\n        column-gap: 1px;\n        font-family: weasyprint;\n        font-size: 2px;\n        line-height: 1;\n      }\n      span {\n        float: footnote;\n      }\n    </style>\n    <div>\n      a<span>de</span> a<span>de</span>\n      a<span>de</span> ab\n      ab ab\n    </div>''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_footnote_column_reported_split(assert_pixels):\n    # When calling block_container_layout() in remove_placeholders(), we should\n    # use the whole skip stack and not just [skip:]\n    assert_pixels('''\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        RRRR_RRRR\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRRRRRR_\n        RRRR_____\n        RRRR_____\n        _________\n        _________\n        _________\n        _________\n        _________\n        RRRRRRRR_\n        RRRRRRRR_\n    ''', '''\n    <style>\n      @page {\n        size: 9px 9px;\n      }\n      div {\n        color: red;\n        columns: 2;\n        column-gap: 1px;\n        font-family: weasyprint;\n        font-size: 2px;\n        line-height: 1;\n      }\n      span {\n        float: footnote;\n      }\n    </style>\n    <div>\n      <article>a<span>de</span> a<span>de</span></article>\n      <article>a<span>de</span> ab ab</article>\n    </div>''')\n"
  },
  {
    "path": "tests/draw/test_gradient.py",
    "content": "\"\"\"Test how gradients are drawn.\"\"\"\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_linear_gradients_1(assert_pixels):\n    assert_pixels('''\n        _____\n        _____\n        _____\n        BBBBB\n        BBBBB\n        RRRRR\n        RRRRR\n        RRRRR\n        RRRRR\n    ''', '''<style>@page { size: 5px 9px; background: linear-gradient(\n      white, white 3px, blue 0, blue 5px, red 0, red\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_2(assert_pixels):\n    assert_pixels('''\n        _____\n        _____\n        _____\n        BBBBB\n        BBBBB\n        RRRRR\n        RRRRR\n        RRRRR\n        RRRRR\n    ''', '''<style>@page { size: 5px 9px; background: linear-gradient(\n      white 3px, blue 0, blue 5px, red 0\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_3(assert_pixels):\n    assert_pixels('''\n        ___BBrrrr\n        ___BBrrrr\n        ___BBrrrr\n        ___BBrrrr\n        ___BBrrrr\n    ''', '''<style>@page { size: 9px 5px; background: linear-gradient(\n      to right, white 3px, blue 0, blue 5px, red 0\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_4(assert_pixels):\n    assert_pixels('''\n        BBBBBBrrrr\n        BBBBBBrrrr\n        BBBBBBrrrr\n        BBBBBBrrrr\n        BBBBBBrrrr\n    ''', '''<style>@page { size: 10px 5px; background: linear-gradient(\n      to right, blue 5px, blue 6px, red 6px, red 9px\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_5(assert_pixels):\n    assert_pixels('''\n        rBrrzBrrrB\n        rBrrrBrrrB\n        rBrrrBrrrB\n        rBrrrBrrrB\n        rBrrzBrrrB\n    ''', '''\n      <style>@page { size: 10px 5px; background: repeating-linear-gradient(\n      to right, blue 50%, blue 60%, red 60%, red 90%\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_6(assert_pixels):\n    assert_pixels('''\n        BBBrrrrrr\n        BBBrrrrrr\n        BBBrrrrrr\n        BBBrrrrrr\n        BBBrrrrrr\n    ''', '''<style>@page { size: 9px 5px; background: linear-gradient(\n      to right, blue 3px, blue 3px, red 3px, red 3px\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_7(assert_pixels):\n    assert_pixels('''\n        hhhhhhhhh\n        hhhhhhhhh\n        hhhhhhhhh\n        hhhhhhhhh\n        hhhhhhhhh\n    ''', '''<style>@page { size: 9px 5px; background:\n    repeating-linear-gradient(\n      to right, black 3px, black 3px, #800080 3px, #800080 3px\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_8(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n    ''', '''<style>@page { size: 9px 5px; background:\n      repeating-linear-gradient(to right, blue 3px)''')\n\n\n@assert_no_logs\ndef test_linear_gradients_9(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n    ''', '''<style>@page { size: 9px 5px; background:\n      repeating-linear-gradient(45deg, blue 3px)''')\n\n\n@assert_no_logs\ndef test_linear_gradients_10(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n    ''', '''<style>@page { size: 9px 5px; background: linear-gradient(\n      45deg, blue 3px, red 3px, red 3px, blue 3px\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_11(assert_pixels):\n    assert_pixels('''\n        BBBrBBBBB\n        BBBrBBBBB\n        BBBrBBBBB\n        BBBrBBBBB\n        BBBrBBBBB\n    ''', '''<style>@page { size: 9px 5px; background: linear-gradient(\n      to right, blue 3px, red 3px, red 4px, blue 4px\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_12(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n        BBBBBBBBB\n    ''', '''<style>@page { size: 9px 5px; background:\n      repeating-linear-gradient(to right, red 3px, blue 3px, blue 4px, red 4px\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_13(assert_pixels):\n    assert_pixels('''\n        _____\n        _____\n        _____\n        SSSSS\n        SSSSS\n        RRRRR\n        RRRRR\n        RRRRR\n        RRRRR\n    ''', '''<style>@page { size: 5px 9px; background: linear-gradient(\n      white, white 3px, rgba(255, 0, 0, 0.751) 0, rgba(255, 0, 0, 0.751) 5px,\n      red 0, red\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_currentcolor(assert_pixels):\n    # Regression test for #1561.\n    assert_pixels('''\n        KKKKK\n        KKKKK\n        KKKKK\n        KKKKK\n        KKKKK\n    ''', '<style>@page { size: 5px 5px; background: linear-gradient(currentcolor)')\n\n\n@assert_no_logs\ndef test_linear_gradients_hints(assert_pixels):\n    assert_pixels('''\n        _____\n        _____\n        _____\n        _____\n        _____\n        _____\n        _____\n        _____\n        zzzzz\n    ''', '''<style>@page { size: 5px 9px; background: linear-gradient(\n      white, 8.9px, red\n    )''')\n\n\n@assert_no_logs\ndef test_linear_gradients_hints_percentage(assert_pixels):\n    assert_pixels('''\n        _____\n        _____\n        _____\n        _____\n        _____\n        _____\n        _____\n        _____\n        zzzzz\n    ''', '''<style>@page { size: 5px 9px; background: linear-gradient(\n      white, 99%, red\n    )''')\n\n\n@assert_no_logs\ndef test_radial_gradients_1(assert_pixels):\n    assert_pixels('''\n        BBBBBB\n        BBBBBB\n        BBBBBB\n        BBBBBB\n        BBBBBB\n        BBBBBB\n    ''', '''<style>@page { size: 6px; background:\n      radial-gradient(red -30%, blue -10%)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_2(assert_pixels):\n    assert_pixels('''\n        RRRRRR\n        RRRRRR\n        RRRRRR\n        RRRRRR\n        RRRRRR\n        RRRRRR\n    ''', '''<style>@page { size: 6px; background:\n      radial-gradient(red 110%, blue 130%)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_3(assert_pixels):\n    assert_pixels('''\n        BzzzzzzzzB\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzRRzzzz\n        zzzzRRzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        BzzzzzzzzB\n    ''', '''<style>@page { size: 10px 16px; background:\n      radial-gradient(red 20%, blue 80%)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_4(assert_pixels):\n    assert_pixels('''\n        BzzzzzzzzB\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzRRzzzz\n        zzzzRRzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        BzzzzzzzzB\n    ''', '''<style>@page { size: 10px 16px; background:\n      radial-gradient(red 50%, blue 50%)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_5(assert_pixels):\n    assert_pixels('''\n        SzzzzzzzzS\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzRRzzzz\n        zzzzRRzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        SzzzzzzzzS\n    ''', '''<style>@page { size: 10px 16px; background:\n      radial-gradient(red 50%, rgba(255, 0, 0, 0.751) 50%)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_negative(assert_pixels):\n    assert_pixels('''\n        BzzzzzzzzB\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzBBzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        BzzzRRzzzB\n        BzzzRRzzzB\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzBBzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        BzzzzzzzzB\n    ''', '''<style>@page { size: 10px 16px; background:\n      radial-gradient(circle, red -1px, red 3px, blue 3px, blue 6px)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_repeating(assert_pixels):\n    assert_pixels('''\n        RzzzzzzzzR\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzBBzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zBzzRRzzBz\n        zBzzRRzzBz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzBBzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        RzzzzzzzzR\n    ''', '''<style>@page { size: 10px 16px; background:\n      repeating-radial-gradient(circle, red 0, red 3px, blue 3px, blue 6px)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_repeating_outer(assert_pixels):\n    assert_pixels('''\n        RzzzzzzzzR\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzBBzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zBzzRRzzBz\n        zBzzRRzzBz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzBBzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        RzzzzzzzzR\n    ''', '''<style>@page { size: 10px 16px; background:\n      repeating-radial-gradient(circle, red 6px, red 9px, blue 9px, blue 12px)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_repeating_outer_partial(assert_pixels):\n    assert_pixels('''\n        RzzzzzzzzR\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzBBzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zBzzRRzzBz\n        zBzzRRzzBz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzBBzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        RzzzzzzzzR\n    ''', '''<style>@page { size: 10px 16px; background:\n      repeating-radial-gradient(circle, blue 3px, blue 6px, red 6px, red 9px)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_repeating_negative(assert_pixels):\n    assert_pixels('''\n        RzzzzzzzzR\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzBBzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zBzzRRzzBz\n        zBzzRRzzBz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzBBzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        zzzzzzzzzz\n        RzzzzzzzzR\n    ''', '''<style>@page { size: 10px 16px; background:\n      repeating-radial-gradient(circle, blue -3px, blue 0, red 0, red 3px)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_hints(assert_pixels):\n    assert_pixels('''\n        zzzzzz\n        zzBBzz\n        zBBBBz\n        zBBBBz\n        zzBBzz\n        zzzzzz\n    ''', '''<style>@page { size: 6px; background:\n      radial-gradient(blue, 4px, white)''')\n\n\n@assert_no_logs\ndef test_radial_gradients_hints_percentage(assert_pixels):\n    assert_pixels('''\n        zzzzzz\n        zzBBzz\n        zBBBBz\n        zBBBBz\n        zzBBzz\n        zzzzzz\n    ''', '''<style>@page { size: 6px; background:\n      radial-gradient(blue, 99%, white)''')\n"
  },
  {
    "path": "tests/draw/test_image.py",
    "content": "\"\"\"Test how images are drawn.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import FakeHTML, assert_no_logs, capture_logs, resource_path\n\ncentered_image = '''\n    ________\n    ________\n    __rBBB__\n    __BBBB__\n    __BBBB__\n    __BBBB__\n    ________\n    ________\n'''\n\ncentered_image_overflow = '''\n    ________\n    ________\n    __rBBBBB\n    __BBBBBB\n    __BBBBBB\n    __BBBBBB\n    __BBBBBB\n    __BBBBBB\n'''\n\nresized_image = '''\n    ____________\n    ____________\n    __rrBBBBBB__\n    __rrBBBBBB__\n    __BBBBBBBB__\n    __BBBBBBBB__\n    __BBBBBBBB__\n    __BBBBBBBB__\n    __BBBBBBBB__\n    __BBBBBBBB__\n    ____________\n    ____________\n'''\n\nsmall_resized_image = '''\n    ____________\n    ____________\n    __rBBB______\n    __BBBB______\n    __BBBB______\n    __BBBB______\n    ____________\n    ____________\n    ____________\n    ____________\n    ____________\n    ____________\n'''\n\nblue_image = '''\n    ________\n    ________\n    __aaaa__\n    __aaaa__\n    __aaaa__\n    __aaaa__\n    ________\n    ________\n'''\n\nno_image = '''\n    ________\n    ________\n    ________\n    ________\n    ________\n    ________\n    ________\n    ________\n'''\n\npage_break = '''\n    ________\n    ________\n    __rBBB__\n    __BBBB__\n    __BBBB__\n    __BBBB__\n    ________\n    ________\n\n    ________\n    ________\n    ________\n    ________\n    ________\n    ________\n    ________\n    ________\n\n    ________\n    ________\n    __rBBB__\n    __BBBB__\n    __BBBB__\n    __BBBB__\n    ________\n    ________\n'''\n\ntable = '''\n    ________\n    ________\n    __rBBB__\n    __BBBB__\n    __BBBB__\n    __BBBB__\n    ________\n    ________\n\n    __rBBB__\n    __BBBB__\n    __BBBB__\n    __BBBB__\n    ________\n    ________\n    ________\n    ________\n'''\n\ncover_image = '''\n    ________\n    ________\n    __BB____\n    __BB____\n    __BB____\n    __BB____\n    ________\n    ________\n'''\n\nborder_image = '''\n    ________\n    _GGGGGG_\n    _GrBBBG_\n    _GBBBBG_\n    _GBBBBG_\n    _GBBBBG_\n    _GGGGGG_\n    ________\n'''\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('filename', 'image'), [\n    ('pattern.svg', centered_image),\n    ('pattern.png', centered_image),\n    ('pattern.palette.png', centered_image),\n    ('pattern.gif', centered_image),\n    ('blue.jpg', blue_image)\n])\ndef test_images(assert_pixels, filename, image):\n    assert_pixels(image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 2px 0 0 2px; font-size: 0 }\n        img { overflow: hidden }\n      </style>\n      <div><img src=\"%s\"></div>''' % filename)\n\n\n@assert_no_logs\n@pytest.mark.parametrize('filename', [\n    'pattern.svg',\n    'pattern.png',\n    'pattern.palette.png',\n    'pattern.gif',\n])\ndef test_resized_images(assert_pixels, filename):\n    assert_pixels(resized_image, '''\n      <style>\n        @page { size: 12px }\n        body { margin: 2px 0 0 2px; font-size: 0 }\n        img { display: block; width: 8px; image-rendering: pixelated;\n              overflow: hidden }\n      </style>\n      <div><img src=\"%s\"></div>''' % filename)\n\n\ndef test_image_overflow(assert_pixels):\n    assert_pixels(centered_image_overflow, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 2px 0 0 2px; font-size: 0 }\n      </style>\n      <div><img src=\"pattern.svg\"></div>''')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('viewbox', 'width', 'height'), [\n    (None, None, None),\n    (None, 4, None),\n    (None, None, 4),\n    (None, 4, 4),\n    ('0 0 4 4', 4, None),\n    ('0 0 4 4', None, 4),\n    ('0 0 4 4', 4, 4),\n])\ndef test_svg_sizing(assert_pixels, viewbox, width, height):\n    assert_pixels(centered_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 2px 0 0 2px; font-size: 0 }\n        svg { display: block }\n      </style>\n      <svg %s %s %s>\n        <rect width=\"4px\" height=\"4px\" fill=\"#00f\" />\n        <rect width=\"1px\" height=\"1px\" fill=\"#f00\" />\n      </svg>''' % (\n          f'width=\"{width}\"' if width else '',\n          f'height=\"{height}px\"' if height else '',\n          f'viewbox=\"{viewbox}\"' if viewbox else ''))\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('viewbox', 'width', 'height', 'image'), [\n    (None, None, None, small_resized_image),\n    (None, 8, None, small_resized_image),\n    (None, None, 8, small_resized_image),\n    (None, 8, 8, small_resized_image),\n    ('0 0 4 4', None, None, resized_image),\n    ('0 0 4 4', 8, None, resized_image),\n    ('0 0 4 4', None, 8, resized_image),\n    ('0 0 4 4', 8, 8, resized_image),\n    ('0 0 4 4', 800, 800, resized_image),\n])\ndef test_svg_resizing(assert_pixels, viewbox, width, height, image):\n    assert_pixels(image, '''\n      <style>\n        @page { size: 12px }\n        body { margin: 2px 0 0 2px; font-size: 0 }\n        svg { display: block; width: 8px }\n      </style>\n      <svg %s %s %s>\n        <rect width=\"4\" height=\"4\" fill=\"#00f\" />\n        <rect width=\"1\" height=\"1\" fill=\"#f00\" />\n      </svg>''' % (\n          f'width=\"{width}\"' if width else '',\n          f'height=\"{height}px\"' if height else '',\n          f'viewbox=\"{viewbox}\"' if viewbox else ''))\n\n\n@assert_no_logs\ndef test_images_block(assert_pixels):\n    assert_pixels(centered_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 0; font-size: 0 }\n        img { display: block; margin: 2px auto 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_images_not_found(assert_pixels):\n    with capture_logs() as logs:\n        assert_pixels(no_image, '''\n          <style>\n            @page { size: 8px }\n            body { margin: 0; font-size: 0 }\n            img { display: block; margin: 2px auto 0 }\n          </style>\n          <div><img src=\"inexistent1.png\" alt=\"\"></div>''')\n    assert len(logs) == 1\n    assert 'ERROR: Failed to load image' in logs[0]\n    assert 'inexistent1.png' in logs[0]\n\n\n@assert_no_logs\ndef test_images_no_src(assert_pixels):\n    assert_pixels(no_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 0; font-size: 0 }\n        img { display: block; margin: 2px auto 0 }\n      </style>\n      <div><img alt=\"\"></div>''')\n\n\n@assert_no_logs\ndef test_images_alt(assert_same_renderings):\n    with capture_logs() as logs:\n        documents = (\n            '''\n              <style>\n                @page { size: 200px 30px }\n                body { margin: 0; font-size: 0 }\n              </style>\n              <div>%s</div>''' % html\n            for html in (\n                'Hello',\n                '<img src=\"inexistent2.png\" alt=\"Hello\">',\n                '<img alt=\"Hello\">',\n                '<img src=\"data:image/svg+xml,<svg></svg>\" alt=\"Hello\">',\n            ))\n        assert_same_renderings(*documents)\n    assert len(logs) == 1\n    assert 'ERROR: Failed to load image' in logs[0]\n    assert 'inexistent2.png' in logs[0]\n\n\n@assert_no_logs\ndef test_images_repeat_transparent(assert_pixels):\n    # Regression test for #1440.\n    assert_pixels('_\\n_\\n_', '''\n      <style>\n        @page { size: 1px }\n        div { height: 100px; width: 100px; background: url(logo_small.png) }\n      </style>\n      <div></div><div></div><div></div>''')\n\n\n@assert_no_logs\ndef test_images_no_width(assert_pixels):\n    assert_pixels(no_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 2px; font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\" alt=\"not shown\"\n                style=\"width: 0; height: 1px\"></div>''')\n\n\n@assert_no_logs\ndef test_images_no_height(assert_pixels):\n    assert_pixels(no_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 2px; font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\" alt=\"not shown\"\n                style=\"width: 1px; height: 0\"></div>''')\n\n\n@assert_no_logs\ndef test_images_no_width_height(assert_pixels):\n    assert_pixels(no_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 2px; font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\" alt=\"not shown\"\n                style=\"width: 0; height: 0\"></div>''')\n\n\n@assert_no_logs\ndef test_images_page_break(assert_pixels):\n    assert_pixels(page_break, '''\n      <style>\n        @page { size: 8px; margin: 2px }\n        body { font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>\n      <div style=\"page-break-before: right\"><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_image_repeat_inline(assert_pixels):\n    # Regression test for #808.\n    assert_pixels(table, '''\n      <style>\n        @page { size: 8px; margin: 0 }\n        table { border-collapse: collapse; margin: 2px }\n        th, td { border: none; padding: 0 }\n        th { height: 4px; line-height: 4px }\n        td { height: 2px }\n        img { vertical-align: top }\n      </style>\n      <table>\n        <thead>\n          <tr><th><img src=\"pattern.png\"></th></tr>\n        </thead>\n        <tbody>\n          <tr><td></td></tr>\n          <tr><td></td></tr>\n        </tbody>\n      </table>''')\n\n\n@assert_no_logs\ndef test_image_repeat_block(assert_pixels):\n    # Regression test for #808.\n    assert_pixels(table, '''\n      <style>\n        @page { size: 8px; margin: 0 }\n        table { border-collapse: collapse; margin: 2px }\n        th, td { border: none; padding: 0 }\n        th { height: 4px }\n        td { height: 2px }\n        img { display: block }\n      </style>\n      <table>\n        <thead>\n          <tr><th><img src=\"pattern.png\"></th></tr>\n        </thead>\n        <tbody>\n          <tr><td></td></tr>\n          <tr><td></td></tr>\n        </tbody>\n      </table>''')\n\n\n@assert_no_logs\ndef test_images_padding(assert_pixels):\n    # Regression test.\n    assert_pixels(centered_image, '''\n      <style>\n        @page { size: 8px }\n        body { font-size: 0 }\n      </style>\n      <div style=\"line-height: 1px\">\n        <img src=pattern.png style=\"padding: 2px 0 0 2px\">\n      </div>''')\n\n\n@assert_no_logs\ndef test_images_in_inline_block(assert_pixels):\n    # Regression test.\n    assert_pixels(centered_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 2px 0 0 2px; font-size: 0 }\n      </style>\n      <div style=\"display: inline-block\">\n        <p><img src=pattern.png></p>\n      </div>''')\n\n\n@assert_no_logs\ndef test_images_transparent_text(assert_pixels):\n    # Regression test for #2131.\n    assert_pixels(centered_image, '''<style>\n        @page { size: 8px }\n        body { margin: 2px 0 0 2px; font-size: 2px; line-height: 0 }\n      </style>\n      <div style=\"color: #0001\">123</div>\n      <img src=pattern.png>\n    ''')\n\n\n@assert_no_logs\ndef test_images_shared_pattern(assert_pixels):\n    # The same image is used in a repeating background,\n    # then in a non-repating <img>.\n    # If Pattern objects are shared carelessly, the image will be repeated.\n    assert_pixels('''\n        ____________\n        ____________\n        __aaaaaaaa__\n        __aaaaaaaa__\n        ____________\n        __aaaa______\n        __aaaa______\n        __aaaa______\n        __aaaa______\n        ____________\n        ____________\n        ____________\n    ''', '''\n      <style>\n        @page { size: 12px }\n        body { margin: 2px; font-size: 0 }\n      </style>\n      <div style=\"background: url(blue.jpg);\n                  height: 2px; margin-bottom: 1px\"></div>\n      <img src=blue.jpg>\n    ''')\n\n\n@assert_no_logs\ndef test_image_resolution(assert_same_renderings):\n    assert_same_renderings(\n        '''\n            <style>@page { size: 20px; margin: 2px }</style>\n            <div style=\"font-size: 0\">\n                <img src=\"pattern.png\" style=\"width: 8px\"></div>\n        ''',\n        '''\n            <style>@page { size: 20px; margin: 2px }</style>\n            <div style=\"image-resolution: .5dppx; font-size: 0\">\n                <img src=\"pattern.png\"></div>\n        ''',\n        '''\n            <style>@page { size: 20px; margin: 2px }\n                   div::before { content: url(pattern.png) }\n            </style>\n            <div style=\"image-resolution: .5x; font-size: 0\"></div>\n        ''',\n        '''\n            <style>@page { size: 20px; margin: 2px }\n            </style>\n            <div style=\"height: 16px; image-resolution: .5dppx;\n                        background: url(pattern.png) no-repeat\"></div>\n        ''',\n    )\n\n\n@assert_no_logs\ndef test_image_cover(assert_pixels):\n    assert_pixels(cover_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 2px 0 0 2px; font-size: 0 }\n        img { object-fit: cover; height: 4px; width: 2px; overflow: hidden }\n      </style>\n      <img src=\"pattern.png\">''')\n\n\n@assert_no_logs\ndef test_image_contain(assert_pixels):\n    assert_pixels(centered_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 1px 0 0 2px; font-size: 0 }\n        img { object-fit: contain; height: 6px; width: 4px; overflow: hidden }\n      </style>\n      <img src=\"pattern.png\">''')\n\n\n@assert_no_logs\ndef test_image_none(assert_pixels):\n    assert_pixels(centered_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 1px 0 0 1px; font-size: 0 }\n        img { object-fit: none; height: 6px; width: 6px }\n      </style>\n      <img src=\"pattern.png\">''')\n\n\n@assert_no_logs\ndef test_image_scale_down(assert_pixels):\n    assert_pixels(centered_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 1px 0 0 1px; font-size: 0 }\n        img { object-fit: scale-down; height: 6px; width: 6px }\n      </style>\n      <img src=\"pattern.png\">''')\n\n\n@assert_no_logs\ndef test_image_position(assert_pixels):\n    assert_pixels(centered_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 1px 0 0 1px; font-size: 0 }\n        img { object-fit: none; height: 6px; width: 6px;\n              object-position: bottom 50% right 50% }\n      </style>\n      <img src=\"pattern.png\">''')\n\n\n@assert_no_logs\ndef test_images_border(assert_pixels):\n    assert_pixels(border_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 0; font-size: 0 }\n        img { margin: 1px; border: 1px solid lime }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_images_border_absolute(assert_pixels):\n    assert_pixels(border_image, '''\n      <style>\n        @page { size: 8px }\n        body { margin: 0; font-size: 0 }\n        img { margin: 1px; border: 1px solid lime; position: absolute }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_image_exif(assert_same_renderings):\n    assert_same_renderings(\n        '''\n            <style>@page { size: 10px }</style>\n            <img style=\"display: block\" src=\"not-optimized.jpg\">\n        ''',\n        '''\n            <style>@page { size: 10px }</style>\n            <img style=\"display: block\" src=\"not-optimized-exif.jpg\">\n        ''',\n        tolerance=25,\n    )\n\n\n@assert_no_logs\ndef test_image_exif_image_orientation(assert_same_renderings):\n    assert_same_renderings(\n        '''\n            <style>@page { size: 10px }</style>\n            <img style=\"display: block; image-orientation: 180deg\"\n                 src=\"not-optimized-exif.jpg\">\n        ''',\n        '''\n            <style>@page { size: 10px }</style>\n            <img style=\"display: block\" src=\"not-optimized-exif.jpg\">\n        ''',\n        tolerance=25,\n    )\n\n\n@assert_no_logs\ndef test_image_exif_image_orientation_keep_format():\n    # Regression test for #1755.\n    pdf = FakeHTML(\n        string='''\n          <style>@page { size: 10px }</style>\n          <img style=\"display: block; image-orientation: 180deg\"\n               src=\"not-optimized-exif.jpg\">''',\n        base_url=resource_path('<inline HTML>')).write_pdf()\n    assert b'DCTDecode' in pdf\n"
  },
  {
    "path": "tests/draw/test_leader.py",
    "content": "\"\"\"Test how leaders are drawn.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_leader_simple(assert_pixels):\n    assert_pixels('''\n        RR__BBBBBBBB__BB\n        RR__BBBBBBBB__BB\n        RRRR__BBBB__BBBB\n        RRRR__BBBB__BBBB\n        RR__BBBB__BBBBBB\n        RR__BBBB__BBBBBB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 6px;\n        }\n        body {\n          color: red;\n          counter-reset: count;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          content: ' ' leader(dotted) ' ' counter(count, lower-roman);\n          counter-increment: count;\n        }\n      </style>\n      <div>a</div>\n      <div>bb</div>\n      <div>c</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_too_long(assert_pixels):\n    assert_pixels('''\n        RRRRRRRRRR______\n        RRRRRRRRRR______\n        BBBBBBBBBBBB__BB\n        BBBBBBBBBBBB__BB\n        RR__RR__RR__RR__\n        RR__RR__RR__RR__\n        RR__RR__RR______\n        RR__RR__RR______\n        BBBBBBBBBB__BBBB\n        BBBBBBBBBB__BBBB\n        RR__RR__RR__RR__\n        RR__RR__RR__RR__\n        RR__BBBB__BBBBBB\n        RR__BBBB__BBBBBB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 14px;\n        }\n        body {\n          color: red;\n          counter-reset: count;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          content: ' ' leader(dotted) ' ' counter(count, lower-roman);\n          counter-increment: count;\n        }\n      </style>\n      <div>aaaaa</div>\n      <div>a a a a a a a</div>\n      <div>a a a a a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_alone(assert_pixels):\n    assert_pixels('''\n        RRBBBBBBBBBBBBBB\n        RRBBBBBBBBBBBBBB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          content: leader(dotted);\n        }\n      </style>\n      <div>a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_content(assert_pixels):\n    assert_pixels('''\n        RR____BB______BB\n        RR____BB______BB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          content: leader(' . ') 'a';\n        }\n      </style>\n      <div>a</div>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_leader_float(assert_pixels):\n    assert_pixels('''\n        bbGRR___BB____BB\n        bbGRR___BB____BB\n        GGGRR___BB____BB\n        ___RR___BB____BB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 4px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        article {\n          background: lime;\n          color: navy;\n          float: left;\n          height: 3px;\n          width: 3px;\n        }\n        div::after {\n          color: blue;\n          content: leader('. ') 'a';\n        }\n      </style>\n      <div>a<article>a</article></div>\n      <div>a</div>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_leader_float_small(assert_pixels):\n    assert_pixels('''\n        bbRRBB__BB____BB\n        bbRRBB__BB____BB\n        RR__BB__BB____BB\n        RR__BB__BB____BB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 4px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        article {\n          background: lime;\n          color: navy;\n          float: left;\n        }\n        div::after {\n          color: blue;\n          content: leader('. ') 'a';\n        }\n      </style>\n      <div>a<article>a</article></div>\n      <div>a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_in_inline(assert_pixels):\n    assert_pixels('''\n        RR__GGBBBBBB__RR\n        RR__GGBBBBBB__RR\n    ''', '''\n      <style>\n        @page {\n          size: 16px 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        span {\n          color: lime;\n        }\n        span::after {\n          color: blue;\n          content: leader('-');\n        }\n      </style>\n      <div>a <span>a</span> a</div>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_leader_bad_alignment(assert_pixels):\n    assert_pixels('''\n        RRRRRR__________\n        RRRRRR__________\n        ______BB______RR\n        ______BB______RR\n    ''', '''\n      <style>\n        @page {\n          size: 16px 4px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          content: leader(' - ') 'a';\n        }\n      </style>\n      <div>aaa</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_simple_rtl(assert_pixels):\n    assert_pixels('''\n        BB__BBBBBBBB__RR\n        BB__BBBBBBBB__RR\n        BBBB__BBBB__RRRR\n        BBBB__BBBB__RRRR\n        BBBBBB__BBBB__RR\n        BBBBBB__BBBB__RR\n    ''', '''\n      <style>\n        @page {\n          size: 16px 6px;\n        }\n        body {\n          color: red;\n          counter-reset: count;\n          direction: rtl;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          /* RTL Mark used in second space */\n          content: ' ' leader(dotted) '‏ ' counter(count, lower-roman);\n          counter-increment: count;\n        }\n      </style>\n      <div>a</div>\n      <div>bb</div>\n      <div>c</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_too_long_rtl(assert_pixels):\n    assert_pixels('''\n        ______RRRRRRRRRR\n        ______RRRRRRRRRR\n        BB__BBBBBBBBBBBB\n        BB__BBBBBBBBBBBB\n        __RR__RR__RR__RR\n        __RR__RR__RR__RR\n        ______RR__RR__RR\n        ______RR__RR__RR\n        BBBB__BBBBBBBBBB\n        BBBB__BBBBBBBBBB\n        __RR__RR__RR__RR\n        __RR__RR__RR__RR\n        BBBBBB__BBBB__RR\n        BBBBBB__BBBB__RR\n    ''', '''\n      <style>\n        @page {\n          size: 16px 14px;\n        }\n        body {\n          color: red;\n          counter-reset: count;\n          direction: rtl;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          /* RTL Mark used in second space */\n          content: ' ' leader(dotted) '‏ ' counter(count, lower-roman);\n          counter-increment: count;\n        }\n      </style>\n      <div>aaaaa</div>\n      <div>a a a a a a a</div>\n      <div>a a a a a</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_float_leader(assert_pixels):\n    # Regression test for #1409.\n    # Leaders in floats are not displayed at all in many cases with the current\n    # implementation, and this case is not really specified. So…\n    assert_pixels('''\n        RR____________BB\n        RR____________BB\n        RRRR__________BB\n        RRRR__________BB\n        RR____________BB\n        RR____________BB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 6px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          content: leader(' . ') 'a';\n          float: right;\n        }\n      </style>\n      <div>a</div>\n      <div>bb</div>\n      <div>c</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_empty_string(assert_pixels):\n    assert_pixels('''\n        RRRR____\n        ________\n    ''', '''\n      <style>\n        @page {\n          size: 8px 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 1px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          content: leader('');\n        }\n      </style>\n      <div>aaaa</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_zero_width_string(assert_pixels):\n    assert_pixels('''\n        RRRR____\n        ________\n    ''', '''\n      <style>\n        @page {\n          size: 8px 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 1px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          content: leader('​');  /* zero-width space */\n        }\n      </style>\n      <div>aaaa</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_absolute(assert_pixels):\n    assert_pixels('''\n        BBBBRRRR\n        ______GG\n    ''', '''\n      <style>\n        @page {\n          size: 8px 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 1px;\n          line-height: 1;\n        }\n        div::before {\n          color: blue;\n          content: leader('z');\n        }\n        article {\n          bottom: 0;\n          color: lime;\n          position: absolute;\n          right: 0;\n        }\n      </style>\n      <div>aa<article>bb</article>aa</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_padding(assert_pixels):\n    assert_pixels('''\n        RR__BBBBBBBB__BB\n        RR__BBBBBBBB__BB\n        __RR__BBBB__BBBB\n        __RR__BBBB__BBBB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 4px;\n        }\n        body {\n          color: red;\n          counter-reset: count;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          content: ' ' leader(dotted) ' ' counter(count, lower-roman);\n          counter-increment: count;\n        }\n        div + div {\n          padding-left: 2px;\n        }\n      </style>\n      <div>a</div>\n      <div>b</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_inline_padding(assert_pixels):\n    assert_pixels('''\n        RR__BBBBBBBB__BB\n        RR__BBBBBBBB__BB\n        __RR__BBBB__BBBB\n        __RR__BBBB__BBBB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 4px;\n        }\n        body {\n          color: red;\n          counter-reset: count;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        span::after {\n          color: blue;\n          content: ' ' leader(dotted) ' ' counter(count, lower-roman);\n          counter-increment: count;\n        }\n        div + div span {\n          padding-left: 2px;\n        }\n      </style>\n      <div><span>a</span></div>\n      <div><span>b</span></div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_margin(assert_pixels):\n    assert_pixels('''\n        RR__BBBBBBBB__BB\n        RR__BBBBBBBB__BB\n        __RR__BBBB__BBBB\n        __RR__BBBB__BBBB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 4px;\n        }\n        body {\n          color: red;\n          counter-reset: count;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        div::after {\n          color: blue;\n          content: ' ' leader(dotted) ' ' counter(count, lower-roman);\n          counter-increment: count;\n        }\n        div + div {\n          margin-left: 2px;\n        }\n      </style>\n      <div>a</div>\n      <div>b</div>\n    ''')\n\n\n@assert_no_logs\ndef test_leader_inline_margin(assert_pixels):\n    assert_pixels('''\n        RR__BBBBBBBB__BB\n        RR__BBBBBBBB__BB\n        __RR__BBBB__BBBB\n        __RR__BBBB__BBBB\n    ''', '''\n      <style>\n        @page {\n          size: 16px 4px;\n        }\n        body {\n          color: red;\n          counter-reset: count;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        span::after {\n          color: blue;\n          content: ' ' leader(dotted) ' ' counter(count, lower-roman);\n          counter-increment: count;\n        }\n        div + div span {\n          margin-left: 2px;\n        }\n      </style>\n      <div><span>a</span></div>\n      <div><span>b</span></div>\n    ''')\n"
  },
  {
    "path": "tests/draw/test_list.py",
    "content": "\"\"\"Test how lists are drawn.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import SANS_FONTS, assert_no_logs\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('position', 'pixels'), [\n    ('outside',\n     #  ++++++++++++++      ++++  <li> horizontal margins: 7px 2px\n     #                ######      <li> width: 12 - 7 - 2 = 3px\n     #              --            list marker margin: 0.5em = 2px\n     #      ********              list marker image is 4px wide\n     '''\n        ____________\n        ____________\n        ___rBBB_____\n        ___BBBB_____\n        ___BBBB_____\n        ___BBBB_____\n        ____________\n        ____________\n        ____________\n        ____________\n     '''),\n    ('inside',\n     #  ++++++++++++++      ++++  <li> horizontal margins: 7px 2px\n     #                ######      <li> width: 12 - 7 - 2 = 3px\n     #                ********    list marker image is 4px wide: overflow\n     '''\n        ____________\n        ____________\n        _______rBBB_\n        _______BBBB_\n        _______BBBB_\n        _______BBBB_\n        ____________\n        ____________\n        ____________\n        ____________\n     ''')\n])\ndef test_list_style_image(assert_pixels, position, pixels):\n    assert_pixels(pixels, '''\n      <style>\n        @page { size: 12px 10px }\n        body { margin: 0; font-family: %s }\n        ul { margin: 2px 2px 0 7px; list-style: url(pattern.png) %s;\n             font-size: 2px }\n      </style>\n      <ul><li></li></ul>''' % (SANS_FONTS, position))\n\n\n@assert_no_logs\ndef test_list_style_image_none(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px }\n        body { margin: 0; font-family: %s }\n        ul { margin: 0 0 0 5px; list-style: none; font-size: 2px; }\n      </style>\n      <ul><li>''' % (SANS_FONTS,))\n"
  },
  {
    "path": "tests/draw/test_opacity.py",
    "content": "\"\"\"Test opacity.\"\"\"\n\nfrom ..testing_utils import assert_no_logs\n\nopacity_source = '''\n    <style>\n        @page { size: 60px 60px }\n        div { background: #000; width: 20px; height: 20px }\n    </style>\n    %s'''\n\n\n@assert_no_logs\ndef test_opacity_zero(assert_same_renderings):\n    assert_same_renderings(\n        opacity_source % '<div></div>',\n        opacity_source % '<div></div><div style=\"opacity: 0\"></div>',\n        opacity_source % '<div></div><div style=\"opacity: 0%\"></div>',\n    )\n\n\n@assert_no_logs\ndef test_opacity_normal_range(assert_same_renderings):\n    assert_same_renderings(\n        opacity_source % '<div style=\"background: rgb(102, 102, 102)\"></div>',\n        opacity_source % '<div style=\"opacity: 0.6\"></div>',\n        opacity_source % '<div style=\"opacity: 60%\"></div>',\n        opacity_source % '<div style=\"opacity: 60.0%\"></div>',\n    )\n\n\n@assert_no_logs\ndef test_opacity_nested(assert_same_renderings):\n    assert_same_renderings(\n        opacity_source % '<div style=\"background: rgb(102, 102, 102)\"></div>',\n        opacity_source % '<div style=\"opacity: 0.6\"></div>',\n        opacity_source % '''\n          <div style=\"background: none; opacity: 0.666666\">\n            <div style=\"opacity: 0.9\"></div>\n          </div>\n        ''',  # 0.9 * 0.666666 == 0.6\n    )\n\n\n@assert_no_logs\ndef test_opacity_percent_clamp_down(assert_same_renderings):\n    assert_same_renderings(\n        opacity_source % '<div></div>',\n        opacity_source % '<div style=\"opacity: 1.2\"></div>',\n        opacity_source % '<div style=\"opacity: 120%\"></div>',\n    )\n\n\n@assert_no_logs\ndef test_opacity_percent_clamp_up(assert_same_renderings):\n    assert_same_renderings(\n        opacity_source % '<div></div>',\n        opacity_source % '<div></div><div style=\"opacity: -0.2\"></div>',\n        opacity_source % '<div></div><div style=\"opacity: -20%\"></div>',\n    )\n\n\n@assert_no_logs\ndef test_opacity_black(assert_same_renderings):\n    # Regression test for #2302.\n    assert_same_renderings(\n        opacity_source % 'a<span style=\"color: rgb(0, 0, 0, 0.5)\">b</span>',\n        opacity_source % 'a<span style=\"opacity: 0.5\">b</span>',\n    )\n"
  },
  {
    "path": "tests/draw/test_overflow.py",
    "content": "\"\"\"Test overflow and clipping.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_overflow_1(assert_pixels):\n    # See test_images\n    assert_pixels('''\n        ________\n        ________\n        __rBBB__\n        __BBBB__\n        ________\n        ________\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px }\n        body { margin: 2px 0 0 2px; font-size: 0 }\n        div { height: 2px; overflow: hidden }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_overflow_2(assert_pixels):\n    # <body> is only 1px high, but its overflow is propageted to the viewport\n    # ie. the padding edge of the page box.\n    assert_pixels('''\n        ________\n        ________\n        __rBBB__\n        __BBBB__\n        __BBBB__\n        ________\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px; margin: 2px 2px 3px 2px }\n        body { height: 1px; overflow: hidden; font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_overflow_3(assert_pixels):\n    # Assert that the border is not clipped by overflow: hidden\n    assert_pixels('''\n        ________\n        ________\n        __BBBB__\n        __B__B__\n        __B__B__\n        __BBBB__\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px; margin: 2px; }\n        div { width: 2px; height: 2px; overflow: hidden;\n              border: 1px solid blue; }\n      </style>\n      <div></div>''')\n\n\n@assert_no_logs\ndef test_overflow_4(assert_pixels):\n    # Assert that the page margins aren't clipped by body's overflow\n    assert_pixels('''\n        rr______\n        rr______\n        __BBBB__\n        __BBBB__\n        __BBBB__\n        __BBBB__\n        ________\n        ________\n    ''', '''\n      <style>\n        @page {\n          size: 8px;\n          margin: 2px;\n          background:#fff;\n          @top-left-corner { content: ''; background:#f00; } }\n        body { overflow: auto; background:#00f; }\n      </style>\n      ''')\n\n\n@assert_no_logs\ndef test_overflow_5(assert_pixels):\n    # Regression test for #2026.\n    assert_pixels('''\n        BBBBBB__\n        BBBBBB__\n        BBBB____\n        BBBB____\n        BBBB____\n        ________\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px }\n        body { font-family: weasyprint; line-height: 1; font-size: 2px }\n        p { color: blue }\n      </style>\n      <p>abc</p>\n      <p style=\"height: 3px; overflow: hidden\">ab<br>ab<br>ab<br>ab</p>\n      ''')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('number', 'css', 'pixels'), [\n    (1, '5px, 5px, 9px, auto', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______rBBBrBg_\n        ______BBBBBBg_\n        ______BBBBBBg_\n        ______BBBBBBg_\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    (2, '5px, 5px, auto, 10px', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______rBBBr___\n        ______BBBBB___\n        ______BBBBB___\n        ______BBBBB___\n        ______rBBBr___\n        ______BBBBB___\n        ______ggggg___\n        ______________\n        ______________\n        ______________\n    '''),\n    (3, '5px, auto, 9px, 10px', '''\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        _grBBBrBBBr___\n        _gBBBBBBBBB___\n        _gBBBBBBBBB___\n        _gBBBBBBBBB___\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n    (4, 'auto, 5px, 9px, 10px', '''\n        ______________\n        ______ggggg___\n        ______rBBBr___\n        ______BBBBB___\n        ______BBBBB___\n        ______BBBBB___\n        ______rBBBr___\n        ______BBBBB___\n        ______BBBBB___\n        ______BBBBB___\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n        ______________\n    '''),\n])\ndef test_clip(assert_pixels, number, css, pixels):\n    assert_pixels(pixels, '''\n      <style>\n        @page { size: 14px 16px }\n        div { margin: 1px; border: 1px green solid;\n              background: url(pattern.png);\n              position: absolute; /* clip only applies on abspos */\n              top: 0; bottom: 2px; left: 0; right: 0;\n              clip: rect(%s); }\n      </style>\n      <div>''' % css)\n"
  },
  {
    "path": "tests/draw/test_page.py",
    "content": "\"\"\"Test how pages are drawn.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rule', 'pixels'), [\n    ('2n', '_R_R_R_R_R'),\n    ('even', '_R_R_R_R_R'),\n    ('2n+1', 'R_R_R_R_R_'),\n    ('odd', 'R_R_R_R_R_'),\n    ('2n+3', '__R_R_R_R_'),\n    ('n', 'RRRRRRRRRR'),\n    ('n-1', 'RRRRRRRRRR'),\n    ('-n+3', 'RRR_______'),\n    ('-2n+3', 'R_R_______'),\n    ('-n-3', '__________'),\n    ('3', '__R_______'),\n    ('0n+0', '__________'),\n])\ndef test_nth_page(assert_pixels, rule, pixels):\n    assert_pixels('\\n'.join(pixels), '''\n      <style>\n        @page { size: 1px 1px }\n        @page:nth(%s) { background: red }\n        p { break-after: page }\n      </style>\n    ''' % rule + 10 * '<p></p>')\n"
  },
  {
    "path": "tests/draw/test_table.py",
    "content": "\"\"\"Test how tables are drawn.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs\n\ntables_source = '''\n  <style>\n    @page { size: 28px }\n    table { margin: 1px; padding: 1px; border-spacing: 1px;\n            border: 1px solid transparent }\n    td { width: 2px; height: 2px; padding: 1px; border: 1px solid transparent }\n    %(extra_css)s\n  </style>\n  <table>\n    <colgroup class=colgroup>\n      <col></col>\n      <col></col>\n    </colgroup>\n    <col></col>\n    <tbody id=tbody>\n      <tr>\n        <td></td>\n        <td rowspan=2></td>\n        <td></td>\n      </tr>\n      <tr>\n        <td colspan=2></td>\n        <td></td>\n      </tr>\n    </tbody>\n    <tr>\n      <td></td>\n      <td></td>\n    </tr>\n  </table>\n'''\n\n\n@assert_no_logs\ndef test_tables_1(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__s____s_s____s_s____s__B_\n        _B__s____s_s____s_s____s__B_\n        _B__s____s_s____s_s____s__B_\n        _B__s____s_s____s_s____s__B_\n        _B__ssssss_s____s_ssssss__B_\n        _B_________s____s_________B_\n        _B__sssssssSssssS_ssssss__B_\n        _B__s______s____S_s____s__B_\n        _B__s______s____S_s____s__B_\n        _B__s______s____S_s____s__B_\n        _B__s______s____S_s____s__B_\n        _B__sssssssSSSSSS_ssssss__B_\n        _B________________________B_\n        _B__ssssss_ssssss_________B_\n        _B__s____s_s____s_________B_\n        _B__s____s_s____s_________B_\n        _B__s____s_s____s_________B_\n        _B__s____s_s____s_________B_\n        _B__ssssss_ssssss_________B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed }\n      td { border-color: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_1_rtl(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__s____s_s____s_s____s__B_\n        _B__s____s_s____s_s____s__B_\n        _B__s____s_s____s_s____s__B_\n        _B__s____s_s____s_s____s__B_\n        _B__ssssss_s____s_ssssss__B_\n        _B_________s____s_________B_\n        _B__ssssss_SssssSsssssss__B_\n        _B__s____s_S____s______s__B_\n        _B__s____s_S____s______s__B_\n        _B__s____s_S____s______s__B_\n        _B__s____s_S____s______s__B_\n        _B__ssssss_SSSSSSsssssss__B_\n        _B________________________B_\n        _B_________ssssss_ssssss__B_\n        _B_________s____s_s____s__B_\n        _B_________s____s_s____s__B_\n        _B_________s____s_s____s__B_\n        _B_________s____s_s____s__B_\n        _B_________ssssss_ssssss__B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed;\n                direction: rtl; }\n      td { border-color: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_2(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBB_________\n        _BBBBBBBBBBBBBBBBBB_________\n        _BB____s____s____BB_________\n        _BB____s____s____BB_________\n        _BB____s____s____BB_________\n        _BB____s____s____BB_________\n        _BBsssss____sssssBB_________\n        _BB_________s____BB_________\n        _BB_________s____BB_________\n        _BB_________s____BB_________\n        _BB_________s____BB_________\n        _BBssssssssssssssBB_________\n        _BB____s____s____BB_________\n        _BB____s____s____BB_________\n        _BB____s____s____BB_________\n        _BB____s____s____BB_________\n        _BBBBBBBBBBBBBBBBBB_________\n        _BBBBBBBBBBBBBBBBBB_________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border: 2px solid #00f; table-layout: fixed;\n                border-collapse: collapse }\n      td { border-color: #ff7f7f }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_2_rtl(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _________BBBBBBBBBBBBBBBBBB_\n        _________BBBBBBBBBBBBBBBBBB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BBsssss____sssssBB_\n        _________BB____s_________BB_\n        _________BB____s_________BB_\n        _________BB____s_________BB_\n        _________BB____s_________BB_\n        _________BBssssssssssssssBB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BBBBBBBBBBBBBBBBBB_\n        _________BBBBBBBBBBBBBBBBBB_\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      body { direction: rtl; }\n      table { border: 2px solid #00f; table-layout: fixed; border-collapse: collapse }\n      td { border-color: #ff7f7f }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_3(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _tttttttttttttttttttttttttt_\n        _t________________________t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BB____s____s____BB_____t_\n        _t_BB____s____s____BB_____t_\n        _t_BB____s____s____BB_____t_\n        _t_BB____s____s____BB_____t_\n        _t_BBsssss____sssssBB_____t_\n        _t_BB_________s____BB_____t_\n        _t_BB_________s____BB_____t_\n        _t_BB_________s____BB_____t_\n        _t_BB_________s____BB_____t_\n        _t_BBssssssssssssssBB_____t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _tttttttttttttttttttttttttt_\n        ____________________________\n        ____________________________\n        _tttttttttttttttttttttttttt_\n        _t_BBssssssssssssssBB_____t_\n        _t_BB____s____s____BB_____t_\n        _t_BB____s____s____BB_____t_\n        _t_BB____s____s____BB_____t_\n        _t_BB____s____s____BB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t_BBBBBBBBBBBBBBBBBB_____t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _tttttttttttttttttttttttttt_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border: solid #00f; border-width: 8px 2px;\n              table-layout: fixed; border-collapse: collapse }\n      td { border-color: #ff7f7f }\n      @page { size: 28px 26px; margin: 1px;\n              border: 1px solid rgba(0, 255, 0, 0.5); }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_3_rtl(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _tttttttttttttttttttttttttt_\n        _t________________________t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BBsssss____sssssBB_t_\n        _t_____BB____s_________BB_t_\n        _t_____BB____s_________BB_t_\n        _t_____BB____s_________BB_t_\n        _t_____BB____s_________BB_t_\n        _t_____BBssssssssssssssBB_t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _tttttttttttttttttttttttttt_\n        ____________________________\n        ____________________________\n        _tttttttttttttttttttttttttt_\n        _t_____BBssssssssssssssBB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _tttttttttttttttttttttttttt_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      body { direction: rtl; }\n      table { border: solid #00f; border-width: 8px 2px;\n              table-layout: fixed; border-collapse: collapse }\n      td { border-color: #ff7f7f }\n      @page { size: 28px 26px; margin: 1px;\n              border: 1px solid rgba(0, 255, 0, 0.5); }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_4(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B_________ssssss_________B_\n        _B__sssssssSSSSSS_ssssss__B_\n        _B__sssssssSSSSSS_ssssss__B_\n        _B__sssssssSSSSSS_ssssss__B_\n        _B__sssssssSSSSSS_ssssss__B_\n        _B__sssssssSSSSSS_ssssss__B_\n        _B__sssssssSSSSSS_ssssss__B_\n        _B________________________B_\n        _B__ssssss_ssssss_________B_\n        _B__ssssss_ssssss_________B_\n        _B__ssssss_ssssss_________B_\n        _B__ssssss_ssssss_________B_\n        _B__ssssss_ssssss_________B_\n        _B__ssssss_ssssss_________B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed }\n      td { background: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_4_rtl(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B__ssssss_ssssss_ssssss__B_\n        _B_________ssssss_________B_\n        _B__ssssss_SSSSSSsssssss__B_\n        _B__ssssss_SSSSSSsssssss__B_\n        _B__ssssss_SSSSSSsssssss__B_\n        _B__ssssss_SSSSSSsssssss__B_\n        _B__ssssss_SSSSSSsssssss__B_\n        _B__ssssss_SSSSSSsssssss__B_\n        _B________________________B_\n        _B_________ssssss_ssssss__B_\n        _B_________ssssss_ssssss__B_\n        _B_________ssssss_ssssss__B_\n        _B_________ssssss_ssssss__B_\n        _B_________ssssss_ssssss__B_\n        _B_________ssssss_ssssss__B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed; direction: rtl }\n      td { background: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_5(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B_________uuuuuu_________B_\n        _B__uuuuuuupppppp_uuuuuu__B_\n        _B__uuuuuuupppppp_uuuuuu__B_\n        _B__uuuuuuupppppp_uuuuuu__B_\n        _B__uuuuuuupppppp_uuuuuu__B_\n        _B__uuuuuuupppppp_uuuuuu__B_\n        _B__uuuuuuupppppp_uuuuuu__B_\n        _B________________________B_\n        _B__ssssss_ssssss_________B_\n        _B__ssssss_ssssss_________B_\n        _B__ssssss_ssssss_________B_\n        _B__ssssss_ssssss_________B_\n        _B__ssssss_ssssss_________B_\n        _B__ssssss_ssssss_________B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed }\n      #tbody { background: rgba(0, 0, 255, 1) }\n      tr { background: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_5_rtl(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B_________uuuuuu_________B_\n        _B__uuuuuu_ppppppuuuuuuu__B_\n        _B__uuuuuu_ppppppuuuuuuu__B_\n        _B__uuuuuu_ppppppuuuuuuu__B_\n        _B__uuuuuu_ppppppuuuuuuu__B_\n        _B__uuuuuu_ppppppuuuuuuu__B_\n        _B__uuuuuu_ppppppuuuuuuu__B_\n        _B________________________B_\n        _B_________ssssss_ssssss__B_\n        _B_________ssssss_ssssss__B_\n        _B_________ssssss_ssssss__B_\n        _B_________ssssss_ssssss__B_\n        _B_________ssssss_ssssss__B_\n        _B_________ssssss_ssssss__B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed; direction: rtl }\n      #tbody { background: rgba(0, 0, 255, 1) }\n      tr { background: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_6(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__uuuuuu_uuuuuu_ssssss__B_\n        _B__uuuuuu_uuuuuu_ssssss__B_\n        _B__uuuuuu_uuuuuu_ssssss__B_\n        _B__uuuuuu_uuuuuu_ssssss__B_\n        _B__uuuuuu_uuuuuu_ssssss__B_\n        _B__uuuuuu_uuuuuu_ssssss__B_\n        _B_________uuuuuu_________B_\n        _B__uuuuuuupppppp_ssssss__B_\n        _B__uuuuuuupppppp_ssssss__B_\n        _B__uuuuuuupppppp_ssssss__B_\n        _B__uuuuuuupppppp_ssssss__B_\n        _B__uuuuuuupppppp_ssssss__B_\n        _B__uuuuuuupppppp_ssssss__B_\n        _B________________________B_\n        _B__uuuuuu_uuuuuu_________B_\n        _B__uuuuuu_uuuuuu_________B_\n        _B__uuuuuu_uuuuuu_________B_\n        _B__uuuuuu_uuuuuu_________B_\n        _B__uuuuuu_uuuuuu_________B_\n        _B__uuuuuu_uuuuuu_________B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed;}\n      .colgroup { background: rgba(0, 0, 255, 1) }\n      col { background: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_6_rtl(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__ssssss_uuuuuu_uuuuuu__B_\n        _B__ssssss_uuuuuu_uuuuuu__B_\n        _B__ssssss_uuuuuu_uuuuuu__B_\n        _B__ssssss_uuuuuu_uuuuuu__B_\n        _B__ssssss_uuuuuu_uuuuuu__B_\n        _B__ssssss_uuuuuu_uuuuuu__B_\n        _B_________uuuuuu_________B_\n        _B__ssssss_ppppppuuuuuuu__B_\n        _B__ssssss_ppppppuuuuuuu__B_\n        _B__ssssss_ppppppuuuuuuu__B_\n        _B__ssssss_ppppppuuuuuuu__B_\n        _B__ssssss_ppppppuuuuuuu__B_\n        _B__ssssss_ppppppuuuuuuu__B_\n        _B________________________B_\n        _B_________uuuuuu_uuuuuu__B_\n        _B_________uuuuuu_uuuuuu__B_\n        _B_________uuuuuu_uuuuuu__B_\n        _B_________uuuuuu_uuuuuu__B_\n        _B_________uuuuuu_uuuuuu__B_\n        _B_________uuuuuu_uuuuuu__B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed; direction: rtl }\n      .colgroup { background: rgba(0, 0, 255, 1) }\n      col { background: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_7(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uBBBBu_uBBBBu_uBBBBu__B_\n        _B__uBBBBu_uBBBBu_uBBBBu__B_\n        _B__uBBBBu_uBBBBu_uBBBBu__B_\n        _B__uBBBBu_uBBBBu_uBBBBu__B_\n        _B__uuuuuu_uBBBBu_uuuuuu__B_\n        _B_________uBBBBu_________B_\n        _B__ssssssspuuuup_ssssss__B_\n        _B__s______uBBBBp_s____s__B_\n        _B__s______uBBBBp_s____s__B_\n        _B__s______uBBBBp_s____s__B_\n        _B__s______uBBBBp_s____s__B_\n        _B__ssssssspppppp_ssssss__B_\n        _B________________________B_\n        _B__ssssss_ssssss_________B_\n        _B__s____s_s____s_________B_\n        _B__s____s_s____s_________B_\n        _B__s____s_s____s_________B_\n        _B__s____s_s____s_________B_\n        _B__ssssss_ssssss_________B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed }\n      #tbody tr:first-child { background: blue }\n      td { border-color: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_7_rtl(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__uuuuuu_uuuuuu_uuuuuu__B_\n        _B__uBBBBu_uBBBBu_uBBBBu__B_\n        _B__uBBBBu_uBBBBu_uBBBBu__B_\n        _B__uBBBBu_uBBBBu_uBBBBu__B_\n        _B__uBBBBu_uBBBBu_uBBBBu__B_\n        _B__uuuuuu_uBBBBu_uuuuuu__B_\n        _B_________uBBBBu_________B_\n        _B__ssssss_puuuupsssssss__B_\n        _B__s____s_pBBBBu______s__B_\n        _B__s____s_pBBBBu______s__B_\n        _B__s____s_pBBBBu______s__B_\n        _B__s____s_pBBBBu______s__B_\n        _B__ssssss_ppppppsssssss__B_\n        _B________________________B_\n        _B_________ssssss_ssssss__B_\n        _B_________s____s_s____s__B_\n        _B_________s____s_s____s__B_\n        _B_________s____s_s____s__B_\n        _B_________s____s_s____s__B_\n        _B_________ssssss_ssssss__B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed; direction: rtl }\n      #tbody tr:first-child { background: blue }\n      td { border-color: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_8(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__uuuuuu_ssssss_ssssss__B_\n        _B__uBBBBu_s____s_s____s__B_\n        _B__uBBBBu_s____s_s____s__B_\n        _B__uBBBBu_s____s_s____s__B_\n        _B__uBBBBu_s____s_s____s__B_\n        _B__uuuuuu_s____s_ssssss__B_\n        _B_________s____s_________B_\n        _B__uuuuuuupuuuup_ssssss__B_\n        _B__uBBBBBBuBBBBp_s____s__B_\n        _B__uBBBBBBuBBBBp_s____s__B_\n        _B__uBBBBBBuBBBBp_s____s__B_\n        _B__uBBBBBBuBBBBp_s____s__B_\n        _B__uuuuuuupppppp_ssssss__B_\n        _B________________________B_\n        _B__uuuuuu_ssssss_________B_\n        _B__uBBBBu_s____s_________B_\n        _B__uBBBBu_s____s_________B_\n        _B__uBBBBu_s____s_________B_\n        _B__uBBBBu_s____s_________B_\n        _B__uuuuuu_ssssss_________B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed }\n      .colgroup col:first-child { background: blue }\n      td { border-color: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_8_rtl(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        _B________________________B_\n        _B________________________B_\n        _B__ssssss_ssssss_uuuuuu__B_\n        _B__s____s_s____s_uBBBBu__B_\n        _B__s____s_s____s_uBBBBu__B_\n        _B__s____s_s____s_uBBBBu__B_\n        _B__s____s_s____s_uBBBBu__B_\n        _B__ssssss_s____s_uuuuuu__B_\n        _B_________s____s_________B_\n        _B__ssssss_puuuupuuuuuuu__B_\n        _B__s____s_pBBBBuBBBBBBu__B_\n        _B__s____s_pBBBBuBBBBBBu__B_\n        _B__s____s_pBBBBuBBBBBBu__B_\n        _B__s____s_pBBBBuBBBBBBu__B_\n        _B__ssssss_ppppppuuuuuuu__B_\n        _B________________________B_\n        _B_________ssssss_uuuuuu__B_\n        _B_________s____s_uBBBBu__B_\n        _B_________s____s_uBBBBu__B_\n        _B_________s____s_uBBBBu__B_\n        _B_________s____s_uBBBBu__B_\n        _B_________ssssss_uuuuuu__B_\n        _B________________________B_\n        _B________________________B_\n        _BBBBBBBBBBBBBBBBBBBBBBBBBB_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      table { border-color: #00f; table-layout: fixed; direction: rtl }\n      .colgroup col:first-child { background: blue }\n      td { border-color: rgba(255, 0, 0, 0.5) }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_9(assert_pixels):\n    assert_pixels('''\n        ______________________\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBB____R____R____BBB_\n        _BBB____R____R____BBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        __R_____R____R_____R__\n        __R_____R____R_____R__\n        __RRRRRRRRRRRRRRRRRR__\n        __R_____R____R_____R__\n        __R_____R____R_____R__\n        __RRRRRRRRRRRRRRRRRR__\n        ______________________\n        ______________________\n        ______________________\n        ______________________\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBB____R____R____BBB_\n        _BBB____R____R____BBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        __R_____R____R_____R__\n        __RRRRRRRRRRRRRRRRRR__\n        ______________________\n        ______________________\n        ______________________\n        ______________________\n        ______________________\n        ______________________\n        ______________________\n    ''', '''\n      <style>\n        @page { size: 22px 18px; margin: 1px }\n        td { border: 1px red solid; width: 4px; height: 2px; }\n      </style>\n      <table style=\"table-layout: fixed; border-collapse: collapse\">\n        <thead style=\"border: blue solid; border-width: 3px;\n            \"><td></td><td></td><td></td></thead>\n        <tr><td></td><td></td><td></td></tr>\n        <tr><td></td><td></td><td></td></tr>\n        <tr><td></td><td></td><td></td></tr>''')\n\n\n@assert_no_logs\ndef test_tables_10(assert_pixels):\n    assert_pixels('''\n        ______________________\n        __RRRRRRRRRRRRRRRRRR__\n        __R_____R____R_____R__\n        __R_____R____R_____R__\n        __RRRRRRRRRRRRRRRRRR__\n        __R_____R____R_____R__\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBB____R____R____BBB_\n        _BBB____R____R____BBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        ______________________\n        ______________________\n        ______________________\n        ______________________\n        __RRRRRRRRRRRRRRRRRR__\n        __R_____R____R_____R__\n        __R_____R____R_____R__\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBB____R____R____BBB_\n        _BBB____R____R____BBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        ______________________\n        ______________________\n        ______________________\n        ______________________\n        ______________________\n    ''', '''\n      <style>\n        @page { size: 22px 17px; margin: 1px }\n        td { border: 1px red solid; width: 4px; height: 2px; }\n      </style>\n      <table style=\"table-layout: fixed; margin-left: 1px; border-collapse: collapse\">\n        <tr><td></td><td></td><td></td></tr>\n        <tr><td></td><td></td><td></td></tr>\n        <tr><td></td><td></td><td></td></tr>\n        <tfoot style=\"border: blue solid; border-width: 3px;\n            \"><td></td><td></td><td></td></tfoot>''')\n\n\n@assert_no_logs\ndef test_tables_11(assert_pixels):\n    # Regression test for #82.\n    assert_pixels('''\n      ____________________\n      ________RRRRRRRRRRR_\n      ________R____R____R_\n      ________R____R____R_\n      ________R____R____R_\n      ________RRRRRRRRRRR_\n      ____________________\n      ____________________\n      ____________________\n      ____________________\n    ''', '''\n      <style>\n        @page { size: 20px 10px; margin: 1px }\n        body { text-align: right; font-size: 0 }\n        table { display: inline-table; width: 11px }\n        td { border: 1px red solid; width: 4px; height: 3px }\n      </style>\n      <table style=\"table-layout: fixed; border-collapse: collapse\">\n        <tr><td></td><td></td></tr>''')\n\n\n@assert_no_logs\ndef test_tables_12(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _________BBBBBBBBBBBBBBBBBB_\n        _________BBBBBBBBBBBBBBBBBB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BBsssss____sssssBB_\n        _________BB____s_________BB_\n        _________BB____s_________BB_\n        _________BB____s_________BB_\n        _________BB____s_________BB_\n        _________BBssssssssssssssBB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BB____s____s____BB_\n        _________BBBBBBBBBBBBBBBBBB_\n        _________BBBBBBBBBBBBBBBBBB_\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      body { direction: rtl }\n      table { border: 2px solid #00f; table-layout: fixed; border-collapse: collapse }\n      td { border-color: #ff7f7f }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_13(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _tttttttttttttttttttttttttt_\n        _t________________________t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BBsssss____sssssBB_t_\n        _t_____BB____s_________BB_t_\n        _t_____BB____s_________BB_t_\n        _t_____BB____s_________BB_t_\n        _t_____BB____s_________BB_t_\n        _t_____BBssssssssssssssBB_t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _tttttttttttttttttttttttttt_\n        ____________________________\n        ____________________________\n        _tttttttttttttttttttttttttt_\n        _t_____BBssssssssssssssBB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BB____s____s____BB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t_____BBBBBBBBBBBBBBBBBB_t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _t________________________t_\n        _tttttttttttttttttttttttttt_\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      body { direction: rtl }\n      table { border: solid #00f; border-width: 8px 2px;\n              table-layout: fixed; border-collapse: collapse }\n      td { border-color: #ff7f7f }\n      @page { size: 28px 26px; margin: 1px;\n              border: 1px solid rgba(0, 255, 0, 0.5); }\n    '''})\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_tables_14(assert_pixels):\n    assert_pixels('''\n        ____________________________\n        _RRR_RRR_RRR________________\n        _RRR_RRR_RRR________________\n        _RRR_RRR_RRR________________\n        _RRR_RRR_RRR________________\n        _RRR_RRR_RRR________________\n        _RRR_RRR_RRR________________\n        _RRR_RRR_RRR________________\n        _RRR_RRR_RRR________________\n        _RRR_RRR_RRR________________\n        _RRR_RRR_RRR________________\n        _____RRR____________________\n        _RRRRRRR_RRR________________\n        _RRRRRRR_RRR________________\n        _RRRRRRR_RRR________________\n        _RRRRRRR_RRR________________\n        _RRRRRRR_RRR________________\n        _RRRRRRR_RRR________________\n        _RRRRRRR_RRR________________\n        _RRRRRRR_RRR________________\n        _RRRRRRR_RRR________________\n        _RRRRRRR_RRR________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        _RRR_RRR____________________\n        _RRR_RRR____________________\n        _RRR_RRR____________________\n        _RRR_RRR____________________\n        _RRR_RRR____________________\n        _RRR_RRR____________________\n        _RRR_RRR____________________\n        _RRR_RRR____________________\n        _RRR_RRR____________________\n        _RRR_RRR____________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n        ____________________________\n    ''', tables_source % {'extra_css': '''\n      @page { size: 28px 26px }\n      table { margin: 0; padding: 0; border: 0 }\n      col { background: red }\n      td { padding: 0; width: 1px; height: 8px }\n    '''})\n\n\n@assert_no_logs\ndef test_tables_15(assert_pixels):\n    # Regression test for #1250.\n    assert_pixels('''\n        ______________________\n        __RRRRRRRRRRRRRRRRRR__\n        __R_____R____R_____R__\n        __R_____R____R_____R__\n        __R_____R____R_____R__\n        __RRRRRRRRRRRRRRRRRR__\n        __R_____R____R_____R__\n        __R_____R____R_____R__\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBB____R____R____BBB_\n        _BBB____R____R____BBB_\n        _BBB____R____R____BBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        ______________________\n        ______________________\n        __RRRRRRRRRRRRRRRRRR__\n        __R________________R__\n        __R________________R__\n        __R________________R__\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBB____R____R____BBB_\n        _BBB____R____R____BBB_\n        _BBB____R____R____BBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        _BBBBBBBBBBBBBBBBBBBB_\n        ______________________\n        ______________________\n        ______________________\n        ______________________\n    ''', '''\n      <style>\n        @page { size: 22px 18px; margin: 1px }\n        td { border: 1px red solid; width: 4px; height: 3px; }\n      </style>\n      <table style=\"table-layout: fixed; margin-left: 1px; border-collapse: collapse\">\n        <tr><td></td><td></td><td></td></tr>\n        <tr><td></td><td></td><td></td></tr>\n        <tr><td colspan=\"3\"></td></tr>\n        <tfoot style=\"border: blue solid; border-width: 3px;\n            \"><td></td><td></td><td></td></tfoot>''')\n\n\n@assert_no_logs\ndef test_tables_16(assert_pixels):\n    assert_pixels('''\n      ____________________\n      _RRRRRRRRRRR________\n      _R____R____R________\n      _R____R____R________\n      _R____R_RRRRRRRRRRR_\n      _RRRRRRRRRRR_R____R_\n      ________R____R____R_\n      ________R____R____R_\n      ________RRRRRRRRRRR_\n      ____________________\n    ''', '''\n      <style>\n        @page { size: 20px 10px; margin: 1px }\n        body { text-align: right; font-size: 0 }\n        table { position: absolute; width: 11px;\n                table-layout: fixed; border-collapse: collapse }\n        td { border: 1px red solid; width: 4px; height: 3px }\n      </style>\n      <table style=\"top: 0; left: 0\">\n        <tr><td></td><td></td></tr>\n      <table style=\"bottom: 0; right: 0\">\n        <tr><td></td><td></td></tr>''')\n\n\n@assert_no_logs\ndef test_tables_17(assert_pixels):\n    assert_pixels('''\n      ________________\n      _RRRRRRRRRRRRRR_\n      _RRRRRRRRRRRRRR_\n      _RR____RR____RR_\n      _RR_BB_RR_BB_RR_\n      _RR_BB_RR_BB_RR_\n      _RR_BB_RR____RR_\n      _RR_BB_RR____RR_\n      _RR____RR____RR_\n      ________________\n      ________________\n      _RR_BB_RR____RR_\n      _RR_BB_RR____RR_\n      _RR_BB_RR____RR_\n      _RR_BB_RR____RR_\n      _RR____RR____RR_\n      _RRRRRRRRRRRRRR_\n      _RRRRRRRRRRRRRR_\n      ________________\n      ________________\n    ''', '''\n      <style>\n        @page { size: 16px 10px; margin: 1px }\n        table { border-collapse: collapse; font-size: 2px; line-height: 1;\n                color: blue; font-family: weasyprint }\n        td { border: 2px red solid; padding: 1px; line-height: 1 }\n      </style>\n      <table><tr><td>a a a a</td><td>a</td></tr>''')\n\n\n@assert_no_logs\ndef test_tables_18(assert_pixels):\n    assert_pixels('''\n      ____________\n      _RRRRRRRRRR_\n      _R________R_\n      _R_RRRRRR_R_\n      _R_R____R_R_\n      _R_R_BB_R_R_\n      _R_R_BB_R_R_\n      _R_R_BB_R_R_\n      _R_R_BB_R_R_\n      _R_R____R_R_\n      ____________\n      ____________\n      _R_R_BB_R_R_\n      _R_R_BB_R_R_\n      _R_R_BB_R_R_\n      _R_R_BB_R_R_\n      _R_R____R_R_\n      _R_RRRRRR_R_\n      _R________R_\n      _RRRRRRRRRR_\n      ____________\n      ____________\n    ''', '''\n      <style>\n        @page { size: 12px 11px; margin: 1px }\n        table { border: 1px red solid; border-spacing: 1px; font-size: 2px;\n                line-height: 1; color: blue; font-family: weasyprint }\n        td { border: 1px red solid; padding: 1px; line-height: 1; }\n      </style>\n      <table><tr><td>a a a a</td></tr>''')\n\n\n@assert_no_logs\ndef test_tables_19(assert_pixels):\n    # Regression test for #1523.\n    assert_pixels('''\n      RR\n      RR\n      RR\n      RR\n      RR\n      RR\n      RR\n      RR\n    ''', '''\n      <style>\n        @page { size: 2px 4px }\n        table { border-collapse: collapse; color: red }\n        body { font-size: 2px; font-family: weasyprint; line-height: 1 }\n      </style>\n      <table><tr><td>a a a a</td></tr></table>''')\n\n\n@assert_no_logs\ndef test_tables_20(assert_pixels):\n    assert_pixels('''\n      ____________________\n      _RRRRRRRRRRRR_______\n      _RBBBBBBBBBBR_______\n      _RRRRRRRRRRRR_______\n      ____________________\n    ''', '''\n      <style>\n        @page { size: 20px 5px; margin: 1px }\n        table { width: 10px; border: 1px red solid }\n        td { height: 1px; background: blue }\n        col, tr, tbody, tfoot { background: lime }\n      </style>\n      <table>\n      <col></col><col></col>\n      <tbody><tr></tr><tr><td></td></tr></tbody>\n      <tfoot></tfoot>''')\n\n\n@assert_no_logs\ndef test_tables_21(assert_pixels):\n    assert_pixels('''\n      _________________________\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _rBBBBBBBBBBrBBBBBBBBBBr_\n      _rBKKKKKKBBBrBKKKKKKBBBr_\n      _rBKKKKKKBBBrBKKKKKKBBBr_\n      _rBBBBBBBBBBrBBBBBBBBBBr_\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _________________________\n      _________________________\n      _________________________\n      _________________________\n      _________________________\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _rBBBBBBBBBBrBBBBBBBBBBr_\n      _rBKKKKKKBBBrBBBBBBBBBBr_\n      _rBKKKKKKBBBrBBBBBBBBBBr_\n      _rBBBBBBBBBBrBBBBBBBBBBr_\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _________________________\n      _________________________\n      _________________________\n      _________________________\n    ''', '''\n      <style>\n        @page { size: 25px 11px; margin: 1px }\n        table { border-collapse: collapse; font: 2px weasyprint; width: 100% }\n        td { background: blue; padding: 1px; border: 1px solid red }\n      </style>\n      <table>\n        <tr><td>abc</td><td>abc</td></tr>\n        <tr><td>abc</td><td></td></tr>''')\n\n\n@assert_no_logs\ndef test_tables_22(assert_pixels):\n    assert_pixels('''\n      _________________________\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _rKKKKKKKKKKrKKKKKKKKKKr_\n      _rKKKKKKKKKKrKKKKKKKKKKr_\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _rKKKKKKBBBBrBBBBBBBBBBr_\n      _rKKKKKKBBBBrBBBBBBBBBBr_\n      _rBBBBBBBBBBrBBBBBBBBBBr_\n      _________________________\n      _________________________\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _rKKKKKKKKKKrKKKKKKKKKKr_\n      _rKKKKKKKKKKrKKKKKKKKKKr_\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _rKKKKKKBBBBrBBBBBBBBBBr_\n      _rKKKKKKBBBBrBBBBBBBBBBr_\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _________________________\n    ''', '''\n      <style>\n        @page { size: 25px 9px; margin: 1px }\n        table { border-collapse: collapse; font: 2px/1 weasyprint }\n        td { background: blue; border: 1px solid red }\n      </style>\n      <table>\n        <thead><tr><td>abcde</td><td>abcde</td></tr></thead>\n        <tbody><tr><td>abc abc</td><td></td></tr></tbody>''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_tables_23(assert_pixels):\n    assert_pixels('''\n      _________________________\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _rKKKKKKKKKKrKKKKKKKKKKr_\n      _rKKKKKKKKKKrKKKKKKKKKKr_\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _rKKKKKKBBBBrBBBBBBBBBBr_\n      _rKKKKKKBBBBrBBBBBBBBBBr_\n      _rBBBBBBBBBBrBBBBBBBBBBr_\n      _________________________\n      _________________________\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _rKKKKKKKKKKrKKKKKKKKKKr_\n      _rKKKKKKKKKKrKKKKKKKKKKr_\n      _rKKKKKKBBBBrBBBBBBBBBBr_\n      _rKKKKKKBBBBrBBBBBBBBBBr_\n      _rrrrrrrrrrrrrrrrrrrrrrr_\n      _________________________\n      _________________________\n    ''', '''\n      <style>\n        @page { size: 25px 9px; margin: 1px }\n        table { border-collapse: collapse; font: 2px/1 weasyprint }\n        td { background: blue; border: 1px solid red }\n        thead td { border-bottom: none }\n      </style>\n      <table>\n        <thead><tr><td>abcde</td><td>abcde</td></tr></thead>\n        <tbody><tr><td>abc abc</td><td></td></tr></tbody>''')\n\n\n@assert_no_logs\ndef test_tables_24(assert_pixels):\n    assert_pixels('''\n        __________________\n        _RKKKKgYYYYYYGGG__\n        _RKKKKgKKYYKKGGG__\n        _BBBBBBKKYYKKGGG__\n        _BBBBBBYYYYYYGGG__\n        _BBBBBBCCCCCCGGG__\n        _BBBBBB___________\n        _BBBBBB___________\n        __________________\n        __________________\n    ''', '''\n      <style>\n        @page { size: 18px 10px }\n        table {\n          border-collapse: collapse;\n          font: 2px/1 weasyprint;\n          margin: 1px;\n        }\n        tr {\n          border-left: 1px solid red;\n          border-right: 3px solid lime;\n        }\n        td.left {\n          background-color: magenta;\n          border-bottom: 5px solid blue;\n          border-right: 1px solid green;\n        }\n        td.right {\n          background-color: yellow;\n          border-bottom: 1px solid cyan;\n          border-left: 1px dotted orange;\n        }\n        td {\n          vertical-align: middle;\n        }\n      </style>\n      <table>\n        <tr>\n          <td class=\"left\">XX</td>\n          <td class=\"right\">X X</td>\n        </tr>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_tables_24_rtl(assert_pixels):\n    assert_pixels('''\n        __________________\n        _RKKKKgYYYYYYGGG__\n        _RKKKKgKKYYKKGGG__\n        _BBBBBBKKYYKKGGG__\n        _BBBBBBYYYYYYGGG__\n        _BBBBBBCCCCCCGGG__\n        _BBBBBB___________\n        _BBBBBB___________\n        __________________\n        __________________\n    ''', '''\n      <style>\n        @page { size: 18px 10px }\n        table {\n          border-collapse: collapse;\n          direction: rtl;\n          font: 2px/1 weasyprint;\n          margin: 1px;\n        }\n        tr {\n          border-left: 1px solid red;\n          border-right: 3px solid lime;\n        }\n        td.left {\n          background-color: magenta;\n          border-bottom: 5px solid blue;\n          border-right: 1px solid green;\n        }\n        td.right {\n          background-color: yellow;\n          border-bottom: 1px solid cyan;\n          border-left: 1px dotted orange;\n        }\n        td {\n          vertical-align: middle;\n        }\n      </style>\n      <table>\n        <tr>\n          <td class=\"right\">X X</td>\n          <td class=\"left\">XX</td>\n        </tr>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_running_elements_table_border_collapse(assert_pixels):\n    assert_pixels(2 * '''\n      KK_____________\n      KK_____________\n      _______________\n      _______________\n      _______________\n      KKKKKKK________\n      KRRKRRK________\n      KRRKRRK________\n      KKKKKKK________\n      KRRKRRK________\n      KRRKRRK________\n      KKKKKKK________\n      _______________\n      _______________\n      _______________\n    ''', '''\n      <style>\n        @page {\n          margin: 0 0 10px 0;\n          size: 15px;\n          @bottom-left { content: element(table) }\n        }\n        body { font: 2px/1 weasyprint }\n        table {\n          border: 1px solid black;\n          border-collapse: collapse;\n          color: red;\n          position: running(table);\n        }\n        td { border: 1px solid black }\n        div { page-break-after: always }\n      </style>\n      <table>\n        <tr> <td>A</td> <td>B</td> </tr>\n        <tr> <td>C</td> <td>D</td> </tr>\n      </table>\n      <div>1</div>\n      <div>2</div>\n    ''')\n\n\n@assert_no_logs\ndef test_running_elements_table_border_collapse_empty(assert_pixels):\n    assert_pixels(2 * '''\n      KK________\n      KK________\n      __________\n      __________\n      __________\n      __________\n      __________\n      __________\n      __________\n      __________\n    ''', '''\n      <style>\n        @page {\n          margin: 0 0 5px 0;\n          size: 10px;\n          @bottom-left { content: element(table) }\n        }\n        body { font: 2px/1 weasyprint }\n        table {\n          border: 1px solid black;\n          border-collapse: collapse;\n          color: red;\n          position: running(table);\n        }\n        td { border: 1px solid black }\n        div { page-break-after: always }\n      </style>\n      <table></table>\n      <div>1</div>\n      <div>2</div>\n    ''')\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_running_elements_table_border_collapse_border_style(assert_pixels):\n    assert_pixels(2 * '''\n      KK_____________\n      KK_____________\n      _______________\n      _______________\n      _______________\n      KKKZ___________\n      KRR_RR_________\n      KRR_RR_________\n      KKKK__Z________\n      KRRKRRK________\n      KRRKRRK________\n      KKKKKKK________\n      _______________\n      _______________\n      _______________\n    ''', '''\n      <style>\n        @page {\n          margin: 0 0 10px 0;\n          size: 15px;\n          @bottom-left { content: element(table) }\n        }\n        body { font: 2px/1 weasyprint }\n        table {\n          border: 1px solid black;\n          border-collapse: collapse;\n          color: red;\n          position: running(table);\n        }\n        td { border: 1px solid black }\n        div { page-break-after: always }\n      </style>\n      <table>\n        <tr> <td>A</td> <td style=\"border-style: hidden\">B</td> </tr>\n        <tr> <td>C</td> <td style=\"border-style: none\">D</td> </tr>\n      </table>\n      <div>1</div>\n      <div>2</div>\n    ''')\n\n\n@assert_no_logs\ndef test_running_elements_table_border_collapse_span(assert_pixels):\n    assert_pixels(2 * '''\n      KK_____________\n      KK_____________\n      _______________\n      _______________\n      _______________\n      KKKKKKKKKK_____\n      KRRKRRKRRK_____\n      KRRKRRKRRK_____\n      K__KKKKKKK_____\n      K__KRR___K_____\n      K__KRR___K_____\n      KKKKKKKKKK_____\n      _______________\n      _______________\n      _______________\n    ''', '''\n      <style>\n        @page {\n          margin: 0 0 10px 0;\n          size: 15px;\n          @bottom-left { content: element(table) }\n        }\n        body { font: 2px/1 weasyprint }\n        table {\n          border: 1px solid black;\n          border-collapse: collapse;\n          color: red;\n          position: running(table);\n        }\n        td { border: 1px solid black }\n        div { page-break-after: always }\n      </style>\n      <table>\n        <tr> <td rowspan=2>A</td> <td>B</td> <td>C</td> </tr>\n        <tr> <td colspan=2>D</td> </tr>\n      </table>\n      <div>1</div>\n      <div>2</div>\n    ''')\n\n\n@assert_no_logs\ndef test_running_elements_table_border_collapse_margin(assert_pixels):\n    assert_pixels(2 * '''\n      KK_____________\n      KK_____________\n      _______________\n      _______________\n      _______________\n      _______________\n      ____KKKKKKK____\n      ____KRRKRRK____\n      ____KRRKRRK____\n      ____KKKKKKK____\n      ____KRRKRRK____\n      ____KRRKRRK____\n      ____KKKKKKK____\n      _______________\n      _______________\n    ''', '''\n      <style>\n        @page {\n          margin: 0 0 10px 0;\n          size: 15px;\n          @bottom-center { content: element(table); width: 100% }\n        }\n        body { font: 2px/1 weasyprint }\n        table {\n          border: 1px solid black;\n          border-collapse: collapse;\n          color: red;\n          margin: 1px auto;\n          position: running(table);\n        }\n        td { border: 1px solid black }\n        div { page-break-after: always }\n      </style>\n      <table>\n        <tr> <td>A</td> <td>B</td> </tr>\n        <tr> <td>C</td> <td>D</td> </tr>\n      </table>\n      <div>1</div>\n      <div>2</div>\n    ''')\n\n\n@assert_no_logs\ndef test_tables_split_row(assert_pixels):\n    assert_pixels('''\n      KKKKKKKK\n      KRRKKRRK\n      KRRKKRRK\n      K__KK__K\n\n      K__KKRRK\n      K__KKRRK\n      KKKKKKKK\n      ________\n    ''', '''\n      <style>\n        @page { margin: 0; size: 8px 4px }\n        td { border: 1px solid black; color: red; vertical-align: top;\n             font-family: weasyprint; font-size: 2px; line-height: 1 }\n      </style>\n      <table>\n        <tr>\n          <td>a</td>\n          <td>a<br>a</td>\n        </tr>\n      </table>''')\n\n\n@assert_no_logs\ndef test_tables_column_background(assert_pixels):\n    # Regression test for #2296.\n    assert_pixels('''\n      KKKKKKKK\n      KRRBBRRK\n      KRRBBRRK\n      KKKKKKKK\n\n      KKKKKKKK\n      KRRBBRRK\n      KRRBBRRK\n      KKKKKKKK\n    ''', '''\n      <style>\n        @page { margin: 0; size: 8px 4px }\n        col { background: blue }\n        td { border: 1px solid black; color: red; vertical-align: top;\n             font-family: weasyprint; font-size: 2px; line-height: 1 }\n      </style>\n      <table>\n        <colgroup>\n          <col>\n        </colgroup>\n        <tr>\n          <td>a a</td>\n        </tr>\n        <tr>\n          <td>a a</td>\n        </tr>\n      </table>''')\n"
  },
  {
    "path": "tests/draw/test_text.py",
    "content": "\"\"\"Test how text is drawn.\"\"\"\n\nimport pytest\n\nfrom weasyprint.text.ffi import pango\n\nfrom ..testing_utils import SANS_FONTS\n\n\ndef test_text_overflow_clip(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n        _RR__RRR_\n        _RR__RRR_\n        _________\n    ''', '''\n      <style>\n        @page {\n          size: 9px 7px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n        }\n        div {\n          line-height: 1;\n          margin: 1px;\n          overflow: hidden;\n          width: 3.5em;\n        }\n      </style>\n      <div>abcde</div>\n      <div style=\"white-space: nowrap\">a bcde</div>''')\n\n\ndef test_text_overflow_ellipsis(assert_pixels):\n    assert_pixels('''\n        _________\n        _RRRRRR__\n        _RRRRRR__\n        _________\n        _RR__RR__\n        _RR__RR__\n        _________\n        _RRRRRR__\n        _RRRRRR__\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n        _RRRRRRR_\n        _RRRRRRR_\n        _________\n    ''', '''\n      <style>\n        @page {\n          size: 9px 16px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n        }\n        div {\n          line-height: 1;\n          margin: 1px;\n          overflow: hidden;\n          text-overflow: ellipsis;\n          width: 3.5em;\n        }\n        div div {\n          margin: 0;\n        }\n      </style>\n      <div>abcde</div>\n      <div style=\"white-space: nowrap\">a bcde</div>\n      <div><span>a<span>b</span>cd</span>e</div>\n      <div><div style=\"text-overflow: clip\">abcde</div></div>\n      <div><div style=\"overflow: visible\">abcde</div></div>\n''')\n\n\ndef test_text_align_rtl_trailing_whitespace(assert_pixels):\n    # Test text alignment for rtl text with trailing space.\n    # Regression test for #1111.\n    assert_pixels('''\n        _________\n        _rrrrBBB_\n        _________\n        _rrrrBBB_\n        _________\n        _BBBrrrr_\n        _________\n        _BBBrrrr_\n        _________\n    ''', '''\n      <style>\n        @page { size: 9px }\n        body { font-family: weasyprint; color: blue; font-size: 1px }\n        p { background: red; line-height: 1; width: 7em; margin: 1em }\n      </style>\n      <!-- &#8207 forces Unicode RTL direction for the following chars -->\n      <p style=\"direction: rtl\"> abc </p>\n      <p style=\"direction: rtl\"> &#8207;abc </p>\n      <p style=\"direction: ltr\"> abc </p>\n      <p style=\"direction: ltr\"> &#8207;abc </p>\n    ''')\n\n\ndef test_rtl_default_direction(assert_pixels):\n    assert_pixels('''\n        _____BBBBB_____\n        _____BBBBB_____\n        _____BBBBB_____\n        _____BBBBB_____\n        BBBBBBBBBB_____\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n        body { font-family: weasyprint; color: blue; font-size: 5px; line-height: 1 }\n      </style>\n      اب\n    ''')\n\n\ndef test_rtl_forced_direction(assert_pixels):\n    assert_pixels('''\n        __________BBBBB\n        __________BBBBB\n        __________BBBBB\n        __________BBBBB\n        _____BBBBBBBBBB\n    ''', '''\n      <style>\n        @page { size: 15px 5px }\n        body { font-family: weasyprint; color: blue; font-size: 5px; line-height: 1 }\n      </style>\n      <div style=\"direction: rtl\">اب</div>\n    ''')\n\n\ndef test_rtl_nested_inline(assert_pixels):\n    assert_pixels('''\n        RRRRR________________BBBBB___________RRRRR___________BBBBB\n        RRRRR________________BBBBB___________RRRRR___________BBBBB\n        RRRRR________________BBBBB___________RRRRR___________BBBBB\n        RRRRR________________BBBBB___________RRRRR___________BBBBB\n        RRRRRRRRRR______BBBBBBBBBB______RRRRRRRRRR______BBBBBBBBBB\n        ______________________________________BBBBB__________RRRRR\n        ______________________________________BBBBB__________RRRRR\n        ______________________________________BBBBB__________RRRRR\n        ______________________________________BBBBB__________RRRRR\n        _________________________________BBBBBBBBBB_____RRRRRRRRRR\n    ''', '''\n      <style>\n        @page { size: 58px 10px }\n        body { font-family: weasyprint; color: blue; font-size: 5px; line-height: 1 }\n        span { color: red }\n      </style>\n      <div style=\"direction: rtl; text-align: justify\">\n        اب <span>اب</span> اب <span>با اب</span> اب\n      </div>\n    ''')\n\n\ndef test_max_lines_ellipsis(assert_pixels):\n    assert_pixels('''\n        BBBBBBBB__\n        BBBBBBBB__\n        BBBBBBBBBB\n        BBBBBBBBBB\n        __________\n        __________\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page {size: 10px 10px;}\n        p {\n          block-ellipsis: auto;\n          color: blue;\n          font-family: weasyprint;\n          font-size: 2px;\n          max-lines: 2;\n        }\n      </style>\n      <p>\n        abcd efgh ijkl\n      </p>\n    ''')\n\n\n@pytest.mark.xfail\ndef test_max_lines_nested(assert_pixels):\n    assert_pixels('''\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        BBBBBBBBBB\n        rrrrrrrrrr\n        rrrrrrrrrr\n        rrrrrrrrrr\n        rrrrrrrrrr\n        BBBBBBBBBB\n        BBBBBBBBBB\n        __________\n        __________\n    ''', '''\n      <style>\n        @page {size: 10px 12px;}\n        div {\n          continue: discard;\n          font-family: weasyprint;\n          font-size: 2px;\n        }\n        #a {\n          color: blue;\n          max-lines: 5;\n        }\n        #b {\n          color: red\n          max-lines: 2;\n        }\n      </style>\n      <div id=a>\n        aaaaa\n        aaaaa\n        <div id=b>\n          bbbbb\n          bbbbb\n          bbbbb\n          bbbbb\n        </div>\n        aaaaa\n        aaaaa\n      </div>\n    ''')\n\n\ndef test_line_clamp(assert_pixels):\n    assert_pixels('''\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBBBBBBBB\n        BBBBBBBBBB\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page {size: 10px 10px;}\n        p {\n          color: blue;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-clamp: 3 \"(…)\";\n        }\n      </style>\n\n      <p>\n        aa a\n        bb b\n        cc c\n        dddd\n        eeee\n        ffff\n        gggg\n        hhhh\n      </p>\n    ''')\n\n\ndef test_line_clamp_none(assert_pixels):\n    assert_pixels('''\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page {size: 10px 10px;}\n        p {\n          color: blue;\n          font-family: weasyprint;\n          font-size: 2px;\n          max-lines: 1;\n          continue: discard;\n          block-ellipsis: \"…\";\n          line-clamp: none;\n        }\n      </style>\n\n      <p>\n        aa a\n        bb b\n        cc c\n      </p>\n    ''')\n\n\ndef test_line_clamp_number(assert_pixels):\n    assert_pixels('''\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BBBB\n        BBBB__BBBB\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page {size: 10px 10px;}\n        p {\n          color: blue;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-clamp: 3;\n        }\n      </style>\n\n      <p>\n        aa a\n        bb b\n        cc c\n        dddd\n        eeee\n      </p>\n    ''')\n\n\ndef test_line_clamp_nested(assert_pixels):\n    assert_pixels('''\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBBBBBBBB\n        BBBBBBBBBB\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page {size: 10px 10px;}\n        div {\n          color: blue;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-clamp: 3 \"(…)\";\n        }\n      </style>\n\n      <div>\n        aa a\n        <p>\n          bb b\n          cc c\n          dddd\n          eeee\n          ffff\n          gggg\n          hhhh\n        </p>\n      </div>\n    ''')\n\n\ndef test_line_clamp_nested_after(assert_pixels):\n    assert_pixels('''\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBB__BB__\n        BBBBBBBBBB\n        BBBBBBBBBB\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page {size: 10px 10px;}\n        div {\n          color: blue;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-clamp: 3 \"(…)\";\n        }\n      </style>\n\n      <div>\n        aa a\n        <p>\n          bb b\n        </p>\n        cc c\n        dddd\n        eeee\n        ffff\n        gggg\n        hhhh\n      </div>\n    ''')\n\n\n@pytest.mark.xfail\ndef test_ellipsis_nested(assert_pixels):\n    assert_pixels('''\n        BBBBBB____\n        BBBBBB____\n        BBBBBB____\n        BBBBBB____\n        BBBBBB____\n        BBBBBB____\n        BBBBBB____\n        BBBBBB____\n        BBBBBBBB__\n        BBBBBBBB__\n    ''', '''\n      <style>\n        @page {size: 10px 10px;}\n        div {\n          block-ellipsis: auto;\n          color: blue;\n          continue: discard;\n          font-family: weasyprint;\n          font-size: 2px;\n        }\n      </style>\n      <div>\n        <p>aaa</p>\n        <p>aaa</p>\n        <p>aaa</p>\n        <p>aaa</p>\n        <p>aaa</p>\n        <p>aaa</p>\n      </div>\n    ''')\n\n\ndef test_text_align_right(assert_pixels):\n    assert_pixels('''\n        _________\n        __RR__RR_\n        __RR__RR_\n        ______RR_\n        ______RR_\n        _________\n    ''', '''\n      <style>\n        @page {\n          size: 9px 6px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n        }\n        div {\n          line-height: 1;\n          margin: 1px;\n          text-align: right;\n        }\n      </style>\n      <div>a c e</div>''')\n\n\ndef test_text_align_justify(assert_pixels):\n    assert_pixels('''\n        _________\n        _RR___RR_\n        _RR___RR_\n        _RR______\n        _RR______\n        _________\n    ''', '''\n      <style>\n        @page {\n          size: 9px 6px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n        }\n        div {\n          line-height: 1;\n          margin: 1px;\n          text-align: justify;\n        }\n      </style>\n      <div>a c e</div>''')\n\n\ndef test_text_align_justify_nbsp(assert_pixels):\n    assert_pixels('''\n        ___________________\n        _RR___RR___RR___RR_\n        _RR___RR___RR___RR_\n        ___________________\n    ''', '''\n      <style>\n        @page {\n          size: 19px 4px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n        }\n        div {\n          line-height: 1;\n          margin: 1px;\n          text-align: justify-all;\n        }\n      </style>\n      <div>a b&nbsp;c&nbsp;d</div>''')\n\n\ndef test_text_word_spacing(assert_pixels):\n    assert_pixels('''\n        ___________________\n        _RR____RR____RR____\n        _RR____RR____RR____\n        ___________________\n    ''', '''\n      <style>\n        @page {\n          size: 19px 4px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n        }\n        div {\n          line-height: 1;\n          margin: 1px;\n          word-spacing: 1em;\n        }\n      </style>\n      <div>a c e</div>''')\n\n\ndef test_text_letter_spacing(assert_pixels):\n    assert_pixels('''\n        ___________________\n        _RR____RR____RR____\n        _RR____RR____RR____\n        ___________________\n    ''', '''\n      <style>\n        @page {\n          size: 19px 4px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n        }\n        div {\n          line-height: 1;\n          margin: 1px;\n          letter-spacing: 2em;\n        }\n      </style>\n      <div>ace</div>''')\n\n\ndef test_text_underline(assert_pixels):\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zsssssssssz_\n        _zsssssssssz_\n        _zuuuuuuuuuz_\n        _zzzzzzzzzzz_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 7px;\n          margin: 2px;\n        }\n        body {\n          color: rgba(255, 0, 0, 0.5);\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue auto;\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_text_underline_offset(assert_pixels):\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zRRRRRRRRRz_\n        _zRRRRRRRRRz_\n        _zzzzzzzzzzz_\n        _zzzzzzzzzzz_\n        _zBBBBBBBBBz_\n        _zzzzzzzzzzz_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 9px;\n          margin: 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue;\n          text-underline-offset: 2px;\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_text_underline_offset_percentage(assert_pixels):\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zRRRRRRRRRz_\n        _zRRRRRRRRRz_\n        _zzzzzzzzzzz_\n        _zzzzzzzzzzz_\n        _zBBBBBBBBBz_\n        _zzzzzzzzzzz_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 9px;\n          margin: 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue;\n          text-underline-offset: 70%;\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_text_underline_offset_calc(assert_pixels):\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zRRRRRRRRRz_\n        _zRRRRRRRRRz_\n        _zzzzzzzzzzz_\n        _zzzzzzzzzzz_\n        _zBBBBBBBBBz_\n        _zzzzzzzzzzz_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 9px;\n          margin: 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue;\n          text-underline-offset: calc(0.5em + 20%);\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_text_underline_thickness(assert_pixels):\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zRRRRRRRRRz_\n        _zRRRRRRRRRz_\n        _zzzzzzzzzzz_\n        _zzzzzzzzzzz_\n        _zBBBBBBBBBz_\n        _zBBBBBBBBBz_\n        _zzzzzzzzzzz_\n    ''', '''\n      <style>\n        @page {\n          size: 13px 9px;\n          margin: 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue 3px;\n          text-underline-offset: 2px;\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_text_underline_thickness_percentage(assert_pixels):\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zRRRRRRRRRz_\n        _zRRRRRRRRRz_\n        _zzzzzzzzzzz_\n        _zzzzzzzzzzz_\n        _zBBBBBBBBBz_\n        _zBBBBBBBBBz_\n        _zzzzzzzzzzz_\n    ''', '''\n      <style>\n        @page {\n          size: 13px 9px;\n          margin: 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue 100%;\n          text-underline-offset: 2px;\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_text_underline_thickness_calc(assert_pixels):\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zRRRRRRRRRz_\n        _zRRRRRRRRRz_\n        _zzzzzzzzzzz_\n        _zzzzzzzzzzz_\n        _zBBBBBBBBBz_\n        _zBBBBBBBBBz_\n        _zzzzzzzzzzz_\n    ''', '''\n      <style>\n        @page {\n          size: 13px 9px;\n          margin: 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue calc(0.5em + 50%);\n          text-underline-offset: 2px;\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_text_overline(assert_pixels):\n    # Ascent value seems to be a bit random, don’t try to get the exact\n    # position of the line\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zzzzzzzzzzz_\n        _zsssssssssz_\n        _zsssssssssz_\n        _zzzzzzzzzzz_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 7px;\n          margin: 2px;\n        }\n        body {\n          color: rgba(255, 0, 0, 0.5);\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: overline blue;\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_text_line_through(assert_pixels):\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zBBBBBBBBBz_\n        _zuuuuuuuuuz_\n        _zBBBBBBBBBz_\n        _zzzzzzzzzzz_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 7px;\n          margin: 2px;\n        }\n        body {\n          color: blue;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: line-through rgba(255, 0, 0, 0.5);\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_text_multiple_text_decoration(assert_pixels):\n    # Regression test for #1621.\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zsssssssssz_\n        _zBBBBBBBBBz_\n        _zuuuuuuuuuz_\n        _zzzzzzzzzzz_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 7px;\n          margin: 2px;\n        }\n        body {\n          color: rgba(255, 0, 0, 0.5);\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline line-through blue;\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_text_nested_text_decoration(assert_pixels):\n    # Regression test for #1621.\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zsssssssssz_\n        _zsssBBBsssz_\n        _zuuuuuuuuuz_\n        _zzzzzzzzzzz_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 7px;\n          margin: 2px;\n        }\n        body {\n          color: rgba(255, 0, 0, 0.5);\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue;\n        }\n        span {\n          text-decoration: line-through blue;\n        }\n      </style>\n      <div>a<span>b</span>c</div>''')\n\n\n@pytest.mark.xfail\ndef test_text_nested_text_decoration_color(assert_pixels):\n    # See weasyprint.css.text_decoration’s TODO\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zRRRRRRRRRz_\n        _zRRRGGGRRRz_\n        _zBBBBBBBBBz_\n        _zzzzzzzzzzz_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 7px;\n          margin: 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue;\n        }\n        span {\n          text-decoration: line-through lime;\n        }\n      </style>\n      <div>a<span>b</span>c</div>''')\n\n\n@pytest.mark.xfail\ndef test_text_nested_block_text_decoration(assert_pixels):\n    # See weasyprint.css.text_decoration’s TODO\n    assert_pixels('''\n        _______\n        _zzzzz_\n        _zRRRz_\n        _zRRRz_\n        _zBBBz_\n        _zRRRz_\n        _zGGGz_\n        _zBBBz_\n        _zRRRz_\n        _zRRRz_\n        _zBBBz_\n        _zzzzz_\n        _______\n    ''', '''\n      <style>\n        @page {\n          size: 7px 13px;\n          margin: 2px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue;\n        }\n        article {\n          text-decoration: line-through lime;\n        }\n      </style>\n      <div>a<article>b</article>c</div>''')\n\n\n@pytest.mark.xfail\ndef test_text_float_text_decoration(assert_pixels):\n    # See weasyprint.css.text_decoration’s TODO\n    assert_pixels('''\n        _____________\n        _zzzzz_______\n        _zRRRz__RRR__\n        _zRRRz__RRR__\n        _zBBBz__RRR__\n        _zzzzz_______\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 7px;\n          margin: 2px;\n        }\n        div {\n          color: red;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration: underline blue;\n        }\n        span {\n          float: right;\n        }\n      </style>\n      <div>a<span>b</span></div>''')\n\n\ndef test_text_decoration_var(assert_pixels):\n    # Regression test for #1697.\n    assert_pixels('''\n        _____________\n        _zzzzzzzzzzz_\n        _zRRRRRRRRRz_\n        _zBBBBBBBBBz_\n        _zRRRRRRRRRz_\n        _zzzzzzzzzzz_\n        _____________\n    ''', '''\n      <style>\n        @page {\n          size: 13px 7px;\n          margin: 2px;\n        }\n        body {\n          --blue: blue;\n          color: red;\n          font-family: weasyprint;\n          font-size: 3px;\n          text-decoration-color: var(--blue);\n          text-decoration-line: line-through;\n        }\n      </style>\n      <div>abc</div>''')\n\n\ndef test_zero_width_character(assert_pixels):\n    # Regression test for #1508.\n    assert_pixels('''\n        ______\n        _RRRR_\n        _RRRR_\n        ______\n    ''', '''\n      <style>\n        @page {\n          size: 6px 4px;\n          margin: 1px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n      </style>\n      <div>a&zwnj;b</div>''')\n\n\ndef test_font_size_very_small(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 4px;\n          margin: 1px;\n        }\n        body {\n          font-family: weasyprint;\n          font-size: 0.00000001px;\n        }\n      </style>\n      test font size zero\n    ''')\n\n\ndef test_missing_glyph_fallback(assert_pixels):\n    # The apostrophe is not included in weasyprint.otf\n    assert_pixels('''\n        ___zzzzzzzzzzzzzzzzz\n        _RRzzzzzzzzzzzzzzzzz\n        _RRzzzzzzzzzzzzzzzzz\n        ___zzzzzzzzzzzzzzzzz\n    ''', '''\n      <style>\n        @page {\n          size: 20px 4px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint, %s;\n          font-size: 2px;\n          line-height: 0;\n          margin: 2px 1px;\n        }\n      </style>a\\'''' % SANS_FONTS)\n\n\ndef test_tabulation_character(assert_pixels):\n    # Regression test for #1515.\n    assert_pixels('''\n        __________\n        _RR____RR_\n        _RR____RR_\n        __________\n    ''', '''\n      <style>\n        @page {\n          size: 10px 4px;\n          margin: 1px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n          tab-size: 3;\n        }\n      </style>\n      <pre>a&Tab;b</pre>''')\n\n\ndef test_otb_font(assert_pixels):\n    assert_pixels('''\n        ____________________\n        __RR______RR________\n        __RR__RR__RR________\n        __RR__RR__RR________\n        ____________________\n        ____________________\n    ''', '''\n      <style>\n        @page {\n          size: 20px 6px;\n          margin: 1px;\n        }\n        @font-face {\n          src: url(weasyprint.otb);\n          font-family: weasyprint-otb;\n        }\n        body {\n          color: red;\n          font-family: weasyprint-otb;\n          font-size: 4px;\n          line-height: 0.8;\n        }\n      </style>\n      AaA''')\n\n\ndef test_huge_justification(assert_pixels):\n    # Regression test for #2262.\n    assert_pixels('''\n        ____\n        _RR_\n        _RR_\n        ____\n    ''', '''\n      <style>\n        @page {\n          size: 4px 4px;\n          margin: 1px;\n        }\n        body {\n          color: red;\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n          text-align: justify-all;\n          width: 100000px;\n        }\n      </style>\n      A B''')\n\n\ndef test_font_variant_caps_small(assert_pixels):\n    assert_pixels('''\n        ________\n        _BB_BB__\n        _BB_B_B_\n        _B__BB__\n        _B__B___\n        ________\n    ''', '''\n      <style>\n        @page {size: 8px 6px}\n        p {\n          color: blue;\n          font-variant-caps: small-caps;\n          font-family: %s;\n          font-size: 6px;\n          line-height: 1;\n        }\n      </style>\n      <p>Pp</p>\n    ''' % SANS_FONTS)\n\n\ndef test_font_variant_caps_all_small(assert_pixels):\n    assert_pixels('''\n        ________\n        BB_BB___\n        B_BB_B__\n        BB_BB___\n        B__B____\n        ________\n    ''', '''\n      <style>\n        @page {size: 8px 6px}\n        p {\n          color: blue;\n          font-variant-caps: all-small-caps;\n          font-family: %s;\n          font-size: 6px;\n          line-height: 1;\n        }\n      </style>\n      <p>Pp</p>\n    ''' % SANS_FONTS)\n\n\ndef test_font_variant_caps_petite(assert_pixels):\n    assert_pixels('''\n        ________\n        _BB_BB__\n        _BB_B_B_\n        _B__BB__\n        _B__B___\n        ________\n    ''', '''\n      <style>\n        @page {size: 8px 6px}\n        p {\n          color: blue;\n          font-variant-caps: petite-caps;\n          font-family: %s;\n          font-size: 6px;\n          line-height: 1;\n        }\n      </style>\n      <p>Pp</p>\n    ''' % SANS_FONTS)\n\n\n# Bug in Pango: https://gitlab.gnome.org/GNOME/pango/-/merge_requests/875\n@pytest.mark.xfail(pango.pango_version() == 15604, reason='Bug in Pango 1.56.4')\ndef test_font_variant_caps_all_petite(assert_pixels):\n    assert_pixels('''\n        ________\n        BB_BB___\n        B_BB_B__\n        BB_BB___\n        B__B____\n        ________\n    ''', '''\n      <style>\n        @page {size: 8px 6px}\n        p {\n          color: blue;\n          font-variant-caps: all-petite-caps;\n          font-family: %s;\n          font-size: 6px;\n          line-height: 1;\n        }\n      </style>\n      <p>Pp</p>\n    ''' % SANS_FONTS)\n\n\ndef test_font_variant_caps_unicase(assert_pixels):\n    assert_pixels('''\n        ________\n        BB______\n        B_B_BB__\n        BB__B_B_\n        B___BB__\n        ____B___\n    ''', '''\n      <style>\n        @page {size: 8px 6px}\n        p {\n          color: blue;\n          font-variant-caps: unicase;\n          font-family: %s;\n          font-size: 6px;\n          line-height: 1;\n        }\n      </style>\n      <p>Pp</p>\n    ''' % SANS_FONTS)\n\n\ndef test_font_variant_caps_titling(assert_pixels):\n    assert_pixels('''\n        _BB_____\n        _BB_____\n        _BB__BB_\n        _B___B_B\n        _____BB_\n        _____B__\n    ''', '''\n      <style>\n        @page {size: 8px 6px}\n        p {\n          color: blue;\n          font-family: %s;\n          font-size: 6px;\n          line-height: 1;\n        }\n      </style>\n      <p>Pp</p>\n    ''' % SANS_FONTS)\n\n\ndef test_unicode_range(assert_pixels):\n    assert_pixels('''\n        __________\n        _RRRRRR___\n        _RRRRRRzz_\n        __________\n    ''', '''\n      <style>\n        @font-face {\n          font-family: uni;\n          src: url(weasyprint.otf);\n          unicode-range: u+41, u+043-045, u+005?;\n        }\n        @page {\n          size: 10px 4px;\n        }\n        body {\n          color: red;\n          font-family: uni;\n          font-size: 2px;\n          line-height: 0;\n          margin: 2px 1px;\n        }\n      </style>ADZB''')\n"
  },
  {
    "path": "tests/draw/test_transform.py",
    "content": "\"\"\"Test transformations.\"\"\"\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_2d_transform_1(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        __BBBr____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px; margin: 2px }\n        body { font-size: 0 }\n        img { transform: rotate(90deg) }\n      </style>\n      <body><img src=\"pattern.png\">''')\n\n\n@assert_no_logs\ndef test_2d_transform_2(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        _____BBBr_\n        _____BBBB_\n        _____BBBB_\n        _____BBBB_\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px; margin: 2px }\n        body { font-size: 0 }\n        img { transform: translateX(3px) rotate(90deg) }\n      </style>\n      <body><img src=\"pattern.png\">''')\n\n\n@assert_no_logs\ndef test_2d_transform_3(assert_pixels):\n    # A translateX after the rotation is actually a translateY\n    assert_pixels('''\n        __________\n        __________\n        __________\n        __________\n        __________\n        __BBBr____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px; margin: 2px }\n        body { font-size: 0 }\n        img { transform: rotate(90deg) translateX(3px) }\n      </style>\n      <body><img src=\"pattern.png\">''')\n\n\n@assert_no_logs\ndef test_2d_transform_4(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        __________\n        __________\n        __________\n        __BBBr____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px; margin: 2px }\n        div { transform: rotate(90deg); font-size: 0; width: 4px }\n        img { transform: translateX(3px) }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_2d_transform_5(assert_pixels):\n    assert_pixels('''\n        ________\n        ________\n        __BBBr__\n        __BBBB__\n        __BBBB__\n        __BBBB__\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px; margin: 2px; }\n        div { transform: matrix(-1, 0, 0, 1, 0, 0); font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_2d_transform_6(assert_pixels):\n    assert_pixels('''\n        ________\n        ________\n        ________\n        ________\n        ___rBBB_\n        ___BBBB_\n        ___BBBB_\n        ___BBBB_\n    ''', '''\n      <style>\n        @page { size: 8px; margin: 2px; }\n        div { transform: translate(1px, 2px); font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_2d_transform_7(assert_pixels):\n    assert_pixels('''\n        ________\n        ________\n        ___rBBB_\n        ___BBBB_\n        ___BBBB_\n        ___BBBB_\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px; margin: 2px; }\n        div { transform: translate(25%, 0); font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_2d_transform_8(assert_pixels):\n    assert_pixels('''\n        ________\n        ________\n        _____rBB\n        _____BBB\n        _____BBB\n        _____BBB\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px; margin: 2px; }\n        div { transform: translateX(0.25em); font-size: 12px }\n        div div { font-size: 0 }\n      </style>\n      <div><div><img src=\"pattern.png\"></div></div>''')\n\n\n@assert_no_logs\ndef test_2d_transform_9(assert_pixels):\n    assert_pixels('''\n        ________\n        __rBBB__\n        __BBBB__\n        __BBBB__\n        __BBBB__\n        ________\n        ________\n        ________\n    ''', '''\n      <style>\n        @page { size: 8px; margin: 2px; }\n        div { transform: translateY(-1px); font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_2d_transform_10(assert_pixels):\n    assert_pixels('''\n        __________\n        _rrBBBBBB_\n        _rrBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px; margin: 2px; }\n        div { transform: scale(2, 2);\n              transform-origin: 1px 1px 1px;\n              image-rendering: pixelated;\n              font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_2d_transform_11(assert_pixels):\n    assert_pixels('''\n        __________\n        __rBBB____\n        __rBBB____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px; margin: 2px; }\n        div { transform: scale(1, 2);\n              transform-origin: 1px 1px;\n              image-rendering: pixelated;\n              font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_2d_transform_12(assert_pixels):\n    assert_pixels('''\n        __________\n        __rBBB____\n        __rBBB____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __BBBB____\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px; margin: 2px; }\n        div { transform: scaleY(2);\n              transform-origin: 1px 1px 0;\n              image-rendering: pixelated;\n              font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_2d_transform_13(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        _rrBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        _BBBBBBBB_\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px; margin: 2px; }\n        div { transform: scaleX(2);\n              transform-origin: 1px 1px;\n              image-rendering: pixelated;\n              font-size: 0 }\n      </style>\n      <div><img src=\"pattern.png\"></div>''')\n\n\n@assert_no_logs\ndef test_2d_transform_opacity(assert_pixels):\n    assert_pixels('''\n        __________\n        __________\n        __________\n        _ss_______\n        _ss_______\n        __________\n        __________\n        __________\n        __________\n        __________\n    ''', '''\n      <style>\n        @page { size: 10px; margin: 2px; }\n        div { transform: translate(-1px, 1px); background: red;\n              height: 2px; width: 2px; opacity: 0.5 }\n      </style>\n      <div></div>''')\n"
  },
  {
    "path": "tests/draw/test_visibility.py",
    "content": "\"\"\"Test visibility.\"\"\"\n\nfrom ..testing_utils import assert_no_logs\n\nvisibility_source = '''\n  <style>\n    @page { size: 12px 7px }\n    body { font-size: 0; line-height: 0 }\n    img { margin: 1px 0 0 1px }\n    %s\n  </style>\n  <div>\n    <img src=\"pattern.png\">\n    <span><img src=\"pattern.png\"></span>\n  </div>'''\n\n\n@assert_no_logs\ndef test_visibility_1(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rBBB_rBBB__\n        _BBBB_BBBB__\n        _BBBB_BBBB__\n        _BBBB_BBBB__\n        ____________\n        ____________\n    ''', visibility_source % '')\n\n\n@assert_no_logs\ndef test_visibility_2(assert_pixels):\n    assert_pixels('''\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n        ____________\n    ''', visibility_source % 'div { visibility: hidden }')\n\n\n@assert_no_logs\ndef test_visibility_3(assert_pixels):\n    assert_pixels('''\n        ____________\n        ______rBBB__\n        ______BBBB__\n        ______BBBB__\n        ______BBBB__\n        ____________\n        ____________\n    ''', visibility_source % 'div { visibility: hidden } '\n                             'span { visibility: visible }')\n\n\n@assert_no_logs\ndef test_visibility_4(assert_pixels):\n    assert_pixels('''\n        ____________\n        _rBBB_rBBB__\n        _BBBB_BBBB__\n        _BBBB_BBBB__\n        _BBBB_BBBB__\n        ____________\n        ____________\n    ''', visibility_source % '@page { visibility: hidden; background: red }')\n"
  },
  {
    "path": "tests/draw/test_whitespace.py",
    "content": "\"\"\"Test how white spaces collapse.\"\"\"\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\ndef test_whitespace_inline(assert_pixels):\n    assert_pixels('''\n        RRRR__RRRR____\n        RRRR__RRRR____\n        ______________\n        ______________\n    ''', '''\n        <style>\n            @page {size: 14px 4px}\n            body {\n              color: red;\n              font-family: weasyprint;\n              font-size: 2px;\n              line-height: 1;\n            }\n        </style>\n        <span>aa </span><span> aa</span>\n    ''')\n\n\n@assert_no_logs\ndef test_whitespace_nested_inline(assert_pixels):\n    assert_pixels('''\n        RRRR__RRRR____\n        RRRR__RRRR____\n        ______________\n        ______________\n    ''', '''\n        <style>\n            @page {size: 14px 4px}\n            body {\n              color: red;\n              font-family: weasyprint;\n              font-size: 2px;\n              line-height: 1;\n            }\n        </style>\n        <span><span>aa </span></span><span><span> aa</span></span>\n    ''')\n\n\n@assert_no_logs\ndef test_whitespace_inline_space_between(assert_pixels):\n    assert_pixels('''\n        RRRR__RRRR____\n        RRRR__RRRR____\n        ______________\n        ______________\n    ''', '''\n        <style>\n            @page {size: 14px 4px}\n            body {\n              color: red;\n              font-family: weasyprint;\n              font-size: 2px;\n              line-height: 1;\n            }\n        </style>\n        <span>aa </span> <span> aa</span>\n    ''')\n\n\n@assert_no_logs\ndef test_whitespace_float_between(assert_pixels):\n    assert_pixels('''\n        RRRR__RRRR__BB\n        RRRR__RRRR__BB\n        ______________\n        ______________\n    ''', '''\n        <style>\n            @page {size: 14px 4px}\n            body {\n              color: red;\n              font-family: weasyprint;\n              font-size: 2px;\n              line-height: 1;\n            }\n            div {float: right; color: blue}\n        </style>\n        <span>aa </span><div>a</div><span> aa</span>\n    ''')\n\n\n@assert_no_logs\ndef test_whitespace_in_float(assert_pixels):\n    assert_pixels('''\n        RRRRRRRR____BB\n        RRRRRRRR____BB\n        ______________\n        ______________\n    ''', '''\n        <style>\n            @page {size: 14px 4px}\n            body {\n              color: red;\n              font-family: weasyprint;\n              font-size: 2px;\n              line-height: 1;\n            }\n            div {\n              color: blue;\n              float: right;\n            }\n        </style>\n        <span>aa</span><div> a </div><span>aa</span>\n    ''')\n\n\n@assert_no_logs\ndef test_whitespace_absolute_between(assert_pixels):\n    assert_pixels('''\n        RRRR__RRRR__BB\n        RRRR__RRRR__BB\n        ______________\n        ______________\n    ''', '''\n        <style>\n            @page {size: 14px 4px}\n            body {\n              color: red;\n              font-family: weasyprint;\n              font-size: 2px;\n              line-height: 1;\n            }\n            div {\n              color: blue;\n              position: absolute;\n              right: 0;\n              top: 0;\n            }\n        </style>\n        <span>aa </span><div>a</div><span> aa</span>\n    ''')\n\n\n@assert_no_logs\ndef test_whitespace_in_absolute(assert_pixels):\n    assert_pixels('''\n        RRRRRRRR____BB\n        RRRRRRRR____BB\n        ______________\n        ______________\n    ''', '''\n        <style>\n            @page {size: 14px 4px}\n            body {\n              color: red;\n              font-family: weasyprint;\n              font-size: 2px;\n              line-height: 1;\n            }\n            div {\n              color: blue;\n              position: absolute;\n              right: 0;\n              top: 0;\n            }\n        </style>\n        <span>aa</span><div> a </div><span>aa</span>\n    ''')\n\n\n@assert_no_logs\ndef test_whitespace_running_between(assert_pixels):\n    assert_pixels('''\n        RRRR__RRRR____\n        RRRR__RRRR____\n        ______BB______\n        ______BB______\n    ''', '''\n        <style>\n            @page {\n              size: 14px 4px;\n              margin: 0 0 2px;\n              @bottom-center {\n                content: element(test);\n              }\n            }\n            body {\n              color: red;\n              font-family: weasyprint;\n              font-size: 2px;\n              line-height: 1;\n            }\n            div {\n              background: green;\n              color: blue;\n              position: running(test);\n            }\n        </style>\n        <span>aa </span><div>a</div><span> aa</span>\n    ''')\n\n\n@assert_no_logs\ndef test_whitespace_in_running(assert_pixels):\n    assert_pixels('''\n        RRRRRRRR______\n        RRRRRRRR______\n        ______BB______\n        ______BB______\n    ''', '''\n        <style>\n            @page {\n              size: 14px 4px;\n              margin: 0 0 2px;\n              @bottom-center {\n                content: element(test);\n              }\n            }\n            body {\n              color: red;\n              font-family: weasyprint;\n              font-size: 2px;\n              line-height: 1;\n            }\n            div {\n              background: green;\n              color: blue;\n              position: running(test);\n            }\n        </style>\n        <span>aa</span><div> a </div><span>aa</span>\n    ''')\n"
  },
  {
    "path": "tests/layout/__init__.py",
    "content": "\"\"\"Tests for layout.\n\nIncludes positioning and dimensioning of boxes, line breaks, page breaks.\n\n\"\"\"\n"
  },
  {
    "path": "tests/layout/test_block.py",
    "content": "\"\"\"Tests for blocks layout.\"\"\"\n\nimport pytest\n\nfrom weasyprint.formatting_structure import boxes\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\ndef test_block_widths():\n    page, = render_pages('''\n      <style>\n        @page { margin: 0; size: 120px 2000px }\n        body { margin: 0 }\n        div { margin: 10px }\n        p { padding: 2px; border-width: 1px; border-style: solid }\n      </style>\n      <div>\n        <p></p>\n        <p style=\"width: 50px\"></p>\n      </div>\n      <div style=\"direction: rtl\">\n        <p style=\"width: 50px; direction: rtl\"></p>\n      </div>\n      <div>\n        <p style=\"margin: 0 10px 0 20px\"></p>\n        <p style=\"width: 50px; margin-left: 20px; margin-right: auto\"></p>\n        <p style=\"width: 50px; margin-left: auto; margin-right: 20px\"></p>\n        <p style=\"width: 50px; margin: auto\"></p>\n\n        <p style=\"margin-left: 20px; margin-right: auto\"></p>\n        <p style=\"margin-left: auto; margin-right: 20px\"></p>\n        <p style=\"margin: auto\"></p>\n\n        <p style=\"width: 200px; margin: auto\"></p>\n\n        <p style=\"min-width: 200px; margin: auto\"></p>\n        <p style=\"max-width: 50px; margin: auto\"></p>\n        <p style=\"min-width: 50px; margin: auto\"></p>\n\n        <p style=\"width: 70%\"></p>\n      </div>\n    ''')\n    html, = page.children\n    assert html.element_tag == 'html'\n    body, = html.children\n    assert body.element_tag == 'body'\n    assert body.width == 120\n\n    divs = body.children\n\n    paragraphs = []\n    for div in divs:\n        assert isinstance(div, boxes.BlockBox)\n        assert div.element_tag == 'div'\n        assert div.width == 100\n        for paragraph in div.children:\n            assert isinstance(paragraph, boxes.BlockBox)\n            assert paragraph.element_tag == 'p'\n            assert paragraph.padding_left == 2\n            assert paragraph.padding_right == 2\n            assert paragraph.border_left_width == 1\n            assert paragraph.border_right_width == 1\n            paragraphs.append(paragraph)\n\n    assert len(paragraphs) == 15\n\n    # width is 'auto'\n    assert paragraphs[0].width == 94\n    assert paragraphs[0].margin_left == 0\n    assert paragraphs[0].margin_right == 0\n\n    # No 'auto', over-constrained equation with ltr, the initial\n    # 'margin-right: 0' was ignored.\n    assert paragraphs[1].width == 50\n    assert paragraphs[1].margin_left == 0\n\n    # No 'auto', over-constrained equation with rtl, the initial\n    # 'margin-left: 0' was ignored.\n    assert paragraphs[2].width == 50\n    assert paragraphs[2].margin_right == 0\n\n    # width is 'auto'\n    assert paragraphs[3].width == 64\n    assert paragraphs[3].margin_left == 20\n\n    # margin-right is 'auto'\n    assert paragraphs[4].width == 50\n    assert paragraphs[4].margin_left == 20\n\n    # margin-left is 'auto'\n    assert paragraphs[5].width == 50\n    assert paragraphs[5].margin_left == 24\n\n    # Both margins are 'auto', remaining space is split in half\n    assert paragraphs[6].width == 50\n    assert paragraphs[6].margin_left == 22\n\n    # width is 'auto', other 'auto' are set to 0\n    assert paragraphs[7].width == 74\n    assert paragraphs[7].margin_left == 20\n\n    # width is 'auto', other 'auto' are set to 0\n    assert paragraphs[8].width == 74\n    assert paragraphs[8].margin_left == 0\n\n    # width is 'auto', other 'auto' are set to 0\n    assert paragraphs[9].width == 94\n    assert paragraphs[9].margin_left == 0\n\n    # sum of non-auto initially is too wide, set auto values to 0\n    assert paragraphs[10].width == 200\n    assert paragraphs[10].margin_left == 0\n\n    # Constrained by min-width, same as above\n    assert paragraphs[11].width == 200\n    assert paragraphs[11].margin_left == 0\n\n    # Constrained by max-width, same as paragraphs[6]\n    assert paragraphs[12].width == 50\n    assert paragraphs[12].margin_left == 22\n\n    # NOT constrained by min-width\n    assert paragraphs[13].width == 94\n    assert paragraphs[13].margin_left == 0\n\n    # 70%\n    assert paragraphs[14].width == 70\n    assert paragraphs[14].margin_left == 0\n\n\n@assert_no_logs\ndef test_block_heights_p():\n    page, = render_pages('''\n      <style>\n        @page { margin: 0; size: 100px 20000px }\n        html, body { margin: 0 }\n        div { margin: 4px; border: 2px solid; padding: 4px }\n        /* Use top margins so that margin collapsing doesn't change result */\n        p { margin: 16px 0 0; border: 4px solid; padding: 8px; height: 50px }\n      </style>\n      <div>\n        <p></p>\n        <!-- Not in normal flow: don't contribute to the parent’s height -->\n        <p style=\"position: absolute\"></p>\n        <p style=\"float: left\"></p>\n      </div>\n      <div> <p></p> <p></p> <p></p> </div>\n      <div style=\"height: 20px\"> <p></p> </div>\n      <div style=\"height: 120px\"> <p></p> </div>\n      <div style=\"max-height: 20px\"> <p></p> </div>\n      <div style=\"min-height: 120px\"> <p></p> </div>\n      <div style=\"min-height: 20px\"> <p></p> </div>\n      <div style=\"max-height: 120px\"> <p></p> </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    heights = [div.height for div in body.children]\n    assert heights == [90, 90 * 3, 20, 120, 20, 120, 90, 90]\n\n\n@assert_no_logs\ndef test_block_heights_img():\n    page, = render_pages('''\n      <style>\n        body { height: 200px; font-size: 0 }\n      </style>\n      <div>\n        <img src=pattern.png style=\"height: 40px\">\n      </div>\n      <div style=\"height: 10%\">\n        <img src=pattern.png style=\"height: 40px\">\n      </div>\n      <div style=\"max-height: 20px\">\n        <img src=pattern.png style=\"height: 40px\">\n      </div>\n      <div style=\"max-height: 10%\">\n        <img src=pattern.png style=\"height: 40px\">\n      </div>\n      <div style=\"min-height: 20px\"></div>\n      <div style=\"min-height: 10%\"></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    heights = [div.height for div in body.children]\n    assert heights == [40, 20, 20, 20, 20, 20]\n\n\n@assert_no_logs\ndef test_block_heights_img_no_body_height():\n    # Same but with no height on body: percentage *-height is ignored\n    page, = render_pages('''\n      <style>\n        body { font-size: 0 }\n      </style>\n        <div>\n          <img src=pattern.png style=\"height: 40px\">\n        </div>\n        <div style=\"height: 10%\">\n          <img src=pattern.png style=\"height: 40px\">\n        </div>\n        <div style=\"max-height: 20px\">\n          <img src=pattern.png style=\"height: 40px\">\n        </div>\n        <div style=\"max-height: 10%\">\n          <img src=pattern.png style=\"height: 40px\">\n        </div>\n        <div style=\"min-height: 20px\"></div>\n        <div style=\"min-height: 10%\"></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    heights = [div.height for div in body.children]\n    assert heights == [40, 40, 20, 40, 20, 0]\n\n\n@assert_no_logs\ndef test_block_percentage_heights_no_html_height():\n    page, = render_pages('''\n      <style>\n        html, body { margin: 0 }\n        body { height: 50% }\n      </style>\n    ''')\n    html, = page.children\n    assert html.element_tag == 'html'\n    body, = html.children\n    assert body.element_tag == 'body'\n\n    # Since html’s height depend on body’s, body’s 50% means 'auto'\n    assert body.height == 0\n\n\n@assert_no_logs\ndef test_block_percentage_heights():\n    page, = render_pages('''\n      <style>\n        html, body { margin: 0 }\n        html { height: 300px }\n        body { height: 50% }\n      </style>\n    ''')\n    html, = page.children\n    assert html.element_tag == 'html'\n    body, = html.children\n    assert body.element_tag == 'body'\n\n    # This time the percentage makes sense\n    assert body.height == 150\n\n\n@assert_no_logs\n@pytest.mark.parametrize('size', [\n    ('width: 10%; height: 1000px',),\n    ('max-width: 10%; max-height: 1000px; height: 2000px',),\n    ('width: 5%; min-width: 10%; min-height: 1000px',),\n    ('width: 10%; height: 1000px; min-width: auto; max-height: none',),\n])\ndef test_box_sizing(size):\n    # https://www.w3.org/TR/css-ui-3/#box-sizing\n    page, = render_pages('''\n      <style>\n        @page { size: 100000px }\n        body { width: 10000px; margin: 0 }\n        div { %s; margin: 100px; padding: 10px; border: 1px solid }\n      </style>\n      <div></div>\n\n      <div style=\"box-sizing: content-box\"></div>\n      <div style=\"box-sizing: padding-box\"></div>\n      <div style=\"box-sizing: border-box\"></div>\n    ''' % size)\n    html, = page.children\n    body, = html.children\n    div_1, div_2, div_3, div_4 = body.children\n    for div in div_1, div_2:\n        assert div.style['box_sizing'] == 'content-box'\n        assert div.width == 1000\n        assert div.height == 1000\n        assert div.padding_width() == 1020\n        assert div.padding_height() == 1020\n        assert div.border_width() == 1022\n        assert div.border_height() == 1022\n        assert div.margin_height() == 1222\n        # margin_width() is the width of the containing block\n\n    # padding-box\n    assert div_3.style['box_sizing'] == 'padding-box'\n    assert div_3.width == 980  # 1000 - 20\n    assert div_3.height == 980\n    assert div_3.padding_width() == 1000\n    assert div_3.padding_height() == 1000\n    assert div_3.border_width() == 1002\n    assert div_3.border_height() == 1002\n    assert div_3.margin_height() == 1202\n\n    # border-box\n    assert div_4.style['box_sizing'] == 'border-box'\n    assert div_4.width == 978  # 1000 - 20 - 2\n    assert div_4.height == 978\n    assert div_4.padding_width() == 998\n    assert div_4.padding_height() == 998\n    assert div_4.border_width() == 1000\n    assert div_4.border_height() == 1000\n    assert div_4.margin_height() == 1200\n\n\n@assert_no_logs\n@pytest.mark.parametrize('size', [\n    ('width: 0; height: 0'),\n    ('max-width: 0; max-height: 0'),\n    ('min-width: 0; min-height: 0; width: 0; height: 0'),\n])\ndef test_box_sizing_zero(size):\n    # https://www.w3.org/TR/css-ui-3/#box-sizing\n    page, = render_pages('''\n      <style>\n        @page { size: 100000px }\n        body { width: 10000px; margin: 0 }\n        div { %s; margin: 100px; padding: 10px; border: 1px solid }\n      </style>\n      <div></div>\n\n      <div style=\"box-sizing: content-box\"></div>\n      <div style=\"box-sizing: padding-box\"></div>\n      <div style=\"box-sizing: border-box\"></div>\n    ''' % size)\n    html, = page.children\n    body, = html.children\n    for div in body.children:\n        assert div.width == 0\n        assert div.height == 0\n        assert div.padding_width() == 20\n        assert div.padding_height() == 20\n        assert div.border_width() == 22\n        assert div.border_height() == 22\n        assert div.margin_height() == 222\n        # margin_width() is the width of the containing block\n\n\nCOLLAPSING = (\n    ('10px', '15px', 15),  # not 25\n    # \"The maximum of the absolute values of the negative adjoining margins is\n    # deducted from the maximum of the positive adjoining margins\"\n    ('-10px', '15px', 5),\n    ('10px', '-15px', -5),\n    ('-10px', '-15px', -15),\n    ('10px', 'auto', 10),  # 'auto' is 0\n)\nNOT_COLLAPSING = (\n    ('10px', '15px', 25),\n    ('-10px', '15px', 5),\n    ('10px', '-15px', -5),\n    ('-10px', '-15px', -25),\n    ('10px', 'auto', 10),  # 'auto' is 0\n)\n\n\n@pytest.mark.parametrize(('margin_1', 'margin_2', 'result'), COLLAPSING)\ndef test_vertical_space_1(margin_1, margin_2, result):\n    # Siblings\n    page, = render_pages('''\n      <style>\n        p { font: 20px/1 serif } /* block height == 20px */\n        #p1 { margin-bottom: %s }\n        #p2 { margin-top: %s }\n      </style>\n      <p id=p1>Lorem ipsum\n      <p id=p2>dolor sit amet\n    ''' % (margin_1, margin_2))\n    html, = page.children\n    body, = html.children\n    p1, p2 = body.children\n    p1_bottom = p1.content_box_y() + p1.height\n    p2_top = p2.content_box_y()\n    assert p2_top - p1_bottom == result\n\n\n@pytest.mark.parametrize(('margin_1', 'margin_2', 'result'), COLLAPSING)\ndef test_vertical_space_2(margin_1, margin_2, result):\n    # Not siblings, first is nested\n    page, = render_pages('''\n      <style>\n        p { font: 20px/1 serif } /* block height == 20px */\n        #p1 { margin-bottom: %s }\n        #p2 { margin-top: %s }\n      </style>\n      <div>\n        <p id=p1>Lorem ipsum\n      </div>\n      <p id=p2>dolor sit amet\n    ''' % (margin_1, margin_2))\n    html, = page.children\n    body, = html.children\n    div, p2 = body.children\n    p1, = div.children\n    p1_bottom = p1.content_box_y() + p1.height\n    p2_top = p2.content_box_y()\n    assert p2_top - p1_bottom == result\n\n\n@pytest.mark.parametrize(('margin_1', 'margin_2', 'result'), COLLAPSING)\ndef test_vertical_space_3(margin_1, margin_2, result):\n    # Not siblings, second is nested\n    page, = render_pages('''\n      <style>\n        p { font: 20px/1 serif } /* block height == 20px */\n        #p1 { margin-bottom: %s }\n        #p2 { margin-top: %s }\n      </style>\n      <p id=p1>Lorem ipsum\n      <div>\n        <p id=p2>dolor sit amet\n      </div>\n    ''' % (margin_1, margin_2))\n    html, = page.children\n    body, = html.children\n    p1, div = body.children\n    p2, = div.children\n    p1_bottom = p1.content_box_y() + p1.height\n    p2_top = p2.content_box_y()\n    assert p2_top - p1_bottom == result\n\n\n@pytest.mark.parametrize(('margin_1', 'margin_2', 'result'), COLLAPSING)\ndef test_vertical_space_4(margin_1, margin_2, result):\n    # Not siblings, second is doubly nested\n    page, = render_pages('''\n      <style>\n        p { font: 20px/1 serif } /* block height == 20px */\n        #p1 { margin-bottom: %s }\n        #p2 { margin-top: %s }\n      </style>\n      <p id=p1>Lorem ipsum\n      <div>\n        <div>\n            <p id=p2>dolor sit amet\n        </div>\n      </div>\n    ''' % (margin_1, margin_2))\n    html, = page.children\n    body, = html.children\n    p1, div1 = body.children\n    div2, = div1.children\n    p2, = div2.children\n    p1_bottom = p1.content_box_y() + p1.height\n    p2_top = p2.content_box_y()\n    assert p2_top - p1_bottom == result\n\n\n@pytest.mark.parametrize(('margin_1', 'margin_2', 'result'), COLLAPSING)\ndef test_vertical_space_5(margin_1, margin_2, result):\n    # Collapsing with children\n    page, = render_pages('''\n      <style>\n        p { font: 20px/1 serif } /* block height == 20px */\n        #div1 { margin-top: %s }\n        #div2 { margin-top: %s }\n      </style>\n      <p>Lorem ipsum\n      <div id=div1>\n        <div id=div2>\n          <p id=p2>dolor sit amet\n        </div>\n      </div>\n    ''' % (margin_1, margin_2))\n    html, = page.children\n    body, = html.children\n    p1, div1 = body.children\n    div2, = div1.children\n    p2, = div2.children\n    p1_bottom = p1.content_box_y() + p1.height\n    p2_top = p2.content_box_y()\n    # Parent and element edge are the same:\n    assert div1.border_box_y() == p2.border_box_y()\n    assert div2.border_box_y() == p2.border_box_y()\n    assert p2_top - p1_bottom == result\n\n\n@pytest.mark.parametrize(('margin_1', 'margin_2', 'result'), NOT_COLLAPSING)\ndef test_vertical_space_6(margin_1, margin_2, result):\n    # Block formatting context: Not collapsing with children\n    page, = render_pages('''\n      <style>\n        p { font: 20px/1 serif } /* block height == 20px */\n        #div1 { margin-top: %s; overflow: hidden }\n        #div2 { margin-top: %s }\n      </style>\n      <p>Lorem ipsum\n      <div id=div1>\n        <div id=div2>\n          <p id=p2>dolor sit amet\n        </div>\n      </div>\n    ''' % (margin_1, margin_2))\n    html, = page.children\n    body, = html.children\n    p1, div1 = body.children\n    div2, = div1.children\n    p2, = div2.children\n    p1_bottom = p1.content_box_y() + p1.height\n    p2_top = p2.content_box_y()\n    assert p2_top - p1_bottom == result\n\n\n@pytest.mark.parametrize(('margin_1', 'margin_2', 'result'), COLLAPSING)\ndef test_vertical_space_7(margin_1, margin_2, result):\n    # Collapsing through an empty div\n    page, = render_pages('''\n      <style>\n        p { font: 20px/1 serif } /* block height == 20px */\n        #p1 { margin-bottom: %s }\n        #p2 { margin-top: %s }\n        div { margin-bottom: %s; margin-top: %s }\n      </style>\n      <p id=p1>Lorem ipsum\n      <div></div>\n      <p id=p2>dolor sit amet\n    ''' % (2 * (margin_1, margin_2)))\n    html, = page.children\n    body, = html.children\n    p1, div, p2 = body.children\n    p1_bottom = p1.content_box_y() + p1.height\n    p2_top = p2.content_box_y()\n    assert p2_top - p1_bottom == result\n\n\n@pytest.mark.parametrize(('margin_1', 'margin_2', 'result'), NOT_COLLAPSING)\ndef test_vertical_space_8(margin_1, margin_2, result):\n    # The root element does not collapse\n    page, = render_pages('''\n      <style>\n        html { margin-top: %s }\n        body { margin-top: %s }\n      </style>\n      <p>Lorem ipsum\n    ''' % (margin_1, margin_2))\n    html, = page.children\n    body, = html.children\n    p1, = body.children\n    p1_top = p1.content_box_y()\n    # Vertical space from y=0\n    assert p1_top == result\n\n\n@pytest.mark.parametrize(('margin_1', 'margin_2', 'result'), COLLAPSING)\ndef test_vertical_space_9(margin_1, margin_2, result):\n    # <body> DOES collapse\n    page, = render_pages('''\n      <style>\n        body { margin-top: %s }\n        div { margin-top: %s }\n      </style>\n      <div>\n        <p>Lorem ipsum\n    ''' % (margin_1, margin_2))\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    p1, = div.children\n    p1_top = p1.content_box_y()\n    # Vertical space from y=0\n    assert p1_top == result\n\n\n@assert_no_logs\ndef test_box_decoration_break_block_slice():\n    # https://www.w3.org/TR/css-backgrounds-3/#the-box-decoration-break\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 100px }\n        p { padding: 2px; border: 3px solid; margin: 5px }\n        img { display: block; height: 40px }\n      </style>\n      <p>\n        <img src=pattern.png>\n        <img src=pattern.png>\n        <img src=pattern.png>\n        <img src=pattern.png>''')\n    html, = page_1.children\n    body, = html.children\n    paragraph, = body.children\n    img_1, img_2 = paragraph.children\n    assert paragraph.position_y == 0\n    assert paragraph.margin_top == 5\n    assert paragraph.border_top_width == 3\n    assert paragraph.padding_top == 2\n    assert paragraph.content_box_y() == 10\n    assert img_1.position_y == 10\n    assert img_2.position_y == 50\n    assert paragraph.height == 90\n    assert paragraph.margin_bottom == 0\n    assert paragraph.border_bottom_width == 0\n    assert paragraph.padding_bottom == 0\n    assert paragraph.margin_height() == 100\n\n    html, = page_2.children\n    body, = html.children\n    paragraph, = body.children\n    img_1, img_2 = paragraph.children\n    assert paragraph.position_y == 0\n    assert paragraph.margin_top == 0\n    assert paragraph.border_top_width == 0\n    assert paragraph.padding_top == 0\n    assert paragraph.content_box_y() == 0\n    assert img_1.position_y == 0\n    assert img_2.position_y == 40\n    assert paragraph.height == 80\n    assert paragraph.padding_bottom == 2\n    assert paragraph.border_bottom_width == 3\n    assert paragraph.margin_bottom == 5\n    assert paragraph.margin_height() == 90\n\n\n@assert_no_logs\ndef test_box_decoration_break_block_clone():\n    # https://www.w3.org/TR/css-backgrounds-3/#the-box-decoration-break\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 100px }\n        p { padding: 2px; border: 3px solid; margin: 5px;\n            box-decoration-break: clone }\n        img { display: block; height: 40px }\n      </style>\n      <p>\n        <img src=pattern.png>\n        <img src=pattern.png>\n        <img src=pattern.png>\n        <img src=pattern.png>''')\n    html, = page_1.children\n    body, = html.children\n    paragraph, = body.children\n    img_1, img_2 = paragraph.children\n    assert paragraph.position_y == 0\n    assert paragraph.margin_top == 5\n    assert paragraph.border_top_width == 3\n    assert paragraph.padding_top == 2\n    assert paragraph.content_box_y() == 10\n    assert img_1.position_y == 10\n    assert img_2.position_y == 50\n    assert paragraph.height == 80\n    # TODO: bottom margin should be 0\n    # https://www.w3.org/TR/css-break-3/#valdef-box-decoration-break-clone\n    # \"Cloned margins are truncated on block-level boxes.\"\n    # See issue #115.\n    assert paragraph.margin_bottom == 5\n    assert paragraph.border_bottom_width == 3\n    assert paragraph.padding_bottom == 2\n    assert paragraph.margin_height() == 100\n\n    html, = page_2.children\n    body, = html.children\n    paragraph, = body.children\n    img_1, img_2 = paragraph.children\n    assert paragraph.position_y == 0\n    assert paragraph.margin_top == 0\n    assert paragraph.border_top_width == 3\n    assert paragraph.padding_top == 2\n    assert paragraph.content_box_y() == 5\n    assert img_1.position_y == 5\n    assert img_2.position_y == 45\n    assert paragraph.height == 80\n    assert paragraph.padding_bottom == 2\n    assert paragraph.border_bottom_width == 3\n    assert paragraph.margin_bottom == 5\n    assert paragraph.margin_height() == 95\n\n\n@assert_no_logs\ndef test_box_decoration_break_clone_bottom_padding():\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 80px; margin: 0 }\n        div { height: 20px }\n        article { padding: 12px; box-decoration-break: clone }\n      </style>\n      <article>\n        <div>a</div>\n        <div>b</div>\n        <div>c</div>\n      </article>''')\n    html, = page_1.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 80 - 2 * 12\n    div_1, div_2 = article.children\n    assert div_1.position_y == 12\n    assert div_2.position_y == 12 + 20\n\n    html, = page_2.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 20\n    div, = article.children\n    assert div.position_y == 12\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_box_decoration_break_slice_bottom_padding():  # pragma: no cover\n    # Last div fits in first, but not article's padding. As it is impossible to\n    # break between a parent and its last child, put last child on next page.\n    # TODO: at the end of block_container_layout, we should check that the box\n    # with its bottom border/padding doesn't cross the bottom line. If it does,\n    # we should re-render the box with a bottom_space including the bottom\n    # border/padding.\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 80px; margin: 0 }\n        div { height: 20px }\n        article { padding: 12px; box-decoration-break: slice }\n      </style>\n      <article>\n        <div>a</div>\n        <div>b</div>\n        <div>c</div>\n      </article>''')\n    html, = page_1.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 80 - 12\n    div_1, div_2 = article.children\n    assert div_1.position_y == 12\n    assert div_2.position_y == 12 + 20\n\n    html, = page_2.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 20\n    div, = article.children\n    assert div.position_y == 0\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_nested_blocks_padding_border():  # pragma: no cover\n    # Same as previous issue, with nested blocks.\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 4px 12px; margin: 0 }\n        html { font: 2px/1 weasyprint }\n        article, section, div {\n          border-top: 1px solid; border-bottom: 1px solid;\n          padding-top: 1px; padding-bottom: 1px;\n        }\n      </style>\n      <article>\n        <section>\n          <div>\n            aaa\n            bbb\n          </div>\n        </section>\n      </article>''')\n    html, = page_1.children\n    body, = html.children\n    div_1, = body.children\n    div_2, = div_1.children\n    div_3, = div_2.children\n\n    html, = page_2.children\n    body, = html.children\n    div_1, = body.children\n    div_2, = div_1.children\n    div_3, = div_2.children\n\n\n@assert_no_logs\ndef test_overflow_non_collapsing_parent():\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 6px 10px; margin: 0 }\n        html { font: 2px/1 weasyprint }\n        div { border-bottom: 3px solid }\n        p { margin-bottom: 2px }\n      </style>\n      abc\n      def\n      <div>\n        <p>\n          aaa\n        </p>\n      </div>\n      ghi''')\n    html, = page_1.children\n    body, = html.children\n    lines, = body.children\n    line_1, line_2 = lines.children\n\n    html, = page_2.children\n    body, = html.children\n    section, lines = body.children\n\n\n@assert_no_logs\ndef test_overflow_auto():\n    page, = render_pages('''\n      <article style=\"overflow: auto\">\n        <div style=\"float: left; height: 50px; margin: 10px\">bla bla bla</div>\n          toto toto''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 50 + 10 + 10\n\n\n@assert_no_logs\ndef test_overflow_hidden_in_flow_layout():\n    page, = render_pages('''\n      <div style=\"overflow: hidden; height: 3px;\">\n        <div>abc</div>\n        <div style=\"height: 100px; margin: 50px;\">def</div>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    parent_div, = body.children\n    assert parent_div.height == 3\n\n\n@assert_no_logs\ndef test_overflow_hidden_out_of_flow_layout():\n    page, = render_pages('''\n      <div style=\"overflow: hidden; height: 3px;\">\n        <div style=\"float: left;\">abc</div>\n        <div style=\"float: right; height: 100px; margin: 50px;\">def</div>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    parent_div, = body.children\n    assert parent_div.height == 3\n\n\n@assert_no_logs\ndef test_box_margin_top_repagination():\n    # Regression test for #943.\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 50px }\n        :root { line-height: 1; font-size: 10px }\n        a::before { content: target-counter(attr(href), page) }\n        div { margin: 20px 0 0; background: yellow }\n      </style>\n      <p><a href=\"#title\"></a></p>\n      <div>1<br/>1<br/>2<br/>2</div>\n      <h1 id=\"title\">title</h1>\n    ''')\n    html, = page_1.children\n    body, = html.children\n    p, div = body.children\n    assert div.margin_top == 20\n    assert div.padding_box_y() == 10 + 20\n\n    html, = page_2.children\n    body, = html.children\n    div, h1 = body.children\n    assert div.margin_top == 0\n    assert div.padding_box_y() == 0\n\n\n@assert_no_logs\ndef test_continue_discard():\n    page_1, = render_pages('''\n      <style>\n        @page { size: 80px; margin: 0 }\n        div { display: inline-block; width: 100%; height: 25px }\n        article { continue: discard; border: 1px solid; line-height: 1 }\n      </style>\n      <article>\n        <div>a</div>\n        <div>b</div>\n        <div>c</div>\n        <div>d</div>\n        <div>e</div>\n        <div>f</div>\n      </article>''')\n    html, = page_1.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 3 * 25\n    div_1, div_2, div_3 = article.children\n    assert div_1.position_y == 1\n    assert div_2.position_y == 1 + 25\n    assert div_3.position_y == 1 + 25 * 2\n    assert article.border_bottom_width == 1\n\n\n@assert_no_logs\ndef test_continue_discard_children():\n    page_1, = render_pages('''\n      <style>\n        @page { size: 80px; margin: 0 }\n        div { display: inline-block; width: 100%; height: 25px }\n        section { border: 1px solid }\n        article { continue: discard; border: 1px solid; line-height: 1 }\n      </style>\n      <article>\n        <section>\n          <div>a</div>\n          <div>b</div>\n          <div>c</div>\n          <div>d</div>\n          <div>e</div>\n          <div>f</div>\n        </section>\n      </article>''')\n    html, = page_1.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 2 + 3 * 25\n    section, = article.children\n    assert section.height == 3 * 25\n    div_1, div_2, div_3 = section.children\n    assert div_1.position_y == 2\n    assert div_2.position_y == 2 + 25\n    assert div_3.position_y == 2 + 25 * 2\n    assert article.border_bottom_width == 1\n\n\n@assert_no_logs\ndef test_block_in_block_with_bottom_padding():\n    # Regression test for #1476.\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 8em 3.5em }\n        body { line-height: 1; font-family: weasyprint }\n        div { padding-bottom: 1em }\n      </style>\n      abc def\n      <div>\n        <p>\n          ghi jkl\n          mno pqr\n        </p>\n      </div>\n      stu vwx''')\n\n    html, = page_1.children\n    body, = html.children\n    anon_body, div = body.children\n    line, = anon_body.children\n    assert line.height == 16\n    assert line.children[0].text == 'abc def'\n    p, = div.children\n    line, = p.children\n    assert line.height == 16\n    assert line.children[0].text == 'ghi jkl'\n\n    html, = page_2.children\n    body, = html.children\n    div, anon_body = body.children\n    p, = div.children\n    line, = p.children\n    assert line.height == 16\n    assert line.children[0].text == 'mno pqr'\n    line, = anon_body.children\n    assert line.height == 16\n    assert line.content_box_y() == 16 + 16  # p content + div padding\n    assert line.children[0].text == 'stu vwx'\n\n\n@assert_no_logs\ndef test_page_breaks_1():\n    # last div does not fit, pushed to next page\n    pages = render_pages('''\n      <style>\n        @page{\n          size: 110px;\n          margin: 10px;\n          padding: 0;\n        }\n        .large {\n          width: 10px;\n          height: 60px;\n        }\n        .small {\n          width: 10px;\n          height: 20px;\n        }\n      </style>\n      <body>\n        <div class=\"large\"></div>\n        <div class=\"small\"></div>\n        <div class=\"large\"></div>\n    ''')\n\n    assert len(pages) == 2\n    page_divs = []\n    for page in pages:\n        divs = [div for div in page.descendants() if div.element_tag == 'div']\n        assert all([div.element_tag == 'div' for div in divs])\n        page_divs.append(divs)\n    positions_y = [[div.position_y for div in divs] for divs in page_divs]\n    assert positions_y == [[10, 70], [10]]\n\n\n@assert_no_logs\ndef test_page_breaks_2():\n    # last div does not fit, pushed to next page\n    # center div must not\n    pages = render_pages('''\n      <style>\n        @page{\n          size: 110px;\n          margin: 10px;\n          padding: 0;\n        }\n        .large {\n          width: 10px;\n          height: 60px;\n        }\n        .small {\n          width: 10px;\n          height: 20px;\n          page-break-after: avoid;\n        }\n      </style>\n      <body>\n        <div class=\"large\"></div>\n        <div class=\"small\"></div>\n        <div class=\"large\"></div>\n    ''')\n\n    assert len(pages) == 2\n    page_divs = []\n    for page in pages:\n        divs = [div for div in page.descendants() if div.element_tag == 'div']\n        assert all([div.element_tag == 'div' for div in divs])\n        page_divs.append(divs)\n    positions_y = [[div.position_y for div in divs] for divs in page_divs]\n    assert positions_y == [[10], [10, 30]]\n\n\n@assert_no_logs\ndef test_page_breaks_3():\n    # center div must be the last element,\n    # but div won't fit and will get pushed anyway\n    pages = render_pages('''\n      <style>\n        @page{\n          size: 110px;\n          margin: 10px;\n          padding: 0;\n        }\n        .large {\n          width: 10px;\n          height: 80px;\n        }\n        .small {\n          width: 10px;\n          height: 20px;\n          page-break-after: avoid;\n        }\n      </style>\n      <body>\n        <div class=\"large\"></div>\n        <div class=\"small\"></div>\n        <div class=\"large\"></div>\n    ''')\n\n    assert len(pages) == 3\n    page_divs = []\n    for page in pages:\n        divs = [div for div in page.descendants() if div.element_tag == 'div']\n        assert all([div.element_tag == 'div' for div in divs])\n        page_divs.append(divs)\n    positions_y = [[div.position_y for div in divs] for divs in page_divs]\n    assert positions_y == [[10], [10], [10]]\n\n\n@assert_no_logs\ndef test_page_break_child_margin_no_collapse():\n    # Regression test for #2453.\n    page1, page2 = render_pages('''\n      <style>\n        @page{ size: 6px }\n      </style>\n      <body>\n        <div style=\"height: 2px\"></div>\n        <section>\n          <div style=\"height: 3px; margin-bottom: 2px\"></div>\n          <div style=\"display: flex\">\n            <span style=\"font: 2px weasyprint\">a</span>\n          </div>\n        </section>\n      </body>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, section = body.children\n    div, = section.children\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    div, = section.children\n\n\n@assert_no_logs\ndef test_min_max_rtl():\n    page1, = render_pages('''\n      <style>\n        @page{ size: 10px }\n      </style>\n      <body style=\"direction: rtl\">\n        <div style=\"height: 5px; width: 1px; max-height: 4px; min-width: 3px\"></div>\n        <div style=\"height: 1px; width: 5px; min-height: 4px; max-width: 3px\"></div>\n      </body>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div1, div2 = body.children\n    assert div1.position_x == 7\n    assert div1.position_y == 0\n    assert div1.height == 4\n    assert div1.width == 3\n    assert div2.position_x == 7\n    assert div2.position_y == 4\n    assert div2.height == 4\n    assert div2.width == 3\n"
  },
  {
    "path": "tests/layout/test_column.py",
    "content": "\"\"\"Tests for multicolumn layout.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\n@pytest.mark.parametrize('css', [\n    'columns: 4',\n    'columns: 100px',\n    'columns: 4 100px',\n    'columns: 100px 4',\n    'column-width: 100px',\n    'column-count: 4',\n])\ndef test_columns(css):\n    page, = render_pages('''\n      <style>\n        div { %s; column-gap: 0 }\n        body { margin: 0; font-family: weasyprint }\n        @page { margin: 0; size: 400px 1000px }\n      </style>\n      <div>\n        Ipsum dolor sit amet,\n        consectetur adipiscing elit.\n        Sed sollicitudin nibh\n        et turpis molestie tristique.\n      </div>\n    ''' % css)\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 4\n    assert [column.width for column in columns] == [100, 100, 100, 100]\n    assert [column.position_x for column in columns] == [0, 100, 200, 300]\n    assert [column.position_y for column in columns] == [0, 0, 0, 0]\n\n\n@pytest.mark.parametrize(('value', 'width'), [\n    ('normal', 16),  # \"normal\" is 1em = 16px\n    ('unknown', 16),  # default value is normal\n    ('15px', 15),\n    ('5%', 15),\n    ('-1em', 16),  # negative values are not allowed\n])\ndef test_column_gap(value, width):\n    page, = render_pages('''\n      <style>\n        div { columns: 3; column-gap: %s }\n        body { margin: 0; font-family: weasyprint }\n        @page { margin: 0; size: 300px 1000px }\n      </style>\n      <div>\n        Ipsum dolor sit amet,\n        consectetur adipiscing elit.\n        Sed sollicitudin nibh\n        et turpis molestie tristique.\n      </div>\n    ''' % value)\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 3\n    assert [column.width for column in columns] == (\n        3 * [100 - 2 * width / 3])\n    assert [column.position_x for column in columns] == (\n        [0, 100 + width / 3, 200 + 2 * width / 3])\n    assert [column.position_y for column in columns] == [0, 0, 0]\n\n\n@assert_no_logs\ndef test_column_span_1():\n    page, = render_pages('''\n      <style>\n        body { margin: 0; font-family: weasyprint; line-height: 1 }\n        div { columns: 2; width: 10em; column-gap: 0 }\n        section { column-span: all; margin: 1em 0 }\n      </style>\n\n      <div>\n        abc def\n        <section>test</section>\n        <section>test</section>\n        ghi jkl\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    column1, column2, section1, section2, column3, column4 = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (5 * 16, 0)\n    assert (section1.content_box_x(), section1.content_box_y()) == (0, 32)\n    assert (section2.content_box_x(), section2.content_box_y()) == (0, 64)\n    assert (column3.position_x, column3.position_y) == (0, 96)\n    assert (column4.position_x, column4.position_y) == (5 * 16, 96)\n\n    assert column1.height == 16\n\n\n@assert_no_logs\ndef test_column_span_2():\n    page, = render_pages('''\n      <style>\n        body { margin: 0; font-family: weasyprint; line-height: 1 }\n        div { columns: 2; width: 10em; column-gap: 0 }\n        section { column-span: all; margin: 1em 0 }\n      </style>\n\n      <div>\n        <section>test</section>\n        abc def\n        ghi jkl\n        mno pqr\n        stu vwx\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    section1, column1, column2 = div.children\n    assert (section1.content_box_x(), section1.content_box_y()) == (0, 16)\n    assert (column1.position_x, column1.position_y) == (0, 3 * 16)\n    assert (column2.position_x, column2.position_y) == (5 * 16, 3 * 16)\n\n    assert column1.height == column2.height == 16 * 4\n\n\n@assert_no_logs\ndef test_column_span_3():\n    page1, page2 = render_pages('''\n      <style>\n        @page { margin: 0; size: 8px 3px }\n        body { font-family: weasyprint; font-size: 1px }\n        div { columns: 2; column-gap: 0; line-height: 1 }\n        section { column-span: all }\n      </style>\n      <div>\n        abc def\n        ghi jkl\n        <section>line1 line2</section>\n        mno pqr\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    column1, column2, section = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (4, 0)\n    assert (section.position_x, section.position_y) == (0, 2)\n\n    assert column1.children[0].children[0].children[0].text == 'abc'\n    assert column1.children[0].children[1].children[0].text == 'def'\n    assert column2.children[0].children[0].children[0].text == 'ghi'\n    assert column2.children[0].children[1].children[0].text == 'jkl'\n    assert section.children[0].children[0].text == 'line1'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    section, column1, column2 = div.children\n    assert (section.position_x, section.position_y) == (0, 0)\n    assert (column1.position_x, column1.position_y) == (0, 1)\n    assert (column2.position_x, column2.position_y) == (4, 1)\n\n    assert section.children[0].children[0].text == 'line2'\n    assert column1.children[0].children[0].children[0].text == 'mno'\n    assert column2.children[0].children[0].children[0].text == 'pqr'\n\n\n@assert_no_logs\ndef test_column_span_4():\n    page1, page2 = render_pages('''\n      <style>\n        @page { margin: 0; size: 8px 3px }\n        body { font-family: weasyprint; font-size: 1px }\n        div { columns: 2; column-gap: 0; line-height: 1 }\n        section { column-span: all }\n      </style>\n      <div>\n        abc def\n        <section>line1</section>\n        ghi jkl\n        mno pqr\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    column1, column2, section, column3, column4 = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (4, 0)\n    assert (section.position_x, section.position_y) == (0, 1)\n    assert (column3.position_x, column3.position_y) == (0, 2)\n    assert (column4.position_x, column4.position_y) == (4, 2)\n\n    assert column1.children[0].children[0].children[0].text == 'abc'\n    assert column2.children[0].children[0].children[0].text == 'def'\n    assert section.children[0].children[0].text == 'line1'\n    assert column3.children[0].children[0].children[0].text == 'ghi'\n    assert column4.children[0].children[0].children[0].text == 'jkl'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (4, 0)\n\n    assert column1.children[0].children[0].children[0].text == 'mno'\n    assert column2.children[0].children[0].children[0].text == 'pqr'\n\n\n@assert_no_logs\ndef test_column_span_5():\n    page1, page2 = render_pages('''\n      <style>\n        @page { margin: 0; size: 8px 3px }\n        body { font-family: weasyprint; font-size: 1px }\n        div { columns: 2; column-gap: 0; line-height: 1 }\n        section { column-span: all }\n      </style>\n      <div>\n        abc def\n        ghi jkl\n        <section>line1</section>\n        mno pqr\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    column1, column2, section = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (4, 0)\n    assert (section.position_x, section.position_y) == (0, 2)\n\n    assert column1.children[0].children[0].children[0].text == 'abc'\n    assert column1.children[0].children[1].children[0].text == 'def'\n    assert column2.children[0].children[0].children[0].text == 'ghi'\n    assert column2.children[0].children[1].children[0].text == 'jkl'\n    assert section.children[0].children[0].text == 'line1'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (4, 0)\n\n    assert column1.children[0].children[0].children[0].text == 'mno'\n    assert column2.children[0].children[0].children[0].text == 'pqr'\n\n\n@assert_no_logs\ndef test_column_span_6():\n    page1, page2 = render_pages('''\n      <style>\n        @page { margin: 0; size: 8px 3px }\n        body { font-family: weasyprint; font-size: 1px }\n        div { columns: 2; column-gap: 0; line-height: 1 }\n        section { column-span: all }\n      </style>\n      <div>\n        abc def\n        ghi jkl\n        mno pqr\n        <section>line1</section>\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (4, 0)\n\n    assert column1.children[0].children[0].children[0].text == 'abc'\n    assert column1.children[0].children[1].children[0].text == 'def'\n    assert column1.children[0].children[2].children[0].text == 'ghi'\n    assert column2.children[0].children[0].children[0].text == 'jkl'\n    assert column2.children[0].children[1].children[0].text == 'mno'\n    assert column2.children[0].children[2].children[0].text == 'pqr'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    section, = div.children\n    assert section.children[0].children[0].text == 'line1'\n    assert (section.position_x, section.position_y) == (0, 0)\n\n\n@assert_no_logs\ndef test_column_span_7():\n    page1, page2 = render_pages('''\n      <style>\n        @page { margin: 0; size: 8px 3px }\n        body { font-family: weasyprint; font-size: 1px }\n        div { columns: 2; column-gap: 0; line-height: 1 }\n        section { column-span: all; font-size: 2px }\n      </style>\n      <div>\n        abc def\n        ghi jkl\n        <section>l1</section>\n        mno pqr\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (4, 0)\n\n    assert column1.children[0].children[0].children[0].text == 'abc'\n    assert column1.children[0].children[1].children[0].text == 'def'\n    assert column2.children[0].children[0].children[0].text == 'ghi'\n    assert column2.children[0].children[1].children[0].text == 'jkl'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    section, column1, column2 = div.children\n    assert (section.position_x, section.position_y) == (0, 0)\n    assert (column1.position_x, column1.position_y) == (0, 2)\n    assert (column2.position_x, column2.position_y) == (4, 2)\n\n    assert section.children[0].children[0].text == 'l1'\n    assert column1.children[0].children[0].children[0].text == 'mno'\n    assert column2.children[0].children[0].children[0].text == 'pqr'\n\n\n@assert_no_logs\ndef test_column_span_8():\n    page1, page2 = render_pages('''\n      <style>\n        @page { margin: 0; size: 8px 2px }\n        body { font-family: weasyprint; font-size: 1px }\n        div { columns: 2; column-gap: 0; line-height: 1 }\n        section { column-span: all }\n      </style>\n      <div>\n        abc def\n        ghi jkl\n        mno pqr\n        <section>line1</section>\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (4, 0)\n\n    assert column1.children[0].children[0].children[0].text == 'abc'\n    assert column1.children[0].children[1].children[0].text == 'def'\n    assert column2.children[0].children[0].children[0].text == 'ghi'\n    assert column2.children[0].children[1].children[0].text == 'jkl'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    column1, column2, section = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (4, 0)\n    assert (section.position_x, section.position_y) == (0, 1)\n\n    assert column1.children[0].children[0].children[0].text == 'mno'\n    assert column2.children[0].children[0].children[0].text == 'pqr'\n    assert section.children[0].children[0].text == 'line1'\n\n\n@assert_no_logs\ndef test_column_span_9():\n    page1, = render_pages('''\n      <style>\n        @page { margin: 0; size: 8px 3px }\n        body { font-family: weasyprint; font-size: 1px }\n        div { columns: 2; column-gap: 0; line-height: 1 }\n        section { column-span: all }\n      </style>\n      <div>\n        abc\n        <section>line1</section>\n        def ghi\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    column1, section, column2, column3 = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (section.position_x, section.position_y) == (0, 1)\n    assert (column2.position_x, column2.position_y) == (0, 2)\n    assert (column3.position_x, column3.position_y) == (4, 2)\n\n    assert column1.children[0].children[0].children[0].text == 'abc'\n    assert section.children[0].children[0].text == 'line1'\n    assert column2.children[0].children[0].children[0].text == 'def'\n    assert column3.children[0].children[0].children[0].text == 'ghi'\n\n\n@assert_no_logs\ndef test_column_span_balance():\n    page, = render_pages('''\n      <style>\n        @page { margin: 0; size: 8px 5px }\n        body { font-family: weasyprint; font-size: 1px }\n        div { columns: 2; column-gap: 0; line-height: 1; column-fill: auto }\n        section { column-span: all }\n      </style>\n      <div>\n        abc def\n        <section>line1</section>\n        ghi jkl\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    column1, column2, section, column3 = div.children\n    assert (column1.position_x, column1.position_y) == (0, 0)\n    assert (column2.position_x, column2.position_y) == (4, 0)\n    assert (section.position_x, section.position_y) == (0, 1)\n    assert (column3.position_x, column3.position_y) == (0, 2)\n\n    assert column1.children[0].children[0].children[0].text == 'abc'\n    assert column2.children[0].children[0].children[0].text == 'def'\n    assert section.children[0].children[0].text == 'line1'\n    assert column3.children[0].children[0].children[0].text == 'ghi'\n    assert column3.children[0].children[1].children[0].text == 'jkl'\n\n\n@assert_no_logs\ndef test_columns_multipage():\n    page1, page2 = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 2px }\n      </style>\n      <div>a b c d e f g</div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert len(columns[0].children) == 2\n    assert len(columns[1].children) == 2\n    assert columns[0].children[0].children[0].text == 'a'\n    assert columns[0].children[1].children[0].text == 'b'\n    assert columns[1].children[0].children[0].text == 'c'\n    assert columns[1].children[1].children[0].text == 'd'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert len(columns[0].children) == 2\n    assert len(columns[1].children) == 1\n    assert columns[0].children[0].children[0].text == 'e'\n    assert columns[0].children[1].children[0].text == 'f'\n    assert columns[1].children[0].children[0].text == 'g'\n\n\n@assert_no_logs\ndef test_columns_breaks():\n    page1, page2 = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 2px }\n        section { break-before: always; }\n      </style>\n      <div>a<section>b</section><section>c</section></div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert len(columns[0].children) == 1\n    assert len(columns[1].children) == 1\n    assert columns[0].children[0].children[0].children[0].text == 'a'\n    assert columns[1].children[0].children[0].children[0].text == 'b'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 1\n    assert len(columns[0].children) == 1\n    assert columns[0].children[0].children[0].children[0].text == 'c'\n\n\n@assert_no_logs\ndef test_columns_break_after_column_1():\n    page1, = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 10px }\n        section { break-after: column }\n      </style>\n      <div>a b <section>c</section> d</div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert columns[0].children[0].children[0].children[0].text == 'a'\n    assert columns[0].children[0].children[1].children[0].text == 'b'\n    assert columns[0].children[1].children[0].children[0].text == 'c'\n    assert columns[1].children[0].children[0].children[0].text == 'd'\n\n\n@assert_no_logs\ndef test_columns_break_after_column_2():\n    page1, = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 10px }\n        section { break-after: column }\n      </style>\n      <div><section>a</section> b c d</div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert columns[0].children[0].children[0].children[0].text == 'a'\n    assert columns[1].children[0].children[0].children[0].text == 'b'\n    assert columns[1].children[0].children[1].children[0].text == 'c'\n    assert columns[1].children[0].children[2].children[0].text == 'd'\n\n\n@assert_no_logs\ndef test_columns_break_after_avoid_column():\n    page1, = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 10px }\n        section { break-after: avoid-column }\n      </style>\n      <div>a <section>b</section> c d</div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert columns[0].children[0].children[0].children[0].text == 'a'\n    assert columns[0].children[1].children[0].children[0].text == 'b'\n    assert columns[0].children[2].children[0].children[0].text == 'c'\n    assert columns[1].children[0].children[0].children[0].text == 'd'\n\n\n@assert_no_logs\ndef test_columns_break_before_column_1():\n    page1, = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 10px }\n        section { break-before: column }\n      </style>\n      <div>a b c <section>d</section></div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert columns[0].children[0].children[0].children[0].text == 'a'\n    assert columns[0].children[0].children[1].children[0].text == 'b'\n    assert columns[0].children[0].children[2].children[0].text == 'c'\n    assert columns[1].children[0].children[0].children[0].text == 'd'\n\n\n@assert_no_logs\ndef test_columns_break_before_column_2():\n    page1, = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 10px }\n        section { break-before: column }\n      </style>\n      <div>a <section>b</section> c d</div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert columns[0].children[0].children[0].children[0].text == 'a'\n    assert columns[1].children[0].children[0].children[0].text == 'b'\n    assert columns[1].children[1].children[0].children[0].text == 'c'\n    assert columns[1].children[1].children[1].children[0].text == 'd'\n\n\n@assert_no_logs\ndef test_columns_break_before_avoid_column():\n    page1, = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 10px }\n        section { break-before: avoid-column }\n      </style>\n      <div>a b <section>c</section> d</div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert columns[0].children[0].children[0].children[0].text == 'a'\n    assert columns[0].children[0].children[1].children[0].text == 'b'\n    assert columns[0].children[1].children[0].children[0].text == 'c'\n    assert columns[1].children[0].children[0].children[0].text == 'd'\n\n\n@assert_no_logs\ndef test_columns_break_inside_column_1():\n    page1, = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 10px }\n        section { break-inside: avoid-column }\n      </style>\n      <div><section>a b c</section> d</div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert columns[0].children[0].children[0].children[0].text == 'a'\n    assert columns[0].children[0].children[1].children[0].text == 'b'\n    assert columns[0].children[0].children[2].children[0].text == 'c'\n    assert columns[1].children[0].children[0].children[0].text == 'd'\n\n\n@assert_no_logs\ndef test_columns_break_inside_column_2():\n    page1, = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 10px }\n        section { break-inside: avoid-column }\n      </style>\n      <div>a <section>b c d</section></div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    columns = div.children\n    assert len(columns) == 2\n    assert columns[0].children[0].children[0].children[0].text == 'a'\n    assert columns[1].children[0].children[0].children[0].text == 'b'\n    assert columns[1].children[0].children[1].children[0].text == 'c'\n    assert columns[1].children[0].children[2].children[0].text == 'd'\n\n\n@assert_no_logs\ndef test_columns_break_inside_column_not_empty_page():\n    page1, = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font-family: weasyprint;\n               font-size: 1px; line-height: 1px }\n        @page { margin: 0; size: 3px 10px }\n        section { break-inside: avoid-column }\n      </style>\n      <p>p</p>\n      <div><section>a b c</section> d</div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    p, div, = body.children\n    assert p.children[0].children[0].text == 'p'\n    columns = div.children\n    assert len(columns) == 2\n    assert columns[0].children[0].children[0].children[0].text == 'a'\n    assert columns[0].children[0].children[1].children[0].text == 'b'\n    assert columns[0].children[0].children[2].children[0].text == 'c'\n    assert columns[1].children[0].children[0].children[0].text == 'd'\n\n\n@assert_no_logs\ndef test_columns_not_enough_content():\n    page, = render_pages('''\n      <style>\n        div { columns: 5; column-gap: 0 }\n        body { margin: 0; font-family: weasyprint; font-size: 1px }\n        @page { margin: 0; size: 5px }\n      </style>\n      <div>a b c</div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 5\n    columns = div.children\n    assert len(columns) == 3\n    assert [column.width for column in columns] == [1, 1, 1]\n    assert [column.position_x for column in columns] == [0, 1, 2]\n    assert [column.position_y for column in columns] == [0, 0, 0]\n\n\n@assert_no_logs\ndef test_columns_higher_than_page():\n    page1, page2 = render_pages('''\n      <style>\n        div { columns: 5; column-gap: 0 }\n        body { margin: 0; font-family: weasyprint; font-size: 2px }\n        @page { margin: 0; size: 5px 1px }\n      </style>\n      <div>a b c d e f g h</div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 5\n    columns = div.children\n    assert len(columns) == 5\n    assert columns[0].children[0].children[0].text == 'a'\n    assert columns[1].children[0].children[0].text == 'b'\n    assert columns[2].children[0].children[0].text == 'c'\n    assert columns[3].children[0].children[0].text == 'd'\n    assert columns[4].children[0].children[0].text == 'e'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 5\n    columns = div.children\n    assert len(columns) == 3\n    assert columns[0].children[0].children[0].text == 'f'\n    assert columns[1].children[0].children[0].text == 'g'\n    assert columns[2].children[0].children[0].text == 'h'\n\n\n@assert_no_logs\ndef test_columns_empty():\n    page, = render_pages('''\n      <style>\n        div { columns: 3 }\n        body { margin: 0; font-family: weasyprint }\n        @page { margin: 0; size: 3px; font-size: 1px }\n      </style>\n      <div></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 3\n    assert div.height == 0\n    columns = div.children\n    assert len(columns) == 0\n\n\n@assert_no_logs\n@pytest.mark.parametrize('prop', ['height', 'min-height'])\ndef test_columns_fixed_height(prop):\n    # TODO: we should test when the height is too small\n    page, = render_pages('''\n      <style>\n        div { columns: 4; column-gap: 0; %s: 10px }\n        body { margin: 0; font-family: weasyprint; line-height: 1px }\n        @page { margin: 0; size: 4px 50px; font-size: 1px }\n      </style>\n      <div>a b c</div>\n    ''' % prop)\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 4\n    columns = div.children\n    assert len(columns) == 3\n    assert [column.width for column in columns] == [1, 1, 1]\n    assert [column.height for column in columns] == [10, 10, 10]\n    assert [column.position_x for column in columns] == [0, 1, 2]\n    assert [column.position_y for column in columns] == [0, 0, 0]\n\n\n@assert_no_logs\ndef test_columns_padding():\n    page, = render_pages('''\n      <style>\n        div { columns: 4; column-gap: 0; padding: 1px }\n        body { margin: 0; font-family: weasyprint; line-height: 1px }\n        @page { margin: 0; size: 6px 50px; font-size: 1px }\n      </style>\n      <div>a b c</div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 4\n    assert div.height == 1\n    assert div.padding_width() == 6\n    assert div.padding_height() == 3\n    columns = div.children\n    assert len(columns) == 3\n    assert [column.width for column in columns] == [1, 1, 1]\n    assert [column.height for column in columns] == [1, 1, 1]\n    assert [column.position_x for column in columns] == [1, 2, 3]\n    assert [column.position_y for column in columns] == [1, 1, 1]\n\n\n@assert_no_logs\ndef test_columns_bottom_margin():\n    page, = render_pages('''\n      <style>\n        div { columns: 2; column-gap: 1px }\n        body { margin: 0; font: 2px / 1 weasyprint }\n        p { margin-bottom: 1em }\n        @page { margin: 0; size: 5px 7px; font-size: 1px }\n      </style>\n      <div><p>a b c</p></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    p, = column1.children\n    line1, line2 = p.children\n    assert line1.children[0].text == 'a'\n    assert line2.children[0].text == 'b'\n    p, = column2.children\n    line1, = p.children\n    assert line1.children[0].text == 'c'\n\n\n@assert_no_logs\ndef test_columns_relative():\n    page, = render_pages('''\n      <style>\n        article { position: absolute; top: 3px }\n        div { columns: 4; column-gap: 0; position: relative;\n              top: 1px; left: 2px }\n        body { margin: 0; font-family: weasyprint; line-height: 1px }\n        @page { margin: 0; size: 4px 50px; font-size: 1px }\n      </style>\n      <div>a b c d<article>e</article></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 4\n    columns = div.children\n    assert [column.width for column in columns] == [1, 1, 1, 1]\n    assert [column.position_x for column in columns] == [2, 3, 4, 5]\n    assert [column.position_y for column in columns] == [1, 1, 1, 1]\n    column4 = columns[-1]\n    column_line, = column4.children\n    _, absolute_article = column_line.children\n    absolute_line, = absolute_article.children\n    span, = absolute_line.children\n    assert span.position_x == 5  # Default position of the 4th column\n    assert span.position_y == 4  # div's 1px + span's 3px\n\n\n@assert_no_logs\ndef test_columns_regression_1():\n    # Regression test for #659.\n    page1, page2, page3 = render_pages('''\n      <style>\n        @page {margin: 0; width: 100px; height: 100px}\n        body {margin: 0; font-size: 1px}\n      </style>\n      <div style=\"height:95px\">A</div>\n      <div style=\"column-count:2\">\n        <div style=\"height:20px\">B1</div>\n        <div style=\"height:20px\">B2</div>\n        <div style=\"height:20px\">B3</div>\n      </div>\n      <div style=\"height:95px\">C</div>\n    ''')\n\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    assert div.position_y == 0\n    assert div.children[0].children[0].text == 'A'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    assert div.position_y == 0\n    column1, column2 = div.children\n    assert column1.position_y == column2.position_y == 0\n    div1, div2 = column1.children\n    div3, = column2.children\n    assert div1.position_y == div3.position_y == 0\n    assert div2.position_y == 20\n    assert div1.children[0].children[0].text == 'B1'\n    assert div2.children[0].children[0].text == 'B2'\n    assert div3.children[0].children[0].text == 'B3'\n\n    html, = page3.children\n    body, = html.children\n    div, = body.children\n    assert div.position_y == 0\n    assert div.children[0].children[0].text == 'C'\n\n\n@assert_no_logs\ndef test_columns_regression_2():\n    # Regression test for #659.\n    page1, page2 = render_pages('''\n      <style>\n        @page {margin: 0; width: 100px; height: 100px}\n        body {margin: 0; font-size: 1px}\n      </style>\n      <div style=\"column-count:2\">\n        <div style=\"height:20px\">B1</div>\n        <div style=\"height:60px\">B2</div>\n        <div style=\"height:60px\">B3</div>\n        <div style=\"height:60px\">B4</div>\n      </div>\n    ''')\n\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    assert div.position_y == 0\n    column1, column2 = div.children\n    assert column1.position_y == column2.position_y == 0\n    div1, div2 = column1.children\n    div3, = column2.children\n    assert div1.position_y == div3.position_y == 0\n    assert div2.position_y == 20\n    assert div1.children[0].children[0].text == 'B1'\n    assert div2.children[0].children[0].text == 'B2'\n    assert div3.children[0].children[0].text == 'B3'\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    assert div.position_y == 0\n    column1, = div.children\n    assert column1.position_y == 0\n    div1, = column1.children\n    assert div1.position_y == div3.position_y == 0\n    assert div1.children[0].children[0].text == 'B4'\n\n\n@assert_no_logs\ndef test_columns_regression_3():\n    # Regression test for #659.\n    page, = render_pages('''\n      <style>\n        @page {margin: 0; width: 100px; height: 100px}\n        body {margin: 0; font-size: 10px}\n      </style>\n      <div style=\"column-count:2\">\n        <div style=\"height:20px; margin:5px\">B1</div>\n        <div style=\"height:60px\">B2</div>\n        <div style=\"height:60px\">B3</div>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.position_y == 0\n    column1, column2 = div.children\n    assert column1.position_y == column2.position_y == 0\n    div1, div2 = column1.children\n    div3, = column2.children\n    assert div1.position_y == div3.position_y == 0\n    assert div2.position_y == 30\n    assert div.height == 5 + 20 + 5 + 60\n    assert div1.children[0].children[0].text == 'B1'\n    assert div2.children[0].children[0].text == 'B2'\n    assert div3.children[0].children[0].text == 'B3'\n\n\n@assert_no_logs\ndef test_columns_regression_4():\n    # Regression test for #897.\n    page, = render_pages('''\n      <div style=\"position:absolute\">\n        <div style=\"column-count:2\">\n          <div>a</div>\n        </div>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.position_y == 0\n    column1, = div.children\n    assert column1.position_y == 0\n    div1, = column1.children\n    assert div1.position_y == 0\n\n\n@assert_no_logs\ndef test_columns_regression_5():\n    # Regression test for #1191.\n    render_pages('''\n      <style>\n        @page {width: 100px; height: 100px}\n      </style>\n      <div style=\"height: 1px\"></div>\n      <div style=\"columns: 2\">\n        <div style=\"break-after: avoid\">\n          <div style=\"height: 50px\"></div>\n        </div>\n        <div style=\"break-after: avoid\">\n          <div style=\"height: 50px\"></div>\n          <p>a</p>\n        </div>\n      </div>\n      <div style=\"height: 50px\"></div>\n    ''')\n\n\n@assert_no_logs\ndef test_columns_regression_6():\n    # Regression test for #2103.\n    render_pages('''\n      <style>\n        @page {width: 100px; height: 100px}\n      </style>\n      <div style=\"columns: 2; column-width: 100px; width: 10px\">abc def</div>\n    ''')\n\n\n@assert_no_logs\ndef test_columns_regression_7():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 50px 10px }\n        body { font-size: 2px; line-height: 1 }\n      </style>\n      <div style=\"height: 8px\"></div>\n      <div style=\"column-count: 2\">\n        <div>a</div>\n        <div style=\"break-inside: avoid\">b<br>c<br>d</div>\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    assert div.position_y == 0\n    assert not div.children\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    assert column1.position_y == 0\n    assert column2.position_y == 0\n\n\n@assert_no_logs\ndef test_columns_margin_top():\n    page1, = render_pages('''\n      <style>\n        @page { size: 50px 10px }\n        body { font-size: 2px; line-height: 1 }\n      </style>\n      <div style=\"column-count: 2; margin-top: 1em\">\n        a<br>b\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    assert div.position_y == 0\n    column1, column2 = div.children\n    assert column1.position_y == column2.position_y == 2\n    assert column1.children[0].position_y == column2.children[0].position_y == 2\n\n\n@assert_no_logs\ndef test_columns_grid():\n    # Regression test for #2680.\n    page1, = render_pages('''\n      <div style=\"columns: 2\">\n        <div>1</div>\n        <div style=\"display: grid\">2</div>\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n"
  },
  {
    "path": "tests/layout/test_flex.py",
    "content": "\"\"\"Tests for flex layout.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\ndef test_flex_direction_row():\n    page, = render_pages('''\n      <article style=\"display: flex\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert div1.position_x == article.position_x\n    assert div1.position_x < div2.position_x < div3.position_x\n\n\n@assert_no_logs\ndef test_flex_direction_row_max_width():\n    page, = render_pages('''\n      <article style=\"display: flex; max-width: 100px\">\n        <div></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    assert article.width == 100\n\n\n@assert_no_logs\ndef test_flex_direction_row_min_height():\n    page, = render_pages('''\n      <article style=\"display: flex; min-height: 100px\">\n        <div></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 100\n\n\n@assert_no_logs\ndef test_flex_direction_row_rtl():\n    page, = render_pages('''\n      <article style=\"display: flex; direction: rtl\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert div1.position_x + div1.width == article.position_x + article.width\n    assert div1.position_x > div2.position_x > div3.position_x\n\n\n@assert_no_logs\ndef test_flex_direction_row_rtl_gap():\n    page, = render_pages('''\n      <article style=\"display: flex; direction: rtl; gap: 10px\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert div1.position_x + div1.width == article.position_x + article.width\n    assert div1.position_x == div2.position_x + div2.width + 10\n    assert div2.position_x == div3.position_x + div3.width + 10\n\n\n@assert_no_logs\ndef test_flex_direction_row_reverse():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: row-reverse\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'C'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'A'\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert div3.position_x + div3.width == article.position_x + article.width\n    assert div1.position_x < div2.position_x < div3.position_x\n\n\n@assert_no_logs\ndef test_flex_direction_row_reverse_rtl():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: row-reverse;\n      direction: rtl\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'C'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'A'\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert div3.position_x == article.position_x\n    assert div1.position_x > div2.position_x > div3.position_x\n\n\n@assert_no_logs\ndef test_flex_direction_column():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: column\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x == div2.position_x == div3.position_x == article.position_x\n    assert div1.position_y == article.position_y\n    assert div1.position_y < div2.position_y < div3.position_y\n\n\n@assert_no_logs\ndef test_flex_direction_column_min_width():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: column; min-height: 100px\">\n        <div></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 100\n\n\n@assert_no_logs\ndef test_flex_direction_column_max_height():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-flow: column wrap; max-height: 100px\">\n        <div style=\"height: 40px\">A</div>\n        <div style=\"height: 40px\">B</div>\n        <div style=\"height: 40px\">C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 100\n    div1, div2, div3 = article.children\n    assert div1.height == 40\n    assert div1.position_x == 0\n    assert div1.position_y == 0\n    assert div2.height == 40\n    assert div2.position_x == 0\n    assert div2.position_y == 40\n    assert div3.height == 40\n    assert div3.position_x == div1.width\n    assert div3.position_y == 0\n\n\n@assert_no_logs\ndef test_flex_direction_column_rtl():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: column;\n      direction: rtl\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x == div2.position_x == div3.position_x == article.position_x\n    assert div1.position_y == article.position_y\n    assert div1.position_y < div2.position_y < div3.position_y\n\n\n@assert_no_logs\ndef test_flex_direction_column_reverse():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: column-reverse\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'C'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'A'\n    assert div1.position_x == div2.position_x == div3.position_x == article.position_x\n    assert div3.position_y + div3.height == article.position_y + article.height\n    assert div1.position_y < div2.position_y < div3.position_y\n\n\n@assert_no_logs\ndef test_flex_direction_column_reverse_rtl():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: column-reverse;\n      direction: rtl\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'C'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'A'\n    assert div1.position_x == div2.position_x == div3.position_x == article.position_x\n    assert div3.position_y + div3.height == article.position_y + article.height\n    assert div1.position_y < div2.position_y < div3.position_y\n\n\n@assert_no_logs\ndef test_flex_direction_column_box_sizing():\n    page, = render_pages('''\n      <style>\n        article {\n          box-sizing: border-box;\n          display: flex;\n          flex-direction: column;\n          height: 10px;\n          padding-top: 5px;\n          width: 10px;\n        }\n      </style>\n      <article></article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    assert article.width == 10\n    assert article.height == 5\n    assert article.padding_top == 5\n\n\n@assert_no_logs\ndef test_flex_row_wrap():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-flow: wrap; width: 50px\">\n        <div style=\"width: 20px\">A</div>\n        <div style=\"width: 20px\">B</div>\n        <div style=\"width: 20px\">C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_y == div2.position_y == article.position_y\n    assert div3.position_y == article.position_y + div2.height\n    assert div1.position_x == div3.position_x == article.position_x\n    assert div1.position_x < div2.position_x\n\n\n@assert_no_logs\ndef test_flex_column_wrap():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-flow: column wrap; height: 50px\">\n        <div style=\"height: 20px\">A</div>\n        <div style=\"height: 20px\">B</div>\n        <div style=\"height: 20px\">C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x == div2.position_x == article.position_x\n    assert div3.position_x == article.position_x + div2.width\n    assert div1.position_y == div3.position_y == article.position_y\n    assert div1.position_y < div2.position_y\n\n\n@assert_no_logs\ndef test_flex_row_wrap_reverse():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-flow: wrap-reverse; width: 50px\">\n        <div style=\"width: 20px\">A</div>\n        <div style=\"width: 20px\">B</div>\n        <div style=\"width: 20px\">C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'C'\n    assert div2.children[0].children[0].text == 'A'\n    assert div3.children[0].children[0].text == 'B'\n    assert div1.position_y == article.position_y\n    assert div2.position_y == div3.position_y == article.position_y + div1.height\n    assert div1.position_x == div2.position_x == article.position_x\n    assert div2.position_x < div3.position_x\n\n\n@assert_no_logs\ndef test_flex_column_wrap_reverse():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-flow: column wrap-reverse;\n                      height: 50px\">\n        <div style=\"height: 20px\">A</div>\n        <div style=\"height: 20px\">B</div>\n        <div style=\"height: 20px\">C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'C'\n    assert div2.children[0].children[0].text == 'A'\n    assert div3.children[0].children[0].text == 'B'\n    assert div1.position_x == article.position_x\n    assert div2.position_x == div3.position_x == article.position_x + div1.width\n    assert div1.position_y == div2.position_y == article.position_y\n    assert div2.position_y < div3.position_y\n\n\n@assert_no_logs\ndef test_flex_direction_column_fixed_height_container():\n    page, = render_pages('''\n      <section style=\"height: 10px\">\n        <article style=\"display: flex; flex-direction: column\">\n          <div>A</div>\n          <div>B</div>\n          <div>C</div>\n        </article>\n      </section>\n    ''')\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    article, = section.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x == div2.position_x == div3.position_x == article.position_x\n    assert div1.position_y == article.position_y\n    assert div1.position_y < div2.position_y < div3.position_y\n    assert section.height == 10\n    assert article.height > 10\n\n\n@assert_no_logs\ndef test_flex_direction_column_fixed_height():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: column; height: 10px\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x == div2.position_x == div3.position_x == article.position_x\n    assert div1.position_y == article.position_y\n    assert div1.position_y < div2.position_y < div3.position_y\n    assert article.height == 10\n    assert div3.position_y > 10\n\n\n@assert_no_logs\ndef test_flex_direction_column_fixed_height_wrap():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: column; height: 10px;\n                      flex-wrap: wrap\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x < div2.position_x < div3.position_x\n    assert div1.position_y == article.position_y\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert article.height == 10\n\n\n@assert_no_logs\ndef test_flex_direction_column_break():\n    # Regression test for issue #2066.\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 4px 5px }\n      </style>\n      <article style=\"display: flex; flex-direction: column; font: 2px weasyprint\">\n        <div>A<br>B<br>C</div>\n      </article>\n    ''')\n    html, = page1.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.children[0].children[0].text == 'A'\n    assert div.children[1].children[0].text == 'B'\n    assert div.height == 5\n    html, = page2.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.children[0].children[0].text == 'C'\n    assert div.height == 2\n\n\n@assert_no_logs\ndef test_flex_direction_column_break_margin():\n    # Regression test for issue #1967.\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 4px 7px }\n      </style>\n      <body style=\"font: 2px weasyprint\">\n        <p style=\"margin: 1px 0\">a</p>\n        <article style=\"display: flex; flex-direction: column\">\n          <div>A<br>B<br>C</div>\n        </article>\n      </body>\n    ''')\n    html, = page1.children\n    body, = html.children\n    p, article = body.children\n    assert p.margin_height() == 4\n    assert article.position_y == 4\n    div, = article.children\n    assert div.children[0].children[0].text == 'A'\n    assert div.height == 3\n    html, = page2.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.children[0].children[0].text == 'B'\n    assert div.children[1].children[0].text == 'C'\n    assert div.height == 4\n\n\n@assert_no_logs\ndef test_flex_direction_column_break_border():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 8px 7px }\n        article, div { border: 1px solid black }\n      </style>\n      <article style=\"display: flex; flex-direction: column; font: 2px weasyprint\">\n        <div>A B C</div>\n      </article>\n    ''')\n    html, = page1.children\n    body, = html.children\n    article, = body.children\n    assert article.border_height() == 7\n    assert article.border_top_width == 1\n    assert article.border_bottom_width == 0\n    div, = article.children\n    assert div.children[0].children[0].text == 'A'\n    assert div.children[1].children[0].text == 'B'\n    assert div.border_height() == 6\n    assert div.border_top_width == 1\n    assert div.border_bottom_width == 0\n    html, = page2.children\n    body, = html.children\n    article, = body.children\n    assert article.border_height() == 4\n    assert article.border_top_width == 0\n    assert article.border_bottom_width == 1\n    div, = article.children\n    assert div.children[0].children[0].text == 'C'\n    assert div.border_height() == 3\n    assert div.border_top_width == 0\n    assert div.border_bottom_width == 1\n\n\n@assert_no_logs\ndef test_flex_direction_column_break_multiple_children():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 4px 5px }\n      </style>\n      <article style=\"display: flex; flex-direction: column; font: 2px weasyprint\">\n        <div>A</div>\n        <div>B<br>C</div>\n      </article>\n    ''')\n    html, = page1.children\n    body, = html.children\n    article, = body.children\n    div1, div2 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div1.height == 2\n    assert div2.children[0].children[0].text == 'B'\n    assert div2.height == 3\n    html, = page2.children\n    body, = html.children\n    article, = body.children\n    div2, = article.children\n    assert div2.children[0].children[0].text == 'C'\n    assert div2.height == 2\n\n\n@assert_no_logs\ndef test_flex_item_min_width():\n    page, = render_pages('''\n      <article style=\"display: flex\">\n        <div style=\"min-width: 30px\">A</div>\n        <div style=\"min-width: 50px\">B</div>\n        <div style=\"min-width: 5px\">C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x == 0\n    assert div1.width == 30\n    assert div2.position_x == 30\n    assert div2.width == 50\n    assert div3.position_x == 80\n    assert div3.width > 5\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n\n\n@assert_no_logs\ndef test_flex_item_min_height():\n    page, = render_pages('''\n      <article style=\"display: flex\">\n        <div style=\"min-height: 30px\">A</div>\n        <div style=\"min-height: 50px\">B</div>\n        <div style=\"min-height: 5px\">C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.height == div2.height == div3.height == article.height == 50\n\n\n@assert_no_logs\ndef test_flex_auto_margin():\n    # Regression test for issue #800.\n    page, = render_pages('<div style=\"display: flex; margin: auto\"><div>')\n    page, = render_pages(\n        '<div style=\"display: flex; flex-direction: column; margin: auto\"><div>')\n\n\n@assert_no_logs\ndef test_flex_item_auto_margin_sized():\n    # Regression test for issue #2054.\n    page, = render_pages('''\n      <style>\n        div {\n          margin: auto;\n          display: flex;\n          width: 160px;\n          height: 160px;\n        }\n      </style>\n      <article>\n        <div></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.margin_left != 0\n\n\n@assert_no_logs\ndef test_flex_no_baseline():\n    # Regression test for issue #765.\n    page, = render_pages('''\n      <div class=\"references\" style=\"display: flex; align-items: baseline;\">\n        <div></div>\n      </div>''')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('align', 'height', 'y1', 'y2'), [\n    ('flex-start', 50, 0, 10),\n    ('flex-end', 50, 30, 40),\n    ('space-around', 60, 10, 40),\n    ('space-between', 50, 0, 40),\n    ('space-evenly', 50, 10, 30),\n])\ndef test_flex_align_content(align, height, y1, y2):\n    # Regression test for issue #811.\n    page, = render_pages('''\n      <style>\n        article {\n          align-content: %s;\n          display: flex;\n          flex-wrap: wrap;\n          font-family: weasyprint;\n          font-size: 10px;\n          height: %dpx;\n          line-height: 1;\n        }\n        section {\n          width: 100%%;\n        }\n      </style>\n      <article>\n        <section><span>Lorem</span></section>\n        <section><span>Lorem</span></section>\n      </article>\n    ''' % (align, height))\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    section1, section2 = article.children\n    line1, = section1.children\n    line2, = section2.children\n    span1, = line1.children\n    span2, = line2.children\n    assert section1.position_x == span1.position_x == 0\n    assert section1.position_y == span1.position_y == y1\n    assert section2.position_x == span2.position_x == 0\n    assert section2.position_y == span2.position_y == y2\n\n\n@assert_no_logs\ndef test_flex_item_percentage():\n    # Regression test for issue #885.\n    page, = render_pages('''\n      <div style=\"display: flex; font-size: 15px; line-height: 1\">\n        <div style=\"height: 100%\">a</div>\n      </div>''')\n    html, = page.children\n    body, = html.children\n    flex, = body.children\n    flex_item, = flex.children\n    assert flex_item.height == 15\n\n\n@assert_no_logs\ndef test_flex_undefined_percentage_height_multiple_lines():\n    # Regression test for issue #1204.\n    page, = render_pages('''\n      <div style=\"display: flex; flex-wrap: wrap; height: 100%\">\n        <div style=\"width: 100%\">a</div>\n        <div style=\"width: 100%\">b</div>\n      </div>''')\n\n\n@assert_no_logs\ndef test_flex_absolute():\n    # Regression test for issue #1536.\n    page, = render_pages('''\n      <div style=\"display: flex; position: absolute\">\n        <div>a</div>\n      </div>''')\n\n\n@assert_no_logs\ndef test_flex_percent_height():\n    page, = render_pages('''\n      <style>\n        .a { height: 10px; width: 10px; }\n        .b { height: 10%; width: 100%; display: flex; flex-direction: column; }\n      </style>\n      <div class=\"a\"\">\n        <div class=\"b\"></div>\n      </div>''')\n    html, = page.children\n    body, = html.children\n    a, = body.children\n    b, = a.children\n    assert a.height == 10\n    assert b.height == 1\n\n\n@assert_no_logs\ndef test_flex_percent_height_auto():\n    # Regression test for issue #2146.\n    page, = render_pages('''\n      <style>\n        .a { width: 10px; }\n        .b { height: 10%; width: 100%; display: flex; flex-direction: column; }\n      </style>\n      <div class=\"a\"\">\n        <div class=\"b\"></div>\n      </div>''')\n\n\n@assert_no_logs\ndef test_flex_break_inside_avoid():\n    # Regression test for issue #2183.\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 6px 4px }\n        html { font-family: weasyprint; font-size: 2px }\n      </style>\n      <article style=\"display: flex; flex-wrap: wrap\">\n        <div>ABC</div>\n        <div style=\"break-inside: avoid\">abc def</div>\n      </article>''')\n    html, = page1.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    html, = page2.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n\n\n@assert_no_logs\ndef test_flex_absolute_content():\n    # Regression test for issue #996.\n    page, = render_pages('''\n      <section style=\"display: flex; position: relative\">\n         <h1 style=\"position: absolute; top: 0; right: 0\">TEST</h1>\n         <p>Hello world!</p>\n      </section>''')\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    h1, p = section.children\n    assert h1.position_x != 0\n    assert h1.position_y == 0\n    assert p.position_x == 0\n    assert p.position_y == 0\n\n\n@assert_no_logs\ndef test_flex_column_height():\n    # Regression test for issue #2222.\n    page, = render_pages('''\n      <section style=\"display: flex; width: 10em\">\n        <article style=\"display: flex; flex-direction: column\">\n          <div>\n            Lorem ipsum dolor sit amet\n          </div>\n        </article>\n        <article style=\"display: flex; flex-direction: column\">\n          <div>\n            Lorem ipsum dolor sit amet\n          </div>\n        </article>\n      </section>\n    ''')\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    article1, article2 = section.children\n    assert article1.height == section.height\n    assert article2.height == section.height\n\n\n@assert_no_logs\ndef test_flex_column_height_margin():\n    # Regression test for issue #2222.\n    page, = render_pages('''\n      <section style=\"display: flex; flex-direction: column; width: 10em\">\n        <article style=\"margin: 5px\">\n          Lorem ipsum dolor sit amet\n        </article>\n        <article style=\"margin: 10px\">\n          Lorem ipsum dolor sit amet\n        </article>\n      </section>\n    ''')\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    article1, article2 = section.children\n    assert section.height == article1.margin_height() + article2.margin_height()\n\n\n@assert_no_logs\ndef test_flex_column_width():\n    # Regression test for issue #1171.\n    page, = render_pages('''\n      <main style=\"display: flex; flex-direction: column;\n                   width: 40px; height: 50px; font: 2px weasyprint\">\n        <section style=\"width: 100%; height: 5px\">a</section>\n        <section style=\"display: flex; flex: auto; flex-direction: column;\n                        justify-content: space-between; width: 100%\">\n          <div>b</div>\n          <div>c</div>\n        </section>\n      </main>\n    ''')\n    html, = page.children\n    body, = html.children\n    main, = body.children\n    section1, section2 = main.children\n    div1, div2 = section2.children\n    assert section1.width == section2.width == main.width\n    assert div1.width == div2.width\n    assert div1.position_y == 5\n    assert div2.position_y == 48\n\n\n@assert_no_logs\ndef test_flex_column_in_flex_row():\n    page, = render_pages('''\n      <body style=\"display: flex; flex-wrap: wrap; font: 2px weasyprint\">\n        <article>1</article>\n        <section style=\"display: flex; flex-direction: column\">\n          <div>2</div>\n        </section>\n      </body>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, section = body.children\n    assert article.position_y == section.position_y == 0\n    assert article.position_x == 0\n    assert section.position_x == 2\n\n\n@assert_no_logs\ndef test_flex_overflow():\n    # Regression test for issue #2292.\n    page, = render_pages('''\n      <style>\n        article {\n          display: flex;\n        }\n        section {\n          overflow: hidden;\n          width: 5px;\n        }\n      </style>\n      <article>\n        <section>A</section>\n        <section>B</section>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    section_1 = article.children[0]\n    section_2 = article.children[1]\n    assert section_1.position_x == 0\n    assert section_2.position_x == 5\n\n\n@assert_no_logs\ndef test_flex_column_overflow():\n    # Regression test for issue #2304.\n    render_pages('''\n      <style>\n        @page {size: 20px}\n      </style>\n      <div style=\"display: flex; flex-direction: column\">\n        <div></div>\n        <div><div style=\"height: 40px\"></div></div>\n        <div><div style=\"height: 5px\"></div></div>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_inline_flex():\n    render_pages('''\n      <style>\n        @page {size: 20px}\n      </style>\n      <div style=\"display: inline-flex; flex-direction: column\">\n        <div>test</div>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_inline_flex_empty_child():\n    render_pages('''\n      <style>\n        @page {size: 20px}\n      </style>\n      <div style=\"display: inline-flex; flex-direction: column\">\n        <div></div>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_inline_flex_absolute_baseline():\n    render_pages('''\n      <style>\n        @page {size: 20px}\n      </style>\n      <div style=\"display: inline-flex; flex-direction: column\">\n        <div style=\"position: absolute\">abs</div>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_flex_item_overflow():\n    # Regression test for issue #2359.\n    page, = render_pages('''\n      <div style=\"display: flex; font: 2px weasyprint; width: 12px\">\n        <div>ab</div>\n        <div>c d e</div>\n        <div>f</div>\n      </div>''')\n    html, = page.children\n    body, = html.children\n    flex, = body.children\n    div1, div2, div3 = flex.children\n    assert div1.width == 4\n    assert div2.width == 6\n    assert div3.width == 2\n    line1, line2 = div2.children\n    text1, = line1.children\n    text2, = line2.children\n    assert text1.text == 'c d'\n    assert text2.text == 'e'\n\n\n@assert_no_logs\n@pytest.mark.parametrize('direction', ['row', 'column'])\ndef test_flex_item_child_bottom_margin(direction):\n    # Regression test for issue #2449.\n    page, = render_pages('''\n      <div style=\"display: flex; font: 2px weasyprint; flex-direction: %s\">\n        <section>\n          <div style=\"margin: 2px 0\">ab</div>\n        </section>\n      </div>''' % direction)\n    html, = page.children\n    body, = html.children\n    flex, = body.children\n    assert flex.content_box_y() == 0\n    assert flex.height == 6\n    section, = flex.children\n    assert section.content_box_y() == 0\n    assert section.height == 6\n    div, = section.children\n    assert div.content_box_y() == 2\n    assert div.height == 2\n\n\n@assert_no_logs\ndef test_flex_direction_row_inline_block():\n    # Regression test for issue #1652.\n    page, = render_pages('''\n      <article style=\"display: flex; font: 2px weasyprint; width: 14px\">\n        <div style=\"display: inline-block\">A B C D E F</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.width == 14\n    assert div.children[0].children[0].text == 'A B C D'\n    assert div.children[1].children[0].text == 'E F'\n\n\n@assert_no_logs\ndef test_flex_float():\n    page, = render_pages('''\n      <article style=\"display: flex\">\n        <div style=\"float: left\">A</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.children[0].children[0].text == 'A'\n\n\n@assert_no_logs\ndef test_flex_float_in_flex_item():\n    # Regression test for issue #1356.\n    page, = render_pages('''\n      <article style=\"display: flex; font: 2px weasyprint\">\n        <div style=\"width: 10px\"><span style=\"float: right\">abc</span></div>\n        <div style=\"width: 10px\"><span style=\"float: right\">abc</span></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2 = article.children\n    span1, = div1.children\n    assert span1.position_y == 0\n    assert span1.position_x + span1.width == 10\n    span2, = div2.children\n    assert span2.position_y == 0\n    assert span2.position_x + span2.width == 20\n\n\n@assert_no_logs\ndef test_flex_direction_row_defined_main():\n    page, = render_pages('''\n      <article style=\"display: flex\">\n        <div style=\"width: 10px; padding: 1px\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.width == 10\n    assert div.margin_width() == 12\n\n\n@assert_no_logs\ndef test_flex_direction_row_defined_main_border_box():\n    page, = render_pages('''\n      <article style=\"display: flex\">\n        <div style=\"box-sizing: border-box; width: 10px; padding: 1px\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.width == 8\n    assert div.margin_width() == 10\n\n\n@assert_no_logs\ndef test_flex_direction_column_defined_main():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: column\">\n        <div style=\"height: 10px; padding: 1px\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.height == 10\n    assert div.margin_height() == 12\n\n\n@assert_no_logs\ndef test_flex_direction_column_defined_main_border_box():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: column\">\n        <div style=\"box-sizing: border-box; height: 10px; padding: 1px\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.height == 8\n    assert div.margin_height() == 10\n\n\n@assert_no_logs\ndef test_flex_item_negative_margin():\n    page, = render_pages('''\n      <article style=\"display: flex\">\n        <div style=\"margin-left: -20px; height: 10px; width: 10px\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.height == 10\n    assert div.width == 10\n\n\n@assert_no_logs\ndef test_flex_item_auto_margin_main():\n    page, = render_pages('''\n      <article style=\"display: flex; width: 100px\">\n        <div style=\"margin-left: auto; height: 10px; width: 10px\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.height == 10\n    assert div.width == 10\n    assert div.margin_left == 90\n\n\n@assert_no_logs\ndef test_flex_item_auto_margin_cross():\n    page, = render_pages('''\n      <article style=\"display: flex; height: 100px\">\n        <div style=\"margin-top: auto; height: 10px; width: 10px\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.height == 10\n    assert div.width == 10\n    assert div.margin_top == 90\n\n\n@assert_no_logs\ndef test_flex_direction_column_item_auto_margin():\n    page, = render_pages('''\n      <div style=\"font: 2px weasyprint; width: 30px; display: flex;\n                  flex-direction: column; align-items: flex-start\">\n          <article style=\"margin: 0 auto\">XXXX</article>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    article, = div.children\n    assert article.width == 8\n    assert article.margin_left == 11\n\n\n@assert_no_logs\ndef test_flex_item_auto_margin_flex_basis():\n    page, = render_pages('''\n      <article style=\"display: flex\">\n        <div style=\"margin-left: auto; height: 10px; flex-basis: 10px\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert div.height == 10\n    assert div.width == 10\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('align', 'x1', 'x2', 'x3'), [\n    ('start', 0, 2, 4),\n    ('flex-start', 0, 2, 4),\n    ('left', 0, 2, 4),\n    ('end', 6, 8, 10),\n    ('flex-end', 6, 8, 10),\n    ('right', 6, 8, 10),\n    ('center', 3, 5, 7),\n    ('space-between', 0, 5, 10),\n    ('space-around', 1, 5, 9),\n    ('space-evenly', 1.5, 5, 8.5),\n])\ndef test_flex_direction_row_justify(align, x1, x2, x3):\n    page, = render_pages(f'''\n      <article style=\"width: 12px; font: 2px weasyprint;\n                      display: flex; justify-content: {align}\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert article.position_x == 0\n    assert div1.position_x == x1\n    assert div2.position_x == x2\n    assert div3.position_x == x3\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('align', 'y1', 'y2', 'y3'), [\n    ('start', 0, 2, 4),\n    ('flex-start', 0, 2, 4),\n    ('left', 0, 2, 4),\n    ('end', 6, 8, 10),\n    ('flex-end', 6, 8, 10),\n    ('right', 6, 8, 10),\n    ('center', 3, 5, 7),\n    ('space-between', 0, 5, 10),\n    ('space-around', 1, 5, 9),\n    ('space-evenly', 1.5, 5, 8.5),\n])\ndef test_flex_direction_column_justify(align, y1, y2, y3):\n    page, = render_pages(f'''\n      <article style=\"height: 12px; font: 2px weasyprint;\n                      display: flex; flex-direction: column; justify-content: {align}\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x == div2.position_x == div3.position_x == article.position_x\n    assert article.position_y == 0\n    assert div1.position_y == y1\n    assert div2.position_y == y2\n    assert div3.position_y == y3\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('align', 'x1', 'x2', 'x3'), [\n    ('start', 0, 4, 8),\n    ('flex-start', 0, 4, 8),\n    ('left', 0, 4, 8),\n    ('end', 6, 10, 14),\n    ('flex-end', 6, 10, 14),\n    ('right', 6, 10, 14),\n    ('center', 3, 7, 11),\n    ('space-between', 0, 7, 14),\n    ('space-around', 1, 7, 13),\n    ('space-evenly', 1.5, 7, 12.5),\n])\ndef test_flex_direction_row_justify_gap(align, x1, x2, x3):\n    page, = render_pages(f'''\n      <article style=\"width: 16px; font: 2px weasyprint; gap: 2px;\n                      display: flex; justify-content: {align}\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert article.position_x == 0\n    assert div1.position_x == x1\n    assert div2.position_x == x2\n    assert div3.position_x == x3\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('align', 'y1', 'y2', 'y3'), [\n    ('start', 0, 4, 8),\n    ('flex-start', 0, 4, 8),\n    ('left', 0, 4, 8),\n    ('end', 6, 10, 14),\n    ('flex-end', 6, 10, 14),\n    ('right', 6, 10, 14),\n    ('center', 3, 7, 11),\n    ('space-between', 0, 7, 14),\n    ('space-around', 1, 7, 13),\n    ('space-evenly', 1.5, 7, 12.5),\n])\ndef test_flex_direction_column_justify_gap(align, y1, y2, y3):\n    page, = render_pages(f'''\n      <article style=\"height: 16px; font: 2px weasyprint; gap: 2px;\n                      display: flex; flex-direction: column; justify-content: {align}\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x == div2.position_x == div3.position_x == article.position_x\n    assert article.position_y == 0\n    assert div1.position_y == y1\n    assert div2.position_y == y2\n    assert div3.position_y == y3\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('align', 'x1', 'x2', 'x3'), [\n    ('start', 0, 4, 0),\n    ('flex-start', 0, 4, 0),\n    ('left', 0, 4, 0),\n    ('end', 3, 7, 7),\n    ('flex-end', 3, 7, 7),\n    ('right', 3, 7, 7),\n    ('center', 1.5, 5.5, 3.5),\n    ('space-between', 0, 7, 0),\n    ('space-around', 0.75, 6.25, 3.5),\n    ('space-evenly', 1, 6, 3.5),\n])\ndef test_flex_direction_row_justify_gap_wrap(align, x1, x2, x3):\n    page, = render_pages(f'''\n      <article style=\"width: 9px; font: 2px weasyprint; gap: 1px 2px;\n                      display: flex; flex-wrap: wrap; justify-content: {align}\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_y == div2.position_y == article.position_y == 0\n    assert div3.position_y == 3\n    assert article.position_x == 0\n    assert div1.position_x == x1\n    assert div2.position_x == x2\n    assert div3.position_x == x3\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('align', 'y1', 'y2', 'y3'), [\n    ('start', 0, 4, 0),\n    ('flex-start', 0, 4, 0),\n    ('left', 0, 4, 0),\n    ('end', 3, 7, 7),\n    ('flex-end', 3, 7, 7),\n    ('right', 3, 7, 7),\n    ('center', 1.5, 5.5, 3.5),\n    ('space-between', 0, 7, 0),\n    ('space-around', 0.75, 6.25, 3.5),\n    ('space-evenly', 1, 6, 3.5),\n])\ndef test_flex_direction_column_justify_gap_wrap(align, y1, y2, y3):\n    page, = render_pages(f'''\n      <article style=\"height: 9px; width: 9px; font: 2px weasyprint; gap: 2px 1px;\n                      display: flex; flex-wrap: wrap; flex-direction: column;\n                      justify-content: {align}\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x == div2.position_x == article.position_x == 0\n    assert div3.position_x == 5\n    assert article.position_y == 0\n    assert div1.position_y == y1\n    assert div2.position_y == y2\n    assert div3.position_y == y3\n\n\n@assert_no_logs\ndef test_flex_direction_row_stretch_no_grow():\n    page, = render_pages('''\n      <article style=\"font: 2px weasyprint; width: 10px;\n                      display: flex; justify-content: stretch\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert div1.width == div2.width == div3.width == 2\n    assert div1.position_x == article.position_x == 0\n    assert div2.position_x == 2\n    assert div3.position_x == 4\n\n\n@assert_no_logs\ndef test_flex_direction_row_stretch_grow():\n    page, = render_pages('''\n      <article style=\"font: 2px weasyprint; width: 10px;\n                      display: flex; justify-content: stretch\">\n        <div>A</div>\n        <div style=\"flex-grow: 3\">B</div>\n        <div style=\"flex-grow: 1\">C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert div1.width == 2\n    assert div2.width == 5\n    assert div3.width == 3\n    assert div1.position_x == article.position_x == 0\n    assert div2.position_x == 2\n    assert div3.position_x == 7\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('align', 'x1', 'x2', 'x3'), [\n    ('start', 0, 6, 12),\n    ('flex-start', 0, 6, 12),\n    ('left', 0, 6, 12),\n    ('end', 6, 12, 18),\n    ('flex-end', 6, 12, 18),\n    ('right', 6, 12, 18),\n    ('center', 3, 9, 15),\n    ('space-between', 0, 9, 18),\n    ('space-around', 1, 9, 17),\n    ('space-evenly', 1.5, 9, 16.5),\n])\ndef test_flex_direction_row_justify_margin_padding(align, x1, x2, x3):\n    page, = render_pages(f'''\n      <article style=\"width: 20px; font: 2px weasyprint;\n                      display: flex; justify-content: {align}\">\n        <div style=\"margin: 0 1em\">A</div>\n        <div style=\"padding: 0 1em\">B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_y == div2.position_y == div3.position_y == article.position_y\n    assert article.position_x == 0\n    assert article.width == 20\n    assert div1.position_x == x1\n    assert div2.position_x == x2\n    assert div3.position_x == x3\n    assert div1.margin_width() == 6\n    assert div2.margin_width() == 6\n    assert div3.margin_width() == 2\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('align', 'y1', 'y2', 'y3'), [\n    ('start', 0, 6, 12),\n    ('flex-start', 0, 6, 12),\n    ('left', 0, 6, 12),\n    ('end', 6, 12, 18),\n    ('flex-end', 6, 12, 18),\n    ('right', 6, 12, 18),\n    ('center', 3, 9, 15),\n    ('space-between', 0, 9, 18),\n    ('space-around', 1, 9, 17),\n    ('space-evenly', 1.5, 9, 16.5),\n])\ndef test_flex_direction_column_justify_margin_padding(align, y1, y2, y3):\n    page, = render_pages(f'''\n      <article style=\"height: 20px; font: 2px weasyprint;\n                      display: flex; flex-direction: column; justify-content: {align}\">\n        <div style=\"margin: 1em 0\">A</div>\n        <div style=\"padding: 1em 0\">B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2, div3 = article.children\n    assert div1.children[0].children[0].text == 'A'\n    assert div2.children[0].children[0].text == 'B'\n    assert div3.children[0].children[0].text == 'C'\n    assert div1.position_x == div2.position_x == div3.position_x == article.position_x\n    assert article.position_y == 0\n    assert article.height == 20\n    assert div1.position_y == y1\n    assert div2.position_y == y2\n    assert div3.position_y == y3\n    assert div1.margin_height() == 6\n    assert div2.margin_height() == 6\n    assert div3.margin_height() == 2\n\n\n@assert_no_logs\ndef test_flex_item_table():\n    # Regression test for issue #1805.\n    page, = render_pages('''\n      <article style=\"display: flex; font: 2px weasyprint\">\n        <table><tr><td>A</tr></td></table>\n        <table><tr><td>B</tr></td></table>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    table_wrapper1, table_wrapper2 = article.children\n    assert table_wrapper1.width == table_wrapper2.width == 2\n    assert table_wrapper1.position_x == 0\n    assert table_wrapper2.position_x == 2\n\n\n@assert_no_logs\ndef test_flex_item_table_width():\n    # Regression test for issue #1805.\n    page, = render_pages('''\n      <article style=\"display: flex; font: 2px weasyprint; width: 40px\">\n        <table style=\"width: 25%\"><tr><td>A</tr></td></table>\n        <table style=\"width: 25%\"><tr><td>B</tr></td></table>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    table_wrapper1, table_wrapper2 = article.children\n    assert table_wrapper1.width == table_wrapper2.width == 10\n    assert table_wrapper1.position_x == 0\n    assert table_wrapper2.position_x == 10\n\n\n@assert_no_logs\ndef test_flex_width_on_parent():\n    page, = render_pages('''\n      <div style=\"font: 2px weasyprint; width: 30px; display: flex;\n                  flex-direction: column; align-items: flex-start\">\n          <article>XXXX</article>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    article, = div.children\n    assert article.width == 8\n\n\n@assert_no_logs\ndef test_flex_column_item_flex_1():\n    page, = render_pages('''\n      <div style=\"font: 2px weasyprint; display: flex; flex-direction: column\">\n          <article>XXXX</article>\n          <article style=\"flex: 1\">XXXX</article>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.height == 4\n\n\n@assert_no_logs\ndef test_flex_row_item_flex_0():\n    page, = render_pages('''\n      <div style=\"font: 2px weasyprint; display: flex\">\n          <article style=\"flex: 0\">XXXX</article>\n          <article style=\"flex: 0\">XXXX</article>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    article1, article2 = div.children\n    assert article1.position_x == 0\n    assert article1.width == 8\n    assert article2.position_x == 8\n    assert article2.width == 8\n\n\n@assert_no_logs\ndef test_flex_item_intrinsic_width():\n    page, = render_pages('''\n      <div style=\"width: 100px; height: 100px;\n                  display: flex; flex-direction: column; align-items: center\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 200 100\">\n          <rect width=\"200\" height=\"100\" fill=\"red\" />\n        </svg>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    svg, = div.children\n    assert svg.width == 100\n\n\n@assert_no_logs\ndef test_flex_align_content_negative():\n    page, = render_pages('''\n      <div style=\"height: 6px; width: 20px;\n                  display: flex; flex-wrap: wrap; align-content: center\">\n        <span style=\"height: 2px; flex: none; margin: 1px; width: 8px\"></span>\n        <span style=\"height: 2px; flex: none; margin: 1px; width: 8px\"></span>\n        <span style=\"height: 2px; flex: none; margin: 1px; width: 8px\"></span>\n        <span style=\"height: 2px; flex: none; margin: 1px; width: 8px\"></span>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    span1, span2, span3, span4 = div.children\n    span1.height == span2.height == span3.height == span4.height == 2\n    span1.width == span2.width == span3.width == span4.width == 8\n    assert span1.position_x == span3.position_x == 0\n    assert span2.position_x == span4.position_x == 10\n    assert span1.position_y == span2.position_y == -1\n    assert span3.position_y == span4.position_y == 3\n\n\n@assert_no_logs\ndef test_flex_shrink():\n    page, = render_pages('''\n      <article style=\"display: flex; width: 300px\">\n        <div style=\"flex: 0 2 auto; width: 300px\"></div>\n        <div style=\"width: 200px\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2 = article.children\n    assert div1.width == div2.width == 150\n\n\n@assert_no_logs\ndef test_flex_item_intrinsic_width_shrink():\n    page, = render_pages('''\n      <div style=\"width: 10px; height: 100px; display: flex; flex-direction: column\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" width=\"100\">\n          <rect width=\"100\" height=\"100\" fill=\"red\" />\n        </svg>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    svg, = div.children\n    assert svg.width == 100\n\n\n@assert_no_logs\ndef test_flex_item_intrinsic_height_shrink():\n    page, = render_pages('''\n      <div style=\"width: 100px; height: 10px; display: flex; line-height: 0\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" height=\"100\">\n          <rect width=\"100\" height=\"100\" fill=\"red\" />\n        </svg>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    svg, = div.children\n    assert svg.height == 100\n\n\n@assert_no_logs\ndef test_flex_wrap_in_flex():\n    page, = render_pages('''\n      <main style=\"display: flex; font: 2px weasyprint\">\n        <div style=\"display: flex; flex-wrap: wrap\">\n          <section style=\"width: 25%\">A</section>\n          <section style=\"flex: 1 75%\">B</section>\n        </div>\n      </main>\n    ''')\n    html, = page.children\n    body, = html.children\n    main, = body.children\n    div, = main.children\n    section1, section2 = div.children\n    assert section1.position_y == section2.position_y == 0\n    assert section1.position_x == 0\n    assert section2.position_x == 1  # 25% * 4\n\n\n@assert_no_logs\ndef test_flex_auto_break_before():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 4px 5px }\n        body { font: 2px weasyprint }\n      </style>\n      <p>A<br>B</p>\n      <article style=\"display: flex\">\n        <div>A</div>\n      </article>\n    ''')\n    html, = page1.children\n    body, = html.children\n    p, = body.children\n    assert p.height == 4\n    html, = page2.children\n    body, = html.children\n    article, = body.children\n    assert article.height == 2\n\n\n@assert_no_logs\ndef test_flex_grow_in_flex_column():\n    page, = render_pages('''\n      <html style=\"width: 14px\">\n        <body style=\"display: flex; flex-direction: column;\n                     border: 1px solid; padding: 1px\">\n          <main style=\"flex: 1 1 auto; min-height: 0\">\n            <div style=\"height: 5px\">\n    ''')\n    html, = page.children\n    body, = html.children\n    main, = body.children\n    _, div, _ = main.children\n    assert body.height == div.height == 5\n    assert body.width == div.width == 10\n    assert body.margin_width() == 14\n    assert body.margin_height() == 9\n\n\n@assert_no_logs\ndef test_flex_collapsing_margin():\n    page, = render_pages('''\n      <p style=\"margin-bottom: 20px; height: 100px\">ABC</p>\n      <article style=\"display: flex; margin-top: 10px\">\n        <div>A</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    p, article = body.children\n    div, = article.children\n    assert p.position_y == 0\n    assert p.height == 100\n    assert article.position_y == 110\n    assert div.position_y == 120\n\n\n@assert_no_logs\ndef test_flex_direction_column_next_page():\n    # Regression test for issue #2414.\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 4px 5px }\n        html { font: 2px/1 weasyprint }\n      </style>\n      <div>1</div>\n      <article style=\"display: flex; flex-direction: column\">\n        <div>A</div>\n        <div>B</div>\n        <div>C</div>\n      </article>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, article = body.children\n    assert div.children[0].children[0].text == '1'\n    assert div.children[0].children[0].position_y == 0\n    assert article.children[0].children[0].children[0].text == 'A'\n    assert article.children[0].children[0].children[0].position_y == 2\n    html, = page2.children\n    body, = html.children\n    article, = body.children\n    assert article.children[0].children[0].children[0].text == 'B'\n    assert article.children[0].children[0].children[0].position_y == 0\n    assert article.children[1].children[0].children[0].text == 'C'\n    assert article.children[1].children[0].children[0].position_y == 2\n\n\n@assert_no_logs\ndef test_flex_1_item_padding():\n    page, = render_pages('''\n      <article style=\"display: flex; width: 100px; font: 2px weasyprint\">\n        <div>abc</div>\n        <div style=\"flex: 1; padding-right: 5em\">def</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2 = article.children\n    assert div1.border_width() + div2.border_width() == article.width\n\n\n@assert_no_logs\ndef test_flex_1_item_padding_direction_column():\n    page, = render_pages('''\n      <article style=\"display: flex; flex-direction: column; height: 100px;\n                      font: 2px weasyprint\">\n        <div>abc</div>\n        <div style=\"flex: 1; padding-top: 5em\">def</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2 = article.children\n    assert div1.border_height() + div2.border_height() == article.height\n\n\n@assert_no_logs\ndef test_flex_item_replaced():\n    page, = render_pages('''\n      <div style=\"display: flex\">\n        <svg style=\"display: block\" height=\"100\" width=\"100\" xmlns=\"http://www.w3.org/2000/svg\">\n          <circle r=\"45\" cx=\"50\" cy=\"50\" fill=\"red\" />\n        </svg>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    svg, = div.children\n    assert svg.width == svg.height == 100\n\n\n@assert_no_logs\ndef test_flex_nested_column():\n    # Regression test for issue #2442.\n    page, = render_pages('''\n      <section style=\"display: flex; flex-direction: column; width: 200px\">\n        <div style=\"display: flex; flex-direction: column\">\n          <p>\n            A\n          </p>\n        </div>\n      </section>\n    ''')\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    div, = section.children\n    p, = div.children\n    assert p.width == 200\n\n\n@assert_no_logs\ndef test_flex_blockify_image():\n    page, = render_pages('''\n      <article style=\"display: flex; line-height: 2\">\n        <img src=\"pattern.png\">\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    img, = article.children\n    assert article.height == img.height == 4\n\n\n@assert_no_logs\ndef test_flex_image_max_width():\n    page, = render_pages('''\n      <article style=\"display: flex\">\n        <img src=\"pattern.png\" style=\"max-width: 2px\">\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    img, = article.children\n    assert article.height == img.height == img.width == 2\n\n\n@assert_no_logs\ndef test_flex_image_max_height():\n    page, = render_pages('''\n      <article style=\"display: flex\">\n        <img src=\"pattern.png\" style=\"max-height: 2px\">\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    img, = article.children\n    assert article.height == img.height == img.width == 2\n\n\n@assert_no_logs\ndef test_flex_image_min_width():\n    page, = render_pages('''\n      <article style=\"display: flex; width: 20px\">\n        <img style=\"min-width: 10px; flex: 1 0 auto\" src=\"pattern.png\">\n        <div style=\"flex: 1 0 1px\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    img, div = article.children\n    assert article.height == img.height == img.width == div.height == 10\n\n\n@assert_no_logs\ndef test_flex_image_justify_content():\n    page, = render_pages('''\n      <article style=\"width: 100px; display: flex; justify-content: end\">\n        <img src=\"pattern.png\">\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    img, = article.children\n    assert article.height == img.height == img.width == 4\n    assert article.width == 100\n    assert img.position_x == 96\n\n\n@assert_no_logs\ndef test_flex_root_formatting_context():\n    page, = render_pages('''\n      <html style=\"display: flex\">\n        <div>A</div>\n      </html>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.children[0].children[0].text == 'A'\n    assert div.position_y == body.position_y\n    assert div.position_x == body.position_x\n\n\n@assert_no_logs\ndef test_flex_height_page_overflow():\n    # Regression test for #2689.\n    page, = render_pages('''\n      <style>\n        @page { size: 4px 6px }\n      </style>\n      <section style=\"display: flex; font: 2px weasyprint; height: 2px\">\n        <div style=\"width: 100%; display: flex; align-content: flex-start\">a b c d</div>\n      </section>\n    ''')\n    html, = page.children\n    body, = html.children\n    section, = body.children\n    div, = section.children\n    assert len(div.children[0].children) == 3\n\n\n@assert_no_logs\ndef test_flex_break_after_page():\n    # Regression test for #2469.\n    page1, page2, page3 = render_pages('''\n      <style>\n        @page { size: 5px }\n        body {\n          display: flex; flex-direction: column;\n          font: 2px/1 weasyprint; margin-top: 1px;\n        }\n      </style>\n      <div style=\"height: 10px\">a</div>\n      <section>\n        <div style=\"break-after: page\">b</div>\n        <div>c</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    assert div.position_y == 1\n    assert div.height == 10\n    assert div.children[0].children[0].text == 'a'\n    html, = page2.children\n    body, = html.children\n    assert body.position_y == 0\n    assert body.height == 5\n    section, = body.children\n    div, = section.children\n    assert div.position_y == 0\n    assert div.height == 2\n    assert div.children[0].children[0].text == 'b'\n    html, = page3.children\n    body, = html.children\n    assert body.position_y == 0\n    assert body.height == 2\n    section, = body.children\n    div, = section.children\n    assert div.position_y == 0\n    assert div.height == 2\n    assert div.children[0].children[0].text == 'c'\n"
  },
  {
    "path": "tests/layout/test_float.py",
    "content": "\"\"\"Tests for floating boxes layout.\"\"\"\n\nimport pytest\n\nfrom weasyprint.formatting_structure import boxes\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\ndef outer_area(box):\n    \"\"\"Return the (x, y, w, h) rectangle for the outer area of a box.\"\"\"\n    return (box.position_x, box.position_y,\n            box.margin_width(), box.margin_height())\n\n\n@assert_no_logs\ndef test_floats_1():\n    # adjacent-floats-001\n    page, = render_pages('''\n      <style>\n        div { float: left }\n        img { width: 100px; vertical-align: top }\n      </style>\n      <div><img src=pattern.png /></div>\n      <div><img src=pattern.png /></div>''')\n    html, = page.children\n    body, = html.children\n    div_1, div_2 = body.children\n    assert outer_area(div_1) == (0, 0, 100, 100)\n    assert outer_area(div_2) == (100, 0, 100, 100)\n\n\n@assert_no_logs\ndef test_floats_2():\n    # c414-flt-fit-000\n    page, = render_pages('''\n      <style>\n        body { width: 290px }\n        div { float: left; width: 100px;  }\n        img { width: 60px; vertical-align: top }\n      </style>\n      <div><img src=pattern.png /><!-- 1 --></div>\n      <div><img src=pattern.png /><!-- 2 --></div>\n      <div><img src=pattern.png /><!-- 4 --></div>\n      <img src=pattern.png /><!-- 3\n      --><img src=pattern.png /><!-- 5 -->''')\n    html, = page.children\n    body, = html.children\n    div_1, div_2, div_4, anon_block = body.children\n    line_3, line_5 = anon_block.children\n    img_3, = line_3.children\n    img_5, = line_5.children\n    assert outer_area(div_1) == (0, 0, 100, 60)\n    assert outer_area(div_2) == (100, 0, 100, 60)\n    assert outer_area(img_3) == (200, 0, 60, 60)\n\n    assert outer_area(div_4) == (0, 60, 100, 60)\n    assert outer_area(img_5) == (100, 60, 60, 60)\n\n\n@assert_no_logs\ndef test_floats_3():\n    # c414-flt-fit-002\n    page, = render_pages('''\n      <style type=\"text/css\">\n        body { width: 200px }\n        p { width: 70px; height: 20px }\n        .left { float: left }\n        .right { float: right }\n      </style>\n      <p class=\"left\"> ⇦ A 1 </p>\n      <p class=\"left\"> ⇦ B 2 </p>\n      <p class=\"left\"> ⇦ A 3 </p>\n      <p class=\"right\"> B 4 ⇨ </p>\n      <p class=\"left\"> ⇦ A 5 </p>\n      <p class=\"right\"> B 6 ⇨ </p>\n      <p class=\"right\"> B 8 ⇨ </p>\n      <p class=\"left\"> ⇦ A 7 </p>\n      <p class=\"left\"> ⇦ A 9 </p>\n      <p class=\"left\"> ⇦ B 10 </p>\n    ''')\n    html, = page.children\n    body, = html.children\n    positions = [(paragraph.position_x, paragraph.position_y)\n                 for paragraph in body.children]\n    assert positions == [\n        (0, 0), (70, 0), (0, 20), (130, 20), (0, 40), (130, 40),\n        (130, 60), (0, 60), (0, 80), (70, 80), ]\n\n\n@assert_no_logs\ndef test_floats_4():\n    # c414-flt-wrap-000 ... more or less\n    page, = render_pages('''\n      <style>\n        body { width: 100px }\n        p { float: left; height: 100px }\n        img { width: 60px; vertical-align: top }\n      </style>\n      <p style=\"width: 20px\"></p>\n      <p style=\"width: 100%\"></p>\n      <img src=pattern.png /><img src=pattern.png />\n    ''')\n    html, = page.children\n    body, = html.children\n    p_1, p_2, anon_block = body.children\n    line_1, line_2 = anon_block.children\n    assert anon_block.position_y == 0\n    assert (line_1.position_x, line_1.position_y) == (20, 0)\n    assert (line_2.position_x, line_2.position_y) == (0, 200)\n\n\n@assert_no_logs\ndef test_floats_5():\n    # c414-flt-wrap-000 with text ... more or less\n    page, = render_pages('''\n      <style>\n        body { width: 100px; font: 60px weasyprint; }\n        p { float: left; height: 100px }\n        img { width: 60px; vertical-align: top }\n      </style>\n      <p style=\"width: 20px\"></p>\n      <p style=\"width: 100%\"></p>\n      A B\n    ''')\n    html, = page.children\n    body, = html.children\n    p_1, p_2, anon_block = body.children\n    line_1, line_2 = anon_block.children\n    assert anon_block.position_y == 0\n    assert (line_1.position_x, line_1.position_y) == (20, 0)\n    assert (line_2.position_x, line_2.position_y) == (0, 200)\n\n\n@assert_no_logs\ndef test_floats_6():\n    # floats-placement-vertical-001b\n    page, = render_pages('''\n      <style>\n        body { width: 90px; font-size: 0 }\n        img { vertical-align: top }\n      </style>\n      <body>\n      <span>\n        <img src=pattern.png style=\"width: 50px\" />\n        <img src=pattern.png style=\"width: 50px\" />\n        <img src=pattern.png style=\"float: left; width: 30px\" />\n      </span>\n    ''')\n    html, = page.children\n    body, = html.children\n    line_1, line_2 = body.children\n    span_1, = line_1.children\n    span_2, = line_2.children\n    img_1, = span_1.children\n    img_2, img_3 = span_2.children\n    assert outer_area(img_1) == (0, 0, 50, 50)\n    assert outer_area(img_2) == (30, 50, 50, 50)\n    assert outer_area(img_3) == (0, 50, 30, 30)\n\n\n@assert_no_logs\ndef test_floats_7():\n    # Variant of the above: no <span>\n    page, = render_pages('''\n      <style>\n        body { width: 90px; font-size: 0 }\n        img { vertical-align: top }\n      </style>\n      <body>\n      <img src=pattern.png style=\"width: 50px\" />\n      <img src=pattern.png style=\"width: 50px\" />\n      <img src=pattern.png style=\"float: left; width: 30px\" />\n    ''')\n    html, = page.children\n    body, = html.children\n    line_1, line_2 = body.children\n    img_1, = line_1.children\n    img_2, img_3 = line_2.children\n    assert outer_area(img_1) == (0, 0, 50, 50)\n    assert outer_area(img_2) == (30, 50, 50, 50)\n    assert outer_area(img_3) == (0, 50, 30, 30)\n\n\n@assert_no_logs\ndef test_floats_8():\n    # Floats do no affect other pages\n    page_1, page_2 = render_pages('''\n      <style>\n        body { width: 90px; font-size: 0 }\n        img { vertical-align: top }\n      </style>\n      <body>\n      <img src=pattern.png style=\"float: left; width: 30px\" />\n      <img src=pattern.png style=\"width: 50px\" />\n      <div style=\"page-break-before: always\"></div>\n      <img src=pattern.png style=\"width: 50px\" />\n    ''')\n    html, = page_1.children\n    body, = html.children\n    float_img, anon_block, = body.children\n    line, = anon_block.children\n    img_1, = line.children\n    assert outer_area(float_img) == (0, 0, 30, 30)\n    assert outer_area(img_1) == (30, 0, 50, 50)\n\n    html, = page_2.children\n    body, = html.children\n    div, anon_block = body.children\n    line, = anon_block.children\n    img_2, = line.children\n\n\n@assert_no_logs\ndef test_floats_9():\n    # Regression test for #263.\n    page, = render_pages('''<div style=\"top:100%; float:left\">''')\n\n\n@assert_no_logs\ndef test_floats_page_breaks_1():\n    # Tests floated images shorter than the page\n    pages = render_pages('''\n      <style>\n        @page { size: 100px; margin: 10px }\n        img { height: 45px; width:70px; float: left;}\n      </style>\n      <body>\n        <img src=pattern.png>\n          <!-- page break should be here !!! -->\n        <img src=pattern.png>\n    ''')\n\n    assert len(pages) == 2\n\n    page_images = []\n    for page in pages:\n        images = [d for d in page.descendants() if d.element_tag == 'img']\n        assert all([img.element_tag == 'img' for img in images])\n        assert all([img.position_x == 10 for img in images])\n        page_images.append(images)\n    positions_y = [[img.position_y for img in images]\n                   for images in page_images]\n    assert positions_y == [[10], [10]]\n\n\n@assert_no_logs\ndef test_floats_page_breaks_2():\n    # Tests floated images taller than the page\n    pages = render_pages('''\n      <style>\n        @page { size: 100px; margin: 10px }\n        img { height: 81px; width:70px; float: left;}\n      </style>\n      <body>\n        <img src=pattern.png>\n          <!-- page break should be here !!! -->\n        <img src=pattern.png>\n    ''')\n\n    assert len(pages) == 2\n\n    page_images = []\n    for page in pages:\n        images = [d for d in page.descendants() if d.element_tag == 'img']\n        assert all([img.element_tag == 'img' for img in images])\n        assert all([img.position_x == 10 for img in images])\n        page_images.append(images)\n    positions_y = [[img.position_y for img in images]\n                   for images in page_images]\n    assert positions_y == [[10], [10]]\n\n\n@assert_no_logs\ndef test_floats_page_breaks_3():\n    # Tests floated images shorter than the page\n    pages = render_pages('''\n      <style>\n        @page { size: 100px; margin: 10px }\n        img { height: 30px; width:70px; float: left;}\n      </style>\n      <body>\n        <img src=pattern.png>\n        <img src=pattern.png>\n          <!-- page break should be here !!! -->\n        <img src=pattern.png>\n        <img src=pattern.png>\n          <!-- page break should be here !!! -->\n        <img src=pattern.png>\n    ''')\n\n    assert len(pages) == 3\n\n    page_images = []\n    for page in pages:\n        images = [d for d in page.descendants() if d.element_tag == 'img']\n        assert all([img.element_tag == 'img' for img in images])\n        assert all([img.position_x == 10 for img in images])\n        page_images.append(images)\n    positions_y = [[img.position_y for img in images]\n                   for images in page_images]\n    assert positions_y == [[10, 40], [10, 40], [10]]\n\n\n@assert_no_logs\ndef test_preferred_widths_1():\n    def get_float_width(body_width):\n        page, = render_pages('''\n          <body style=\"width: %spx; font-family: weasyprint\">\n          <p style=\"white-space: pre-line; float: left\">\n            Lorem ipsum dolor sit amet,\n              consectetur elit\n          </p>\n                   <!--  ^  No-break space here  -->\n        ''' % body_width)\n        html, = page.children\n        body, = html.children\n        paragraph, = body.children\n        return paragraph.width\n    # Preferred minimum width:\n    assert get_float_width(10) == len('consectetur elit') * 16\n    # Preferred width:\n    assert get_float_width(1000000) == len('Lorem ipsum dolor sit amet,') * 16\n\n\n@assert_no_logs\ndef test_preferred_widths_2():\n    # Non-regression test:\n    # Incorrect whitespace handling in preferred width used to cause\n    # unnecessary line break.\n    page, = render_pages('''\n      <p style=\"float: left\">Lorem <em>ipsum</em> dolor.</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert len(paragraph.children) == 1\n    assert isinstance(paragraph.children[0], boxes.LineBox)\n\n\n@assert_no_logs\ndef test_preferred_widths_3():\n    page, = render_pages('''\n      <style>img { width: 20px }</style>\n      <p style=\"float: left\">\n        <img src=pattern.png><img src=pattern.png><br>\n        <img src=pattern.png></p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 40\n\n\n@assert_no_logs\ndef test_preferred_widths_4():\n    page, = render_pages(\n        '<style>'\n        '  p { font: 20px weasyprint }'\n        '</style>'\n        '<p style=\"float: left\">XX<br>XX<br>X</p>')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 40\n\n\n@assert_no_logs\ndef test_preferred_widths_5():\n    # The space is the start of the line is collapsed.\n    page, = render_pages(\n        '<style>'\n        '  p { font: 20px weasyprint }'\n        '</style>'\n        '<p style=\"float: left\">XX<br> XX<br>X</p>')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.width == 40\n\n\n@assert_no_logs\ndef test_float_in_inline_1():\n    page, = render_pages('''\n      <style>\n        body {\n          font-family: weasyprint;\n          font-size: 20px;\n        }\n        p {\n          width: 14em;\n          text-align: justify;\n        }\n        span {\n          float: right;\n        }\n      </style>\n      <p>\n        aa bb <a><span>cc</span> ddd</a> ee ff\n      </p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line1, line2 = paragraph.children\n\n    p1, a, p2 = line1.children\n    assert p1.width == 6 * 20\n    assert p1.text == 'aa bb '\n    assert p1.position_x == 0 * 20\n    assert p2.width == 3 * 20\n    assert p2.text == ' ee'\n    assert p2.position_x == 9 * 20\n    span, a_text = a.children\n    assert a_text.width == 3 * 20  # leading space collapse\n    assert a_text.text == 'ddd'\n    assert a_text.position_x == 6 * 20\n    assert span.width == 2 * 20\n    assert span.children[0].children[0].text == 'cc'\n    assert span.position_x == 12 * 20\n\n    p3, = line2.children\n    assert p3.width == 2 * 20\n\n\n@assert_no_logs\ndef test_float_in_inline_2():\n    page, = render_pages('''\n      <style>\n        @page {\n          size: 10em;\n        }\n        article {\n          font-family: weasyprint;\n          line-height: 1;\n        }\n        div {\n          float: left;\n          width: 50%;\n        }\n      </style>\n      <article>\n        <span>\n          <div>a b c</div>\n          1 2 3 4 5 6\n        </span>\n      </article>''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    line1, line2 = article.children\n    span1, = line1.children\n    div, text = span1.children\n    assert div.children[0].children[0].text.strip() == 'a b c'\n    assert text.text.strip() == '1 2 3'\n    span2, = line2.children\n    text, = span2.children\n    assert text.text.strip() == '4 5 6'\n\n\n@assert_no_logs\ndef test_float_in_inline_3():\n    page, = render_pages('''\n      <style>\n        @page {\n          size: 10em;\n        }\n        article {\n          font-family: weasyprint;\n          line-height: 1;\n        }\n        div {\n          float: left;\n          width: 50%;\n        }\n      </style>\n      <article>\n        <span>\n          1 2 3 <div>a b c</div> 4 5 6\n        </span>\n      </article>''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    line1, line2 = article.children\n    span1, = line1.children\n    text, div = span1.children\n    assert text.text.strip() == '1 2 3'\n    assert div.children[0].children[0].text.strip() == 'a b c'\n    span2, = line2.children\n    text, = span2.children\n    assert text.text.strip() == '4 5 6'\n\n\n@assert_no_logs\ndef test_float_in_inline_4():\n    page, = render_pages('''\n      <style>\n        @page {\n          size: 10em;\n        }\n        article {\n          font-family: weasyprint;\n          line-height: 1;\n        }\n        div {\n          float: left;\n          width: 50%;\n        }\n      </style>\n      <article>\n        <span>\n          1 2 3 4 <div>a b c</div> 5 6\n        </span>\n      </article>''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    line1, line2 = article.children\n    span1, div = line1.children\n    text1, text2 = span1.children\n    assert text1.text.strip() == '1 2 3 4'\n    assert text2.text.strip() == '5'\n    assert div.position_y == 16\n    assert div.children[0].children[0].text.strip() == 'a b c'\n    span2, = line2.children\n    text, = span2.children\n    assert text.text.strip() == '6'\n\n\n@assert_no_logs\ndef test_float_next_line():\n    page, = render_pages('''\n      <style>\n        body {\n          font-family: weasyprint;\n          font-size: 20px;\n        }\n        p {\n          text-align: justify;\n          width: 13em;\n        }\n        span {\n          float: left;\n        }\n      </style>\n      <p>pp pp pp pp <a><span>ppppp</span> aa</a> pp pp pp pp pp</p>''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line1, line2, line3 = paragraph.children\n    assert len(line1.children) == 1\n    assert len(line3.children) == 1\n    a, p = line2.children\n    span, a_text = a.children\n    assert span.position_x == 0\n    assert span.width == 5 * 20\n    assert a_text.position_x == a.position_x == 5 * 20\n    assert a_text.width == a.width == 2 * 20\n    assert p.position_x == 7 * 20\n\n\n@assert_no_logs\ndef test_float_text_indent_1():\n    page, = render_pages('''\n      <style>\n        body {\n          font-family: weasyprint;\n          font-size: 20px;\n        }\n        p {\n          text-align: justify;\n          text-indent: 1em;\n          width: 14em;\n        }\n        span {\n          float: left;\n        }\n      </style>\n      <p><a>aa <span>float</span> aa</a></p>''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line1, = paragraph.children\n    a, = line1.children\n    a1, span, a2 = a.children\n    span_text, = span.children\n    assert span.position_x == span_text.position_x == 0\n    assert span.width == span_text.width == (\n        (1 + 5) * 20)  # text-indent + span text\n    assert a1.width == 3 * 20\n    assert a1.position_x == (1 + 5 + 1) * 20  # span + a1 text-indent\n    assert a2.width == 2 * 20  # leading space collapse\n    assert a2.position_x == (1 + 5 + 1 + 3) * 20  # span + a1 t-i + a1\n\n\n@assert_no_logs\ndef test_float_text_indent_2():\n    page, = render_pages('''\n      <style>\n        body {\n          font-family: weasyprint;\n          font-size: 20px;\n        }\n        p {\n          text-align: justify;\n          text-indent: 1em;\n          width: 14em;\n        }\n        span {\n          float: left;\n        }\n      </style>\n      <p>\n        oooooooooooo\n        <a>aa <span>float</span> aa</a></p>''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line1, line2 = paragraph.children\n\n    p1, = line1.children\n    assert p1.position_x == 1 * 20  # text-indent\n    assert p1.width == 12 * 20  # p text\n\n    a, = line2.children\n    a1, span, a2 = a.children\n    span_text, = span.children\n    assert span.position_x == span_text.position_x == 0\n    assert span.width == span_text.width == (\n        (1 + 5) * 20)  # text-indent + span text\n    assert a1.width == 3 * 20\n    assert a1.position_x == (1 + 5) * 20  # span\n    assert a2.width == 2 * 20  # leading space collapse\n    assert a2.position_x == (1 + 5 + 3) * 20  # span + a1\n\n\n@assert_no_logs\ndef test_float_text_indent_3():\n    page, = render_pages('''\n      <style>\n        body {\n          font-family: weasyprint;\n          font-size: 20px;\n        }\n        p {\n          text-align: justify;\n          text-indent: 1em;\n          width: 14em;\n        }\n        span {\n          float: right;\n        }\n      </style>\n      <p>\n        oooooooooooo\n        <a>aa <span>float</span> aa</a>\n        oooooooooooo\n      </p>''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line1, line2, line3 = paragraph.children\n\n    p1, = line1.children\n    assert p1.position_x == 1 * 20  # text-indent\n    assert p1.width == 12 * 20  # p text\n\n    a, = line2.children\n    a1, span, a2 = a.children\n    span_text, = span.children\n    assert span.position_x == span_text.position_x == (14 - 5 - 1) * 20\n    assert span.width == span_text.width == (\n        (1 + 5) * 20)  # text-indent + span text\n    assert a1.position_x == 0  # span\n    assert a2.width == 2 * 20  # leading space collapse\n    assert a2.position_x == (14 - 5 - 1 - 2) * 20\n\n    p2, = line3.children\n    assert p2.position_x == 0\n    assert p2.width == 12 * 20  # p text\n\n\n@assert_no_logs\ndef test_float_previous_break():\n    page1, page2 = render_pages('''\n      <style>\n        @page {\n          size: 13px;\n        }\n        body {\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        p {\n          break-after: avoid;\n        }\n        section {\n          break-inside: avoid;\n          float: left;\n        }\n      </style>\n      <article>\n        oooooo\n        oooooo\n        oooooo\n        oooooo\n      </article>\n      <p>xxxxxx</p>\n      <p>yyyyyy</p>\n      <section>\n        <div>aaaaaa</div>\n        <div>cccccc</div>\n      </section>\n      <article>\n        dddddd\n      </article>''')\n\n    html, = page1.children\n    body, = html.children\n    article, = body.children\n\n    html, = page2.children\n    body, = html.children\n    p1, p2, section, article = body.children\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_float_fail():\n    page, = render_pages('''\n      <style>\n        body {\n          font-family: weasyprint;\n          font-size: 20px;\n        }\n        p {\n          text-align: justify;\n          width: 12em;\n        }\n        span {\n          float: left;\n          background: red;\n        }\n        a {\n          background: yellow;\n        }\n      </style>\n      <p>bb bb pp bb pp pb <a><span>pp pp</span> apa</a> bb bb</p>''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line1, line2, line3 = paragraph.children\n\n\ndef test_float_table_aborted_row():\n    page1, page2 = render_pages('''\n      <style>\n        @page {size: 10px 7px}\n        body {font-family: weasyprint; font-size: 2px; line-height: 1}\n        div {float: right; orphans: 1}\n        td {break-inside: avoid}\n      </style>\n      <table><tbody>\n        <tr><td>abc</td></tr>\n        <tr><td>abc</td></tr>\n        <tr><td>def <div>f<br>g</div> ghi</td></tr>\n      </tbody></table>\n    ''')\n\n    html, = page1.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    tbody, = table.children\n    for tr in tbody.children:\n        td, = tr.children\n        line, = td.children\n        textbox, = line.children\n        assert textbox.text == 'abc'\n\n    html, = page2.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    tbody, = table.children\n    tr, = tbody.children\n    td, = tr.children\n    line1, line2 = td.children\n    textbox, div = line1.children\n    assert textbox.text == 'def '\n    textbox, = line2.children\n    assert textbox.text == 'ghi'\n    line1, line2 = div.children\n    textbox, br = line1.children\n    assert textbox.text == 'f'\n    textbox, = line2.children\n    assert textbox.text == 'g'\n\n\ndef test_formatting_context_avoid_rtl():\n    render_pages('''\n      <div style=\"direction: rtl\">\n        <div style=\"overflow: hidden\"></div>\n      </div>\n    ''')\n\n\n@assert_no_logs\ndef test_nested_right_float():\n    # Regression test for #1510.\n    page, = render_pages('''\n      <style>\n        body { width: 100px; font: 20px weasyprint }\n        div { float: right; width: 50px }\n      </style>\n      <b><i><div></div></i></b>ab c''')\n    html, = page.children\n    body, = html.children\n    line1, line2 = body.children\n    assert line1.width == 40\n    assert line2.width == 20\n\n\n@assert_no_logs\ndef test_first_letter_float():\n    # Regression test for #1859.\n    page, = render_pages('''\n      <style>\n        body { width: 100px; font: 20px weasyprint }\n        p:first-letter { float: left }\n      </style>\n      <p>Lor''')\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    line1, = p.children\n    first_letter, text = line1.children\n    assert first_letter.position_x == 0\n    # TODO: fix problem described in #1859.\n    # assert text.position_x == 20\n"
  },
  {
    "path": "tests/layout/test_footnotes.py",
    "content": "\"\"\"Tests for footnotes layout.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs, render_pages, tree_position\n\n\n@assert_no_logs\ndef test_inline_footnote():\n    page, = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>abc<span>de</span></div>''')\n    html, footnote_area = page.children\n    body, = html.children\n    div, = body.children\n    div_textbox, footnote_call = div.children[0].children\n    assert div_textbox.text == 'abc'\n    assert footnote_call.children[0].text == '1'\n    assert div_textbox.position_y == 0\n\n    footnote_marker, footnote_textbox = (\n        footnote_area.children[0].children[0].children)\n    assert footnote_marker.children[0].text == '1.'\n    assert footnote_textbox.text == 'de'\n    assert footnote_area.position_y == 5\n\n\n@assert_no_logs\ndef test_block_footnote():\n    page, = render_pages('''\n        <style>\n          @page {\n              size: 9px 7px;\n          }\n          div {\n              font-family: weasyprint;\n              font-size: 2px;\n              line-height: 1;\n          }\n          div.footnote {\n              float: footnote;\n          }\n        </style>\n        <div>abc<div class=\"footnote\">de</div></div>''')\n    html, footnote_area = page.children\n    body, = html.children\n    div, = body.children\n    div_textbox, footnote_call = div.children[0].children\n    assert div_textbox.text == 'abc'\n    assert footnote_call.children[0].text == '1'\n    assert div_textbox.position_y == 0\n    footnote_marker, footnote_textbox = (\n     footnote_area.children[0].children[0].children)\n    assert footnote_marker.children[0].text == '1.'\n    assert footnote_textbox.text == 'de'\n    assert footnote_area.position_y == 5\n\n\n@assert_no_logs\ndef test_long_footnote():\n    page, = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>abc<span>de f</span></div>''')\n    html, footnote_area = page.children\n    body, = html.children\n    div, = body.children\n    div_textbox, footnote_call = div.children[0].children\n    assert div_textbox.text == 'abc'\n    assert footnote_call.children[0].text == '1'\n    assert div_textbox.position_y == 0\n    footnote_line1, footnote_line2 = footnote_area.children[0].children\n    footnote_marker, footnote_content1 = footnote_line1.children\n    footnote_content2 = footnote_line2.children[0]\n    assert footnote_marker.children[0].text == '1.'\n    assert footnote_content1.text == 'de'\n    assert footnote_area.position_y == 3\n    assert footnote_content2.text == 'f'\n    assert footnote_content2.position_y == 5\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_after_marker_footnote():\n    # TODO: this syntax is in the specification, but we’re currently limited to\n    # one pseudo element per selector, according to CSS 2.1:\n    # https://drafts.csswg.org/css2/#selector-syntax\n    # and Selectors Level 3:\n    # https://drafts.csswg.org/selectors-3/#selector-syntax\n    # This limitation doesn’t exist anymore in Selectors Level 4:\n    # https://drafts.csswg.org/selectors-4/#typedef-compound-selector\n    page, = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n            ::footnote-marker::after {\n                content: '|';\n            }\n        </style>\n        <div>abc<span>de</span></div>''')\n    html, footnote_area = page.children\n    footnote_marker, _ = footnote_area.children[0].children[0].children\n    assert footnote_marker.children[0].text == '1.|'\n\n\n@assert_no_logs\ndef test_several_footnote():\n    page1, page2, = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>abcd e<span>fg</span> hijk l<span>mn</span></div>''')\n    html1, footnote_area1 = page1.children\n    body1, = html1.children\n    div1, = body1.children\n    div1_line1, div1_line2 = div1.children\n    assert div1_line1.children[0].text == 'abcd'\n    div1_line2_text, div1_footnote_call = div1.children[1].children\n    assert div1_line2_text.text == 'e'\n    assert div1_footnote_call.children[0].text == '1'\n    footnote_marker1, footnote_textbox1 = (\n        footnote_area1.children[0].children[0].children)\n    assert footnote_marker1.children[0].text == '1.'\n    assert footnote_textbox1.text == 'fg'\n\n    html2, footnote_area2 = page2.children\n    body2, = html2.children\n    div2, = body2.children\n    div2_line1, div2_line2 = div2.children\n    assert div2_line1.children[0].text == 'hijk'\n    div2_line2_text, div2_footnote_call = div2.children[1].children\n    assert div2_line2_text.text == 'l'\n    assert div2_footnote_call.children[0].text == '2'\n    footnote_marker2, footnote_textbox2 = (\n        footnote_area2.children[0].children[0].children)\n    assert footnote_marker2.children[0].text == '2.'\n    assert footnote_textbox2.text == 'mn'\n\n\n@assert_no_logs\ndef test_reported_footnote_1():\n    page1, page2, = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>abc<span>f1</span> hij<span>f2</span></div>''')\n    html1, footnote_area1 = page1.children\n    body1, = html1.children\n    div1, = body1.children\n    div_line1, div_line2 = div1.children\n    div_line1_text, div_footnote_call1 = div_line1.children\n    assert div_line1_text.text == 'abc'\n    assert div_footnote_call1.children[0].text == '1'\n    div_line2_text, div_footnote_call2 = div_line2.children\n    assert div_line2_text.text == 'hij'\n    assert div_footnote_call2.children[0].text == '2'\n\n    footnote_marker1, footnote_textbox1 = (\n        footnote_area1.children[0].children[0].children)\n    assert footnote_marker1.children[0].text == '1.'\n    assert footnote_textbox1.text == 'f1'\n\n    html2, footnote_area2 = page2.children\n    assert not html2.children\n    footnote_marker2, footnote_textbox2 = (\n        footnote_area2.children[0].children[0].children)\n    assert footnote_marker2.children[0].text == '2.'\n    assert footnote_textbox2.text == 'f2'\n\n\n@assert_no_logs\ndef test_reported_footnote_2():\n    page1, page2, = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>abc<span>f1</span> hij<span>f2</span> wow</div>''')\n    html1, footnote_area1 = page1.children\n    body1, = html1.children\n    div1, = body1.children\n    div_line1, div_line2 = div1.children\n    div_line1_text, div_footnote_call1 = div_line1.children\n    assert div_line1_text.text == 'abc'\n    assert div_footnote_call1.children[0].text == '1'\n    div_line2_text, div_footnote_call2 = div_line2.children\n    assert div_line2_text.text == 'hij'\n    assert div_footnote_call2.children[0].text == '2'\n    footnote_marker1, footnote_textbox1 = (\n        footnote_area1.children[0].children[0].children)\n    assert footnote_marker1.children[0].text == '1.'\n    assert footnote_textbox1.text == 'f1'\n\n    html2, footnote_area2 = page2.children\n    body2, = html2.children\n    div2, = body2.children\n    div2_line, = div2.children\n    assert div2_line.children[0].text == 'wow'\n    footnote_marker2, footnote_textbox2 = (\n        footnote_area2.children[0].children[0].children)\n    assert footnote_marker2.children[0].text == '2.'\n    assert footnote_textbox2.text == 'f2'\n\n\n@assert_no_logs\ndef test_reported_footnote_3():\n    page1, page2, = render_pages('''\n        <style>\n            @page {\n                size: 9px 10px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>\n          abc<span>1</span>\n          def<span>v long 2</span>\n          ghi<span>3</span>\n        </div>''')\n    html1, footnote_area1 = page1.children\n    body1, = html1.children\n    div1, = body1.children\n    line1, line2, line3 = div1.children\n    assert line1.children[0].text == 'abc'\n    assert line1.children[1].children[0].text == '1'\n    assert line2.children[0].text == 'def'\n    assert line2.children[1].children[0].text == '2'\n    assert line3.children[0].text == 'ghi'\n    assert line3.children[1].children[0].text == '3'\n    footnote1, = footnote_area1.children\n    assert footnote1.children[0].children[0].children[0].text == '1.'\n    assert footnote1.children[0].children[1].text == '1'\n\n    html2, footnote_area2 = page2.children\n    footnote2, footnote3 = footnote_area2.children\n    assert footnote2.children[0].children[0].children[0].text == '2.'\n    assert footnote2.children[0].children[1].text == 'v'\n    assert footnote2.children[1].children[0].text == 'long'\n    assert footnote2.children[2].children[0].text == '2'\n    assert footnote3.children[0].children[0].children[0].text == '3.'\n    assert footnote3.children[0].children[1].text == '3'\n\n\n@assert_no_logs\ndef test_reported_sequential_footnote():\n    pages = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>\n            a<span>b</span><span>c</span><span>d</span><span>e</span>\n        </div>''')\n\n    positions = [\n        tree_position(pages, lambda box: getattr(box, 'text', None) == letter)\n        for letter in 'abcde']\n    assert sorted(positions) == positions\n\n\n@assert_no_logs\ndef test_reported_sequential_footnote_second_line():\n    pages = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>\n            aaa a<span>b</span><span>c</span><span>d</span><span>e</span>\n        </div>''')\n\n    positions = [\n        tree_position(pages, lambda box: getattr(box, 'text', None) == letter)\n        for letter in 'abc']\n    assert sorted(positions) == positions\n\n\n@assert_no_logs\ndef test_footnote_report_orphans():\n    page1, page2 = render_pages('''\n      <style>\n        @page {\n          font-family: weasyprint;\n          size: 20px;\n        }\n        body {\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n          orphans: 2;\n          widows: 2;\n        }\n        span {\n          float: footnote;\n        }\n      </style>\n      <div>\n        a<br>\n        b<br>\n        c<br>\n        d<br>\n        e\n      </div>\n      <div>\n        f<span>1</span><span>2</span><span>3</span><span>4</span><br>\n        g<br>\n        h<br>\n        i\n      </div>''')\n    html, footnote_area = page1.children\n    body, = html.children\n    div1, div2 = body.children\n    assert len(div1.children) == 5\n    assert len(div2.children) == 2\n    assert len(footnote_area.children) == 3\n    html, footnote_area = page2.children\n    body, = html.children\n    div, = body.children\n    assert len(div.children) == 2\n    assert len(footnote_area.children) == 1\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('css', 'tail'), [\n    ('p { break-inside: avoid }', '<br>e<br>f'),\n    ('p { widows: 4 }', '<br>e<br>f'),\n    ('p + p { break-before: avoid }', '</p><p>e<br>f'),\n    ('p + p { break-before: avoid }', '<span>y</span><span>z</span></p><p>e'),\n])\ndef test_footnote_area_after_call(css, tail):\n    pages = render_pages('''\n        <style>\n            @page {\n                size: 9px 10px;\n                margin: 0;\n            }\n            body {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n                orphans: 2;\n                widows: 2;\n                margin: 0;\n            }\n            span {\n                float: footnote;\n            }\n            %s\n        </style>\n        <div>a<br>b</div>\n        <p>c<br>d<span>x</span>%s</p>''' % (css, tail))\n\n    footnote_call = tree_position(\n        pages, lambda box: box.element_tag == 'span::footnote-call')\n    footnote_area = tree_position(\n        pages, lambda box: type(box).__name__ == 'FootnoteAreaBox')\n    assert footnote_call < footnote_area\n\n\n@assert_no_logs\ndef test_footnote_display_inline():\n    page, = render_pages('''\n        <style>\n            @page {\n                size: 9px 50px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n                footnote-display: inline;\n            }\n        </style>\n        <div>abc<span>d</span> fgh<span>i</span></div>''')\n    html, footnote_area = page.children\n    body, = html.children\n    div, = body.children\n    div_line1, div_line2 = div.children\n    div_textbox1, footnote_call1 = div_line1.children\n    div_textbox2, footnote_call2 = div_line2.children\n    assert div_textbox1.text == 'abc'\n    assert div_textbox2.text == 'fgh'\n    assert footnote_call1.children[0].text == '1'\n    assert footnote_call2.children[0].text == '2'\n    line = footnote_area.children[0]\n    footnote_mark1, footnote_textbox1 = line.children[0].children\n    footnote_mark2, footnote_textbox2 = line.children[1].children\n    assert footnote_mark1.children[0].text == '1.'\n    assert footnote_textbox1.text == 'd'\n    assert footnote_mark2.children[0].text == '2.'\n    assert footnote_textbox2.text == 'i'\n\n\n@assert_no_logs\ndef test_footnote_longer_than_space_left():\n    page1, page2 = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>abc<span>def ghi jkl</span></div>''')\n    html1, = page1.children\n    body1, = html1.children\n    div, = body1.children\n    div_textbox, footnote_call = div.children[0].children\n    assert div_textbox.text == 'abc'\n    assert footnote_call.children[0].text == '1'\n\n    html2, footnote_area = page2.children\n    assert not html2.children\n    footnote_line1, footnote_line2, footnote_line3 = (\n        footnote_area.children[0].children)\n    footnote_marker, footnote_content1 = footnote_line1.children\n    footnote_content2 = footnote_line2.children[0]\n    footnote_content3 = footnote_line3.children[0]\n    assert footnote_marker.children[0].text == '1.'\n    assert footnote_content1.text == 'def'\n    assert footnote_content2.text == 'ghi'\n    assert footnote_content3.text == 'jkl'\n\n\n@assert_no_logs\ndef test_footnote_longer_than_page():\n    # Nothing is defined for this use case in the specification. In WeasyPrint,\n    # the content simply overflows.\n    page1, page2 = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>abc<span>def ghi jkl mno</span></div>''')\n    html1, = page1.children\n    body1, = html1.children\n    div, = body1.children\n    div_textbox, footnote_call = div.children[0].children\n    assert div_textbox.text == 'abc'\n    assert footnote_call.children[0].text == '1'\n\n    html2, footnote_area2 = page2.children\n    assert not html2.children\n    footnote_line1, footnote_line2, footnote_line3, footnote_line4 = (\n        footnote_area2.children[0].children)\n    footnote_marker1, footnote_content1 = footnote_line1.children\n    footnote_content2 = footnote_line2.children[0]\n    footnote_content3 = footnote_line3.children[0]\n    footnote_content4 = footnote_line4.children[0]\n    assert footnote_marker1.children[0].text == '1.'\n    assert footnote_content1.text == 'def'\n    assert footnote_content2.text == 'ghi'\n    assert footnote_content3.text == 'jkl'\n    assert footnote_content4.text == 'mno'\n\n\n@assert_no_logs\ndef test_footnote_policy_line():\n    page1, page2 = render_pages('''\n        <style>\n            @page {\n                size: 9px 9px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n                orphans: 2;\n                widows: 2;\n            }\n            span {\n                float: footnote;\n                footnote-policy: line;\n            }\n        </style>\n        <div>abc def ghi jkl<span>1</span></div>''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    linebox1, linebox2 = div.children\n    assert linebox1.children[0].text == 'abc'\n    assert linebox2.children[0].text == 'def'\n\n    html, footnote_area = page2.children\n    body, = html.children\n    div, = body.children\n    linebox1, linebox2 = div.children\n    assert linebox1.children[0].text == 'ghi'\n    assert linebox2.children[0].text == 'jkl'\n    assert linebox2.children[1].children[0].text == '1'\n\n    footnote_marker, footnote_textbox = (\n        footnote_area.children[0].children[0].children)\n    assert footnote_marker.children[0].text == '1.'\n    assert footnote_textbox.text == '1'\n\n\n@assert_no_logs\ndef test_footnote_policy_block():\n    page1, page2 = render_pages('''\n        <style>\n            @page {\n                size: 9px 9px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n                footnote-policy: block;\n            }\n        </style>\n        <div>abc</div><div>def ghi jkl<span>1</span></div>''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    linebox1, = div.children\n    assert linebox1.children[0].text == 'abc'\n\n    html, footnote_area = page2.children\n    body, = html.children\n    div, = body.children\n    linebox1, linebox2, linebox3 = div.children\n    assert linebox1.children[0].text == 'def'\n    assert linebox2.children[0].text == 'ghi'\n    assert linebox3.children[0].text == 'jkl'\n    assert linebox3.children[1].children[0].text == '1'\n\n    footnote_marker, footnote_textbox = (\n        footnote_area.children[0].children[0].children)\n    assert footnote_marker.children[0].text == '1.'\n    assert footnote_textbox.text == '1'\n\n\n@assert_no_logs\ndef test_footnote_repagination():\n    page, = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            div::after {\n                content: counter(pages);\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>ab<span>de</span></div>''')\n    html, footnote_area = page.children\n    body, = html.children\n    div, = body.children\n    div_textbox, footnote_call, div_after = div.children[0].children\n    assert div_textbox.text == 'ab'\n    assert footnote_call.children[0].text == '1'\n    assert div_textbox.position_y == 0\n    assert div_after.children[0].text == '1'\n\n    footnote_marker, footnote_textbox = (\n        footnote_area.children[0].children[0].children)\n    assert footnote_marker.children[0].text == '1.'\n    assert footnote_textbox.text == 'de'\n    assert footnote_area.position_y == 5\n\n\n@assert_no_logs\ndef test_reported_footnote_repagination():\n    # Regression test for #1700.\n    page1, page2 = render_pages('''\n        <style>\n            @page {\n                size: 5px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n            }\n            a::after {\n                content: target-counter(attr(href), page);\n            }\n        </style>\n        <div><a href=\"#i\">a</a> bb<span>de</span> <i id=\"i\">fg</i></div>''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    line1, line2 = div.children\n    a, = line1.children\n    assert a.children[0].text == 'a'\n    assert a.children[1].children[0].text == '2'\n    b, footnote_call, _ = line2.children\n    assert b.text == 'bb'\n    assert footnote_call.children[0].text == '1'\n\n    html, footnote_area = page2.children\n    body, = html.children\n    div, = body.children\n    line1, = div.children\n    i, = line1.children\n    assert i.children[0].text == 'fg'\n\n    footnote_marker, footnote_textbox = (\n        footnote_area.children[0].children[0].children)\n    assert footnote_marker.children[0].text == '1.'\n    assert footnote_textbox.text == 'de'\n    assert footnote_area.position_y == 3\n\n\n@assert_no_logs\ndef test_footnote_max_height():\n    page1, page2 = render_pages('''\n      <style>\n        @page {\n            size: 12px 6px;\n\n            @footnote {\n                margin-left: 1px;\n                max-height: 4px;\n            }\n        }\n        div {\n            font-family: weasyprint;\n            font-size: 2px;\n            line-height: 1;\n        }\n        div.footnote {\n            float: footnote;\n        }\n      </style>\n      <div>ab<div class=\"footnote\">c</div><div class=\"footnote\">d</div>\n      <div class=\"footnote\">e</div></div>\n      <div>fg</div>''')\n    html1, footnote_area1 = page1.children\n    body1, = html1.children\n    div, = body1.children\n    div_textbox, footnote_call1, footnote_call2, space, footnote_call3 = (\n        div.children[0].children)\n    assert div_textbox.text == 'ab'\n    assert footnote_call1.children[0].text == '1'\n    assert footnote_call2.children[0].text == '2'\n    assert space.text == ' '\n    assert footnote_call3.children[0].text == '3'\n    footnote1, footnote2 = footnote_area1.children\n    footnote_line1, = footnote1.children\n    footnote_marker1, footnote_content1 = footnote_line1.children\n    assert footnote_marker1.children[0].text == '1.'\n    assert footnote_content1.text == 'c'\n    footnote_line2, = footnote2.children\n    footnote_marker2, footnote_content2 = footnote_line2.children\n    assert footnote_marker2.children[0].text == '2.'\n    assert footnote_content2.text == 'd'\n\n    html2, footnote_area2 = page2.children\n    body2, = html2.children\n    div2, = body2.children\n    div_textbox2, = div2.children[0].children\n    assert div_textbox2.text == 'fg'\n    footnote_line3, = footnote_area2.children[0].children\n    footnote_marker3, footnote_content3 = footnote_line3.children\n    assert footnote_marker3.children[0].text == '3.'\n    assert footnote_content3.text == 'e'\n\n\ndef test_footnote_table_aborted_row():\n    page1, page2 = render_pages('''\n      <style>\n        @page {size: 10px 35px}\n        body {font-family: weasyprint; font-size: 2px}\n        tr {height: 10px}\n        .footnote {float: footnote}\n      </style>\n      <table><tbody>\n        <tr><td>abc</td></tr>\n        <tr><td>abc</td></tr>\n        <tr><td>abc</td></tr>\n        <tr><td>def<div class=\"footnote\">f</div></td></tr>\n      </tbody></table>\n    ''')\n\n    html, = page1.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    tbody, = table.children\n    for tr in tbody.children:\n        td, = tr.children\n        line, = td.children\n        textbox, = line.children\n        assert textbox.text == 'abc'\n\n    html, footnote_area = page2.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    tbody, = table.children\n    tr, = tbody.children\n    td, = tr.children\n    line, = td.children\n    textbox, call = line.children\n    assert textbox.text == 'def'\n    footnote, = footnote_area.children\n    line, = footnote.children\n    marker, textbox = line.children\n    assert textbox.text == 'f'\n\n\ndef test_footnote_table_aborted_group():\n    page1, page2 = render_pages('''\n      <style>\n        @page {size: 10px 35px}\n        body {font-family: weasyprint; font-size: 2px}\n        tr {height: 10px}\n        tbody {break-inside: avoid}\n        .footnote {float: footnote}\n      </style>\n      <table>\n        <tbody>\n          <tr><td>abc</td></tr>\n          <tr><td>abc</td></tr>\n        </tbody>\n        <tbody>\n          <tr><td>def<div class=\"footnote\">f</div></td></tr>\n          <tr><td>ghi</td></tr>\n        </tbody>\n      </table>\n    ''')\n\n    html, = page1.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    tbody, = table.children\n    for tr in tbody.children:\n        td, = tr.children\n        line, = td.children\n        textbox, = line.children\n        assert textbox.text == 'abc'\n\n    html, footnote_area = page2.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    tbody, = table.children\n    tr1, tr2 = tbody.children\n    td, = tr1.children\n    line, = td.children\n    textbox, call = line.children\n    assert textbox.text == 'def'\n    td, = tr2.children\n    line, = td.children\n    textbox, = line.children\n    assert textbox.text == 'ghi'\n    footnote, = footnote_area.children\n    line, = footnote.children\n    marker, textbox = line.children\n    assert textbox.text == 'f'\n\n\n@assert_no_logs\ndef test_footnote_bottom_margin():\n    page, = render_pages('''\n        <style>\n            @page {\n                size: 9px 7px;\n            }\n            div {\n                font-family: weasyprint;\n                font-size: 2px;\n                line-height: 1;\n            }\n            span {\n                float: footnote;\n                margin-bottom: 1px;\n            }\n        </style>\n        <div>abc<span>de</span></div>''')\n    html, footnote_area = page.children\n    body, = html.children\n    div, = body.children\n    div_textbox, footnote_call = div.children[0].children\n    assert div_textbox.text == 'abc'\n    assert footnote_call.children[0].text == '1'\n    assert div_textbox.position_y == 0\n\n    footnote_marker, footnote_textbox = (\n        footnote_area.children[0].children[0].children)\n    assert footnote_marker.children[0].text == '1.'\n    assert footnote_textbox.text == 'de'\n    assert footnote_area.position_y == 5\n\n\n@assert_no_logs\ndef test_footnotes_column_converge_end():\n    page1, page2 = render_pages('''\n        <style>\n            @page {\n                size: 9px;\n            }\n            html {\n                font: 2px/1 weasyprint;\n            }\n            div {\n                columns: 2;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>\n            a<span>1</span>\n            b<span>2</span>\n            c\n            d<span>3</span>\n        </div>\n        <p>e</p>\n    ''')\n\n    # Page 1 contains a b c d 1 2.\n    html, footnote_area = page1.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    assert column1.height == column2.height == 4\n    assert footnote_area.height == 4\n\n    # Page 2 contains e 3.\n    html, footnote_area = page2.children\n    body, = html.children\n    p, = body.children\n    assert p.height == 2\n    assert footnote_area.height == 2\n\n\n@assert_no_logs\ndef test_footnotes_column_converge():\n    page1, page2 = render_pages('''\n        <style>\n            @page {\n                size: 9px;\n            }\n            html {\n                font: 2px/1 weasyprint;\n            }\n            div {\n                columns: 2;\n            }\n            span {\n                float: footnote;\n            }\n        </style>\n        <div>\n            a<span>1</span>\n            b<span>2</span>\n            c\n            d<span>3</span>\n            e\n            f\n        </div>\n        <p>g</p>\n    ''')\n\n    # Page 1 contains a b c d 1 2.\n    html, footnote_area = page1.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    assert column1.height == column2.height == 4\n    assert footnote_area.height == 4\n\n    # Page 2 contains e f g 3.\n    html, footnote_area = page2.children\n    body, = html.children\n    div, p = body.children\n    column1, column2 = div.children\n    assert column1.height == column2.height == p.height == 2\n    assert footnote_area.height == 2\n"
  },
  {
    "path": "tests/layout/test_grid.py",
    "content": "\"\"\"Tests for grid layout.\"\"\"\n\nfrom math import isclose\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\ndef test_grid_empty():\n    page, = render_pages('''\n      <article style=\"display: grid\">\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    assert article.position_x == 0\n    assert article.position_y == 0\n    assert article.width == html.width\n    assert article.height == 0\n\n\n@assert_no_logs\ndef test_grid_single_item():\n    page, = render_pages('''\n      <article style=\"display: grid\">\n        <div>a</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert article.position_x == div.position_x == 0\n    assert article.position_y == div.position_y == 0\n    assert article.width == div.width == html.width\n\n\n@assert_no_logs\ndef test_grid_single_auto_width():\n    page, = render_pages('''\n      <article style=\"display: grid\">\n        <div style=\"padding: 0 2px; justify-self: start\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert article.position_x == div.position_x == 0\n    assert article.position_y == div.position_y == 0\n    assert article.width == html.width\n    assert div.width == 0\n    assert div.padding_width() == 4\n\n\n@assert_no_logs\ndef test_grid_single_percentage_width():\n    page, = render_pages('''\n      <article style=\"display: grid\">\n        <div style=\"justify-self: start; width: 100%\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div, = article.children\n    assert article.position_x == div.position_x == 0\n    assert article.position_y == div.position_y == 0\n    assert article.width == html.width == div.padding_width()\n\n\n@assert_no_logs\ndef test_grid_rows():\n    page, = render_pages('''\n      <article style=\"display: grid\">\n        <div>a</div>\n        <div>b</div>\n        <div>c</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c = article.children\n    assert div_a.position_x == div_b.position_x == div_c.position_x == 0\n    assert div_a.position_y < div_b.position_y < div_c.position_y\n    assert div_a.height == div_b.height == div_c.height\n    assert div_a.width == div_b.width == div_c.width == html.width == article.width\n\n\n@assert_no_logs\ndef test_grid_template_fr():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-rows: auto 1fr;\n          grid-template-columns: auto 1fr;\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div>a</div> <div>b</div>\n        <div>c</div> <div>d</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d = article.children\n    assert div_a.position_x == div_c.position_x == 0\n    assert div_b.position_x == div_d.position_x == 2\n    assert div_a.height == div_b.height == div_c.height == div_d.height == 2\n    assert div_a.width == div_c.width == 2\n    assert div_b.width == div_d.width == 8\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_areas():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-areas: 'a b' 'c d';\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div>a</div> <div>b</div>\n        <div>c</div> <div>d</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d = article.children\n    assert div_a.position_x == div_c.position_x == 0\n    assert div_b.position_x == div_d.position_x == 5\n    assert div_a.position_y == div_b.position_y == 0\n    assert div_c.position_y == div_d.position_y == 2\n    assert div_a.height == div_b.height == div_c.height == div_d.height == 2\n    assert div_a.width == div_b.width == div_c.width == div_d.width == 5\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_areas_grid_area():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-areas: 'b a' 'd c';\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div style=\"grid-area: a\">a</div> <div style=\"grid-area: b\">b</div>\n        <div style=\"grid-area: c\">c</div> <div style=\"grid-area: d\">d</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d = article.children\n    assert div_b.position_x == div_d.position_x == 0\n    assert div_a.position_x == div_c.position_x == 5\n    assert div_a.position_y == div_b.position_y == 0\n    assert div_c.position_y == div_d.position_y == 2\n    assert div_a.height == div_b.height == div_c.height == div_d.height == 2\n    assert div_a.width == div_b.width == div_c.width == div_d.width == 5\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_areas_empty_row():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-areas: 'b a' 'd a' 'd c';\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div style=\"grid-area: a\">a</div> <div style=\"grid-area: b\">b</div>\n        <div style=\"grid-area: c\">c</div> <div style=\"grid-area: d\">d</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d = article.children\n    assert div_b.position_x == div_d.position_x == 0\n    assert div_a.position_x == div_c.position_x == 5\n    assert div_a.position_y == div_b.position_y == 0\n    assert div_c.position_y == div_d.position_y == 2\n    assert div_a.height == div_b.height == div_c.height == div_d.height == 2\n    assert div_a.width == div_b.width == div_c.width == div_d.width == 5\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_areas_multiple_rows():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-areas: 'b a' 'd a' '. c';\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div style=\"grid-area: a\">a</div> <div style=\"grid-area: b\">b</div>\n        <div style=\"grid-area: c\">c</div> <div style=\"grid-area: d\">d</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d = article.children\n    assert div_b.position_x == div_d.position_x == 0\n    assert div_a.position_x == div_c.position_x == 5\n    assert div_a.position_y == div_b.position_y == 0\n    assert div_c.position_y == 4\n    assert div_d.position_y == 2\n    assert div_a.height == 4\n    assert div_b.height == div_c.height == div_d.height == 2\n    assert div_a.width == div_b.width == div_c.width == div_d.width == 5\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_areas_multiple_columns():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-areas: 'b b' 'c a';\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div style=\"grid-area: a\">a</div>\n        <div style=\"grid-area: b\">b</div>\n        <div style=\"grid-area: c\">c</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c = article.children\n    assert div_b.position_x == div_c.position_x == 0\n    assert div_a.position_x == 5\n    assert div_a.position_y == div_c.position_y == 2\n    assert div_b.position_y == 0\n    assert div_a.height == div_b.height == div_c.height == 2\n    assert div_a.width == div_c.width == 5\n    assert div_b.width == 10\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_areas_overlap():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-areas: 'a b' 'c d';\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div style=\"grid-area: a\">a</div>\n        <div style=\"grid-area: a\">a</div>\n        <div style=\"grid-area: a\">a</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a1, div_a2, div_a3 = article.children\n    assert div_a1.position_x == div_a2.position_x == div_a3.position_x == 0\n    assert div_a1.position_y == div_a2.position_y == div_a3.position_y == 0\n    assert div_a1.width == div_a2.width == div_a3.width == 6  # 2 + (10-2) / 2\n    assert div_a1.height == div_a2.height == div_a3.height == 2\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_big_span():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-columns: 50% 50%;\n          line-height: 1;\n          width: 6px;\n        }\n      </style>\n      <article>\n        <div>a</div>\n        <div style=\"grid-row: span 2\">b b b b</div>\n        <div>a</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_1, div_2, div_3 = article.children\n    assert div_1.position_x == div_3.position_x == 0\n    assert div_2.position_x == 3\n    assert div_1.position_y == div_2.position_y == 0\n    assert div_3.position_y == 4\n    assert div_1.width == div_2.width == div_3.width == 3\n    assert div_1.height == div_3.height == 4\n    assert div_2.height == article.height == 8\n    assert article.width == 6\n\n\n@assert_no_logs\ndef test_grid_template_multiple_big_span():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-columns: 50% 50%;\n          line-height: 1;\n          width: 6px;\n        }\n      </style>\n      <article>\n        <div>a</div>\n        <div style=\"grid-row: span 3\">b b b b b b</div>\n        <div>a</div>\n        <div>a</div>\n        <div style=\"grid-row: span 2\">c</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_1, div_2, div_3, div_4, div_5 = article.children\n    assert div_1.position_x == div_3.position_x == div_4.position_x == 0\n    assert div_5.position_x == 0\n    assert div_2.position_x == 3\n    assert div_1.position_y == div_2.position_y == 0\n    assert div_3.position_y == 4\n    assert div_4.position_y == 8\n    assert div_5.position_y == 12\n    assert div_1.width == div_2.width == div_3.width == div_5.width == 3\n    assert div_4.width == 3\n    assert div_1.height == div_3.height == div_4.height == 4\n    assert div_2.height == 12\n    assert div_5.height == 2\n    assert article.height == 14\n    assert article.width == 6\n\n\n@assert_no_logs\ndef test_grid_template_areas_span_overflow():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-columns: 50% 50%;\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div style=\"\">a</div>\n        <div style=\"grid-column: span 2\">a</div>\n        <div style=\"\">a</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a1, div_a2, div_a3 = article.children\n    assert div_a1.position_x == div_a2.position_x == div_a3.position_x == 0\n    assert div_a1.position_y == 0\n    assert div_a2.position_y == 2\n    assert div_a3.position_y == 4\n    assert div_a1.width == div_a3.width == 5\n    assert div_a2.width == 10\n    assert div_a1.height == div_a2.height == div_a3.height == 2\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_areas_extra_span():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-areas: 'a . b' 'c d d';\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div style=\"grid-area: a\">a</div>\n        <div style=\"grid-area: b\">b</div>\n        <div style=\"grid-area: c\">c</div>\n        <div style=\"grid-area: d\">d</div>\n        <div style=\"grid-row: span 2; grid-column: span 2\">e</div>\n        <div>f</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d, div_e, div_f = article.children\n    assert div_a.position_x == div_c.position_x == div_e.position_x == 0\n    assert div_d.position_x == 4  # 2 + (10 - 2×3) / 2\n    assert div_b.position_x == div_f.position_x == 6\n    assert div_a.position_y == div_b.position_y == 0\n    assert div_c.position_y == div_d.position_y == 2\n    assert div_e.position_y == div_f.position_y == 4\n    assert div_a.width == div_b.width == div_c.width == div_f.width == 4\n    assert div_d.width == div_e.width == 6\n    assert {div.height for div in article.children} == {2}\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_areas_extra_span_dense():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-auto-flow: dense;\n          grid-template-areas: 'a . b' 'c d d';\n          line-height: 1;\n          width: 9px;\n        }\n      </style>\n      <article>\n        <div style=\"grid-area: a\">a</div>\n        <div style=\"grid-area: b\">b</div>\n        <div style=\"grid-area: c\">c</div>\n        <div style=\"grid-area: d\">d</div>\n        <div style=\"grid-row: span 2; grid-column: span 2\">e</div>\n        <div>f</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d, div_e, div_f = article.children\n    assert div_a.position_x == div_c.position_x == div_e.position_x == 0\n    assert div_d.position_x == div_f.position_x == 3\n    assert div_b.position_x == 6\n    assert div_a.position_y == div_b.position_y == div_f.position_y == 0\n    assert div_c.position_y == div_d.position_y == 2\n    assert div_e.position_y == 4\n    assert div_a.width == div_b.width == div_c.width == div_f.width == 3\n    assert div_d.width == div_e.width == 6\n    assert {div.height for div in article.children} == {2}\n    assert article.height == 6\n    assert article.width == 9\n\n\n@assert_no_logs\ndef test_grid_area_multiple_values():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-columns: 5px 5px;\n          grid-template-rows: 2px 2px;\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div style=\"grid-area: 2 / 1 / 3 / 2\">a</div>\n        <div style=\"grid-area: 1 / 1 / 2 / 3\">b</div>\n        <div style=\"grid-area: 2 / 2 / 3 / 3\">c</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c = article.children\n    assert div_a.position_x == div_b.position_x == 0\n    assert div_c.position_x == 5\n    assert div_b.position_y == 0\n    assert div_a.position_y == div_c.position_y == 2\n    assert div_a.height == div_b.height == div_c.height == 2\n    assert div_a.width == div_c.width == 5\n    assert div_b.width == 10\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_repeat_fr():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-columns: repeat(2, 1fr 2fr);\n          line-height: 1;\n          width: 12px;\n        }\n      </style>\n      <article>\n        <div>a</div>\n        <div>b</div>\n        <div>c</div>\n        <div>d</div>\n        <div>e</div>\n        <div>f</div>\n        <div>g</div>\n        <div>h</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d, div_e, div_f, div_g, div_h = article.children\n    assert div_a.position_x == div_e.position_x == 0\n    assert div_b.position_x == div_f.position_x == 2\n    assert div_c.position_x == div_g.position_x == 6\n    assert div_d.position_x == div_h.position_x == 8\n    assert div_a.position_y == div_b.position_y == 0\n    assert div_c.position_y == div_d.position_y == 0\n    assert div_e.position_y == div_f.position_y == 2\n    assert div_g.position_y == div_h.position_y == 2\n    assert div_a.width == div_c.width == div_e.width == div_g.width == 2\n    assert div_b.width == div_d.width == div_f.width == div_h.width == 4\n    assert {div.height for div in article.children} == {2}\n    assert article.width == 12\n\n\n@assert_no_logs\ndef test_grid_template_shorthand_fr():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template: auto 1fr / auto 1fr auto;\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div>a</div>\n        <div>b</div>\n        <div>c</div>\n        <div>d</div>\n        <div>e</div>\n        <div>f</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d, div_e, div_f = article.children\n    assert div_a.position_x == div_d.position_x == 0\n    assert div_b.position_x == div_e.position_x == 2\n    assert div_c.position_x == div_f.position_x == 8\n    assert div_a.position_y == div_b.position_y == div_c.position_y == 0\n    assert div_d.position_y == div_e.position_y == div_f.position_y == 2\n    assert div_a.width == div_c.width == div_d.width == div_f.width == 2\n    assert div_b.width == div_e.width == 6\n    assert {div.height for div in article.children} == {2}\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_shorthand_auto_flow_rows_fr_size():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid: auto-flow 1fr / 6px;\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div>a</div>\n        <div>b</div>\n        <div>c</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c = article.children\n    assert div_a.position_x == div_b.position_x == div_c.position_x == 0\n    assert div_a.position_y == 0\n    assert div_b.position_y == 2\n    assert div_c.position_y == 4\n    assert div_a.width == div_b.width == div_c.width == 6\n    assert {div.height for div in article.children} == {2}\n    assert article.width == 10\n\n\n@assert_no_logs\ndef test_grid_template_fr_too_large():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-columns: 1fr 1fr;\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div>a</div><div>bbb</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b = article.children\n    assert div_a.position_x == 0\n    assert div_b.position_x == 4\n    assert div_a.position_y == div_b.position_y == 0\n    assert div_a.height == div_b.height == 2\n    assert div_a.width == 4\n    assert div_b.width == 6\n    assert article.width == 10\n\n\ndef test_grid_shorthand_auto_flow_columns_none_dense():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid: none / auto-flow dense 1fr;\n          line-height: 1;\n          width: 12px;\n        }\n      </style>\n      <article>\n        <div>a</div>\n        <div>b</div>\n        <div>c</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c = article.children\n    assert div_a.position_x == 0\n    assert div_b.position_x == 4\n    assert div_c.position_x == 8\n    assert div_a.position_y == div_b.position_y == div_c.position_y == 0\n    assert div_a.height == div_b.height == div_c.height == 2\n    assert {div.width for div in article.children} == {4}\n    assert article.width == 12\n\n\n@assert_no_logs\ndef test_grid_template_fr_undefined_free_space():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-rows: 1fr 1fr;\n          grid-template-columns: 1fr 1fr;\n          line-height: 1;\n          width: 10px;\n        }\n      </style>\n      <article>\n        <div>a</div> <div>b<br>b<br>b<br>b</div>\n        <div>c</div> <div>d</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d = article.children\n    assert div_a.position_x == div_c.position_x == 0\n    assert div_b.position_x == div_d.position_x == 5\n    assert div_a.height == div_b.height == div_c.height == div_d.height == 8\n    assert div_a.width == div_c.width == 5\n    assert div_b.width == div_d.width == 5\n    assert article.width == 10\n    assert article.height == 16\n\n\n@assert_no_logs\ndef test_grid_column_start():\n    page, = render_pages('''\n      <style>\n        dl {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-columns: max-content auto;\n          line-height: 1;\n          width: 10px;\n        }\n        dt {\n          display: block;\n          grid-column-start: 1;\n        }\n        dd {\n          display: block;\n          grid-column-start: 2;\n        }\n      </style>\n      <dl>\n        <dt>A</dt>\n        <dd>A1</dd>\n        <dd>A2</dd>\n        <dt>B</dt>\n        <dd>B1</dd>\n        <dd>B2</dd>\n      </dl>\n    ''')\n    html, = page.children\n    body, = html.children\n    dl, = body.children\n    dt_a, dd_a1, dd_a2, dt_b, dd_b1, dd_b2 = dl.children\n    assert dt_a.position_y == dd_a1.position_y == 0\n    assert dd_a2.position_y == 2\n    assert dt_b.position_y == dd_b1.position_y == 4\n    assert dd_b2.position_y == 6\n    assert dt_a.position_x == dt_b.position_x == 0\n    assert dd_a1.position_x == dd_a2.position_x == 2\n    assert dd_b1.position_x == dd_b2.position_x == 2\n\n\n@assert_no_logs\ndef test_grid_column_start_blockified():\n    page, = render_pages('''\n      <style>\n        dl {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-columns: max-content auto;\n          line-height: 1;\n          width: 10px;\n        }\n        dt {\n          display: inline;\n          grid-column-start: 1;\n        }\n        dd {\n          display: inline;\n          grid-column-start: 2;\n        }\n      </style>\n      <dl>\n        <dt>A</dt>\n        <dd>A1</dd>\n        <dd>A2</dd>\n        <dt>B</dt>\n        <dd>B1</dd>\n        <dd>B2</dd>\n      </dl>\n    ''')\n    html, = page.children\n    body, = html.children\n    dl, = body.children\n    dt_a, dd_a1, dd_a2, dt_b, dd_b1, dd_b2 = dl.children\n    assert dt_a.position_y == dd_a1.position_y == 0\n    assert dd_a2.position_y == 2\n    assert dt_b.position_y == dd_b1.position_y == 4\n    assert dd_b2.position_y == 6\n    assert dt_a.position_x == dt_b.position_x == 0\n    assert dd_a1.position_x == dd_a2.position_x == 2\n    assert dd_b1.position_x == dd_b2.position_x == 2\n\n\n@assert_no_logs\ndef test_grid_undefined_free_space():\n    page, = render_pages('''\n      <style>\n        body {\n          font-family: weasyprint;\n          font-size: 2px;\n          line-height: 1;\n        }\n        .columns {\n          display: grid;\n          grid-template-columns: 1fr 1fr;\n          width: 8px;\n        }\n        .rows {\n          display: grid;\n          grid-template-rows: 1fr 1fr;\n        }\n      </style>\n      <div class=\"columns\">\n        <div class=\"rows\">\n          <div>aa</div>\n          <div>b</div>\n        </div>\n        <div class=\"rows\">\n          <div>c<br>c</div>\n          <div>d</div>\n        </div>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div_c, = body.children\n    div_c1, div_c2 = div_c.children\n    div_r11, div_r12 = div_c1.children\n    div_r21, div_r22 = div_c2.children\n    assert div_r11.position_x == div_r12.position_x == 0\n    assert div_r21.position_x == div_r22.position_x == 4\n    assert div_r11.position_y == div_r21.position_y == 0\n    assert div_r12.position_y == div_r22.position_y == 4\n    assert div_r11.height == div_r12.height == div_r21.height == div_r22.height == 4\n    assert div_r11.width == div_r12.width == div_r21.width == div_r22.width == 4\n    assert div_c.width == 8\n\n\n@assert_no_logs\ndef test_grid_padding():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-rows: auto 1fr;\n          grid-template-columns: auto 1fr;\n          line-height: 1;\n          width: 14px;\n        }\n      </style>\n      <article>\n        <div style=\"padding: 1px\">a</div> <div>b</div>\n        <div>c</div> <div style=\"padding: 2px\">d</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d = article.children\n    assert div_a.position_x == div_c.position_x == div_c.content_box_x() == 0\n    assert div_a.content_box_x() == 1\n    assert div_b.position_x == div_b.content_box_x() == div_d.position_x == 4\n    assert div_d.content_box_x() == 6\n    assert div_a.width == 2\n    assert div_b.width == 10\n    assert div_c.width == 4\n    assert div_d.width == 6\n    assert article.width == 14\n    assert div_a.position_y == div_b.position_y == div_b.content_box_y() == 0\n    assert div_a.content_box_y() == 1\n    assert div_c.position_y == div_c.content_box_y() == div_d.position_y == 4\n    assert div_d.content_box_y() == 6\n    assert div_a.height == div_d.height == 2\n    assert div_b.height == 4\n    assert div_c.height == 6\n    assert article.height == 10\n\n\n@assert_no_logs\ndef test_grid_border():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-rows: auto 1fr;\n          grid-template-columns: auto 1fr;\n          line-height: 1;\n          width: 14px;\n        }\n      </style>\n      <article>\n        <div style=\"border: 1px solid\">a</div> <div>b</div>\n        <div>c</div> <div style=\"border: 2px solid\">d</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d = article.children\n    assert div_a.position_x == div_c.position_x == div_c.padding_box_x() == 0\n    assert div_a.padding_box_x() == 1\n    assert div_b.position_x == div_b.padding_box_x() == div_d.position_x == 4\n    assert div_d.padding_box_x() == 6\n    assert div_a.width == 2\n    assert div_b.width == 10\n    assert div_c.width == 4\n    assert div_d.width == 6\n    assert article.width == 14\n    assert div_a.position_y == div_b.position_y == div_b.padding_box_y() == 0\n    assert div_a.padding_box_y() == 1\n    assert div_c.position_y == div_c.padding_box_y() == div_d.position_y == 4\n    assert div_d.padding_box_y() == 6\n    assert div_a.height == div_d.height == 2\n    assert div_b.height == 4\n    assert div_c.height == 6\n    assert article.height == 10\n\n\n@assert_no_logs\ndef test_grid_border_box():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-rows: auto 1fr;\n          grid-template-columns: auto 1fr;\n          line-height: 1;\n          width: 14px;\n        }\n        div { box-sizing: border-box }\n      </style>\n      <article>\n        <div style=\"border: 1px solid; width: 7px\">a</div> <div>b</div>\n        <div>c</div> <div style=\"padding: 2px; height: 7px\">d</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d = article.children\n    assert div_a.position_x == div_c.position_x == div_c.padding_box_x() == 0\n    assert div_a.padding_box_x() == 1\n    assert div_b.position_x == div_b.padding_box_x() == div_d.position_x == 7\n    assert div_d.content_box_x() == 9\n    assert div_a.width == 5\n    assert div_b.width == 7\n    assert div_c.width == 7\n    assert div_d.width == 3\n    assert article.width == 14\n    assert div_a.position_y == div_b.position_y == div_b.padding_box_y() == 0\n    assert div_a.padding_box_y() == 1\n    assert div_c.position_y == div_c.padding_box_y() == div_d.position_y == 4\n    assert div_d.content_box_y() == 6\n    assert div_a.height == 2\n    assert div_b.height == 4\n    assert div_c.height == 7\n    assert div_d.height == 3\n    assert article.height == 11\n\n\n@assert_no_logs\ndef test_grid_item_border_box():\n    page, = render_pages('''\n      <style>\n        @page { size: 50px }\n      </style>\n      <article style=\"display: grid\">\n        <section style=\"box-sizing: border-box; padding: 5px\">\n          <div>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    section, = article.children\n    assert section.margin_width() == 50\n    assert section.width == 40\n    assert section.position_x == section.position_y == 0\n    div, = section.children\n    assert div.width == div.margin_width() == 40\n    assert div.position_x == div.position_y == 5\n\n\n@assert_no_logs\ndef test_grid_border_split():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 5px }\n        body { font: 2px / 1 weasyprint }\n      </style>\n      <article style=\"display: grid; border-top: 1px solid; border-bottom: 1px solid;\n                      margin: 1px 0; padding: 1px 0\">\n        <div>a<br>b</div>\n      </article>\n    ''')\n    html, = page1.children\n    body, = html.children\n    article, = body.children\n    assert article.border_top_width == 1\n    assert article.border_bottom_width == 0\n    assert article.margin_height() == 5\n    section, = article.children\n    assert section.position_x == 0\n    assert section.position_y == 3\n    assert section.width == html.width\n    assert section.height == 2\n\n    html, = page2.children\n    body, = html.children\n    article, = body.children\n    assert article.border_top_width == 0\n    assert article.border_bottom_width == 1\n    assert article.margin_height() == 5\n    section, = article.children\n    assert section.position_x == 0\n    assert section.position_y == 0\n    assert section.width == html.width\n    assert section.height == 2\n\n\n@assert_no_logs\ndef test_grid_margin():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-template-rows: auto 1fr;\n          grid-template-columns: auto 1fr;\n          line-height: 1;\n          width: 14px;\n        }\n      </style>\n      <article>\n        <div style=\"margin: 1px\">a</div> <div>b</div>\n        <div>c</div> <div style=\"margin: 2px\">d</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d = article.children\n    assert div_a.position_x == div_c.position_x == div_c.border_box_x() == 0\n    assert div_a.border_box_x() == 1\n    assert div_b.position_x == div_b.border_box_x() == div_d.position_x == 4\n    assert div_d.border_box_x() == 6\n    assert div_a.width == 2\n    assert div_b.width == 10\n    assert div_c.width == 4\n    assert div_d.width == 6\n    assert article.width == 14\n    assert div_a.position_y == div_b.position_y == div_b.border_box_y() == 0\n    assert div_a.border_box_y() == 1\n    assert div_c.position_y == div_c.border_box_y() == div_d.position_y == 4\n    assert div_d.border_box_y() == 6\n    assert div_a.height == div_d.height == 2\n    assert div_b.height == 4\n    assert div_c.height == 6\n    assert article.height == 10\n\n\n@assert_no_logs\ndef test_grid_item_margin():\n    # Regression test for #2154.\n    page, = render_pages('''\n      <style>\n        article { display: grid }\n        div { margin: auto }\n      </style>\n      <article>\n        <div>a</div>\n        <div>b</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b = article.children\n    # TODO: Test auto margin values.\n\n\n@assert_no_logs\ndef test_grid_auto_flow_column():\n    page, = render_pages('''\n      <article style=\"display: grid; grid-auto-flow: column\">\n        <div>a</div>\n        <div>a</div>\n        <div>a</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c = article.children\n    assert div_a.position_x < div_b.position_x < div_c.position_x\n    assert div_a.position_y == div_b.position_y == div_c.position_y == 0\n    assert div_a.width == div_b.width == div_c.width\n    assert div_a.height == div_b.height == div_c.height == html.height == article.height\n\n\n@assert_no_logs\ndef test_grid_template_areas_extra_span_column_dense():\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          grid-auto-flow: column dense;\n          grid-template-areas: 'a . b' 'c d d';\n          line-height: 1;\n          width: 12px;\n        }\n      </style>\n      <article>\n        <div style=\"grid-area: a\">a</div>\n        <div style=\"grid-area: b\">b</div>\n        <div style=\"grid-area: c\">c</div>\n        <div style=\"grid-area: d\">d</div>\n        <div style=\"grid-row: span 2\">e</div>\n        <div>f</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b, div_c, div_d, div_e, div_f = article.children\n    assert div_a.position_x == div_c.position_x == 0\n    assert div_d.position_x == div_f.position_x == 3\n    assert div_b.position_x == 6\n    assert div_e.position_x == 9\n    assert (\n        div_a.position_y == div_b.position_y ==\n        div_e.position_y == div_f.position_y == 0)\n    assert div_c.position_y == div_d.position_y == 2\n    assert (\n        div_a.width == div_b.width == div_c.width ==\n        div_e.width == div_f.width == 3)\n    assert div_d.width == 6\n    assert (\n        div_a.height == div_b.height == div_c.height ==\n        div_d.height == div_f.height == 2)\n    assert div_e.height == 4\n    assert article.height == 4\n    assert article.width == 12\n\n\n@assert_no_logs\ndef test_grid_gap_explicit_grid_column():\n    # Regression test for #2187.\n    page, = render_pages('''\n      <style>\n        article {\n          display: grid;\n          font-family: weasyprint;\n          font-size: 2px;\n          gap: 2px;\n          grid-template-columns: 1fr;\n          line-height: 1;\n          width: 12px;\n        }\n      </style>\n      <article>\n        <div>a</div>\n        <div style=\"grid-column: 1\">b</div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div_a, div_b = article.children\n    assert div_a.position_x == div_b.position_x == 0\n    assert div_a.position_y == 0\n    assert div_b.position_y == 4\n    assert article.height == 6\n    assert article.width == 12\n\n\n@assert_no_logs\ndef test_grid_break():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 10px 11px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n      </style>\n      <section>\n        <div>1</div>\n        <div>2</div>\n        <div>3</div>\n        <div>4</div>\n        <div>5</div>\n        <div>6</div>\n        <div>7a<br>7b<br>7c</div>\n        <div>8</div>\n        <div>9</div>\n        <div>10</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 11\n    div1, div2, div3, div4, div5, div6, div7, div8 = section.children\n    assert div1.position_x == div3.position_x == div5.position_x == div7.position_x == 0\n    assert div2.position_x == div4.position_x == div6.position_x == div8.position_x == 5\n    assert div1.position_y == div2.position_y == 0\n    assert div1.height == div2.height == 2\n    assert div3.position_y == div4.position_y == 2\n    assert div3.height == div4.height == 2\n    assert div5.position_y == div6.position_y == 4\n    assert div5.height == div6.height == 2\n    assert div7.position_y == div8.position_y == 6\n    assert div7.height == div8.height == 5\n    line1, line2 = div7.children\n    text, br = line1.children\n    assert text.text == '7a'\n    text, br = line2.children\n    assert text.text == '7b'\n    line, = div8.children\n    text, = line.children\n    assert text.text == '8'\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 4\n    div7, div8, div9, div10 = section.children\n    assert div7.position_x == div9.position_x == 0\n    assert div8.position_x == div10.position_x == 5\n    assert div7.position_y == div8.position_y == 0\n    assert div9.position_y == div10.position_y == 2\n    assert div7.height == div8.height == div9.height == div10.height == 2\n    line, = div7.children\n    text, = line.children\n    assert text.text == '7c'\n    assert not div8.children\n\n\n@assert_no_logs\ndef test_grid_break_order_negative():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 10px 11px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n      </style>\n      <section>\n        <div>3</div>\n        <div>4</div>\n        <div>5</div>\n        <div>6</div>\n        <div>7a<br>7b<br>7c</div>\n        <div>8</div>\n        <div>9</div>\n        <div>10</div>\n        <div style=\"order: -2\">1</div>\n        <div style=\"order: -1\">2</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 11\n    div1, div2, div3, div4, div5, div6, div7, div8 = section.children\n    assert div1.position_x == div3.position_x == div5.position_x == div7.position_x == 0\n    assert div2.position_x == div4.position_x == div6.position_x == div8.position_x == 5\n    assert div1.position_y == div2.position_y == 0\n    assert div1.height == div2.height == 2\n    assert div3.position_y == div4.position_y == 2\n    assert div3.height == div4.height == 2\n    assert div5.position_y == div6.position_y == 4\n    assert div5.height == div6.height == 2\n    assert div7.position_y == div8.position_y == 6\n    assert div7.height == div8.height == 5\n    line1, line2 = div7.children\n    text, br = line1.children\n    assert text.text == '7a'\n    text, br = line2.children\n    assert text.text == '7b'\n    line, = div8.children\n    text, = line.children\n    assert text.text == '8'\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 4\n    div7, div8, div9, div10 = section.children\n    assert div7.position_x == div9.position_x == 0\n    assert div8.position_x == div10.position_x == 5\n    assert div7.position_y == div8.position_y == 0\n    assert div9.position_y == div10.position_y == 2\n    assert div7.height == div8.height == div9.height == div10.height == 2\n    line, = div7.children\n    text, = line.children\n    assert text.text == '7c'\n    assert not div8.children\n\n\n@assert_no_logs\ndef test_grid_break_order_positive():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 10px 11px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n      </style>\n      <section>\n        <div>1</div>\n        <div>2</div>\n        <div style=\"order: 9\">9</div>\n        <div style=\"order: 10\">10</div>\n        <div>3</div>\n        <div>4</div>\n        <div>5</div>\n        <div>6</div>\n        <div>7a<br>7b<br>7c</div>\n        <div>8</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 11\n    div1, div2, div3, div4, div5, div6, div7, div8 = section.children\n    assert div1.position_x == div3.position_x == div5.position_x == div7.position_x == 0\n    assert div2.position_x == div4.position_x == div6.position_x == div8.position_x == 5\n    assert div1.position_y == div2.position_y == 0\n    assert div1.height == div2.height == 2\n    assert div3.position_y == div4.position_y == 2\n    assert div3.height == div4.height == 2\n    assert div5.position_y == div6.position_y == 4\n    assert div5.height == div6.height == 2\n    assert div7.position_y == div8.position_y == 6\n    assert div7.height == div8.height == 5\n    line1, line2 = div7.children\n    text, br = line1.children\n    assert text.text == '7a'\n    text, br = line2.children\n    assert text.text == '7b'\n    line, = div8.children\n    text, = line.children\n    assert text.text == '8'\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 4\n    div7, div8, div9, div10 = section.children\n    assert div7.position_x == div9.position_x == 0\n    assert div8.position_x == div10.position_x == 5\n    assert div7.position_y == div8.position_y == 0\n    assert div9.position_y == div10.position_y == 2\n    assert div7.height == div8.height == div9.height == div10.height == 2\n    line, = div7.children\n    text, = line.children\n    assert text.text == '7c'\n    assert not div8.children\n    line, = div9.children\n    text, = line.children\n    assert text.text == '9'\n    line, = div10.children\n    text, = line.children\n    assert text.text == '10'\n\n\n@assert_no_logs\ndef test_grid_break_border():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 12px 18px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n        div { border: 1px solid }\n      </style>\n      <section>\n        <div>1</div>\n        <div>2</div>\n        <div>3</div>\n        <div>4</div>\n        <div>5</div>\n        <div>6</div>\n        <div>7a<br>7b<br>7c</div>\n        <div>8</div>\n        <div>9</div>\n        <div>10</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 18\n    div1, div2, div3, div4, div5, div6, div7, div8 = section.children\n    for div in section.children:\n        assert div.border_top_width == 1\n        assert div.border_left_width == 1\n        assert div.border_right_width == 1\n    for div in div1, div2, div3, div4, div5, div6:\n        assert div.border_bottom_width == 1\n    for div in div7, div8:\n        assert div.border_bottom_width == 0\n    assert div1.position_x == div3.position_x == div5.position_x == div7.position_x == 0\n    assert div2.position_x == div4.position_x == div6.position_x == div8.position_x == 6\n    assert div1.position_y == div2.position_y == 0\n    assert div1.height == div2.height == 2\n    assert div1.width == div2.width == 4\n    assert div3.position_y == div4.position_y == 4\n    assert div3.height == div4.height == 2\n    assert div5.position_y == div6.position_y == 8\n    assert div5.height == div6.height == 2\n    assert div7.position_y == div8.position_y == 12\n    assert div7.height == div8.height == 5\n    line1, line2 = div7.children\n    text, br = line1.children\n    assert text.text == '7a'\n    text, br = line2.children\n    assert text.text == '7b'\n    line, = div8.children\n    text, = line.children\n    assert text.text == '8'\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    # assert section.height == 7\n    div7, div8, div9, div10 = section.children\n    for div in section.children:\n        assert div.border_bottom_width == 1\n        assert div.border_left_width == 1\n        assert div.border_right_width == 1\n    for div in div7, div8:\n        assert div.border_top_width == 0\n    for div in div9, div10:\n        assert div.border_top_width == 1\n    assert div7.position_x == div9.position_x == 0\n    assert div8.position_x == div10.position_x == 6\n    assert div7.position_y == div8.position_y == 0\n    assert div9.position_y == div10.position_y == 3\n    assert div7.height == div8.height == div9.height == div10.height == 2\n    assert div7.width == div8.width == div9.width == div10.width == 4\n    line, = div7.children\n    text, = line.children\n    assert text.text == '7c'\n    assert not div8.children\n\n\n@assert_no_logs\ndef test_grid_break_multiple():\n    page1, page2, page3 = render_pages('''\n      <style>\n        @page { size: 10px 11px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n      </style>\n      <section>\n        <div>1</div>\n        <div>2</div>\n        <div>3</div>\n        <div>4</div>\n        <div>5</div>\n        <div>6</div>\n        <div>7a<br>7b<br>7c<br>7d<br>7e<br>7f<br>7g<br>7h</div>\n        <div>8</div>\n        <div>9</div>\n        <div>10</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 11\n    div1, div2, div3, div4, div5, div6, div7, div8 = section.children\n    assert div1.position_x == div3.position_x == div5.position_x == div7.position_x == 0\n    assert div2.position_x == div4.position_x == div6.position_x == div8.position_x == 5\n    assert div1.position_y == div2.position_y == 0\n    assert div1.height == div2.height == 2\n    assert div3.position_y == div4.position_y == 2\n    assert div3.height == div4.height == 2\n    assert div5.position_y == div6.position_y == 4\n    assert div5.height == div6.height == 2\n    assert div7.position_y == div8.position_y == 6\n    assert div7.height == div8.height == 5\n    line1, line2 = div7.children\n    text, br = line1.children\n    assert text.text == '7a'\n    text, br = line2.children\n    assert text.text == '7b'\n    line, = div8.children\n    text, = line.children\n    assert text.text == '8'\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 11\n    div7, div8 = section.children\n    assert div7.position_x == 0\n    assert div8.position_x == 5\n    assert div7.height == div8.height == 11\n    line1, line2, line3, line4, line5 = div7.children\n    text, _ = line1.children\n    assert text.text == '7c'\n    text, _ = line2.children\n    assert text.text == '7d'\n    text, _ = line3.children\n    assert text.text == '7e'\n    text, _ = line4.children\n    assert text.text == '7f'\n    text, _ = line5.children\n    assert text.text == '7g'\n    assert not div8.children\n\n    html, = page3.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 4\n    div7, div8, div9, div10 = section.children\n    assert div7.position_x == div9.position_x == 0\n    assert div8.position_x == div10.position_x == 5\n    assert div7.position_y == div8.position_y == 0\n    assert div9.position_y == div10.position_y == 2\n    assert div7.height == div8.height == div9.height == div10.height == 2\n    line, = div7.children\n    text, = line.children\n    assert text.text == '7h'\n    assert not div8.children\n\n\n@assert_no_logs\ndef test_grid_break_span():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 10px 11px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n      </style>\n      <section>\n        <div>1</div>\n        <div>2</div>\n        <div>3</div>\n        <div>4</div>\n        <div>5</div>\n        <div>6</div>\n        <div style=\"grid-row: span 3\">7a<br>7b<br>7c<br>7d</div>\n        <div>8</div>\n        <div>9</div>\n        <div>10</div>\n        <div>11</div>\n        <div>12</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 11\n    div1, div2, div3, div4, div5, div6, div7, div8, div9 = section.children\n    assert div1.position_x == div3.position_x == div5.position_x == div7.position_x == 0\n    assert div2.position_x == div4.position_x == div6.position_x == div8.position_x == 5\n    assert div1.position_y == div2.position_y == 0\n    assert div1.height == div2.height == 2\n    assert div3.position_y == div4.position_y == 2\n    assert div3.height == div4.height == 2\n    assert div5.position_y == div6.position_y == 4\n    assert div5.height == div6.height == 2\n    assert div7.position_y == div8.position_y == 6\n    assert div7.height == 5\n    assert isclose(div8.height, 8 / 3)\n    assert isclose(div9.height, 7 / 3)\n    line1, line2 = div7.children\n    text, br = line1.children\n    assert text.text == '7a'\n    text, br = line2.children\n    assert text.text == '7b'\n    line, = div8.children\n    text, = line.children\n    assert text.text == '8'\n    line, = div9.children\n    text, = line.children\n    assert text.text == '9'\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    assert isclose(section.height, 6)\n    div7, div9, div10, div11, div12 = section.children\n    assert div7.position_x == 0\n    assert div8.position_x == 5\n    assert div7.height == 4\n    line1, line2 = div7.children\n    text, br = line1.children\n    assert text.text == '7c'\n    text, = line2.children\n    assert text.text == '7d'\n    assert not div9.children\n    assert div10.position_x == 5\n    assert isclose(div10.position_y, 4 / 3)\n    line, = div10.children\n    text, = line.children\n    assert text.text == '10'\n    assert div11.position_x == 0\n    assert div12.position_x == 5\n    assert div11.position_y == div12.position_y\n    assert isclose(div11.position_y, 4)\n    line, = div11.children\n    text, = line.children\n    assert text.text == '11'\n    line, = div12.children\n    text, = line.children\n    assert text.text == '12'\n\n\n@assert_no_logs\ndef test_grid_break_item_avoid():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 10px 11px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n      </style>\n      <section>\n        <div>1</div>\n        <div>2</div>\n        <div>3</div>\n        <div>4</div>\n        <div>5</div>\n        <div>6</div>\n        <div>7a<br>7b<br>7c</div>\n        <div style=\"break-inside: avoid\">8</div>\n        <div>9</div>\n        <div>10</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 11\n    div1, div2, div3, div4, div5, div6 = section.children\n    assert div1.position_x == div3.position_x == div5.position_x == 0\n    assert div2.position_x == div4.position_x == div6.position_x == 5\n    assert div1.position_y == div2.position_y == 0\n    assert div1.height == div2.height == 2\n    assert div3.position_y == div4.position_y == 2\n    assert div3.height == div4.height == 2\n    assert div5.position_y == div6.position_y == 4\n    assert div5.height == div6.height == 2\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 8\n    div7, div8, div9, div10 = section.children\n    assert div7.position_x == div9.position_x == 0\n    assert div8.position_x == div10.position_x == 5\n    assert div7.position_y == div8.position_y == 0\n    assert div9.position_y == div10.position_y == 6\n    assert div7.height == div8.height == 6\n    assert div9.height == div10.height == 2\n    assert len(div7.children) == 3\n    assert len(div8.children) == 1\n\n\n@assert_no_logs\ndef test_grid_break_item_avoid_auto():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 10px 11px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n      </style>\n      <section>\n        <div>1</div>\n        <div>2</div>\n        <div>3</div>\n        <div>4</div>\n        <div>5</div>\n        <div>6</div>\n        <div style=\"break-inside: avoid\">7a<br>7b<br>7c</div>\n        <div style=\"break-inside: auto\">8</div>\n        <div>9</div>\n        <div>10</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 11\n    div1, div2, div3, div4, div5, div6 = section.children\n    assert div1.position_x == div3.position_x == div5.position_x == 0\n    assert div2.position_x == div4.position_x == div6.position_x == 5\n    assert div1.position_y == div2.position_y == 0\n    assert div1.height == div2.height == 2\n    assert div3.position_y == div4.position_y == 2\n    assert div3.height == div4.height == 2\n    assert div5.position_y == div6.position_y == 4\n    assert div5.height == div6.height == 2\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    assert section.height == 8\n    div7, div8, div9, div10 = section.children\n    assert div7.position_x == div9.position_x == 0\n    assert div8.position_x == div10.position_x == 5\n    assert div7.position_y == div8.position_y == 0\n    assert div9.position_y == div10.position_y == 6\n    assert div7.height == div8.height == 6\n    assert div9.height == div10.height == 2\n    assert len(div7.children) == 3\n    assert len(div8.children) == 1\n\n\n@assert_no_logs\ndef test_grid_break_container():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 10px 11px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n      </style>\n      <p>1<br>2<br>3<br>4</p>\n      <section style=\"break-inside: avoid\">\n        <div>5</div>\n        <div>6</div>\n        <div>7</div>\n        <div>8</div>\n        <div>9</div>\n        <div>10</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    p, = body.children\n    assert len(p.children) == 4\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    assert len(section.children) == 6\n\n\n@assert_no_logs\ndef test_grid_bottom_page():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 10px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n      </style>\n      <div style=\"height: 9px\"></div>\n      <section>\n        <div>1</div>\n        <div>2</div>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n\n\n@assert_no_logs\ndef test_grid_in_grid():\n    # Regression test for #2626.\n    page1, page2, = render_pages('''\n      <style>\n        @page { size: 10px }\n        body { font: 2px / 1 weasyprint }\n        section { display: grid; grid-template-columns: 1fr 1fr }\n      </style>\n      <div style=\"height: 7px\"></div>\n      <section>\n        <section>\n          <div>1 2</div>\n          <div>3 4</div>\n        </section>\n        <section>\n          <div>5 6</div>\n          <div>7 8</div>\n        </section>\n      </section>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, section = body.children\n    subsection1, subsection2 = section.children\n    div1, div2 = subsection1.children\n    assert div1.children[0].children[0].text == '1'\n    assert div2.children[0].children[0].text == '3'\n    div3, div4 = subsection2.children\n    assert div3.children[0].children[0].text == '5'\n    assert div4.children[0].children[0].text == '7'\n\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    subsection1, subsection2 = section.children\n    div1, div2 = subsection1.children\n    assert div1.children[0].children[0].text == '2'\n    assert div2.children[0].children[0].text == '4'\n    div3, div4 = subsection2.children\n    assert div3.children[0].children[0].text == '6'\n    assert div4.children[0].children[0].text == '8'\n\n\n@assert_no_logs\ndef test_grid_in_flex_after_full_height():\n    # Regression test for #2629.\n    page1, page2, = render_pages('''\n      <style>\n        @page { size: 10px }\n      </style>\n      <div style=\"height: 15px\"></div>\n      <div style=\"display: flex; flex-direction: column\">\n        <div style=\"display: grid\">\n          <div></div>\n        </div>\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    assert not div.children\n\n    html, = page2.children\n    body, = html.children\n    flex, = body.children\n    grid, = flex.children\n    grid_item, = grid.children\n    assert not grid_item.children\n\n\n@assert_no_logs\ndef test_grid_in_columns():\n    # Regression test for #2691.\n    page1, page2, = render_pages('''\n      <style>\n        @page { size: 12cm }\n        section { height: 10cm }\n      </style>\n      <div style=\"columns: 2\">\n        <div style=\"display: grid\">\n          <div>\n            <section></section>\n            <section></section>\n            <section></section>\n          </div>\n        </div>\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    grid, = column1.children\n    assert len(grid.children) == 1\n    grid, = column2.children\n    assert len(grid.children) == 1\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    column1, column2 = div.children\n    grid, = column1.children\n    assert len(grid.children) == 1\n    grid, = column2.children\n    assert len(grid.children) == 0\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_grid_in_columns_with_break():\n    # Regression test for #2695.\n    page1, page2, = render_pages('''\n      <style>\n        @page { size: 12px }\n        section { height: 4px }\n      </style>\n      <div style=\"columns: 1\">\n        <div style=\"display: grid; margin-top: 6px\">\n          <div>\n            <section></section>\n            <section></section>\n            <section></section>\n          </div>\n        </div>\n      </div>\n    ''')\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    column1, = div.children\n    grid, = column1.children\n    section1, = grid.children\n    assert section1.position_y == 6\n\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    column1, = div.children\n    grid, = column1.children\n    section2, section3 = grid.children\n    assert section2.position_y == 0\n    assert section3.position_y == 4\n"
  },
  {
    "path": "tests/layout/test_image.py",
    "content": "\"\"\"Tests for images layout.\"\"\"\n\nimport pytest\n\nfrom weasyprint.formatting_structure import boxes\n\nfrom ..testing_utils import assert_no_logs, capture_logs, render_pages\n\n\ndef get_img(html):\n    page, = render_pages(html)\n    html, = page.children\n    body, = html.children\n    if body.children:\n        line, = body.children\n        img, = line.children\n    else:\n        img = None\n    return body, img\n\n\n@pytest.mark.parametrize('html', [\n    '<img src=\"%s\">' % url for url in (\n        'pattern.png', 'pattern.gif', 'blue.jpg', 'pattern.svg',\n        \"data:image/svg+xml,<svg width='4' height='4'></svg>\",\n        \"DatA:image/svg+xml,<svg width='4px' height='4px'></svg>\",\n    )] + [\n        '<embed src=pattern.png>',\n        '<embed src=pattern.svg>',\n        '<embed src=really-a-png.svg type=image/png>',\n        '<embed src=really-a-svg.png type=image/svg+xml>',\n\n        '<object data=pattern.png>',\n        '<object data=pattern.svg>',\n        '<object data=really-a-png.svg type=image/png>',\n        '<object data=really-a-svg.png type=image/svg+xml>',\n    ]\n)\n@assert_no_logs\ndef test_images_1(html):\n    body, img = get_img(html)\n    assert img.width == 4\n    assert img.height == 4\n\n\n@assert_no_logs\ndef test_images_2():\n    # With physical units\n    url = \"data:image/svg+xml,<svg width='2.54cm' height='0.5in'></svg>\"\n    body, img = get_img('<img src=\"%s\">' % url)\n    assert img.width == 96\n    assert img.height == 48\n\n\n@pytest.mark.parametrize('url', [\n    'nonexistent.png',\n    'unknownprotocol://weasyprint.org/foo.png',\n    'data:image/unknowntype,Not an image',\n    # Invalid protocol\n    'datå:image/svg+xml,<svg width=\"4\" height=\"4\"></svg>',\n    # zero-byte images\n    'data:image/png,',\n    'data:image/jpeg,',\n    'data:image/svg+xml,',\n    # Incorrect format\n    'data:image/png,Not a PNG',\n    'data:image/jpeg,Not a JPEG',\n    'data:image/svg+xml,<svg>invalid xml',\n])\n@assert_no_logs\ndef test_images_3(url):\n    # Invalid images\n    with capture_logs() as logs:\n        body, img = get_img(f\"<img src='{url}' alt='invalid image'>\")\n    assert len(logs) == 1\n    assert 'ERROR: Failed to load image' in logs[0]\n    assert isinstance(img, boxes.InlineBox)  # not a replaced box\n    text, = img.children\n    assert text.text == 'invalid image', url\n\n\n@pytest.mark.parametrize('url', [\n    # GIF with JPEG mimetype\n    'data:image/jpeg;base64,'\n    'R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=',\n    # GIF with PNG mimetype\n    'data:image/png;base64,'\n    'R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=',\n    # PNG with JPEG mimetype\n    'data:image/jpeg;base64,'\n    'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC'\n    '0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=',\n    # SVG with PNG mimetype\n    'data:image/png,<svg width=\"1\" height=\"1\"></svg>',\n    'really-a-svg.png',\n    # PNG with SVG\n    'data:image/svg+xml;base64,'\n    'R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=',\n    'really-a-png.svg',\n])\n@assert_no_logs\ndef test_images_4(url):\n    # Sniffing, no logs\n    body, img = get_img(\"<img src='%s'>\" % url)\n\n\n@assert_no_logs\ndef test_images_5():\n    with capture_logs() as logs:\n        render_pages('<img src=nonexistent.png><img src=nonexistent.png>')\n    # Failures are cached too: only one error\n    assert len(logs) == 1\n    assert 'ERROR: Failed to load image' in logs[0]\n\n\n@assert_no_logs\ndef test_images_6():\n    # Layout rules try to preserve the ratio, so the height should be 40px too:\n    body, img = get_img('''<body style=\"font-size: 0\">\n        <img src=\"pattern.png\" style=\"width: 40px\">''')\n    assert body.height == 40\n    assert img.position_y == 0\n    assert img.width == 40\n    assert img.height == 40\n\n\n@assert_no_logs\ndef test_images_7():\n    body, img = get_img('''<body style=\"font-size: 0\">\n        <img src=\"pattern.png\" style=\"height: 40px\">''')\n    assert body.height == 40\n    assert img.position_y == 0\n    assert img.width == 40\n    assert img.height == 40\n\n\n@assert_no_logs\ndef test_images_8():\n    # Same with percentages\n    body, img = get_img('''<body style=\"font-size: 0\"><p style=\"width: 200px\">\n        <img src=\"pattern.png\" style=\"width: 20%\">''')\n    assert body.height == 40\n    assert img.position_y == 0\n    assert img.width == 40\n    assert img.height == 40\n\n\n@assert_no_logs\ndef test_images_9():\n    body, img = get_img('''<body style=\"font-size: 0\">\n        <img src=\"pattern.png\" style=\"min-width: 40px\">''')\n    assert body.height == 40\n    assert img.position_y == 0\n    assert img.width == 40\n    assert img.height == 40\n\n\n@assert_no_logs\ndef test_images_10():\n    body, img = get_img('<img src=\"pattern.png\" style=\"max-width: 2px\">')\n    assert img.width == 2\n    assert img.height == 2\n\n\n@assert_no_logs\ndef test_images_11():\n    # display: table-cell is ignored. XXX Should it?\n    page, = render_pages('''<body style=\"font-size: 0\">\n        <img src=\"pattern.png\" style=\"width: 40px\">\n        <img src=\"pattern.png\" style=\"width: 60px; display: table-cell\">\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    img_1, img_2 = line.children\n    assert body.height == 60\n    assert img_1.width == 40\n    assert img_1.height == 40\n    assert img_2.width == 60\n    assert img_2.height == 60\n    assert img_1.position_y == 20\n    assert img_2.position_y == 0\n\n\n@assert_no_logs\ndef test_images_12():\n    # Block-level image:\n    page, = render_pages('''\n        <style>\n            @page { size: 100px }\n            img { width: 40px; margin: 10px auto; display: block }\n        </style>\n        <body>\n            <img src=\"pattern.png\">\n    ''')\n    html, = page.children\n    body, = html.children\n    img, = body.children\n    assert img.element_tag == 'img'\n    assert img.position_x == 0\n    assert img.position_y == 0\n    assert img.width == 40\n    assert img.height == 40\n    assert img.content_box_x() == 30  # (100 - 40) / 2 == 30px for margin-left\n    assert img.content_box_y() == 10\n\n\n@assert_no_logs\ndef test_images_13():\n    page, = render_pages('''\n        <style>\n            @page { size: 100px }\n            img { min-width: 40%; margin: 10px auto; display: block }\n        </style>\n        <body>\n            <img src=\"pattern.png\">\n    ''')\n    html, = page.children\n    body, = html.children\n    img, = body.children\n    assert img.element_tag == 'img'\n    assert img.position_x == 0\n    assert img.position_y == 0\n    assert img.width == 40\n    assert img.height == 40\n    assert img.content_box_x() == 30  # (100 - 40) / 2 == 30px for margin-left\n    assert img.content_box_y() == 10\n\n\n@assert_no_logs\ndef test_images_14():\n    page, = render_pages('''\n        <style>\n            @page { size: 100px }\n            img { min-width: 40px; margin: 10px auto; display: block }\n        </style>\n        <body>\n            <img src=\"pattern.png\">\n    ''')\n    html, = page.children\n    body, = html.children\n    img, = body.children\n    assert img.element_tag == 'img'\n    assert img.position_x == 0\n    assert img.position_y == 0\n    assert img.width == 40\n    assert img.height == 40\n    assert img.content_box_x() == 30  # (100 - 40) / 2 == 30px for margin-left\n    assert img.content_box_y() == 10\n\n\n@assert_no_logs\ndef test_images_15():\n    page, = render_pages('''\n        <style>\n            @page { size: 100px }\n            img { min-height: 30px; max-width: 2px;\n                  margin: 10px auto; display: block }\n        </style>\n        <body>\n            <img src=\"pattern.png\">\n    ''')\n    html, = page.children\n    body, = html.children\n    img, = body.children\n    assert img.element_tag == 'img'\n    assert img.position_x == 0\n    assert img.position_y == 0\n    assert img.width == 2\n    assert img.height == 30\n    assert img.content_box_x() == 49  # (100 - 2) / 2 == 49px for margin-left\n    assert img.content_box_y() == 10\n\n\n@assert_no_logs\ndef test_images_16():\n    page, = render_pages('''\n        <body style=\"float: left\">\n        <img style=\"height: 200px; margin: 10px; display: block\" src=\"\n            data:image/svg+xml,\n            <svg width='150' height='100'></svg>\n        \">\n    ''')\n    html, = page.children\n    body, = html.children\n    img, = body.children\n    assert body.width == 320\n    assert body.height == 220\n    assert img.element_tag == 'img'\n    assert img.width == 300\n    assert img.height == 200\n\n\n@assert_no_logs\ndef test_images_17():\n    page, = render_pages('''\n        <div style=\"width: 300px; height: 300px\">\n        <img src=\"\n            data:image/svg+xml,\n            <svg viewBox='0 0 20 10'></svg>\n        \">''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line, = div.children\n    img, = line.children\n    assert div.width == 300\n    assert div.height == 300\n    assert img.element_tag == 'img'\n    assert img.width == 300\n    assert img.height == 150\n\n\n@assert_no_logs\ndef test_images_18():\n    # Regression test for #1050.\n    page, = render_pages('''\n        <img style=\"position: absolute\" src=\"\n            data:image/svg+xml,\n            <svg viewBox='0 0 20 10'></svg>\n        \">''')\n\n\n@pytest.mark.parametrize(('html', 'children'), [\n    ('<embed>', []),\n    ('<embed src=\"unknown\">', []),\n    ('<object></object>', []),\n    ('<object data=\"unknown\"></object>', []),\n    ('<object>abc</object>', ['TextBox']),\n    ('<object data=\"unknown\">abc</object>', ['TextBox']),\n])\ndef test_images_19(html, children):\n    body, img = get_img(html)\n    img_children = [\n        type(child).__name__ for child in getattr(img, 'children', [])]\n    assert img_children == children\n\n\n@assert_no_logs\ndef test_linear_gradient():\n    red = (1, 0, 0, 1)\n    lime = (0, 1, 0, 1)\n    blue = (0, 0, 1, 1)\n\n    def layout(gradient_css, type_='linear', init=(),\n               positions=[0, 1], colors=[blue, lime]):\n        page, = render_pages('<style>@page { background: ' + gradient_css)\n        layer, = page.background.layers\n        result = layer.image.layout(400, 300, {})\n        assert result[0] == 1\n        assert result[1] == type_\n        assert result[2] == (None if init is None else pytest.approx(init))\n        assert result[3] == pytest.approx(positions)\n        for color1, color2 in zip(result[4], colors):\n            assert tuple(color1) == pytest.approx(color2)\n\n    layout('linear-gradient(blue)', 'solid', None, [], [blue])\n    layout('repeating-linear-gradient(blue)', 'solid', None, [], [blue])\n    layout('linear-gradient(blue, lime)', init=(200, 0, 200, 300))\n    layout('repeating-linear-gradient(blue, lime)', init=(200, 0, 200, 300))\n    layout('repeating-linear-gradient(blue, lime 100px)',\n           positions=[0, 1, 1, 2, 2, 3], init=(200, 0, 200, 300))\n\n    layout('linear-gradient(to bottom, blue, lime)', init=(200, 0, 200, 300))\n    layout('linear-gradient(to top, blue, lime)', init=(200, 300, 200, 0))\n    layout('linear-gradient(to right, blue, lime)', init=(0, 150, 400, 150))\n    layout('linear-gradient(to left, blue, lime)', init=(400, 150, 0, 150))\n\n    layout('linear-gradient(to top left, blue, lime)',\n           init=(344, 342, 56, -42))\n    layout('linear-gradient(to top right, blue, lime)',\n           init=(56, 342, 344, -42))\n    layout('linear-gradient(to bottom left, blue, lime)',\n           init=(344, -42, 56, 342))\n    layout('linear-gradient(to bottom right, blue, lime)',\n           init=(56, -42, 344, 342))\n\n    layout('linear-gradient(270deg, blue, lime)', init=(400, 150, 0, 150))\n    layout('linear-gradient(.75turn, blue, lime)', init=(400, 150, 0, 150))\n    layout('linear-gradient(45deg, blue, lime)', init=(25, 325, 375, -25))\n    layout('linear-gradient(.125turn, blue, lime)', init=(25, 325, 375, -25))\n    layout('linear-gradient(.375turn, blue, lime)', init=(25, -25, 375, 325))\n    layout('linear-gradient(.625turn, blue, lime)', init=(375, -25, 25, 325))\n    layout('linear-gradient(.875turn, blue, lime)', init=(375, 325, 25, -25))\n\n    layout('linear-gradient(blue 2em, lime 20%)', init=(200, 32, 200, 60))\n    layout('linear-gradient(blue 100px, red, blue, red 160px, lime)',\n           init=(200, 100, 200, 300), colors=[blue, red, blue, red, lime],\n           positions=[0, .1, .2, .3, 1])\n    layout('linear-gradient(blue -100px, blue 0, red -12px, lime 50%)',\n           init=(200, -100, 200, 150), colors=[blue, blue, red, lime],\n           positions=[0, .4, .4, 1])\n    layout('linear-gradient(blue, blue, red, lime -7px)',\n           init=(200, -1, 200, 1), colors=[blue, blue, blue, red, lime, lime],\n           positions=[0, 0.5, 0.5, 0.5, 0.5, 1])\n    layout('repeating-linear-gradient(blue, blue, lime, lime -7px)',\n           'solid', None, [], [(0, .5, .5, 1)])\n\n\n@assert_no_logs\ndef test_radial_gradient():\n    red = (1, 0, 0, 1)\n    lime = (0, 1, 0, 1)\n    blue = (0, 0, 1, 1)\n\n    def layout(gradient_css, type_='radial', init=(),\n               positions=[0, 1], colors=[blue, lime], scale_y=1):\n        if type_ == 'radial':\n            center_x, center_y, radius0, radius1 = init\n            init = (center_x, center_y / scale_y, radius0,\n                    center_x, center_y / scale_y, radius1)\n        page, = render_pages('<style>@page { background: ' + gradient_css)\n        layer, = page.background.layers\n        result = layer.image.layout(400, 300, {})\n        assert result[0] == scale_y\n        assert result[1] == type_\n        assert result[2] == (None if init is None else pytest.approx(init))\n        assert result[3] == pytest.approx(positions)\n        for color1, color2 in zip(result[4], colors):\n            assert tuple(color1) == pytest.approx(color2)\n\n    layout('radial-gradient(blue)', 'solid', None, [], [blue])\n    layout('repeating-radial-gradient(blue)', 'solid', None, [], [blue])\n    layout('radial-gradient(100px, blue, lime)',\n           init=(200, 150, 0, 100))\n\n    layout('radial-gradient(100px at right 20px bottom 30px, lime, red)',\n           init=(380, 270, 0, 100), colors=[lime, red])\n    layout('radial-gradient(0 0, blue, lime)',\n           init=(200, 150, 0, 1e-7))\n    layout('radial-gradient(1px 0, blue, lime)',\n           init=(200, 150, 0, 1e7), scale_y=1e-14)\n    layout('radial-gradient(0 1px, blue, lime)',\n           init=(200, 150, 0, 1e-7), scale_y=1e14)\n    layout('repeating-radial-gradient(100px 200px, blue, lime)',\n           positions=[0, 1, 1, 2, 2, 3], init=(200, 150, 0, 300),\n           scale_y=(200 / 100))\n    layout('repeating-radial-gradient(42px, blue -100px, lime 100px)',\n           positions=[-0.5, 0, 0, 1], init=(200, 150, 0, 300),\n           colors=[(0, 0.5, 0.5, 1), lime, blue, lime])\n    layout('radial-gradient(42px, blue -20px, lime -1px)',\n           'solid', None, [], [lime])\n    layout('radial-gradient(42px, blue -20px, lime 0)',\n           'solid', None, [], [lime])\n    layout('radial-gradient(42px, blue -20px, lime 20px)',\n           init=(200, 150, 0, 20), colors=[(0, .5, .5, 1), lime])\n\n    layout('radial-gradient(100px 120px, blue, lime)',\n           init=(200, 150, 0, 100), scale_y=(120 / 100))\n    layout('radial-gradient(25% 40%, blue, lime)',\n           init=(200, 150, 0, 100), scale_y=(120 / 100))\n\n    layout('radial-gradient(circle closest-side, blue, lime)',\n           init=(200, 150, 0, 150))\n    layout('radial-gradient(circle closest-side at 150px 50px, blue, lime)',\n           init=(150, 50, 0, 50))\n    layout('radial-gradient(circle closest-side at 45px 50px, blue, lime)',\n           init=(45, 50, 0, 45))\n    layout('radial-gradient(circle closest-side at 420px 50px, blue, lime)',\n           init=(420, 50, 0, 20))\n    layout('radial-gradient(circle closest-side at 420px 281px, blue, lime)',\n           init=(420, 281, 0, 19))\n\n    layout('radial-gradient(closest-side, blue 20%, lime)',\n           init=(200, 150, 40, 200), scale_y=(150 / 200))\n    layout('radial-gradient(closest-side at 300px 20%, blue, lime)',\n           init=(300, 60, 0, 100), scale_y=(60 / 100))\n    layout('radial-gradient(closest-side at 10% 230px, blue, lime)',\n           init=(40, 230, 0, 40), scale_y=(70 / 40))\n\n    layout('radial-gradient(circle farthest-side, blue, lime)',\n           init=(200, 150, 0, 200))\n    layout('radial-gradient(circle farthest-side at 150px 50px, blue, lime)',\n           init=(150, 50, 0, 250))\n    layout('radial-gradient(circle farthest-side at 45px 50px, blue, lime)',\n           init=(45, 50, 0, 355))\n    layout('radial-gradient(circle farthest-side at 420px 50px, blue, lime)',\n           init=(420, 50, 0, 420))\n    layout('radial-gradient(circle farthest-side at 220px 310px, blue, lime)',\n           init=(220, 310, 0, 310))\n\n    layout('radial-gradient(farthest-side, blue, lime)',\n           init=(200, 150, 0, 200), scale_y=(150 / 200))\n    layout('radial-gradient(farthest-side at 300px 20%, blue, lime)',\n           init=(300, 60, 0, 300), scale_y=(240 / 300))\n    layout('radial-gradient(farthest-side at 10% 230px, blue, lime)',\n           init=(40, 230, 0, 360), scale_y=(230 / 360))\n\n    layout('radial-gradient(circle closest-corner, blue, lime)',\n           init=(200, 150, 0, 250))\n    layout('radial-gradient(circle closest-corner at 340px 80px, blue, lime)',\n           init=(340, 80, 0, 100))\n    layout('radial-gradient(circle closest-corner at 0 342px, blue, lime)',\n           init=(0, 342, 0, 42))\n\n    layout('radial-gradient(closest-corner, blue, lime)',\n           init=(200, 150, 0, 200 * 2 ** 0.5), scale_y=(150 / 200))\n    layout('radial-gradient(closest-corner at 450px 100px, blue, lime)',\n           init=(450, 100, 0, 50 * 2 ** 0.5), scale_y=(100 / 50))\n    layout('radial-gradient(closest-corner at 40px 210px, blue, lime)',\n           init=(40, 210, 0, 40 * 2 ** 0.5), scale_y=(90 / 40))\n\n    layout('radial-gradient(circle farthest-corner, blue, lime)',\n           init=(200, 150, 0, 250))\n    layout('radial-gradient(circle farthest-corner'\n           ' at 300px -100px, blue, lime)',\n           init=(300, -100, 0, 500))\n    layout('radial-gradient(circle farthest-corner at 400px 0, blue, lime)',\n           init=(400, 0, 0, 500))\n\n    layout('radial-gradient(farthest-corner, blue, lime)',\n           init=(200, 150, 0, 200 * 2 ** 0.5), scale_y=(150 / 200))\n    layout('radial-gradient(farthest-corner at 450px 100px, blue, lime)',\n           init=(450, 100, 0, 450 * 2 ** 0.5), scale_y=(200 / 450))\n    layout('radial-gradient(farthest-corner at 40px 210px, blue, lime)',\n           init=(40, 210, 0, 360 * 2 ** 0.5), scale_y=(210 / 360))\n\n\n@pytest.mark.parametrize(('props', 'div_width'), [\n    ({}, 4),\n    ({'min-width': '10px'}, 10),\n    ({'max-width': '1px'}, 1),\n    ({'width': '10px'}, 10),\n    ({'width': '1px'}, 1),\n    ({'min-height': '10px'}, 10),\n    ({'max-height': '1px'}, 1),\n    ({'height': '10px'}, 10),\n    ({'height': '1px'}, 1),\n    ({'min-width': '10px', 'min-height': '1px'}, 10),\n    ({'min-width': '1px', 'min-height': '10px'}, 10),\n    ({'max-width': '10px', 'max-height': '1px'}, 1),\n    ({'max-width': '1px', 'max-height': '10px'}, 1),\n])\ndef test_image_min_max_width(props, div_width):\n    default = {\n        'min-width': 'auto', 'max-width': 'none', 'width': 'auto',\n        'min-height': 'auto', 'max-height': 'none', 'height': 'auto'}\n    page, = render_pages('''\n      <style> img { display: block; %s } </style>\n      <div style=\"display: inline-block\">\n        <img src=\"pattern.png\"><img src=\"pattern.svg\">\n      </div>''' % ';'.join(\n          f'{key}: {props.get(key, value)}' for key, value in default.items()))\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    div, = line.children\n    assert div.width == div_width\n\n\n@pytest.mark.parametrize(('css', 'width'), [\n    ('width: 10px', 10),\n    ('width: 1px', 1),\n    ('height: 10px', 20),\n    ('height: 1px', 2),\n])\ndef test_svg_no_size_width(css, width):\n    # Size is undefined when both width and heigh are not set.\n    # https://drafts.csswg.org/css2/#inline-replaced-width\n    page, = render_pages('''\n      <style> svg { %s } </style>\n      <div style=\"display: inline-block\">\n        <svg viewBox=\"0 0 8 4\"></svg>\n      </div>''' % css)\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    div, = line.children\n    assert div.width == width\n\n\n@pytest.mark.parametrize(('css', 'width'), [\n    ('width: 10px', 10),\n    ('width: 1px', 1),\n    ('height: 10px', 20),\n    ('height: 1px', 2),\n])\ndef test_svg_no_size_min_width(css, width):\n    # Size is undefined when both width and heigh are not set.\n    # https://drafts.csswg.org/css2/#inline-replaced-width\n    page, = render_pages('''\n      <style> svg { %s } </style>\n      <table>\n        <tr>\n          <td><svg viewBox=\"0 0 8 4\"></svg></td>\n          <td style=\"width: 1000mm\"></td>\n        </tr>\n      </table>''' % css)\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td1, td2 = row.children\n    assert td1.width == width\n"
  },
  {
    "path": "tests/layout/test_inline.py",
    "content": "\"\"\"Tests for inlines layout.\"\"\"\n\nimport pytest\n\nfrom weasyprint.formatting_structure import boxes\n\nfrom ..testing_utils import SANS_FONTS, assert_no_logs, render_pages\n\n\n@assert_no_logs\ndef test_empty_linebox():\n    page, = render_pages('<p> </p>')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert len(paragraph.children) == 0\n    assert paragraph.height == 0\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_empty_linebox_removed_space():\n    # Whitespace removed at the beginning of the line => empty line => no line\n    page, = render_pages('''\n      <style>\n        p { width: 1px }\n      </style>\n      <p><br>  </p>\n    ''')\n    page, = render_pages('<p> </p>')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    # TODO: The second line should be removed\n    assert len(paragraph.children) == 1\n\n\n@assert_no_logs\ndef test_breaking_linebox():\n    page, = render_pages('''\n      <style>\n      p { font-size: 13px;\n          width: 300px;\n          font-family: %(fonts)s;\n          background-color: #393939;\n          color: #FFFFFF;\n          line-height: 1;\n          text-decoration: underline overline line-through;}\n      </style>\n      <p><em>Lorem<strong> Ipsum <span>is very</span>simply</strong><em>\n      dummy</em>text of the printing and. naaaa </em> naaaa naaaa naaaa\n      naaaa naaaa naaaa naaaa naaaa</p>\n    ''' % {'fonts': SANS_FONTS})\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert len(list(paragraph.children)) == 3\n\n    lines = paragraph.children\n    for line in lines:\n        assert line.style['font_size'] == 13\n        assert line.element_tag == 'p'\n        for child in line.children:\n            assert child.element_tag in ('em', 'p')\n            assert child.style['font_size'] == 13\n            for child_child in child.children:\n                assert child.element_tag in ('em', 'strong', 'span')\n                assert child.style['font_size'] == 13\n\n\n@assert_no_logs\ndef test_position_x_ltr():\n    page, = render_pages('''\n      <style>\n        span {\n          padding: 0 10px 0 15px;\n          margin: 0 2px 0 3px;\n          border: 1px solid;\n         }\n      </style>\n      <body><span>a<br>b<br>c</span>''')\n    html, = page.children\n    body, = html.children\n    line1, line2, line3 = body.children\n    span1, = line1.children\n    assert span1.position_x == 0\n    text1, br1 = span1.children\n    assert text1.position_x == 15 + 3 + 1\n    span2, = line2.children\n    assert span2.position_x == 0\n    text2, br2 = span2.children\n    assert text2.position_x == 0\n    span3, = line3.children\n    assert span3.position_x == 0\n    text3, = span3.children\n    assert text3.position_x == 0\n\n\n@assert_no_logs\ndef test_position_x_rtl():\n    page, = render_pages('''\n      <style>\n        body {\n          direction: rtl;\n          width: 100px;\n        }\n        span {\n          padding: 0 10px 0 15px;\n          margin: 0 2px 0 3px;\n          border: 1px solid;\n         }\n      </style>\n      <body><span>a<br>b<br>c</span>''')\n    html, = page.children\n    body, = html.children\n    line1, line2, line3 = body.children\n    span1, = line1.children\n    text1, br1 = span1.children\n    assert span1.position_x == 100 - text1.width - (10 + 2 + 1)\n    assert text1.position_x == 100 - text1.width - (10 + 2 + 1)\n    span2, = line2.children\n    text2, br2 = span2.children\n    assert span2.position_x == 100 - text2.width\n    assert text2.position_x == 100 - text2.width\n    span3, = line3.children\n    text3, = span3.children\n    assert span3.position_x == 100 - text3.width - (15 + 3 + 1)\n    assert text3.position_x == 100 - text3.width\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_1():\n    # See https://unicode.org/reports/tr14/\n    page, = render_pages('<pre>a\\nb\\rc\\r\\nd\\u2029e</pre>')\n    html, = page.children\n    body, = html.children\n    pre, = body.children\n    lines = pre.children\n    texts = []\n    for line in lines:\n        text_box, = line.children\n        texts.append(text_box.text)\n    assert texts == ['a', 'b', 'c', 'd', 'e']\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_2():\n    html_sample = '''\n      <p style=\"width: %d.5em; font-family: weasyprint\">ab\n      <span style=\"padding-right: 1em; margin-right: 1em\">c def</span>g\n      hi</p>'''\n    for i in range(16):\n        page, = render_pages(html_sample % i)\n        html, = page.children\n        body, = html.children\n        p, = body.children\n        lines = p.children\n\n        if i in (0, 1, 2, 3):\n            line_1, line_2, line_3, line_4 = lines\n\n            textbox_1, = line_1.children\n            assert textbox_1.text == 'ab'\n\n            span_1, = line_2.children\n            textbox_1, = span_1.children\n            assert textbox_1.text == 'c'\n\n            span_1, textbox_2 = line_3.children\n            textbox_1, = span_1.children\n            assert textbox_1.text == 'def'\n            assert textbox_2.text == 'g'\n\n            textbox_1, = line_4.children\n            assert textbox_1.text == 'hi'\n        elif i in (4, 5, 6, 7, 8):\n            line_1, line_2, line_3 = lines\n\n            textbox_1, span_1 = line_1.children\n            assert textbox_1.text == 'ab '\n            textbox_2, = span_1.children\n            assert textbox_2.text == 'c'\n\n            span_1, textbox_2 = line_2.children\n            textbox_1, = span_1.children\n            assert textbox_1.text == 'def'\n            assert textbox_2.text == 'g'\n\n            textbox_1, = line_3.children\n            assert textbox_1.text == 'hi'\n        elif i in (9, 10):\n            line_1, line_2 = lines\n\n            textbox_1, span_1 = line_1.children\n            assert textbox_1.text == 'ab '\n            textbox_2, = span_1.children\n            assert textbox_2.text == 'c'\n\n            span_1, textbox_2 = line_2.children\n            textbox_1, = span_1.children\n            assert textbox_1.text == 'def'\n            assert textbox_2.text == 'g hi'\n        elif i in (11, 12, 13):\n            line_1, line_2 = lines\n\n            textbox_1, span_1, textbox_3 = line_1.children\n            assert textbox_1.text == 'ab '\n            textbox_2, = span_1.children\n            assert textbox_2.text == 'c def'\n            assert textbox_3.text == 'g'\n\n            textbox_1, = line_2.children\n            assert textbox_1.text == 'hi'\n        else:\n            line_1, = lines\n\n            textbox_1, span_1, textbox_3 = line_1.children\n            assert textbox_1.text == 'ab '\n            textbox_2, = span_1.children\n            assert textbox_2.text == 'c def'\n            assert textbox_3.text == 'g hi'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_3():\n    # Regression test for #560.\n    page, = render_pages(\n        '<div style=\"width: 5.5em; font-family: weasyprint\">'\n        'aaaa aaaa a [<span>aaa</span>]')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line1, line2, line3, line4 = div.children\n    assert line1.children[0].text == line2.children[0].text == 'aaaa'\n    assert line3.children[0].text == 'a'\n    text1, span, text2 = line4.children\n    assert text1.text == '['\n    assert text2.text == ']'\n    assert span.children[0].text == 'aaa'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_4():\n    # Regression test for #560.\n    page, = render_pages(\n        '<div style=\"width: 5.5em; font-family: weasyprint\">'\n        'aaaa a <span>b c</span>d')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line1, line2, line3 = div.children\n    assert line1.children[0].text == 'aaaa'\n    assert line2.children[0].text == 'a '\n    assert line2.children[1].children[0].text == 'b'\n    assert line3.children[0].children[0].text == 'c'\n    assert line3.children[1].text == 'd'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_5():\n    # Regression test for #580.\n    page, = render_pages(\n        '<div style=\"width: 5.5em; font-family: weasyprint\">'\n        '<span>aaaa aaaa a a a</span><span>bc</span>')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line1, line2, line3, line4 = div.children\n    assert line1.children[0].children[0].text == 'aaaa'\n    assert line2.children[0].children[0].text == 'aaaa'\n    assert line3.children[0].children[0].text == 'a a'\n    assert line4.children[0].children[0].text == 'a'\n    assert line4.children[1].children[0].text == 'bc'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_6():\n    # Regression test for #586.\n    page, = render_pages(\n        '<div style=\"width: 5.5em; font-family: weasyprint\">'\n        'a a <span style=\"white-space: nowrap\">/ccc</span>')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line1, line2 = div.children\n    assert line1.children[0].text == 'a a'\n    assert line2.children[0].children[0].text == '/ccc'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_7():\n    # Regression test for #660.\n    page, = render_pages(\n        '<div style=\"width: 3.5em; font-family: weasyprint\">'\n        '<span><span>abc d e</span></span><span>f')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line1, line2, line3 = div.children\n    assert line1.children[0].children[0].children[0].text == 'abc'\n    assert line2.children[0].children[0].children[0].text == 'd'\n    assert line3.children[0].children[0].children[0].text == 'e'\n    assert line3.children[1].children[0].text == 'f'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_8():\n    # Regression test for #783.\n    page, = render_pages(\n        '<p style=\"font-family: weasyprint\"><span>\\n'\n        'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\n'\n        'bbbbbbbbbbb\\n'\n        '<b>cccc</b></span>ddd</p>')\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    line1, line2 = p.children\n    assert line1.children[0].children[0].text == (\n        'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbb')\n    assert line2.children[0].children[0].children[0].text == 'cccc'\n    assert line2.children[1].text == 'ddd'\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_breaking_linebox_regression_9():\n    # Regression test for #783.\n    # TODO: inlines.can_break_inside return False for span but we can break\n    # before the <b> tag. can_break_inside should be fixed.\n    page, = render_pages(\n        '<p style=\"font-family: weasyprint\"><span>\\n'\n        'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbb\\n'\n        '<b>cccc</b></span>ddd</p>')\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    line1, line2 = p.children\n    assert line1.children[0].children[0].text == (\n        'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbb')\n    assert line2.children[0].children[0].children[0].text == 'cccc'\n    assert line2.children[1].text == 'ddd'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_10():\n    # Regression test for #923.\n    page, = render_pages(\n        '<p style=\"width:195px; font-family: weasyprint\">'\n        '  <span>'\n        '    <span>xxxxxx YYY yyyyyy yyy</span>'\n        '    ZZZZZZ zzzzz'\n        '  </span> )x '\n        '</p>')\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    line1, line2, line3, line4 = p.children\n    assert line1.children[0].children[0].children[0].text == 'xxxxxx YYY'\n    assert line2.children[0].children[0].children[0].text == 'yyyyyy yyy'\n    assert line3.children[0].children[0].text == 'ZZZZZZ zzzzz'\n    assert line4.children[0].text == ')x'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_11():\n    # Regression test for #953.\n    page, = render_pages(\n        '<p style=\"width:10em; font-family: weasyprint\">'\n        '  line 1<br><span>123 567 90</span>x'\n        '</p>')\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    line1, line2, line3 = p.children\n    assert line1.children[0].text == 'line 1'\n    assert line2.children[0].children[0].text == '123 567'\n    assert line3.children[0].children[0].text == '90'\n    assert line3.children[1].text == 'x'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_12():\n    # Regression test for #953.\n    page, = render_pages(\n        '<p style=\"width:10em; font-family: weasyprint\">'\n        '  <br><span>123 567 90</span>x'\n        '</p>')\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    line1, line2, line3 = p.children\n    assert line2.children[0].children[0].text == '123 567'\n    assert line3.children[0].children[0].text == '90'\n    assert line3.children[1].text == 'x'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_13():\n    # Regression test for #953.\n    page, = render_pages(\n        '<p style=\"width:10em; font-family: weasyprint\">'\n        '  123 567 90 <span>123 567 90</span>x'\n        '</p>')\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    line1, line2, line3 = p.children\n    assert line1.children[0].text == '123 567 90'\n    assert line2.children[0].children[0].text == '123 567'\n    assert line3.children[0].children[0].text == '90'\n    assert line3.children[1].text == 'x'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_14():\n    # Regression test for #1638.\n    page, = render_pages(\n        '<style>'\n        '  body {font-family: weasyprint; width: 3em}'\n        '</style>'\n        '<span> <span>a</span> b</span><span>c</span>')\n    html, = page.children\n    body, = html.children\n    line1, line2 = body.children\n    assert line1.children[0].children[0].children[0].text == 'a'\n    assert line2.children[0].children[0].text == 'b'\n    assert line2.children[1].children[0].text == 'c'\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_15():\n    # Regression test for #5507.\n    page, = render_pages(\n        '<style>'\n        '  body {font-family: weasyprint; font-size: 4px}'\n        '  pre {float: left}'\n        '</style>'\n        '<pre>ab©\\n'\n        'déf\\n'\n        'ghïj\\n'\n        'klm</pre>')\n    html, = page.children\n    body, = html.children\n    pre, = body.children\n    line1, line2, line3, line4 = pre.children\n    assert line1.children[0].text == 'ab©'\n    assert line2.children[0].text == 'déf'\n    assert line3.children[0].text == 'ghïj'\n    assert line4.children[0].text == 'klm'\n    assert line1.children[0].width == 4 * 3\n    assert line2.children[0].width == 4 * 3\n    assert line3.children[0].width == 4 * 4\n    assert line4.children[0].width == 4 * 3\n    assert pre.width == 4 * 4\n\n\n@assert_no_logs\ndef test_breaking_linebox_regression_16():\n    # Regression test for #1973.\n    page, = render_pages(\n        '<style>'\n        '  body {font-family: weasyprint; font-size: 4px}'\n        '  p {float: left}'\n        '</style>'\n        '<p>tést</p>'\n        '<pre>ab©\\n'\n        'déf\\n'\n        'ghïj\\n'\n        'klm</pre>')\n    html, = page.children\n    body, = html.children\n    p, pre = body.children\n    line1, = p.children\n    assert line1.children[0].text == 'tést'\n    assert p.width == 4 * 4\n    line1, line2, line3, line4 = pre.children\n    assert line1.children[0].text == 'ab©'\n    assert line2.children[0].text == 'déf'\n    assert line3.children[0].text == 'ghïj'\n    assert line4.children[0].text == 'klm'\n    assert line1.children[0].width == 4 * 3\n    assert line2.children[0].width == 4 * 3\n    assert line3.children[0].width == 4 * 4\n    assert line4.children[0].width == 4 * 3\n\n\n@assert_no_logs\ndef test_linebox_text():\n    page, = render_pages('''\n      <style>\n        p { width: 165px; font-family:%(fonts)s;}\n      </style>\n      <p><em>Lorem Ipsum</em>is very <strong>coool</strong></p>\n    ''' % {'fonts': SANS_FONTS})\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    lines = list(paragraph.children)\n    assert len(lines) == 2\n\n    text = ' '.join(\n        (''.join(box.text for box in line.descendants()\n                 if isinstance(box, boxes.TextBox)))\n        for line in lines)\n    assert text == 'Lorem Ipsumis very coool'\n\n\n@assert_no_logs\ndef test_linebox_positions():\n    for width, expected_lines in [(165, 2), (1, 5), (0, 5)]:\n        page = '''\n          <style>\n            p { width:%(width)spx; font-family:%(fonts)s;\n                line-height: 20px }\n          </style>\n          <p>this is test for <strong>Weasyprint</strong></p>'''\n        page, = render_pages(page % {'fonts': SANS_FONTS, 'width': width})\n        html, = page.children\n        body, = html.children\n        paragraph, = body.children\n        lines = list(paragraph.children)\n        assert len(lines) == expected_lines\n\n        ref_position_y = lines[0].position_y\n        ref_position_x = lines[0].position_x\n        for line in lines:\n            assert ref_position_y == line.position_y\n            assert ref_position_x == line.position_x\n            for box in line.children:\n                assert ref_position_x == box.position_x\n                ref_position_x += box.width\n                assert ref_position_y == box.position_y\n            assert ref_position_x - line.position_x <= line.width\n            ref_position_x = line.position_x\n            ref_position_y += line.height\n\n\n@assert_no_logs\ndef test_forced_line_breaks_pre():\n    # These lines should be small enough to fit on the default A4 page\n    # with the default 12pt font-size.\n    page, = render_pages('''\n      <style> pre { line-height: 42px }</style>\n      <pre>Lorem ipsum dolor sit amet,\n          consectetur adipiscing elit.\n\n\n          Sed sollicitudin nibh\n\n          et turpis molestie tristique.</pre>\n    ''')\n    html, = page.children\n    body, = html.children\n    pre, = body.children\n    assert pre.element_tag == 'pre'\n    lines = pre.children\n    assert all(isinstance(line, boxes.LineBox) for line in lines)\n    assert len(lines) == 7\n    assert [line.height for line in lines] == [42] * 7\n\n\n@assert_no_logs\ndef test_forced_line_breaks_paragraph():\n    page, = render_pages('''\n      <style> p { line-height: 42px }</style>\n      <p>Lorem ipsum dolor sit amet,<br>\n        consectetur adipiscing elit.<br><br><br>\n        Sed sollicitudin nibh<br>\n        <br>\n\n        et turpis molestie tristique.</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    assert paragraph.element_tag == 'p'\n    lines = paragraph.children\n    assert all(isinstance(line, boxes.LineBox) for line in lines)\n    assert len(lines) == 7\n    assert [line.height for line in lines] == [42] * 7\n\n\n@assert_no_logs\ndef test_inlinebox_splitting():\n    # Regression test for #389.\n    # The text is strange to test some corner cases.\n    for width in [10000, 100, 10, 0]:\n        page, = render_pages('''\n          <style>p { font-family:%(fonts)s; width: %(width)spx; }</style>\n          <p><strong>WeasyPrint is a frée softwäre ./ visual rendèring enginè\n                     for HTML !!! and CSS.</strong></p>\n        ''' % {'fonts': SANS_FONTS, 'width': width})\n        html, = page.children\n        body, = html.children\n        paragraph, = body.children\n        lines = paragraph.children\n        if width == 10000:\n            assert len(lines) == 1\n        else:\n            assert len(lines) > 1\n        text_parts = []\n        for line in lines:\n            strong, = line.children\n            text, = strong.children\n            text_parts.append(text.text)\n        assert ' '.join(text_parts) == (\n            'WeasyPrint is a frée softwäre ./ visual '\n            'rendèring enginè for HTML !!! and CSS.')\n\n\n@assert_no_logs\ndef test_whitespace_processing():\n    for source in ['a', '  a  ', ' \\n  \\ta', ' a\\t ']:\n        page, = render_pages('<p><em>%s</em></p>' % source)\n        html, = page.children\n        body, = html.children\n        p, = body.children\n        line, = p.children\n        em, = line.children\n        text, = em.children\n        assert text.text == 'a', 'source was %r' % (source,)\n\n        page, = render_pages(\n            '<p style=\"white-space: pre-line\">\\n\\n<em>%s</em></pre>' %\n            source.replace('\\n', ' '))\n        html, = page.children\n        body, = html.children\n        p, = body.children\n        _line1, _line2, line3 = p.children\n        em, = line3.children\n        text, = em.children\n        assert text.text == 'a', 'source was %r' % (source,)\n\n\n@assert_no_logs\ndef test_inline_replaced_auto_margins():\n    page, = render_pages('''\n      <style>\n        @page { size: 200px }\n        img { display: inline; margin: auto; width: 50px }\n      </style>\n      <body><img src=\"pattern.png\" />''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    img, = line.children\n    assert img.margin_top == 0\n    assert img.margin_right == 0\n    assert img.margin_bottom == 0\n    assert img.margin_left == 0\n\n\n@assert_no_logs\ndef test_empty_inline_auto_margins():\n    page, = render_pages('''\n      <style>\n        @page { size: 200px }\n        span { margin: auto }\n      </style>\n      <body><span></span>''')\n    html, = page.children\n    body, = html.children\n    block, = body.children\n    span, = block.children\n    assert span.margin_top != 0\n    assert span.margin_right == 0\n    assert span.margin_bottom != 0\n    assert span.margin_left == 0\n\n\n@assert_no_logs\ndef test_font_stretch():\n    page, = render_pages('''\n      <style>\n        p { float: left; font-family: %s }\n      </style>\n      <p>Hello, world!</p>\n      <p style=\"font-stretch: condensed\">Hello, world!</p>\n    ''' % SANS_FONTS)\n    html, = page.children\n    body, = html.children\n    p_1, p_2 = body.children\n    normal = p_1.width\n    condensed = p_2.width\n    assert condensed < normal\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('source', 'lines_count'), [\n    ('<body>hyphénation', 1),  # Default: no hyphenation\n    ('<body lang=fr>hyphénation', 1),  # lang only: no hyphenation\n    ('<body style=\"hyphens: auto\">hyphénation', 1),  # hyphens only: no hyph.\n    ('<body style=\"hyphens: auto\" lang=fr>hyphénation', 4),  # both: hyph.\n    ('<body>hyp&shy;hénation', 2),  # Hyphenation with soft hyphens\n    ('<body style=\"hyphens: none\">hyp&shy;hénation', 1),  # … unless disabled\n])\ndef test_line_count(source, lines_count):\n    page, = render_pages('<html style=\"width: 5em; font-family: weasyprint\">' + source)\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) == lines_count\n\n\n@assert_no_logs\ndef test_vertical_align_1():\n    #            +-------+      <- position_y = 0\n    #      +-----+       |\n    # 40px |     |       | 60px\n    #      |     |       |\n    #      +-----+-------+      <- baseline\n    page, = render_pages('''\n      <span>\n        <img src=\"pattern.png\" style=\"width: 40px\"\n        ><img src=\"pattern.png\" style=\"width: 60px\"\n      ></span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span, = line.children\n    img_1, img_2 = span.children\n    assert img_1.height == 40\n    assert img_2.height == 60\n    assert img_1.position_y == 20\n    assert img_2.position_y == 0\n    # 60px + the descent of the font below the baseline\n    assert 60 < line.height < 70\n    assert body.height == line.height\n\n\n@assert_no_logs\ndef test_vertical_align_2():\n    #            +-------+      <- position_y = 0\n    #       35px |       |\n    #      +-----+       | 60px\n    # 40px |     |       |\n    #      |     +-------+      <- baseline\n    #      +-----+  15px\n    page, = render_pages('''\n      <span>\n        <img src=\"pattern.png\" style=\"width: 40px; vertical-align: -15px\"\n        ><img src=\"pattern.png\" style=\"width: 60px\"></span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span, = line.children\n    img_1, img_2 = span.children\n    assert img_1.height == 40\n    assert img_2.height == 60\n    assert img_1.position_y == 35\n    assert img_2.position_y == 0\n    assert line.height == 75\n    assert body.height == line.height\n\n\n@assert_no_logs\ndef test_vertical_align_3():\n    # Same as previously, but with percentages\n    page, = render_pages('''\n      <span style=\"line-height: 10px\">\n        <img src=\"pattern.png\" style=\"width: 40px; vertical-align: -150%\"\n        ><img src=\"pattern.png\" style=\"width: 60px\"></span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span, = line.children\n    img_1, img_2 = span.children\n    assert img_1.height == 40\n    assert img_2.height == 60\n    assert img_1.position_y == 35\n    assert img_2.position_y == 0\n    assert line.height == 75\n    assert body.height == line.height\n\n\n@assert_no_logs\ndef test_vertical_align_4():\n    # Same again, but have the vertical-align on an inline box.\n    page, = render_pages('''\n      <span style=\"line-height: 10px\">\n        <span style=\"line-height: 10px; vertical-align: -15px\">\n          <img src=\"pattern.png\" style=\"width: 40px\"></span>\n        <img src=\"pattern.png\" style=\"width: 60px\"></span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span_1, = line.children\n    span_2, _whitespace, img_2 = span_1.children\n    img_1, = span_2.children\n    assert img_1.height == 40\n    assert img_2.height == 60\n    assert img_1.position_y == 35\n    assert img_2.position_y == 0\n    assert line.height == 75\n    assert body.height == line.height\n\n\n@assert_no_logs\ndef test_vertical_align_5():\n    # Same as previously, but with percentages\n    page, = render_pages(\n        '<span style=\"line-height: 12px; font-size: 12px; font-family: weasyprint\">'\n        '<img src=\"pattern.png\" style=\"width: 40px; vertical-align: middle\">'\n        '<img src=\"pattern.png\" style=\"width: 60px\"></span>')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span, = line.children\n    img_1, img_2 = span.children\n    assert img_1.height == 40\n    assert img_2.height == 60\n    # middle of the image (position_y + 20) is at half the ex-height above\n    # the baseline of the parent. The ex-height of weasyprint.otf is 0.8em\n    # TODO: ex unit doesn't work with @font-face fonts, see computed_values.py\n    # assert img_1.position_y == 35.2  # 60 - 0.5 * 0.8 * font-size - 40/2\n    assert img_2.position_y == 0\n    # assert line.height == 75.2\n    assert body.height == line.height\n\n\n@assert_no_logs\ndef test_vertical_align_6():\n    # sup and sub currently mean +/- 0.5 em\n    # With the initial 16px font-size, that’s 8px.\n    page, = render_pages('''\n      <span style=\"line-height: 10px\">\n        <img src=\"pattern.png\" style=\"width: 60px\"\n        ><img src=\"pattern.png\" style=\"width: 40px; vertical-align: super\"\n        ><img src=\"pattern.png\" style=\"width: 40px; vertical-align: sub\"\n      ></span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span, = line.children\n    img_1, img_2, img_3 = span.children\n    assert img_1.height == 60\n    assert img_2.height == 40\n    assert img_3.height == 40\n    assert img_1.position_y == 0\n    assert img_2.position_y == 12  # 20 - 16 * 0.5\n    assert img_3.position_y == 28  # 20 + 16 * 0.5\n    assert line.height == 68\n    assert body.height == line.height\n\n\n@assert_no_logs\ndef test_vertical_align_7():\n    page, = render_pages('''\n      <body style=\"line-height: 10px\">\n        <span>\n          <img src=\"pattern.png\" style=\"vertical-align: text-top\"\n          ><img src=\"pattern.png\" style=\"vertical-align: text-bottom\"\n        ></span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span, = line.children\n    img_1, img_2 = span.children\n    assert img_1.height == 4\n    assert img_2.height == 4\n    assert img_1.position_y == 0\n    assert img_2.position_y == 12  # 16 - 4\n    assert line.height == 16\n    assert body.height == line.height\n\n\n@assert_no_logs\ndef test_vertical_align_8():\n    # This case used to cause an exception:\n    # The second span has no children but should count for line heights\n    # since it has padding.\n    page, = render_pages('''<span style=\"line-height: 1.5\">\n      <span style=\"padding: 1px\"></span></span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span_1, = line.children\n    span_2, = span_1.children\n    assert span_1.height == 16\n    assert span_2.height == 16\n    # The line’s strut does not has 'line-height: normal' but the result should\n    # be smaller than 1.5.\n    assert span_1.margin_height() == 24\n    assert span_2.margin_height() == 24\n    assert line.height == 24\n\n\n@assert_no_logs\ndef test_vertical_align_9():\n    page, = render_pages('''\n      <span>\n        <img src=\"pattern.png\" style=\"width: 40px; vertical-align: -15px\"\n        ><img src=\"pattern.png\" style=\"width: 60px\"\n      ></span><div style=\"display: inline-block; vertical-align: 3px\">\n        <div>\n          <div style=\"height: 100px\">foo</div>\n          <div>\n            <img src=\"pattern.png\" style=\"\n                 width: 40px; vertical-align: -15px\"\n            ><img src=\"pattern.png\" style=\"width: 60px\"\n          ></div>\n        </div>\n      </div>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span, div_1 = line.children\n    assert line.height == 178\n    assert body.height == line.height\n\n    # Same as earlier\n    img_1, img_2 = span.children\n    assert img_1.height == 40\n    assert img_2.height == 60\n    assert img_1.position_y == 138\n    assert img_2.position_y == 103\n\n    div_2, = div_1.children\n    div_3, div_4 = div_2.children\n    div_line, = div_4.children\n    div_img_1, div_img_2 = div_line.children\n    assert div_1.position_y == 0\n    assert div_1.height == 175\n    assert div_3.height == 100\n    assert div_line.height == 75\n    assert div_img_1.height == 40\n    assert div_img_2.height == 60\n    assert div_img_1.position_y == 135\n    assert div_img_2.position_y == 100\n\n\n@assert_no_logs\ndef test_vertical_align_10():\n    # The first two images bring the top of the line box 30px above\n    # the baseline and 10px below.\n    # Each of the inner span\n    page, = render_pages('''\n      <span style=\"font-size: 0\">\n        <img src=\"pattern.png\" style=\"vertical-align: 26px\">\n        <img src=\"pattern.png\" style=\"vertical-align: -10px\">\n        <span style=\"vertical-align: top\">\n          <img src=\"pattern.png\" style=\"vertical-align: -10px\">\n          <span style=\"vertical-align: -10px\">\n            <img src=\"pattern.png\" style=\"vertical-align: bottom\">\n          </span>\n        </span>\n        <span style=\"vertical-align: bottom\">\n          <img src=\"pattern.png\" style=\"vertical-align: 6px\">\n        </span>\n      </span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span_1, = line.children\n    img_1, img_2, span_2, span_4 = span_1.children\n    img_3, span_3 = span_2.children\n    img_4, = span_3.children\n    img_5, = span_4.children\n    assert body.height == line.height\n    assert line.height == 40\n    assert img_1.position_y == 0\n    assert img_2.position_y == 36\n    assert img_3.position_y == 6\n    assert img_4.position_y == 36\n    assert img_5.position_y == 30\n\n\n@assert_no_logs\ndef test_vertical_align_11():\n    page, = render_pages('''\n      <span style=\"font-size: 0\">\n        <img src=\"pattern.png\" style=\"vertical-align: bottom\">\n        <img src=\"pattern.png\" style=\"vertical-align: top; height: 100px\">\n      </span>\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span, = line.children\n    img_1, img_2 = span.children\n    assert img_1.position_y == 96\n    assert img_2.position_y == 0\n\n\n@assert_no_logs\ndef test_vertical_align_12():\n    # Reference for the next test\n    page, = render_pages('''\n      <span style=\"font-size: 0; vertical-align: top\">\n        <img src=\"pattern.png\">\n      </span>\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span, = line.children\n    img_1, = span.children\n    assert img_1.position_y == 0\n\n\n@assert_no_logs\ndef test_vertical_align_13():\n    # Should be the same as above\n    page, = render_pages('''\n      <span style=\"font-size: 0; vertical-align: top; display: inline-block\">\n        <img src=\"pattern.png\">\n      </span>''')\n    html, = page.children\n    body, = html.children\n    line_1, = body.children\n    span, = line_1.children\n    line_2, = span.children\n    img_1, = line_2.children\n    assert img_1.element_tag == 'img'\n    assert img_1.position_y == 0\n\n\n@assert_no_logs\ndef test_box_decoration_break_inline_slice():\n    # https://www.w3.org/TR/css-backgrounds-3/#the-box-decoration-break\n    page_1, = render_pages('''\n      <style>\n        @page { size: 100px }\n        span { font-family: weasyprint; box-decoration-break: slice;\n               padding: 5px; border: 1px solid black }\n      </style>\n      <span>a<br/>b<br/>c</span>''')\n    html, = page_1.children\n    body, = html.children\n    line_1, line_2, line_3 = body.children\n    span, = line_1.children\n    assert span.width == 16\n    assert span.margin_width() == 16 + 5 + 1\n    text, br = span.children\n    assert text.position_x == 5 + 1\n    span, = line_2.children\n    assert span.width == 16\n    assert span.margin_width() == 16\n    text, br = span.children\n    assert text.position_x == 0\n    span, = line_3.children\n    assert span.width == 16\n    assert span.margin_width() == 16 + 5 + 1\n    text, = span.children\n    assert text.position_x == 0\n\n\n@assert_no_logs\ndef test_box_decoration_break_inline_clone():\n    # https://www.w3.org/TR/css-backgrounds-3/#the-box-decoration-break\n    page_1, = render_pages('''\n      <style>\n        @page { size: 100px }\n        span { font-size: 12pt; font-family: weasyprint;\n               box-decoration-break: clone;\n               padding: 5px; border: 1px solid black }\n      </style>\n      <span>a<br/>b<br/>c</span>''')\n    html, = page_1.children\n    body, = html.children\n    line_1, line_2, line_3 = body.children\n    span, = line_1.children\n    assert span.width == 16\n    assert span.margin_width() == 16 + 2 * (5 + 1)\n    text, br = span.children\n    assert text.position_x == 5 + 1\n    span, = line_2.children\n    assert span.width == 16\n    assert span.margin_width() == 16 + 2 * (5 + 1)\n    text, br = span.children\n    assert text.position_x == 5 + 1\n    span, = line_3.children\n    assert span.width == 16\n    assert span.margin_width() == 16 + 2 * (5 + 1)\n    text, = span.children\n    assert text.position_x == 5 + 1\n\n\n@assert_no_logs\ndef test_bidi_position_x_invariant():\n    page, = render_pages('''\n      <style>\n        .float-border {\n          float: left;\n          border-right: 100px solid black;\n        }\n      </style>\n      <div class=\"float-border\" style=\"direction: ltr\">abc</div>\n      <div>&nbsp;</div>\n      <div class=\"float-border\" style=\"direction: rtl\">abc</div>\n    ''')\n    html, = page.children\n    body, = html.children\n    block_ltr, _, block_rtl = body.children\n\n    line_ltr, = block_ltr.children\n    text_ltr, = line_ltr.children\n\n    line_rtl, = block_rtl.children\n    text_rtl, = line_rtl.children\n\n    assert block_ltr.position_x == block_rtl.position_x\n    assert line_ltr.position_x == line_rtl.position_x\n    assert text_ltr.position_x == text_rtl.position_x\n\n\n@assert_no_logs\ndef test_nested_waiting_children_width():\n    # Regression test for #2275.\n    page, = render_pages(\n        '<body style=\"width: 3em; font-family: weasyprint\">'\n        '<b><i style=\"width: 100%\">a b</i>c')\n    html, = page.children\n    body, = html.children\n    line1, line2 = body.children\n    assert line1.children[0].children[0].children[0].text == 'a'\n    assert line2.children[0].children[0].children[0].text == 'b'\n    assert line2.children[0].children[1].text == 'c'\n"
  },
  {
    "path": "tests/layout/test_inline_block.py",
    "content": "\"\"\"Tests for inline blocks layout.\"\"\"\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\ndef test_inline_block_sizes():\n    page, = render_pages('''\n      <style>\n        @page { margin: 0; size: 200px 2000px }\n        body { margin: 0 }\n        div { display: inline-block; }\n      </style>\n      <div> </div>\n      <div>a</div>\n      <div style=\"margin: 10px; height: 100px\"></div>\n      <div style=\"margin-left: 10px; margin-top: -50px;\n                  padding-right: 20px;\"></div>\n      <div>\n        Ipsum dolor sit amet,\n        consectetur adipiscing elit.\n        Sed sollicitudin nibh\n        et turpis molestie tristique.\n      </div>\n      <div style=\"width: 100px; height: 100px;\n                  padding-left: 10px; margin-right: 10px;\n                  margin-top: -10px; margin-bottom: 50px\"></div>\n      <div style=\"font-size: 0\">\n        <div style=\"min-width: 10px; height: 10px\"></div>\n        <div style=\"width: 10%\">\n          <div style=\"width: 10px; height: 10px\"></div>\n        </div>\n      </div>\n      <div style=\"min-width: 150px\">foo</div>\n      <div style=\"max-width: 10px\n        \">Supercalifragilisticexpialidocious</div>''')\n    html, = page.children\n    assert html.element_tag == 'html'\n    body, = html.children\n    assert body.element_tag == 'body'\n    assert body.width == 200\n\n    line_1, line_2, line_3, line_4 = body.children\n\n    # First line:\n    # White space in-between divs ends up preserved in TextBoxes\n    div_1, _, div_2, _, div_3, _, div_4, _ = line_1.children\n\n    # First div, one ignored space collapsing with next space\n    assert div_1.element_tag == 'div'\n    assert div_1.width == 0\n\n    # Second div, one letter\n    assert div_2.element_tag == 'div'\n    assert 0 < div_2.width < 20\n\n    # Third div, empty with margin\n    assert div_3.element_tag == 'div'\n    assert div_3.width == 0\n    assert div_3.margin_width() == 20\n    assert div_3.height == 100\n\n    # Fourth div, empty with margin and padding\n    assert div_4.element_tag == 'div'\n    assert div_4.width == 0\n    assert div_4.margin_width() == 30\n\n    # Second line:\n    div_5, _ = line_2.children\n\n    # Fifth div, long text, full-width div\n    assert div_5.element_tag == 'div'\n    assert len(div_5.children) > 1\n    assert div_5.width == 200\n\n    # Third line:\n    div_6, _, div_7, _ = line_3.children\n\n    # Sixth div, empty div with fixed width and height\n    assert div_6.element_tag == 'div'\n    assert div_6.width == 100\n    assert div_6.margin_width() == 120\n    assert div_6.height == 100\n    assert div_6.margin_height() == 140\n\n    # Seventh div\n    assert div_7.element_tag == 'div'\n    assert div_7.width == 20\n    child_line, = div_7.children\n    # Spaces have font-size: 0, they get removed\n    child_div_1, child_div_2 = child_line.children\n    assert child_div_1.element_tag == 'div'\n    assert child_div_1.width == 10\n    assert child_div_2.element_tag == 'div'\n    assert child_div_2.width == 2\n    grandchild, = child_div_2.children\n    assert grandchild.element_tag == 'div'\n    assert grandchild.width == 10\n\n    div_8, _, div_9 = line_4.children\n    assert div_8.width == 150\n    assert div_9.width == 10\n\n\n@assert_no_logs\ndef test_inline_block_with_margin():\n    # Regression test for #1235.\n    page_1, = render_pages('''\n      <style>\n        @page { size: 100px }\n        span { font-family: weasyprint; display: inline-block; margin: 0 30px }\n      </style>\n      <span>a b c d e f g h i j k l</span>''')\n    html, = page_1.children\n    body, = html.children\n    line_1, = body.children\n    span, = line_1.children\n    assert span.width == 40  # 100 - 2 * 30\n"
  },
  {
    "path": "tests/layout/test_list.py",
    "content": "\"\"\"Tests for lists layout.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\n@pytest.mark.parametrize('inside', ['inside', '',])\n@pytest.mark.parametrize(('style', 'character'), [\n    ('circle', '◦ '),\n    ('disc', '• '),\n    ('square', '▪ '),\n])\ndef test_lists_style(inside, style, character):\n    page, = render_pages('''\n      <style>\n        body { margin: 0 }\n        ul { margin-left: 50px; list-style: %s %s }\n      </style>\n      <ul>\n        <li>abc</li>\n      </ul>\n    ''' % (inside, style))\n    html, = page.children\n    body, = html.children\n    unordered_list, = body.children\n    list_item, = unordered_list.children\n    if inside:\n        line, = list_item.children\n        marker, content = line.children\n        marker_text, = marker.children\n    else:\n        marker, line_container, = list_item.children\n        assert marker.position_x == list_item.position_x - marker.width\n        assert marker.position_y == list_item.position_y\n        line, = line_container.children\n        content, = line.children\n        marker_line, = marker.children\n        marker_text, = marker_line.children\n    assert marker_text.text == character\n    assert content.text == 'abc'\n\n\ndef test_lists_empty_item():\n    # Regression test for #873.\n    page, = render_pages('''\n      <ul>\n        <li>a</li>\n        <li></li>\n        <li>a</li>\n      </ul>\n    ''')\n    html, = page.children\n    body, = html.children\n    unordered_list, = body.children\n    li1, li2, li3 = unordered_list.children\n    assert li1.position_y != li2.position_y != li3.position_y\n\n\n@pytest.mark.xfail\ndef test_lists_whitespace_item():\n    # Regression test for #873.\n    page, = render_pages('''\n      <ul>\n        <li>a</li>\n        <li> </li>\n        <li>a</li>\n      </ul>\n    ''')\n    html, = page.children\n    body, = html.children\n    unordered_list, = body.children\n    li1, li2, li3 = unordered_list.children\n    assert li1.position_y != li2.position_y != li3.position_y\n\n\ndef test_lists_page_break():\n    # Regression test for #945.\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 300px 100px }\n        ul { font-size: 30px; font-family: weasyprint; margin: 0 }\n      </style>\n      <ul>\n        <li>a</li>\n        <li>a</li>\n        <li>a</li>\n        <li>a</li>\n      </ul>\n    ''')\n    html, = page1.children\n    body, = html.children\n    ul, = body.children\n    assert len(ul.children) == 3\n    for li in ul.children:\n        assert len(li.children) == 2\n\n    html, = page2.children\n    body, = html.children\n    ul, = body.children\n    assert len(ul.children) == 1\n    for li in ul.children:\n        assert len(li.children) == 2\n\n\ndef test_lists_page_break_margin():\n    # Regression test for #1058.\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 300px 100px }\n        ul { font-size: 30px; font-family: weasyprint; margin: 0 }\n        p { margin: 10px 0 }\n      </style>\n      <ul>\n        <li><p>a</p></li>\n        <li><p>a</p></li>\n        <li><p>a</p></li>\n        <li><p>a</p></li>\n      </ul>\n    ''')\n    for page in (page1, page2):\n        html, = page.children\n        body, = html.children\n        ul, = body.children\n        assert len(ul.children) == 2\n        for li in ul.children:\n            assert len(li.children) == 2\n            assert (\n                li.children[0].position_y ==\n                li.children[1].children[0].position_y)\n"
  },
  {
    "path": "tests/layout/test_logical.py",
    "content": "\"\"\"Tests for logical properties.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs\n\n\n@assert_no_logs\n@pytest.mark.parametrize('documents', [\n    (\n        '<div style=\"border-top: 1px solid\">',\n        '<div style=\"border-block-start: 1px solid\">',\n    ), (\n        '<div style=\"border-left: 1px solid\">',\n        '<div style=\"border-inline-start: 1px solid\">',\n    ), (\n        '<div style=\"border-inline-start: 1px solid\">',\n        '<article style=\"direction: rtl\">'\n        '  <div style=\"border-inline-end: 1px solid\">',\n    ), (\n        '<div style=\"border-style: solid; border-width: 1px 2px\">',\n        '<div style=\"border-style: solid; border-width: logical 1px 2px\">',\n    ), (\n        '<div style=\"padding-top: 1px\">',\n        '<div style=\"padding-block-start: 1px\">',\n    ), (\n        '<div style=\"padding: 1px 2px 3px 4px\">',\n        '<div style=\"padding: logical 1px 4px 3px 2px\">',\n        '<div style=\"padding: 1px 2px;'\n        '            padding-inline-start: 4px; padding-block-end: 3px\">',\n    ), (\n        '<div style=\"margin-right: 1px\">',\n        '<div style=\"margin-inline-end: 1px\">',\n        '<article style=\"direction: rtl\">'\n        '  <div style=\"margin-inline-start: 1px\">',\n        '<article style=\"direction: rtl\">'\n        '  <div style=\"margin: logical 0 1px 0 0\">',\n    ), (\n        '<div style=\"border-radius: 0 0 0 4px\">',\n        '<div style=\"border-bottom-left-radius: 4px\">',\n        '<div style=\"border-end-start-radius: 4px\">',\n        '<article style=\"direction: rtl\">'\n        '  <div style=\"border-end-end-radius: 4px\">',\n    ), (\n        '<div style=\"width: 5px\">',\n        '<div style=\"inline-size: 5px\">',\n        '<div style=\"max-inline-size: 5px\">',\n        '<div style=\"max-inline-size: 5px; min-block-size: 2px\">',\n    ), (\n        '<div style=\"height: 6px\">',\n        '<div style=\"block-size: 6px\">',\n        '<div style=\"min-block-size: 6px\">',\n        '<div style=\"min-block-size: 6px; max-inline-size: 10px\">',\n    ), (\n        '<div style=\"position: absolute; width: 5px; inset: auto 1px auto auto\">',\n        '<div style=\"position: absolute; width: 5px; right: 1px\">',\n        '<div style=\"position: absolute; width: 5px; inset-inline-end: 1px\">',\n        '<div style=\"position: absolute; width: 5px; inset-inline: auto 1px\">',\n    ), (\n        '<div style=\"float: left; width: 5px\">',\n        '<div style=\"float: inline-start; width: 5px\">',\n        '<article style=\"direction: rtl\">'\n        '  <div style=\"float: inline-end; width: 5px\">',\n    ), (\n        '<div style=\"float: left; width: 5px\"></div>'\n        '<div style=\"float: left; width: 3px\"></div>',\n        '<div style=\"float: left; width: 5px\"></div>'\n        '<div style=\"float: left; width: 3px; clear: right\"></div>',\n        '<div style=\"float: inline-start; width: 5px\"></div>'\n        '<div style=\"float: left; width: 3px; clear: inline-end\"></div>',\n        '<div style=\"float: left; width: 5px\"></div>'\n        '<div style=\"float: inline-start; width: 3px; clear: inline-end\"></div>',\n        '<article style=\"direction: rtl\">'\n        '  <div style=\"float: left; width: 5px\"></div>'\n        '  <div style=\"float: inline-end; width: 3px; clear: inline-start\"></div>',\n    ), (\n        '<div style=\"float: left; width: 5px\"></div>'\n        '<div style=\"float: left; width: 3px; clear: left\"></div>',\n        '<div style=\"float: inline-start; width: 5px\"></div>'\n        '<div style=\"float: left; width: 3px; clear: inline-start\"></div>',\n        '<div style=\"float: inline-start; width: 5px\"></div>'\n        '<div style=\"float: inline-start; width: 3px; clear: left\"></div>',\n        '<div style=\"float: inline-start; width: 5px\"></div>'\n        '<div style=\"float: inline-start; width: 3px; clear: both\"></div>',\n        '<article style=\"direction: rtl\">'\n        '  <div style=\"float: inline-end; width: 5px\"></div>'\n        '  <div style=\"float: inline-end; width: 3px; clear: left\"></div>',\n        '<article style=\"direction: rtl\">'\n        '  <div style=\"float: left; width: 5px\"></div>'\n        '  <div style=\"float: inline-end; width: 3px; clear: inline-end\"></div>',\n    ),\n])\ndef test_logical(assert_same_renderings, documents):\n    base_style = '''\n    <style>\n      @page { size: 10px }\n      div { background: pink; background-clip: padding-box; height: 5px }\n    </style>\n    '''\n    assert_same_renderings(*(base_style + document for document in documents))\n\n"
  },
  {
    "path": "tests/layout/test_page.py",
    "content": "\"\"\"Tests for pages layout.\"\"\"\n\nimport pytest\n\nfrom weasyprint.formatting_structure import boxes\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('size', 'width', 'height'), [\n    ('auto', 793, 1122),\n    ('2in 10in', 192, 960),\n    ('242px', 242, 242),\n    ('letter', 816, 1056),\n    ('letter portrait', 816, 1056),\n    ('letter landscape', 1056, 816),\n    ('portrait', 793, 1122),\n    ('landscape', 1122, 793),\n])\ndef test_page_size_basic(size, width, height):\n    \"\"\"Test the layout for ``@page`` properties.\"\"\"\n    page, = render_pages('<style>@page { size: %s; }</style>' % size)\n    assert int(page.margin_width()) == width\n    assert int(page.margin_height()) == height\n\n\n@assert_no_logs\ndef test_page_size_with_margin():\n    page, = render_pages('''<style>\n      @page { size: 200px 300px; margin: 10px 10% 20% 1in }\n      body { margin: 8px }\n    </style>\n    <p style=\"margin: 0\">''')\n    assert page.margin_width() == 200\n    assert page.margin_height() == 300\n    assert page.position_x == 0\n    assert page.position_y == 0\n    assert page.width == 84  # 200px - 10% - 1 inch\n    assert page.height == 230  # 300px - 10px - 20%\n\n    html, = page.children\n    assert html.element_tag == 'html'\n    assert html.position_x == 96  # 1in\n    assert html.position_y == 10  # root element’s margins do not collapse\n    assert html.width == 84\n\n    body, = html.children\n    assert body.element_tag == 'body'\n    assert body.position_x == 96  # 1in\n    assert body.position_y == 10\n    # body has margins in the UA stylesheet\n    assert body.margin_left == 8\n    assert body.margin_right == 8\n    assert body.margin_top == 8\n    assert body.margin_bottom == 8\n    assert body.width == 68\n\n    paragraph, = body.children\n    assert paragraph.element_tag == 'p'\n    assert paragraph.position_x == 104  # 1in + 8px\n    assert paragraph.position_y == 18  # 10px + 8px\n    assert paragraph.width == 68\n\n\n@assert_no_logs\ndef test_page_size_with_margin_border_padding():\n    page, = render_pages('''<style> @page {\n      size: 100px; margin: 1px 2px; padding: 4px 8px;\n      border-width: 16px 32px; border-style: solid;\n    }</style>''')\n    assert page.width == 16  # 100 - 2 * 42\n    assert page.height == 58  # 100 - 2 * 21\n    html, = page.children\n    assert html.element_tag == 'html'\n    assert html.position_x == 42  # 2 + 8 + 32\n    assert html.position_y == 21  # 1 + 4 + 16\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('margin', 'top', 'right', 'bottom', 'left'), [\n    ('auto', 15, 10, 15, 10),\n    ('5px 5px auto auto', 5, 5, 25, 15),\n])\ndef test_page_size_margins(margin, top, right, bottom, left):\n    page, = render_pages('''<style>@page {\n      size: 106px 206px; width: 80px; height: 170px;\n      padding: 1px; border: 2px solid; margin: %s }</style>''' % margin)\n    assert page.margin_top == top\n    assert page.margin_right == right\n    assert page.margin_bottom == bottom\n    assert page.margin_left == left\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('style', 'width', 'height'), [\n    ('size: 4px 10000px; width: 100px; height: 100px;'\n     'padding: 1px; border: 2px solid; margin: 3px',\n     112, 112),\n    ('size: 1000px; margin: 100px; max-width: 500px; min-height: 1500px',\n     700, 1700),\n    ('size: 1000px; margin: 100px; min-width: 1500px; max-height: 500px',\n     1700, 700),\n])\ndef test_page_size_over_constrained(style, width, height):\n    page, = render_pages('<style>@page { %s }</style>' % style)\n    assert page.margin_width() == width\n    assert page.margin_height() == height\n\n\n@assert_no_logs\n@pytest.mark.parametrize('html', [\n    '<div>1</div>',\n    '<div></div>',\n    '<img src=pattern.png>'\n])\ndef test_page_breaks(html):\n    pages = render_pages('''\n      <style>\n        @page { size: 100px; margin: 10px }\n        body { margin: 0 }\n        div { height: 30px; font-size: 20px }\n        img { height: 30px; display: block }\n      </style>\n      %s''' % (5 * html))\n    page_children = []\n    for page in pages:\n        html, = page.children\n        body, = html.children\n        children = body.children\n        assert all([child.element_tag in ('div', 'img') for child in children])\n        assert all([child.position_x == 10 for child in children])\n        page_children.append(children)\n    assert [\n        [child.position_y for child in page_child]\n        for page_child in page_children] == [[10, 40], [10, 40], [10]]\n\n\n@assert_no_logs\ndef test_page_breaks_box_split():\n    # If floats round the wrong way, a block that gets filled to the end of a\n    # page due to breaking over the page may be forced onto the next page\n    # because it is slightly taller than can fit on the previous page, even if\n    # it wouldn't have been without being filled. These numbers aren't ideal,\n    # but they do seem to trigger the issue.\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 982.4146981627297px; margin: 0 }\n        div { font-size: 5px; height: 200.0123456789px; margin: 0; padding: 0 }\n        figure { margin: 0; padding: 0 }\n      </style>\n      <div>text</div>\n      <div>text</div><!-- no page break here -->\n      <section>\n        <div>line1</div>\n        <div>line2</div><!-- page break here -->\n        <div>line3</div>\n        <div>line4</div>\n      </section>\n    ''')\n    html, = page_1.children\n    body, = html.children\n    assert len(body.children) == 3\n    div1, div2, section = body.children\n    assert len(section.children) == 2\n\n    html, = page_2.children\n    body, = html.children\n    section, = body.children\n    assert len(section.children) == 2\n\n\n@assert_no_logs\ndef test_page_breaks_complex_1():\n    page_1, page_2, page_3, page_4 = render_pages('''\n      <style>\n        @page { margin: 10px }\n        @page :left { margin-left: 50px }\n        @page :right { margin-right: 50px }\n        html { page-break-before: left }\n        div { page-break-after: left }\n        ul { page-break-before: always }\n      </style>\n      <div>1</div>\n      <p>2</p>\n      <p>3</p>\n      <article>\n        <section>\n          <ul><li>4</li></ul>\n        </section>\n      </article>\n    ''')\n\n    # The first page is a right page on rtl, but not here because of\n    # page-break-before on the root element.\n    assert page_1.margin_left == 50  # left page\n    assert page_1.margin_right == 10\n    html, = page_1.children\n    body, = html.children\n    div, = body.children\n    line, = div.children\n    text, = line.children\n    assert div.element_tag == 'div'\n    assert text.text == '1'\n\n    html, = page_2.children\n    assert page_2.margin_left == 10\n    assert page_2.margin_right == 50  # right page\n    assert not html.children  # empty page to get to a left page\n\n    assert page_3.margin_left == 50  # left page\n    assert page_3.margin_right == 10\n    html, = page_3.children\n    body, = html.children\n    p_1, p_2 = body.children\n    assert p_1.element_tag == 'p'\n    assert p_2.element_tag == 'p'\n\n    assert page_4.margin_left == 10\n    assert page_4.margin_right == 50  # right page\n    html, = page_4.children\n    body, = html.children\n    article, = body.children\n    section, = article.children\n    ulist, = section.children\n    assert ulist.element_tag == 'ul'\n\n\n@assert_no_logs\ndef test_page_breaks_complex_2():\n    # Reference for the following test:\n    # Without any 'avoid', this breaks after the <div>\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 140px; margin: 0 }\n        img { height: 25px; vertical-align: top }\n      </style>\n      <img src=pattern.png>\n      <div>\n        <p><img src=pattern.png><br/><img src=pattern.png><p>\n        <p><img src=pattern.png><br/><img src=pattern.png><p>\n      </div><!-- page break here -->\n      <img src=pattern.png>\n    ''')\n    html, = page_1.children\n    body, = html.children\n    img_1, div = body.children\n    assert img_1.position_y == 0\n    assert img_1.height == 25\n    assert div.position_y == 25\n    assert div.height == 100\n\n    html, = page_2.children\n    body, = html.children\n    img_2, = body.children\n    assert img_2.position_y == 0\n    assert img_2.height == 25\n\n\n@assert_no_logs\ndef test_page_breaks_complex_3():\n    # Adding a few page-break-*: avoid, the only legal break is\n    # before the <div>\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 140px; margin: 0 }\n        img { height: 25px; vertical-align: top }\n      </style>\n      <img src=pattern.png><!-- page break here -->\n      <div>\n        <p style=\"page-break-inside: avoid\">\n          <img src=pattern.png><br/><img src=pattern.png></p>\n        <p style=\"page-break-before: avoid; page-break-after: avoid; widows: 2\"\n          ><img src=pattern.png><br/><img src=pattern.png></p>\n      </div>\n      <img src=pattern.png>\n    ''')\n    html, = page_1.children\n    body, = html.children\n    img_1, = body.children\n    assert img_1.position_y == 0\n    assert img_1.height == 25\n\n    html, = page_2.children\n    body, = html.children\n    div, img_2 = body.children\n    assert div.position_y == 0\n    assert div.height == 100\n    assert img_2.position_y == 100\n    assert img_2.height == 25\n\n\n@assert_no_logs\ndef test_page_breaks_complex_4():\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 140px; margin: 0 }\n        img { height: 25px; vertical-align: top }\n      </style>\n      <img src=pattern.png><!-- page break here -->\n      <div>\n        <div>\n          <p style=\"page-break-inside: avoid\">\n            <img src=pattern.png><br/><img src=pattern.png></p>\n          <p style=\"page-break-before:avoid; page-break-after:avoid; widows:2\"\n            ><img src=pattern.png><br/><img src=pattern.png></p>\n        </div>\n        <img src=pattern.png>\n      </div>\n    ''')\n    html, = page_1.children\n    body, = html.children\n    img_1, = body.children\n    assert img_1.position_y == 0\n    assert img_1.height == 25\n\n    html, = page_2.children\n    body, = html.children\n    outer_div, = body.children\n    inner_div, img_2 = outer_div.children\n    assert inner_div.position_y == 0\n    assert inner_div.height == 100\n    assert img_2.position_y == 100\n    assert img_2.height == 25\n\n\n@assert_no_logs\ndef test_page_breaks_complex_5():\n    # Reference for the next test\n    page_1, page_2, page_3 = render_pages('''\n      <style>\n        @page { size: 100px; margin: 0 }\n        img { height: 30px; display: block; }\n      </style>\n      <div>\n        <img src=pattern.png style=\"page-break-after: always\">\n        <section>\n          <img src=pattern.png>\n          <img src=pattern.png>\n        </section>\n      </div>\n      <img src=pattern.png><!-- page break here -->\n      <img src=pattern.png>\n    ''')\n    html, = page_1.children\n    body, = html.children\n    div, = body.children\n    assert div.height == 100\n    html, = page_2.children\n    body, = html.children\n    div, img_4 = body.children\n    assert div.height == 60\n    assert img_4.height == 30\n    html, = page_3.children\n    body, = html.children\n    img_5, = body.children\n    assert img_5.height == 30\n\n\n@assert_no_logs\ndef test_page_breaks_complex_6():\n    page_1, page_2, page_3 = render_pages('''\n      <style>\n        @page { size: 100px; margin: 0 }\n        img { height: 30px; display: block; }\n      </style>\n      <div>\n        <img src=pattern.png style=\"page-break-after: always\">\n        <section>\n          <img src=pattern.png><!-- page break here -->\n          <img src=pattern.png style=\"page-break-after: avoid\">\n        </section>\n      </div>\n      <img src=pattern.png style=\"page-break-after: avoid\">\n      <img src=pattern.png>\n    ''')\n    html, = page_1.children\n    body, = html.children\n    div, = body.children\n    assert div.height == 100\n    html, = page_2.children\n    body, = html.children\n    div, = body.children\n    section, = div.children\n    img_2, = section.children\n    assert img_2.height == 30\n    # TODO: currently this is 60: we do not increase the used height of blocks\n    # to make them fill the blank space at the end of the age when we remove\n    # children from them for some break-*: avoid.\n    # See TODOs in blocks.block_container_layout\n    # assert div.height == 100\n    html, = page_3.children\n    body, = html.children\n    div, img_4, img_5, = body.children\n    assert div.height == 30\n    assert img_4.height == 30\n    assert img_5.height == 30\n\n\n@assert_no_logs\ndef test_page_breaks_complex_7():\n    page_1, page_2, page_3 = render_pages('''\n      <style>\n        @page { @bottom-center { content: counter(page) } }\n        @page:blank { @bottom-center { content: none } }\n      </style>\n      <p style=\"page-break-after: right\">foo</p>\n      <p>bar</p>\n    ''')\n    assert len(page_1.children) == 2  # content and @bottom-center\n    assert len(page_2.children) == 1  # content only\n    assert len(page_3.children) == 2  # content and @bottom-center\n\n\n@assert_no_logs\ndef test_page_breaks_complex_8():\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 75px; margin: 0 }\n        div { height: 20px }\n      </style>\n      <div></div>\n      <section>\n        <div></div>\n        <div style=\"page-break-after: avoid\">\n          <div style=\"position: absolute\"></div>\n          <div style=\"position: fixed\"></div>\n        </div>\n      </section>\n      <div></div>\n    ''')\n    html, = page_1.children\n    body, _div = html.children\n    div_1, section = body.children\n    div_2, = section.children\n    assert div_1.position_y == 0\n    assert div_2.position_y == 20\n    assert div_1.height == 20\n    assert div_2.height == 20\n    html, = page_2.children\n    body, = html.children\n    section, div_4 = body.children\n    div_3, = section.children\n    absolute, fixed = div_3.children\n    assert div_3.position_y == 0\n    assert div_4.position_y == 20\n    assert div_3.height == 20\n    assert div_4.height == 20\n\n\n@assert_no_logs\ndef test_page_breaks_complex_9():\n    # Regression test for #1979.\n    page_1, page_2, page_3, page_4, page_5 = render_pages('''\n      <style>\n        @page { size: 75px; margin: 0 }\n        div { height: 20px; margin: 10px }\n      </style>\n      <div style=\"height: 40px\"></div>\n      <div></div>\n      <div style=\"break-before: left\"></div>\n      <div style=\"break-before: right\"></div>\n    ''')\n    html, = page_1.children\n    body, = html.children\n    div_1, = body.children\n    assert div_1.content_box_x() == 10\n    assert div_1.content_box_y() == 10\n    html, = page_2.children\n    body, = html.children\n    div_2, = body.children\n    assert div_2.content_box_x() == 10\n    assert div_2.content_box_y() == 0  # Unforced page break\n    html, = page_3.children\n    assert not html.children  # Empty page\n    html, = page_4.children\n    body, = html.children\n    div_3, = body.children\n    assert div_3.content_box_x() == 10\n    assert div_3.content_box_y() == 10  # Forced page break\n    html, = page_5.children\n    body, = html.children\n    div_4, = body.children\n    assert div_4.content_box_x() == 10\n    assert div_4.content_box_y() == 10  # Forced page break\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('break_after', 'margin_break', 'margin_top'), [\n    ('page', 'auto', 5),\n    ('auto', 'auto', 0),\n    ('page', 'keep', 5),\n    ('auto', 'keep', 5),\n    ('page', 'discard', 0),\n    ('auto', 'discard', 0),\n])\ndef test_margin_break(break_after, margin_break, margin_top):\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 70px; margin: 0 }\n        div { height: 63px; margin: 5px 0 8px;\n              break-after: %s; margin-break: %s }\n      </style>\n      <section>\n        <div></div>\n      </section>\n      <section>\n        <div></div>\n      </section>\n    ''' % (break_after, margin_break))\n    html, = page_1.children\n    body, = html.children\n    section, = body.children\n    div, = section.children\n    assert div.margin_top == 5\n\n    html, = page_2.children\n    body, = html.children\n    section, = body.children\n    div, = section.children\n    assert div.margin_top == margin_top\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_margin_break_clearance():\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 70px; margin: 0 }\n        div { height: 63px; margin: 5px 0 8px; break-after: page }\n      </style>\n      <section>\n        <div></div>\n      </section>\n      <section>\n        <div style=\"border-top: 1px solid black\">\n          <div></div>\n        </div>\n      </section>\n    ''')\n    html, = page_1.children\n    body, = html.children\n    section, = body.children\n    div, = section.children\n    assert div.margin_top == 5\n\n    html, = page_2.children\n    body, = html.children\n    section, = body.children\n    div_1, = section.children\n    assert div_1.margin_top == 0\n    div_2, = div_1.children\n    assert div_2.margin_top == 5\n    assert div_2.content_box_y() == 5\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('direction', 'page_break', 'pages_number'), [\n    ('ltr', 'recto', 3),\n    ('ltr', 'verso', 2),\n    ('rtl', 'recto', 3),\n    ('rtl', 'verso', 2),\n    ('ltr', 'right', 3),\n    ('ltr', 'left', 2),\n    ('rtl', 'right', 2),\n    ('rtl', 'left', 3),\n])\ndef test_recto_verso_break(direction, page_break, pages_number):\n    pages = render_pages('''\n      <style>\n        html { direction: %s }\n        p { break-before: %s }\n      </style>\n      abc\n      <p>def</p>\n    ''' % (direction, page_break))\n    assert len(pages) == pages_number\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('direction', 'page_break', 'first_page'), [\n    ('ltr', 'recto', 'right'),\n    ('ltr', 'verso', 'left'),\n    ('rtl', 'recto', 'left'),\n    ('rtl', 'verso', 'right'),\n    ('ltr', 'right', 'right'),\n    ('ltr', 'left', 'left'),\n    ('rtl', 'right', 'right'),\n    ('rtl', 'left', 'left'),\n])\ndef test_recto_verso_break_root(direction, page_break, first_page):\n    page, = render_pages('''\n      <style>\n        @page:left { size: 4px /* len('left') */ }\n        @page:right { size: 5px /* len('right') */ }\n        html { direction: %s; break-before: %s }\n      </style>\n      abc\n    ''' % (direction, page_break))\n    assert page.width == len(first_page)\n\n\n@assert_no_logs\ndef test_page_names_1():\n    pages = render_pages('''\n      <style>\n        @page { size: 100px 100px }\n        section { page: small }\n      </style>\n      <div>\n        <section>large</section>\n      </div>\n    ''')\n    page1, = pages\n    assert (page1.width, page1.height) == (100, 100)\n\n\n@assert_no_logs\ndef test_page_names_2():\n    pages = render_pages('''\n      <style>\n        @page { size: 100px 100px }\n        @page narrow { margin: 1px }\n        section { page: small }\n      </style>\n      <div>\n        <section>large</section>\n      </div>\n    ''')\n    page1, = pages\n    assert (page1.width, page1.height) == (100, 100)\n\n\n@assert_no_logs\ndef test_page_names_3():\n    pages = render_pages('''\n      <style>\n        @page { margin: 0 }\n        @page narrow { size: 100px 200px }\n        @page large { size: 200px 100px }\n        div { page: narrow }\n        section { page: large }\n      </style>\n      <div>\n        <section>large</section>\n        <section>large</section>\n        <p>narrow</p>\n      </div>\n    ''')\n    page1, page2 = pages\n\n    assert (page1.width, page1.height) == (200, 100)\n    html, = page1.children\n    body, = html.children\n    div, = body.children\n    section1, section2 = div.children\n    assert section1.element_tag == section2.element_tag == 'section'\n\n    assert (page2.width, page2.height) == (100, 200)\n    html, = page2.children\n    body, = html.children\n    div, = body.children\n    p, = div.children\n    assert p.element_tag == 'p'\n\n\n@assert_no_logs\ndef test_page_names_4():\n    pages = render_pages('''\n      <style>\n        @page { size: 200px 200px; margin: 0 }\n        @page small { size: 100px 100px }\n        p { page: small }\n      </style>\n      <section>normal</section>\n      <section>normal</section>\n      <p>small</p>\n      <section>small</section>\n    ''')\n    page1, page2 = pages\n\n    assert (page1.width, page1.height) == (200, 200)\n    html, = page1.children\n    body, = html.children\n    section1, section2 = body.children\n    assert section1.element_tag == section2.element_tag == 'section'\n\n    assert (page2.width, page2.height) == (100, 100)\n    html, = page2.children\n    body, = html.children\n    p, section = body.children\n    assert p.element_tag == 'p'\n    assert section.element_tag == 'section'\n\n\n@assert_no_logs\ndef test_page_names_5():\n    pages = render_pages('''\n      <style>\n        @page { size: 200px 200px; margin: 0 }\n        @page small { size: 100px 100px }\n        div { page: small }\n      </style>\n      <section><p>a</p>b</section>\n      <section>c<div>d</div></section>\n    ''')\n    page1, page2 = pages\n\n    assert (page1.width, page1.height) == (200, 200)\n    html, = page1.children\n    body, = html.children\n    section1, section2 = body.children\n    assert section1.element_tag == section2.element_tag == 'section'\n    p, line = section1.children\n    line, = section2.children\n\n    assert (page2.width, page2.height) == (100, 100)\n    html, = page2.children\n    body, = html.children\n    section2, = body.children\n    div, = section2.children\n\n\n@assert_no_logs\ndef test_page_names_6():\n    pages = render_pages('''\n      <style>\n        @page { margin: 0 }\n        @page large { size: 200px 200px }\n        @page small { size: 100px 100px }\n        section { page: large }\n        div { page: small }\n      </style>\n      <section>a<p>b</p>c</section>\n      <section>d<div>e</div>f</section>\n    ''')\n    page1, page2, page3 = pages\n\n    assert (page1.width, page1.height) == (200, 200)\n    html, = page1.children\n    body, = html.children\n    section1, section2 = body.children\n    assert section1.element_tag == section2.element_tag == 'section'\n    line1, p, line2 = section1.children\n    line, = section2.children\n\n    assert (page2.width, page2.height) == (100, 100)\n    html, = page2.children\n    body, = html.children\n    section2, = body.children\n    div, = section2.children\n\n    assert (page3.width, page3.height) == (200, 200)\n    html, = page3.children\n    body, = html.children\n    section2, = body.children\n    line, = section2.children\n\n\n@assert_no_logs\ndef test_page_names_7():\n    pages = render_pages('''\n      <style>\n        @page { size: 200px 200px; margin: 0 }\n        @page small { size: 100px 100px }\n        p { page: small; break-before: right }\n      </style>\n      <section>normal</section>\n      <section>normal</section>\n      <p>small</p>\n      <section>small</section>\n    ''')\n    page1, page2, page3 = pages\n\n    assert (page1.width, page1.height) == (200, 200)\n    html, = page1.children\n    body, = html.children\n    section1, section2 = body.children\n    assert section1.element_tag == section2.element_tag == 'section'\n\n    assert (page2.width, page2.height) == (200, 200)\n    html, = page2.children\n    assert not html.children\n\n    assert (page3.width, page3.height) == (100, 100)\n    html, = page3.children\n    body, = html.children\n    p, section = body.children\n    assert p.element_tag == 'p'\n    assert section.element_tag == 'section'\n\n\n@assert_no_logs\ndef test_page_names_8():\n    pages = render_pages('''\n      <style>\n        @page small { size: 100px 100px }\n        section { page: small }\n        p { line-height: 80px }\n      </style>\n      <section>\n        <p>small</p>\n        <p>small</p>\n      </section>\n    ''')\n    page1, page2 = pages\n\n    assert (page1.width, page1.height) == (100, 100)\n    html, = page1.children\n    body, = html.children\n    section, = body.children\n    p, = section.children\n    assert section.element_tag == 'section'\n    assert p.element_tag == 'p'\n\n    assert (page2.width, page2.height) == (100, 100)\n    html, = page2.children\n    body, = html.children\n    section, = body.children\n    p, = section.children\n    assert section.element_tag == 'section'\n    assert p.element_tag == 'p'\n\n\n@assert_no_logs\ndef test_page_names_9():\n    pages = render_pages('''\n      <style>\n        @page { size: 200px 200px }\n        @page small { size: 100px 100px }\n        section { break-after: page; page: small }\n        article { page: small }\n      </style>\n      <section>\n        <div>big</div>\n        <div>big</div>\n      </section>\n      <article>\n        <div>small</div>\n        <div>small</div>\n      </article>\n    ''')\n    page1, page2, = pages\n\n    assert (page1.width, page1.height) == (100, 100)\n    html, = page1.children\n    body, = html.children\n    section, = body.children\n    assert section.element_tag == 'section'\n\n    assert (page2.width, page2.height) == (100, 100)\n    html, = page2.children\n    body, = html.children\n    article, = body.children\n    assert article.element_tag == 'article'\n\n\n@assert_no_logs\ndef test_page_names_10():\n    pages = render_pages('''\n      <style>\n        #running { position: running(running); }\n        #fixed { position: fixed; }\n        @page { size: 200px 200px; @top-center { content: element(header); }}\n        section { page: small; }\n        @page small { size: 100px 100px; }\n        .pagebreak { break-after: page; }\n      </style>\n      <div id=\"running\">running</div>\n      <div id=\"fixed\">fixed</div>\n      <section>\n        <h1>text</h1>\n        <div class=\"pagebreak\"></div>\n        <article>text</article>\n      </section>\n    ''')\n    page1, page2 = pages\n\n    assert (page1.width, page1.height) == (100, 100)\n    html, running = page1.children\n    body, = html.children\n    fixed, section, = body.children\n    h1, pagebreak = section.children\n    assert h1.element_tag == 'h1'\n\n    assert (page2.width, page2.height) == (100, 100)\n    html, running = page2.children\n    fixed, body = html.children\n    section, = body.children\n    article, = section.children\n    assert article.element_tag == 'article'\n\n\n@assert_no_logs\ndef test_page_groups():\n    pages = render_pages('''\n      <style>\n        @page { size: 200px 200px }\n        @page small { size: 100px 100px }\n        @page :nth(1 of small) { size: 50px 50px }\n        section { page: small }\n        div, div section { break-after: page }\n      </style>\n      <div></div>\n      <article></article>\n      <section>\n        <div></div>\n        <div></div>\n      </section>\n      <section>\n      </section>\n      <div></div>\n      <div></div>\n      <section>\n        <div></div>\n      </section>\n      <div>\n        <section></section>\n        <section></section>\n      </div>\n    ''')\n    page1, page2, page3, page4, page5, page6, page7, page8, page9 = pages\n\n    assert (page1.width, page1.height) == (200, 200)\n    div, = page1.children[0].children[0].children\n    assert div.element_tag == 'div'\n\n    assert (page2.width, page2.height) == (200, 200)\n    article, = page2.children[0].children[0].children\n    assert article.element_tag == 'article'\n\n    assert (page3.width, page3.height) == (50, 50)\n    section, = page3.children[0].children[0].children\n    assert section.element_tag == 'section'\n    div, = section.children\n    assert div.element_tag == 'div'\n\n    assert (page4.width, page4.height) == (100, 100)\n    section, = page4.children[0].children[0].children\n    assert section.element_tag == 'section'\n    div, = section.children\n    assert div.element_tag == 'div'\n\n    assert (page5.width, page5.height) == (50, 50)\n    section, div = page5.children[0].children[0].children\n    assert section.element_tag == 'section'\n    assert div.element_tag == 'div'\n\n    assert (page6.width, page6.height) == (200, 200)\n    div, = page6.children[0].children[0].children\n    assert div.element_tag == 'div'\n\n    assert (page7.width, page7.height) == (50, 50)\n    section, = page7.children[0].children[0].children\n    assert section.element_tag == 'section'\n    div, = section.children\n    assert div.element_tag == 'div'\n\n    assert (page8.width, page8.height) == (50, 50)\n    div, = page8.children[0].children[0].children\n    assert div.element_tag == 'div'\n    section, = div.children\n    assert section.element_tag == 'section'\n\n    assert (page9.width, page9.height) == (50, 50)\n    div, = page9.children[0].children[0].children\n    assert div.element_tag == 'div'\n    section, = div.children\n    assert section.element_tag == 'section'\n\n\n@assert_no_logs\ndef test_page_groups_blank_inside():\n    # Regression test for #1076.\n    pages = render_pages('''\n      <style>\n        @page { size: 100px }\n        @page div { size: 50px }\n        div { page: div }\n        p { break-before: right }\n      </style>\n      <div>\n        <p>1</p>\n        <p>2</p>\n      </div>\n    ''')\n    assert len(pages) == 3\n    for page in pages:\n        assert (page.width, page.height) == (50, 50)\n\n\n@assert_no_logs\ndef test_page_groups_blank_outside():\n    pages = render_pages('''\n      <style>\n        @page { size: 100px }\n        @page p { size: 50px }\n        p { page: p; break-before: right }\n      </style>\n      <div>\n        <p>1</p>\n        <p>2</p>\n      </div>\n    ''')\n    page1, page2, page3 = pages\n    for page in (page1, page3):\n        assert (page.width, page.height) == (50, 50)\n    assert (page2.width, page2.height) == (100, 100)\n\n\n@assert_no_logs\ndef test_page_groups_first_nth():\n    # Regression test for #2429.\n    pages = render_pages('''\n      <style>\n        @page { size: 100px }\n        @page div { size: 50px }\n        @page :nth(2n+1 of div) { size: 30px }\n        div { page: div; break-before: right }\n        p { break-before: page }\n      </style>\n      <div>\n        <p>1</p>\n        <p>2</p>\n        <p>3</p>\n      </div>\n      <div>\n        <p>4</p>\n        <p>5</p>\n      </div>\n    ''')\n    page1, page2, page3, page4, page5, page6 = pages\n    for page in (page1, page3, page5):\n        assert (page.width, page.height) == (30, 30)\n    for page in (page2, page6):\n        assert (page.width, page.height) == (50, 50)\n    assert (page4.width, page4.height) == (100, 100)\n\n@assert_no_logs\ndef test_page_groups_counters():\n    # Regression test for #2485.\n    pages = render_pages('''\n      <style>\n        html { counter-reset: h1-counter }\n        h1 { page-break-before: always; counter-increment: h1-counter; }\n        div.main-content { page: main-content-page-group; break-before: page; }\n        @page { size: 1000px }\n        @page :nth(1 of main-content-page-group) { counter-reset: page 1; size: 500px; }\n        a::after {\n          content: target-counter(attr(href), h1-counter) '.'\n          target-counter(attr(href), page);\n        }\n      </style>\n      <html>\n      <body>\n        INTRO PAGE\n        <div class=\"main-content\">\n        <h1 id=\"t2\">First title</h1>\n        <h1>Title3</h1>\n        <a href=\"#t2\">Second title</a>\n        </div>\n      </body>\n      </html>\n    ''')\n    page1, page2, page3  = pages\n    assert (page1.width, page1.height) == (1000, 1000)\n    assert (page2.width, page2.height) == (500, 500)\n    assert (page3.width, page3.height) == (1000, 1000)\n\n    pages = render_pages('''\n      <!DOCTYPE html>\n      <html>\n        <head>\n          <style>\n            @page { size: 1000px }\n            div { page: group }\n            h1 { break-before: page }\n            p.counter::after { content: counter(pages) }\n            @page :nth(1 of group) { background-color: blue; size: 500px; }\n          </style>\n        </head>\n        <body>\n          INTRO PAGE\n          <div>\n            <h1>Title</h1>\n            <p>content</p>\n            <h1>Title</h1>\n            <p class=\"counter\">content</p>\n          </div>\n          <div>\n            <h1>Title</h1>\n            <p>content</p>\n            <h1>Title</h1>\n            <p class=\"counter\">content</p>\n          </div>\n        </body>\n      </html>\n    ''')\n    page1, page2, page3, page4, page5  = pages\n    assert (page1.width, page1.height) == (1000, 1000)\n    assert (page2.width, page2.height) == (500, 500)\n    assert (page3.width, page3.height) == (1000, 1000)\n    assert (page4.width, page4.height) == (500, 500)\n    assert (page5.width, page5.height) == (1000, 1000)\n\n@assert_no_logs\n@pytest.mark.parametrize(('style', 'line_counts'), [\n    ('orphans: 2; widows: 2', [4, 3]),\n    ('orphans: 5; widows: 2', [0, 7]),\n    ('orphans: 2; widows: 4', [3, 4]),\n    ('orphans: 4; widows: 4', [0, 7]),\n    ('orphans: 2; widows: 2; page-break-inside: avoid', [0, 7]),\n])\ndef test_orphans_widows_avoid(style, line_counts):\n    pages = render_pages('''\n      <style>\n        @page { size: 200px }\n        h1 { height: 120px }\n        p { line-height: 20px;\n            width: 1px; /* line break at each word */\n            %s }\n      </style>\n      <h1>Tasty test</h1>\n      <!-- There is room for 4 lines after h1 on the fist page -->\n      <p>one two three four five six seven</p>\n    ''' % style)\n    for i, page in enumerate(pages):\n        html, = page.children\n        body, = html.children\n        body_children = body.children if i else body.children[1:]  # skip h1\n        count = len(body_children[0].children) if body_children else 0\n        assert line_counts.pop(0) == count\n    assert not line_counts\n\n\n@assert_no_logs\ndef test_page_and_linebox_breaking():\n    # Empty <span/> tests a corner case in skip_first_whitespace()\n    pages = render_pages('''\n      <style>\n        @page { size: 100px; margin: 2px; border: 1px solid }\n        body { margin: 0 }\n        div { font-family: weasyprint; font-size: 20px }\n      </style>\n      <div><span/>1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15</div>\n    ''')\n    texts = []\n    for page in pages:\n        html, = page.children\n        body, = html.children\n        div, = body.children\n        lines = div.children\n        for line in lines:\n            line_texts = []\n            for child in line.descendants():\n                if isinstance(child, boxes.TextBox):\n                    line_texts.append(child.text)\n            texts.append(''.join(line_texts))\n    assert len(pages) == 4\n    assert ''.join(texts) == '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15'\n\n\n@assert_no_logs\ndef test_margin_boxes_fixed_dimension_1():\n    # Corner boxes\n    page, = render_pages('''\n      <style>\n        @page {\n          @top-left-corner {\n            content: 'top_left';\n            padding: 10px;\n          }\n          @top-right-corner {\n            content: 'top_right';\n            padding: 10px;\n          }\n          @bottom-left-corner {\n            content: 'bottom_left';\n            padding: 10px;\n          }\n          @bottom-right-corner {\n            content: 'bottom_right';\n            padding: 10px;\n          }\n          size: 1000px;\n          margin-top: 10%;\n          margin-bottom: 40%;\n          margin-left: 20%;\n          margin-right: 30%;\n        }\n      </style>\n    ''')\n    html, top_left, top_right, bottom_left, bottom_right = page.children\n    for margin_box, text in zip(\n            [top_left, top_right, bottom_left, bottom_right],\n            ['top_left', 'top_right', 'bottom_left', 'bottom_right']):\n\n        line, = margin_box.children\n        text, = line.children\n        assert text == text\n\n    # Check positioning and Rule 1 for fixed dimensions\n    assert top_left.position_x == 0\n    assert top_left.position_y == 0\n    assert top_left.margin_width() == 200  # margin-left\n    assert top_left.margin_height() == 100  # margin-top\n\n    assert top_right.position_x == 700  # size-x - margin-right\n    assert top_right.position_y == 0\n    assert top_right.margin_width() == 300  # margin-right\n    assert top_right.margin_height() == 100  # margin-top\n\n    assert bottom_left.position_x == 0\n    assert bottom_left.position_y == 600  # size-y - margin-bottom\n    assert bottom_left.margin_width() == 200  # margin-left\n    assert bottom_left.margin_height() == 400  # margin-bottom\n\n    assert bottom_right.position_x == 700  # size-x - margin-right\n    assert bottom_right.position_y == 600  # size-y - margin-bottom\n    assert bottom_right.margin_width() == 300  # margin-right\n    assert bottom_right.margin_height() == 400  # margin-bottom\n\n\n@assert_no_logs\ndef test_margin_boxes_fixed_dimension_2():\n    # Test rules 2 and 3\n    page, = render_pages('''\n      <style>\n        @page {\n          margin: 100px 200px;\n          @bottom-left-corner { content: \"\"; margin: 60px }\n        }\n      </style>\n    ''')\n    html, margin_box = page.children\n    assert margin_box.margin_width() == 200\n    assert margin_box.margin_left == 60\n    assert margin_box.margin_right == 60\n    assert margin_box.width == 80  # 200 - 60 - 60\n\n    assert margin_box.margin_height() == 100\n    # total was too big, the outside margin was ignored:\n    assert margin_box.margin_top == 60\n    assert margin_box.margin_bottom == 40  # Not 60\n    assert margin_box.height == 0  # But not negative\n\n\n@assert_no_logs\ndef test_margin_boxes_fixed_dimension_3():\n    # Test rule 3 with a non-auto inner dimension\n    page, = render_pages('''\n      <style>\n        @page {\n          margin: 100px;\n          @left-middle { content: \"\"; margin: 10px; width: 130px }\n        }\n      </style>\n    ''')\n    html, margin_box = page.children\n    assert margin_box.margin_width() == 100\n    assert margin_box.margin_left == -40  # Not 10px\n    assert margin_box.margin_right == 10\n    assert margin_box.width == 130  # As specified\n\n\n@assert_no_logs\ndef test_margin_boxes_fixed_dimension_4():\n    # Test rule 4\n    page, = render_pages('''\n      <style>\n        @page {\n          margin: 100px;\n          @left-bottom {\n            content: \"\";\n            margin-left: 10px;\n            margin-right: auto;\n            width: 70px;\n          }\n        }\n      </style>\n    ''')\n    html, margin_box = page.children\n    assert margin_box.margin_width() == 100\n    assert margin_box.margin_left == 10  # 10px this time, no over-constrain\n    assert margin_box.margin_right == 20\n    assert margin_box.width == 70  # As specified\n\n\n@assert_no_logs\ndef test_margin_boxes_fixed_dimension_5():\n    # Test rules 2, 3 and 4\n    page, = render_pages('''\n      <style>\n        @page {\n          margin: 100px;\n          @right-top {\n            content: \"\";\n            margin-right: 10px;\n            margin-left: auto;\n            width: 130px;\n          }\n        }\n      </style>\n    ''')\n    html, margin_box = page.children\n    assert margin_box.margin_width() == 100\n    assert margin_box.margin_left == 0  # rule 2\n    assert margin_box.margin_right == -30  # rule 3, after rule 2\n    assert margin_box.width == 130  # As specified\n\n\n@assert_no_logs\ndef test_margin_boxes_fixed_dimension_6():\n    # Test rule 5\n    page, = render_pages('''\n      <style>\n        @page {\n          margin: 100px;\n          @top-left { content: \"\"; margin-top: 10px; margin-bottom: auto }\n        }\n      </style>\n    ''')\n    html, margin_box = page.children\n    assert margin_box.margin_height() == 100\n    assert margin_box.margin_top == 10\n    assert margin_box.margin_bottom == 0\n    assert margin_box.height == 90\n\n\n@assert_no_logs\ndef test_margin_boxes_fixed_dimension_7():\n    # Test rule 5\n    page, = render_pages('''\n      <style>\n        @page {\n          margin: 100px;\n          @top-center { content: \"\"; margin: auto 0 }\n        }\n      </style>\n    ''')\n    html, margin_box = page.children\n    assert margin_box.margin_height() == 100\n    assert margin_box.margin_top == 0\n    assert margin_box.margin_bottom == 0\n    assert margin_box.height == 100\n\n\n@assert_no_logs\ndef test_margin_boxes_fixed_dimension_8():\n    # Test rule 6\n    page, = render_pages('''\n      <style>\n        @page {\n          margin: 100px;\n          @bottom-right { content: \"\"; margin: auto; height: 70px }\n        }\n      </style>\n    ''')\n    html, margin_box = page.children\n    assert margin_box.margin_height() == 100\n    assert margin_box.margin_top == 15\n    assert margin_box.margin_bottom == 15\n    assert margin_box.height == 70\n\n\n@assert_no_logs\ndef test_margin_boxes_fixed_dimension_9():\n    # Rule 2 inhibits rule 6\n    page, = render_pages('''\n      <style>\n        @page {\n          margin: 100px;\n          @bottom-center { content: \"\"; margin: auto 0; height: 150px }\n        }\n      </style>\n    ''')\n    html, margin_box = page.children\n    assert margin_box.margin_height() == 100\n    assert margin_box.margin_top == 0\n    assert margin_box.margin_bottom == -50  # outside\n    assert margin_box.height == 150\n\n\ndef images(*widths):\n    return ' '.join(\n        f'url(\\'data:image/svg+xml,<svg width=\"{width}\" height=\"10\"></svg>\\')'\n        for width in widths)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('css', 'widths'), [\n    ('''@top-left { content: %s }\n        @top-center { content: %s }\n        @top-right { content: %s }\n     ''' % (images(50, 50), images(50, 50), images(50, 50)),\n     [100, 100, 100]),  # Use preferred widths if they fit\n    ('''@top-left { content: %s; margin: auto }\n        @top-center { content: %s }\n        @top-right { content: %s }\n     ''' % (images(50, 50), images(50, 50), images(50, 50)),\n     [100, 100, 100]),  # 'auto' margins are set to 0\n    ('''@top-left { content: %s }\n        @top-center { content: %s }\n        @top-right { content: 'foo'; width: 200px }\n     ''' % (images(100, 50), images(300, 150)),\n     [150, 300, 200]),  # Use at least minimum widths, even if boxes overlap\n    ('''@top-left { content: %s }\n        @top-center { content: %s }\n        @top-right { content: %s }\n     ''' % (images(150, 150), images(150, 150), images(150, 150)),\n     [200, 200, 200]),  # Distribute remaining space proportionally\n    ('''@top-left { content: %s }\n        @top-center { content: %s }\n        @top-right { content: %s }\n     ''' % (images(100, 100, 100), images(100, 100), images(10)),\n     [220, 160, 10]),\n    ('''@top-left { content: %s; width: 205px }\n        @top-center { content: %s }\n        @top-right { content: %s }\n     ''' % (images(100, 100, 100), images(100, 100), images(10)),\n     [205, 190, 10]),\n    ('''@top-left { width: 1000px; margin: 1000px; padding: 1000px;\n                    border: 1000px solid }\n        @top-center { content: %s }\n        @top-right { content: %s }\n     ''' % (images(100, 100), images(10)),\n     [200, 10]),  # 'width' and other have no effect without 'content'\n    ('''@top-left { content: ''; width: 200px }\n        @top-center { content: ''; width: 300px }\n        @top-right { content: %s }\n     ''' % images(50, 50),  # This leaves 150px for @top-right’s shrink-to-fit\n     [200, 300, 100]),\n    ('''@top-left { content: ''; width: 200px }\n        @top-center { content: ''; width: 300px }\n        @top-right { content: %s }\n     ''' % images(100, 100, 100),\n     [200, 300, 150]),\n    ('''@top-left { content: ''; width: 200px }\n        @top-center { content: ''; width: 300px }\n        @top-right { content: %s }\n     ''' % images(170, 175),\n     [200, 300, 175]),\n    ('''@top-left { content: ''; width: 200px }\n        @top-right { content: ''; width: 500px }\n     ''',\n     [200, 500]),\n    ('''@top-left { content: ''; width: 200px }\n        @top-right { content: %s }\n     ''' % images(150, 50, 150),\n     [200, 350]),\n    ('''@top-left { content: ''; width: 200px }\n        @top-right { content: %s }\n     ''' % images(150, 50, 150, 200),\n     [200, 400]),\n    ('''@top-left { content: %s }\n        @top-right { content: ''; width: 200px }\n     ''' % images(150, 50, 450),\n     [450, 200]),\n    ('''@top-left { content: %s }\n        @top-right { content: %s }\n     ''' % (images(150, 100), images(10, 120)),\n     [250, 130]),\n    ('''@top-left { content: %s }\n        @top-right { content: %s }\n     ''' % (images(550, 100), images(10, 120)),\n     [550, 120]),\n    ('''@top-left { content: %s }\n        @top-right { content: %s }\n     ''' % (images(250, 60), images(250, 180)),\n     [275, 325]),  # 250 + (100 * 1 / 4), 250 + (100 * 3 / 4)\n])\ndef test_page_style(css, widths):\n    expected_at_keywords = [\n        at_keyword for at_keyword in [\n            '@top-left', '@top-center', '@top-right']\n        if at_keyword + ' { content: ' in css]\n    page, = render_pages('''\n      <style>\n        @page {\n          size: 800px;\n          margin: 100px;\n          padding: 42px;\n          border: 7px solid;\n          %s\n        }\n      </style>\n    ''' % css)\n    assert page.children[0].element_tag == 'html'\n    margin_boxes = page.children[1:]\n    assert [box.at_keyword for box in margin_boxes] == expected_at_keywords\n    offsets = {'@top-left': 0, '@top-center': 0.5, '@top-right': 1}\n    for box in margin_boxes:\n        assert box.position_x == 100 + offsets[box.at_keyword] * (\n            600 - box.margin_width())\n    assert [box.margin_width() for box in margin_boxes] == widths\n\n\n@assert_no_logs\ndef test_margin_boxes_vertical_align():\n    # 3 px ->    +-----+\n    #            |  1  |\n    #            +-----+\n    #\n    #        43 px ->   +-----+\n    #        53 px ->   |  2  |\n    #                   +-----+\n    #\n    #               83 px ->   +-----+\n    #                          |  3  |\n    #               103px ->   +-----+\n    page, = render_pages('''\n      <style>\n        @page {\n          size: 800px;\n          margin: 106px;  /* margin boxes’ content height is 100px */\n\n          @top-left {\n            content: \"foo\"; line-height: 20px; border: 3px solid;\n            vertical-align: top;\n          }\n          @top-center {\n            content: \"foo\"; line-height: 20px; border: 3px solid;\n            vertical-align: middle;\n          }\n          @top-right {\n            content: \"foo\"; line-height: 20px; border: 3px solid;\n            vertical-align: bottom;\n          }\n        }\n      </style>\n    ''')\n    html, top_left, top_center, top_right = page.children\n    line_1, = top_left.children\n    line_2, = top_center.children\n    line_3, = top_right.children\n    assert line_1.position_y == 3\n    assert line_2.position_y == 43\n    assert line_3.position_y == 83\n\n\n@assert_no_logs\ndef test_margin_boxes_element():\n    pages = render_pages('''\n      <style>\n        @page {\n          counter-increment: count;\n          counter-reset: page pages;\n          margin: 50px;\n          size: 200px;\n          @bottom-center {\n            content: counter(page) ' of ' counter(pages)\n                     ' (' counter(count) ')';\n          }\n        }\n        h1 {\n          height: 40px;\n        }\n      </style>\n      <h1>test1</h1>\n      <h1>test2</h1>\n      <h1>test3</h1>\n      <h1>test4</h1>\n      <h1>test5</h1>\n      <h1>test6</h1>\n    ''')\n    footer1_text = ''.join(\n        getattr(node, 'text', '')\n        for node in pages[0].children[1].descendants())\n    assert footer1_text == '0 of 3 (1)'\n\n    footer2_text = ''.join(\n        getattr(node, 'text', '')\n        for node in pages[1].children[1].descendants())\n    assert footer2_text == '0 of 3 (2)'\n\n    footer3_text = ''.join(\n        getattr(node, 'text', '')\n        for node in pages[2].children[1].descendants())\n    assert footer3_text == '0 of 3 (3)'\n\n\n@assert_no_logs\ndef test_margin_boxes_running_element():\n    pages = render_pages('''\n      <style>\n        footer {\n          position: running(footer);\n        }\n        @page {\n          margin: 50px;\n          size: 200px;\n          @bottom-center {\n            content: element(footer);\n          }\n        }\n        body {\n          font-size: 1px\n        }\n        h1 {\n          height: 40px;\n        }\n        .pages:before {\n          content: counter(page);\n        }\n        .pages:after {\n          content: counter(pages);\n        }\n      </style>\n      <footer class=\"pages\"> of </footer>\n      <h1>test1</h1>\n      <h1>test2</h1>\n      <h1>test3</h1>\n      <h1>test4</h1>\n      <h1>test5</h1>\n      <h1>test6</h1>\n      <footer>Static</footer>\n    ''')\n    footer1_text = ''.join(\n        getattr(node, 'text', '')\n        for node in pages[0].children[1].descendants())\n    assert footer1_text == '1 of 3'\n\n    footer2_text = ''.join(\n        getattr(node, 'text', '')\n        for node in pages[1].children[1].descendants())\n    assert footer2_text == '2 of 3'\n\n    footer3_text = ''.join(\n        getattr(node, 'text', '')\n        for node in pages[2].children[1].descendants())\n    assert footer3_text == 'Static'\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('argument', 'texts'), [\n    # TODO: start doesn’t work because running elements are removed from the\n    # original tree, and the current implentation in\n    # layout.get_running_element_for uses the tree to know if it’s at the\n    # beginning of the page\n\n    # ('start', ('', '2-first', '2-last', '3-last', '5')),\n\n    ('first', ('', '2-first', '3-first', '3-last', '5')),\n    ('last', ('', '2-last', '3-last', '3-last', '5')),\n    ('first-except', ('', '', '', '3-last', '')),\n])\ndef test_running_elements(argument, texts):\n    pages = render_pages('''\n      <style>\n        @page {\n          margin: 50px;\n          size: 200px;\n          @bottom-center { content: element(title, %s) }\n        }\n        article { break-after: page }\n        h1 { position: running(title) }\n      </style>\n      <article>\n        <div>1</div>\n      </article>\n      <article>\n        <h1>2-first</h1>\n        <h1>2-last</h1>\n      </article>\n      <article>\n        <p>3</p>\n        <h1>3-first</h1>\n        <h1>3-last</h1>\n      </article>\n      <article>\n      </article>\n      <article>\n        <h1>5</h1>\n      </article>\n    ''' % argument)\n    assert len(pages) == 5\n    for page, text in zip(pages, texts):\n        html, margin = page.children\n        if margin.children:\n            h1, = margin.children\n            line, = h1.children\n            textbox, = line.children\n            assert textbox.text == text\n        else:\n            assert not text\n\n\n@assert_no_logs\ndef test_running_elements_display():\n    page, = render_pages('''\n      <style>\n        @page {\n          margin: 50px;\n          size: 200px;\n          @bottom-left { content: element(inline) }\n          @bottom-center { content: element(block) }\n          @bottom-right { content: element(table) }\n        }\n        table { position: running(table) }\n        div { position: running(block) }\n        span { position: running(inline) }\n      </style>\n      text\n      <table><tr><td>table</td></tr></table>\n      <div>block</div>\n      <span>inline</span>\n    ''')\n    html, left, center, right = page.children\n    assert ''.join(\n        getattr(node, 'text', '') for node in left.descendants()) == 'inline'\n    assert ''.join(\n        getattr(node, 'text', '') for node in center.descendants()) == 'block'\n    assert ''.join(\n        getattr(node, 'text', '') for node in right.descendants()) == 'table'\n\n\n@assert_no_logs\ndef test_running_img():\n    # Regression test.\n    render_pages('''\n      <style>\n        img {\n          position: running(img);\n        }\n        @page {\n          @bottom-center {\n            content: element(img);\n          }\n        }\n      </style>\n      <img src=\"pattern.png\" />\n    ''')\n\n\n@assert_no_logs\ndef test_running_absolute():\n    # Regression test for #1540.\n    render_pages('''\n      <style>\n        footer {\n          position: running(footer);\n        }\n        p {\n          position: absolute;\n        }\n        @page {\n          @bottom-center {\n            content: element(footer);\n          }\n        }\n      </style>\n      <footer>Hello!<p>Bonjour!</p></footer>\n    ''')\n\n\n@assert_no_logs\ndef test_running_flex():\n    # Regression test.\n    render_pages('''\n      <style>\n        footer {\n          display: flex;\n          position: running(footer);\n        }\n        @page {\n          @bottom-center {\n            content: element(footer);\n          }\n        }\n      </style>\n      <footer>\n        Hello!\n      </footer>\n    ''')\n\n\n@assert_no_logs\ndef test_running_float():\n    # Regression test.\n    render_pages('''\n      <style>\n        footer {\n          float: left;\n          position: running(footer);\n        }\n        @page {\n          @bottom-center {\n            content: element(footer);\n          }\n        }\n      </style>\n      <footer>\n        Hello!\n      </footer>\n    ''')\n"
  },
  {
    "path": "tests/layout/test_position.py",
    "content": "\"\"\"Tests for position property.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\ndef test_relative_positioning_1():\n    page, = render_pages('''\n      <style>\n        p { height: 20px }\n      </style>\n      <p>1</p>\n      <div style=\"position: relative; top: 10px\">\n        <p>2</p>\n        <p style=\"position: relative; top: -5px; left: 5px\">3</p>\n        <p>4</p>\n        <p style=\"position: relative; bottom: 5px; right: 5px\">5</p>\n        <p style=\"position: relative\">6</p>\n        <p>7</p>\n      </div>\n      <p>8</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    p1, div, p8 = body.children\n    p2, p3, p4, p5, p6, p7 = div.children\n    assert (p1.position_x, p1.position_y) == (0, 0)\n    assert (div.position_x, div.position_y) == (0, 30)\n    assert (p2.position_x, p2.position_y) == (0, 30)\n    assert (p3.position_x, p3.position_y) == (5, 45)  # (0 + 5, 50 - 5)\n    assert (p4.position_x, p4.position_y) == (0, 70)\n    assert (p5.position_x, p5.position_y) == (-5, 85)  # (0 - 5, 90 - 5)\n    assert (p6.position_x, p6.position_y) == (0, 110)\n    assert (p7.position_x, p7.position_y) == (0, 130)\n    assert (p8.position_x, p8.position_y) == (0, 140)\n    assert div.height == 120\n\n\n@assert_no_logs\ndef test_relative_positioning_2():\n    page, = render_pages('''\n      <style>\n        img { width: 20px }\n        body { font-size: 0 } /* Remove spaces */\n      </style>\n      <body>\n      <span><img src=pattern.png></span>\n      <span style=\"position: relative; left: 10px\">\n        <img src=pattern.png>\n        <img src=pattern.png\n             style=\"position: relative; left: -5px; top: 5px\">\n        <img src=pattern.png>\n        <img src=pattern.png\n             style=\"position: relative; right: 5px; bottom: 5px\">\n        <img src=pattern.png style=\"position: relative\">\n        <img src=pattern.png>\n      </span>\n      <span><img src=pattern.png></span>\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span1, span2, span3 = line.children\n    img1, = span1.children\n    img2, img3, img4, img5, img6, img7 = span2.children\n    img8, = span3.children\n    assert (img1.position_x, img1.position_y) == (0, 0)\n    # Don't test the span2.position_y because it depends on fonts\n    assert span2.position_x == 30\n    assert (img2.position_x, img2.position_y) == (30, 0)\n    assert (img3.position_x, img3.position_y) == (45, 5)  # (50 - 5, y + 5)\n    assert (img4.position_x, img4.position_y) == (70, 0)\n    assert (img5.position_x, img5.position_y) == (85, -5)  # (90 - 5, y - 5)\n    assert (img6.position_x, img6.position_y) == (110, 0)\n    assert (img7.position_x, img7.position_y) == (130, 0)\n    assert (img8.position_x, img8.position_y) == (140, 0)\n    assert span2.width == 120\n\n\n@assert_no_logs\ndef test_relative_positioning_3():\n    page, = render_pages('''\n      <style>\n        img { width: 20px }\n        body { font-size: 0 } /* Remove spaces */\n      </style>\n      <body>\n      <span><img src=pattern.png></span>\n      <span style=\"position: relative; left: 10px; right: 5px\n        \"><img src=pattern.png></span>\n      <span><img src=pattern.png></span>\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span1, span2, span3 = line.children\n    assert span2.position_x == 20 + 10\n\n\n@assert_no_logs\ndef test_relative_positioning_4():\n    page, = render_pages('''\n      <style>\n        img { width: 20px }\n        body { direction: rtl; width: 100px;\n               font-size: 0 } /* Remove spaces */\n      </style>\n      <body>\n      <span><img src=pattern.png></span>\n      <span style=\"position: relative; left: 10px; right: 5px\n        \"><img src=pattern.png></span>\n      <span><img src=pattern.png></span>\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span1, span2, span3 = line.children\n    assert span2.position_x == 100 - 20 - 5 - 20\n\n\n@assert_no_logs\ndef test_absolute_positioning_1():\n    page, = render_pages('''\n      <div style=\"margin: 3px\">\n        <div style=\"height: 20px; width: 20px; position: absolute\"></div>\n        <div style=\"height: 20px; width: 20px; position: absolute;\n                    left: 0\"></div>\n        <div style=\"height: 20px; width: 20px; position: absolute;\n                    top: 0\"></div>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div1, = body.children\n    div2, div3, div4 = div1.children\n    assert div1.height == 0\n    assert (div1.position_x, div1.position_y) == (0, 0)\n    assert (div2.width, div2.height) == (20, 20)\n    assert (div2.position_x, div2.position_y) == (3, 3)\n    assert (div3.width, div3.height) == (20, 20)\n    assert (div3.position_x, div3.position_y) == (0, 3)\n    assert (div4.width, div4.height) == (20, 20)\n    assert (div4.position_x, div4.position_y) == (3, 0)\n\n\n@assert_no_logs\ndef test_absolute_positioning_2():\n    page, = render_pages('''\n      <div style=\"position: relative; width: 20px\">\n        <div style=\"height: 20px; width: 20px; position: absolute\"></div>\n        <div style=\"height: 20px; width: 20px\"></div>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div1, = body.children\n    div2, div3 = div1.children\n    for div in (div1, div2, div3):\n        assert (div.position_x, div.position_y) == (0, 0)\n        assert (div.width, div.height) == (20, 20)\n\n\n@assert_no_logs\ndef test_absolute_positioning_3():\n    page, = render_pages('''\n      <body style=\"font-size: 0\">\n        <img src=pattern.png>\n        <span style=\"position: relative\">\n          <span style=\"position: absolute\">2</span>\n          <span style=\"position: absolute\">3</span>\n          <span>4</span>\n        </span>\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    img, span1 = line.children\n    span2, span3, span4 = span1.children\n    assert span1.position_x == 4\n    assert (span2.position_x, span2.position_y) == (4, 0)\n    assert (span3.position_x, span3.position_y) == (4, 0)\n    assert span4.position_x == 4\n\n\n@assert_no_logs\ndef test_absolute_positioning_4():\n    page, = render_pages('''\n      <style> img { width: 5px; height: 20px} </style>\n      <body style=\"font-size: 0\">\n        <img src=pattern.png>\n        <span style=\"position: absolute\">2</span>\n        <img src=pattern.png>\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    img1, span, img2 = line.children\n    assert (img1.position_x, img1.position_y) == (0, 0)\n    assert (span.position_x, span.position_y) == (5, 0)\n    assert (img2.position_x, img2.position_y) == (5, 0)\n\n\n@assert_no_logs\ndef test_absolute_positioning_5():\n    page, = render_pages('''\n      <style> img { width: 5px; height: 20px} </style>\n      <body style=\"font-size: 0\">\n        <img src=pattern.png>\n        <span style=\"position: absolute; display: block\">2</span>\n        <img src=pattern.png>\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    img1, span, img2 = line.children\n    assert (img1.position_x, img1.position_y) == (0, 0)\n    assert (span.position_x, span.position_y) == (0, 20)\n    assert (img2.position_x, img2.position_y) == (5, 0)\n\n\n@assert_no_logs\ndef test_absolute_positioning_6():\n    page, = render_pages('''\n      <div style=\"position: relative; width: 20px; height: 60px;\n                  border: 10px solid; padding-top: 6px; top: 5px; left: 1px\">\n        <div style=\"height: 20px; width: 20px; position: absolute;\n                    bottom: 50%\"></div>\n        <div style=\"height: 20px; width: 20px; position: absolute;\n                    top: 13px\"></div>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div1, = body.children\n    div2, div3 = div1.children\n    assert (div1.position_x, div1.position_y) == (1, 5)\n    assert (div1.width, div1.height) == (20, 60)\n    assert (div1.border_width(), div1.border_height()) == (40, 86)\n    assert (div2.position_x, div2.position_y) == (11, 28)\n    assert (div2.width, div2.height) == (20, 20)\n    assert (div3.position_x, div3.position_y) == (11, 28)\n    assert (div3.width, div3.height) == (20, 20)\n\n\n@assert_no_logs\ndef test_absolute_positioning_7():\n    page, = render_pages('''\n      <style>\n        @page { size: 1000px 2000px }\n        html { font-size: 0 }\n        p { height: 20px }\n      </style>\n      <p>1</p>\n      <div style=\"width: 100px\">\n        <p>2</p>\n        <p style=\"position: absolute; top: -5px; left: 5px\">3</p>\n        <p style=\"margin: 3px\">4</p>\n        <p style=\"position: absolute; bottom: 5px; right: 15px;\n                  width: 50px; height: 10%;\n                  padding: 3px; margin: 7px\">5\n          <span>\n            <img src=\"pattern.png\">\n            <span style=\"position: absolute\"></span>\n            <span style=\"position: absolute; top: -10px; right: 5px;\n                         width: 20px; height: 15px\"></span>\n          </span>\n        </p>\n        <p style=\"margin-top: 8px\">6</p>\n      </div>\n      <p>7</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    p1, div, p7 = body.children\n    p2, p3, p4, p5, p6 = div.children\n    line, = p5.children\n    span1, = line.children\n    img, span2, span3 = span1.children\n    assert (p1.position_x, p1.position_y) == (0, 0)\n    assert (div.position_x, div.position_y) == (0, 20)\n    assert (p2.position_x, p2.position_y) == (0, 20)\n    assert (p3.position_x, p3.position_y) == (5, -5)\n    assert (p4.position_x, p4.position_y) == (0, 40)\n    # p5 x = page width - right - margin/padding/border - width\n    #      = 1000       - 15    - 2 * 10                - 50\n    #      = 915\n    # p5 y = page height - bottom - margin/padding/border - height\n    #      = 2000        - 5      - 2 * 10                - 200\n    #      = 1775\n    assert (p5.position_x, p5.position_y) == (915, 1775)\n    assert (img.position_x, img.position_y) == (925, 1785)\n    assert (span2.position_x, span2.position_y) == (929, 1785)\n    # span3 x = p5 right - p5 margin - span width - span right\n    #         = 985      - 7         - 20         - 5\n    #         = 953\n    # span3 y = p5 y + p5 margin top + span top\n    #         = 1775 + 7             + -10\n    #         = 1772\n    assert (span3.position_x, span3.position_y) == (953, 1772)\n    # p6 y = p4 y + p4 margin height - margin collapsing\n    #      = 40   + 26               - 3\n    #      = 63\n    assert (p6.position_x, p6.position_y) == (0, 63)\n    assert div.height == 71  # 20*3 + 2*3 + 8 - 3\n    assert (p7.position_x, p7.position_y) == (0, 91)\n\n\n@assert_no_logs\ndef test_absolute_positioning_8():\n    # Regression test for #1264.\n    page, = render_pages('''\n      <style>@page{ width: 50px; height: 50px }</style>\n      <body style=\"font-size: 0\">\n        <div style=\"position: absolute; margin: auto;\n                    left: 0; right: 10px;\n                    top: 0; bottom: 10px;\n                    width: 10px; height: 20px\">\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert (div.content_box_x(), div.content_box_y()) == (15, 10)\n    assert (div.width, div.height) == (10, 20)\n\n\n@assert_no_logs\ndef test_absolute_images():\n    page, = render_pages('''\n      <style>\n        @page { size: 50px; }\n        img { display: block; position: absolute }\n      </style>\n      <div style=\"margin: 10px\">\n        <img src=pattern.png />\n        <img src=pattern.png style=\"left: 15px\" />\n        <img src=pattern.png style=\"top: 15px\" />\n        <img src=pattern.png style=\"bottom: 25px\" />\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    img1, img2, img3, img4 = div.children\n    assert div.height == 0\n    assert (div.position_x, div.position_y) == (0, 0)\n    assert (img1.position_x, img1.position_y) == (10, 10)\n    assert (img1.width, img1.height) == (4, 4)\n    assert (img2.position_x, img2.position_y) == (15, 10)\n    assert (img2.width, img2.height) == (4, 4)\n    assert (img3.position_x, img3.position_y) == (10, 15)\n    assert (img3.width, img3.height) == (4, 4)\n    assert (img4.position_x, img4.position_y) == (10, 21)  # (50 - 4) - 25\n    assert (img4.width, img4.height) == (4, 4)\n\n    # TODO: test the various cases in absolute_replaced()\n\n\n@assert_no_logs\ndef test_fixed_positioning():\n    # TODO:test page-break-before: left/right\n    page_1, page_2, page_3 = render_pages('''\n      a\n      <div style=\"page-break-before: always; page-break-after: always\">\n        <p style=\"position: fixed\">b</p>\n      </div>\n      c\n    ''')\n    html, = page_1.children\n    assert [c.element_tag for c in html.children] == ['body', 'p']\n    html, = page_2.children\n    body, = html.children\n    div, = body.children\n    assert [c.element_tag for c in div.children] == ['p']\n    html, = page_3.children\n    assert [c.element_tag for c in html.children] == ['p', 'body']\n\n\n@assert_no_logs\ndef test_fixed_positioning_regression_1():\n    # Regression test for #641.\n    page_1, page_2 = render_pages('''\n      <style>\n        @page:first { size: 100px 200px }\n        @page { size: 200px 100px; margin: 0 }\n        article { break-after: page }\n        .fixed { position: fixed; top: 10px; width: 20px }\n      </style>\n      <ul class=\"fixed\" style=\"right: 0\"><li>a</li></ul>\n      <img class=\"fixed\" style=\"right: 20px\" src=\"pattern.png\" />\n      <div class=\"fixed\" style=\"right: 40px\">b</div>\n      <article>page1</article>\n      <article>page2</article>\n    ''')\n\n    html, = page_1.children\n    body, = html.children\n    ul, img, div, article = body.children\n    marker = ul.children[0]\n    assert (ul.position_x, ul.position_y) == (80, 10)\n    assert (img.position_x, img.position_y) == (60, 10)\n    assert (div.position_x, div.position_y) == (40, 10)\n    assert (article.position_x, article.position_y) == (0, 0)\n    assert marker.position_x == ul.position_x\n\n    html, = page_2.children\n    ul, img, div, body = html.children\n    marker = ul.children[0]\n    assert (ul.position_x, ul.position_y) == (180, 10)\n    assert (img.position_x, img.position_y) == (160, 10)\n    assert (div.position_x, div.position_y) == (140, 10)\n    assert (article.position_x, article.position_y) == (0, 0)\n    assert marker.position_x == ul.position_x\n\n\n@assert_no_logs\ndef test_fixed_positioning_regression_2():\n    # Regression test for #728.\n    page_1, page_2 = render_pages('''\n      <style>\n        @page { size: 100px 100px }\n        section { break-after: page }\n        .fixed { position: fixed; top: 10px; left: 15px; width: 20px }\n      </style>\n      <div class=\"fixed\">\n        <article class=\"fixed\" style=\"top: 20px\">\n          <header class=\"fixed\" style=\"left: 5px\"></header>\n        </article>\n      </div>\n      <section></section>\n      <pre></pre>\n    ''')\n    html, = page_1.children\n    body, = html.children\n    div, section = body.children\n    assert (div.position_x, div.position_y) == (15, 10)\n    article, = div.children\n    assert (article.position_x, article.position_y) == (15, 20)\n    header, = article.children\n    assert (header.position_x, header.position_y) == (5, 10)\n\n    html, = page_2.children\n    div, body, = html.children\n    assert (div.position_x, div.position_y) == (15, 10)\n    article, = div.children\n    assert (article.position_x, article.position_y) == (15, 20)\n    header, = article.children\n    assert (header.position_x, header.position_y) == (5, 10)\n\n\n@assert_no_logs\ndef test_flex_relative_positioning():\n    page, = render_pages('''\n      <style>\n        @page { size: 100px 100px }\n        article { display: flex }\n        .box { width: 20px; height: 20px }\n        .relative { position: relative; top: 10px; left: 10px }\n      </style>\n      <article>\n        <div class=\"box\"></div>\n        <div class=\"box relative\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2 = article.children\n\n    assert (div2.position_x, div2.position_y) == (30, 10)\n\n\n@assert_no_logs\ndef test_grid_relative_positioning():\n    page, = render_pages('''\n      <style>\n        @page { size: 100px 100px }\n        article { display: grid; grid-template-columns: 20px 20px }\n        .box { width: 20px; height: 20px }\n        .relative { position: relative; top: 10px; left: 10px }\n      </style>\n      <article>\n        <div class=\"box\"></div>\n        <div class=\"box relative\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2 = article.children\n\n    assert (div2.position_x, div2.position_y) == (30, 10)\n\n\n@assert_no_logs\n@pytest.mark.xfail\ndef test_flex_absolute_positioning():\n    \"\"\"TODO: Order is not kept when out-of-flow and in-flow children are mixed.\"\"\"\n    page, = render_pages('''\n      <style>\n        @page { size: 100px 100px }\n        article { display: flex; position: relative }\n        .box { width: 20px; height: 20px }\n        .absolute { position: absolute; top: 10px; left: 10px }\n      </style>\n      <article>\n        <div class=\"box\"></div>\n        <div class=\"box absolute\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    # That’s currently div2, div1\n    div1, div2 = article.children\n\n    assert (div2.position_x, div2.position_y) == (10, 10)\n\n\n@assert_no_logs\n@pytest.mark.xfail\ndef test_grid_absolute_positioning():\n    \"\"\"TODO: Absolutely positioned grid items are not replaced by placeholders .\"\"\"\n    page, = render_pages('''\n      <style>\n        @page { size: 100px 100px }\n        article { display: grid; grid-template-columns: 20px 20px; position: relative }\n        .box { width: 20px; height: 20px }\n        .absolute { position: absolute; top: 10px; left: 10px }\n      </style>\n      <article>\n        <div class=\"box\"></div>\n        <div class=\"box absolute\"></div>\n      </article>\n    ''')\n    html, = page.children\n    body, = html.children\n    article, = body.children\n    div1, div2 = article.children\n\n    assert (div1.position_x, div1.position_y) == (10, 10)\n"
  },
  {
    "path": "tests/layout/test_preferred.py",
    "content": "\"\"\"Tests for shrink-to-fit algorithm.\"\"\"\n\nimport pytest\n\nfrom ..testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\n@pytest.mark.parametrize('margin_left', range(1, 10))\n@pytest.mark.parametrize('font_size', range(1, 10))\ndef test_shrink_to_fit_floating_point_error_1(margin_left, font_size):\n    # See bugs #325 and #288, see commit fac5ee9.\n    page, = render_pages('''\n      <style>\n        @page { size: 100000px 100px }\n        p { float: left; margin-left: 0.%din; font-size: 0.%dem;\n            font-family: weasyprint }\n      </style>\n      <p>this parrot is dead</p>\n    ''' % (margin_left, font_size))\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    assert len(p.children) == 1\n\n\n@assert_no_logs\n@pytest.mark.parametrize('font_size', [1, 5, 10, 50, 100, 1000, 10000])\ndef test_shrink_to_fit_floating_point_error_2(font_size):\n    letters = 1\n    while True:\n        page, = render_pages('''\n          <style>\n            @page { size: %d0pt %d0px }\n            p { font-size: %dpt; font-family: weasyprint }\n          </style>\n          <p>mmm <b>%s a</b></p>\n        ''' % (font_size, font_size, font_size, 'i' * letters))\n        html, = page.children\n        body, = html.children\n        p, = body.children\n        assert len(p.children) in (1, 2)\n        assert len(p.children[0].children) == 2\n        text = p.children[0].children[1].children[0].text\n        assert text\n        if text.endswith('i'):\n            letters = 1\n            break\n        else:\n            letters += 1\n\n\n@assert_no_logs\ndef test_preferred_inline_zero_width_inline_block():\n    page, = render_pages('''\n      <style>\n        div { font: 2px weasyprint; float: left }\n      </style>\n      <div><span style=\"display: inline-block\"></span> a</div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 4\n    assert div.height == 2\n\n\n@assert_no_logs\ndef test_preferred_inline_nested_trailing_spaces():\n    page, = render_pages('''\n      <style>\n        div { font: 2px weasyprint; float: left }\n      </style>\n      <div><span>a </span> </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 2\n    assert div.height == 2\n\n\n@assert_no_logs\ndef test_preferred_inline_trailing_space_in_nested():\n    page, = render_pages('''\n      <style>\n        div { font: 2px weasyprint; float: left }\n      </style>\n      <div><span style=\"display: inline-block\"></span><span><span>a</span> </span></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert div.width == 2\n    assert div.height == 2\n"
  },
  {
    "path": "tests/layout/test_table.py",
    "content": "\"\"\"Tests for layout of tables.\"\"\"\n\nimport pytest\n\nfrom weasyprint.formatting_structure import boxes\nfrom weasyprint.layout.table import collapse_table_borders\n\nfrom ..testing_utils import assert_no_logs, capture_logs, parse_all, render_pages\n\n\ndef _get_grid(html, grid_width, grid_height):\n    html = parse_all(html)\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    border_lists = collapse_table_borders(table, grid_width, grid_height)\n    return tuple(\n        [[(style, width, color) if width else None\n          for _score, (style, width, color) in border]\n         for border in border_list]\n        for border_list in border_lists)\n\n\n@assert_no_logs\ndef test_inline_table():\n    page, = render_pages('''\n      <table style=\"display: inline-table; border-spacing: 10px; margin: 5px\">\n        <tr>\n          <td><img src=pattern.png style=\"width: 20px\"></td>\n          <td><img src=pattern.png style=\"width: 30px\"></td>\n        </tr>\n      </table>\n      foo\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    table_wrapper, text = line.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 5  # 0 + margin-left\n    assert td_1.position_x == 15  # 0 + border-spacing\n    assert td_1.width == 20\n    assert td_2.position_x == 45  # 15 + 20 + border-spacing\n    assert td_2.width == 30\n    assert table.width == 80  # 20 + 30 + 3 * border-spacing\n    assert table_wrapper.margin_width() == 90  # 80 + 2 * margin\n    assert text.position_x == 90\n\n\n@assert_no_logs\ndef test_inline_table_width():\n    page, = render_pages('''\n      <div style=\"font: 2px weasyprint; width: 20px\">\n        <table style=\"display: inline-table; width: 20%\"><tr><td>A</tr></td></table>\n        <table style=\"display: inline-table; width: 20%\"><tr><td>B</tr></td></table>\n      </div>''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line, = div.children\n    table_wrapper1, space, table_wrapper2, tail = line.children\n    assert table_wrapper1.width == table_wrapper2.width == 4\n    assert table_wrapper1.position_x == 0\n    assert table_wrapper2.position_x == 6\n\n\n@assert_no_logs\ndef test_implicit_width_table_col_percent():\n    # Regression test for #169.\n    page, = render_pages('''\n      <table>\n        <col style=\"width:25%\"></col>\n        <col></col>\n        <tr>\n          <td></td>\n          <td></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n\n\n@assert_no_logs\ndef test_implicit_width_table_td_percent():\n    page, = render_pages('''\n      <table>\n        <tr>\n          <td style=\"width:25%\"></td>\n          <td></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n\n\n@assert_no_logs\ndef test_layout_table_fixed_1():\n    page, = render_pages('''\n      <table style=\"table-layout: fixed; border-spacing: 10px; margin: 5px\">\n        <colgroup>\n          <col style=\"width: 20px\" />\n        </colgroup>\n        <tr>\n          <td></td>\n          <td style=\"width: 40px\">a</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 5  # 0 + margin-left\n    assert td_1.position_x == 15  # 5 + border-spacing\n    assert td_1.width == 20\n    assert td_2.position_x == 45  # 15 + 20 + border-spacing\n    assert td_2.width == 40\n    assert table.width == 90  # 20 + 40 + 3 * border-spacing\n\n\n@assert_no_logs\ndef test_layout_table_fixed_2():\n    page, = render_pages('''\n      <table style=\"table-layout: fixed; border-spacing: 10px; width: 200px;\n                    margin: 5px\">\n        <tr>\n          <td style=\"width: 20px\">a</td>\n          <td style=\"width: 40px\"></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 5  # 0 + margin-left\n    assert td_1.position_x == 15  # 5 + border-spacing\n    assert td_1.width == 75  # 20 + ((200 - 20 - 40 - 3 * border-spacing) / 2)\n    assert td_2.position_x == 100  # 15 + 75 + border-spacing\n    assert td_2.width == 95  # 40 + ((200 - 20 - 40 - 3 * border-spacing) / 2)\n    assert table.width == 200\n\n\n@assert_no_logs\ndef test_layout_table_fixed_3():\n    page, = render_pages('''\n      <table style=\"table-layout: fixed; border-spacing: 10px;\n                    width: 110px; margin: 5px\">\n        <tr>\n          <td style=\"width: 40px\">a</td>\n          <td>b</td>\n        </tr>\n        <tr>\n          <td style=\"width: 50px\">a</td>\n          <td style=\"width: 30px\">b</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row_1, row_2 = row_group.children\n    td_1, td_2 = row_1.children\n    td_3, td_4 = row_2.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 5  # 0 + margin-left\n    assert td_1.position_x == 15  # 0 + border-spacing\n    assert td_3.position_x == 15\n    assert td_1.width == 40\n    assert td_2.width == 40\n    assert td_2.position_x == 65  # 15 + 40 + border-spacing\n    assert td_4.position_x == 65\n    assert td_3.width == 40\n    assert td_4.width == 40\n    assert table.width == 110  # 20 + 40 + 3 * border-spacing\n\n\n@assert_no_logs\ndef test_layout_table_fixed_4():\n    page, = render_pages('''\n      <table style=\"table-layout: fixed; border-spacing: 0;\n                    width: 100px; margin: 10px\">\n        <colgroup>\n          <col />\n          <col style=\"width: 20px\" />\n        </colgroup>\n        <tr>\n          <td></td>\n          <td style=\"width: 40px\">a</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 10  # 0 + margin-left\n    assert td_1.position_x == 10\n    assert td_1.width == 80  # 100 - 20\n    assert td_2.position_x == 90  # 10 + 80\n    assert td_2.width == 20\n    assert table.width == 100\n\n\n@assert_no_logs\ndef test_layout_table_fixed_5():\n    # With border-collapse\n    page, = render_pages('''\n      <style>\n        /* Do not apply: */\n        colgroup, col, tbody, tr, td { margin: 1000px }\n      </style>\n      <table style=\"table-layout: fixed;\n                    border-collapse: collapse; border: 10px solid;\n                    /* ignored with collapsed borders: */\n                    border-spacing: 10000px; padding: 1000px\">\n        <colgroup>\n          <col style=\"width: 30px\" />\n        </colgroup>\n        <tbody>\n          <tr>\n            <td style=\"padding: 2px\"></td>\n            <td style=\"width: 34px; padding: 10px; border: 2px solid\"></td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 0\n    assert table.border_left_width == 5  # half of the collapsed 10px border\n    assert td_1.position_x == 5  # border-spacing is ignored\n    assert td_1.margin_width() == 30  # as <col>\n    assert td_1.width == 20  # 30 - 5 (border-left) - 1 (border-right) - 2*2\n    assert td_2.position_x == 35\n    assert td_2.width == 34\n    assert td_2.margin_width() == 60  # 34 + 2*10 + 5 + 1\n    assert table.width == 90  # 30 + 60\n    assert table.margin_width() == 100  # 90 + 2*5 (border)\n\n\n@assert_no_logs\ndef test_layout_table_auto_1():\n    page, = render_pages('''\n      <body style=\"width: 100px\">\n      <table style=\"border-spacing: 10px; margin: auto\">\n        <tr>\n          <td><img src=pattern.png></td>\n          <td><img src=pattern.png></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table_wrapper.width == 38  # Same as table, see below\n    assert table_wrapper.margin_left == 31  # 0 + margin-left = (100 - 38) / 2\n    assert table_wrapper.margin_right == 31\n    assert table.position_x == 31\n    assert td_1.position_x == 41  # 31 + spacing\n    assert td_1.width == 4\n    assert td_2.position_x == 55  # 31 + 4 + spacing\n    assert td_2.width == 4\n    assert table.width == 38  # 3 * spacing + 2 * 4\n\n\n@assert_no_logs\ndef test_layout_table_auto_2():\n    page, = render_pages('''\n      <body style=\"width: 50px\">\n      <table style=\"border-spacing: 1px; margin: 10%\">\n        <tr>\n          <td style=\"border: 3px solid black\"><img src=pattern.png></td>\n          <td style=\"border: 3px solid black\">\n            <img src=pattern.png><img src=pattern.png>\n          </td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 5  # 0 + margin-left\n    assert td_1.position_x == 6  # 5 + border-spacing\n    assert td_1.width == 4\n    assert td_2.position_x == 17  # 6 + 4 + spacing + 2 * border\n    assert td_2.width == 8\n    assert table.width == 27  # 3 * spacing + 4 + 8 + 4 * border\n\n\n@assert_no_logs\ndef test_layout_table_auto_3():\n    page, = render_pages('''\n      <table style=\"border-spacing: 1px; margin: 5px; font-size: 0\">\n        <tr>\n          <td></td>\n          <td><img src=pattern.png><img src=pattern.png></td>\n        </tr>\n        <tr>\n          <td>\n            <img src=pattern.png>\n            <img src=pattern.png>\n            <img src=pattern.png>\n          </td>\n          <td><img src=pattern.png></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row1, row2 = row_group.children\n    td_11, td_12 = row1.children\n    td_21, td_22 = row2.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 5  # 0 + margin-left\n    assert td_11.position_x == td_21.position_x == 6  # 5 + spacing\n    assert td_11.width == td_21.width == 12\n    assert td_12.position_x == td_22.position_x == 19  # 6 + 12 + spacing\n    assert td_12.width == td_22.width == 8\n    assert table.width == 23  # 3 * spacing + 12 + 8\n\n\n@assert_no_logs\ndef test_layout_table_auto_4():\n    page, = render_pages('''\n      <table style=\"border-spacing: 1px; margin: 5px\">\n        <tr>\n          <td style=\"border: 1px solid black\"><img src=pattern.png></td>\n          <td style=\"border: 2px solid black; padding: 1px\">\n            <img src=pattern.png>\n          </td>\n        </tr>\n        <tr>\n          <td style=\"border: 5px solid black\"><img src=pattern.png></td>\n          <td><img src=pattern.png></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row1, row2 = row_group.children\n    td_11, td_12 = row1.children\n    td_21, td_22 = row2.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 5  # 0 + margin-left\n    assert td_11.position_x == td_21.position_x == 6  # 5 + spacing\n    assert td_11.width == 12  # 4 + 2 * 5 - 2 * 1\n    assert td_21.width == 4\n    assert td_12.position_x == td_22.position_x == 21  # 6 + 4 + 2 * b1 + sp\n    assert td_12.width == 4\n    assert td_22.width == 10  # 4 + 2 * 3\n    assert table.width == 27  # 3 * spacing + 4 + 4 + 2 * b1 + 2 * b2\n\n\n@assert_no_logs\ndef test_layout_table_auto_5():\n    page, = render_pages('''\n      <table style=\"width: 1000px; font-family: weasyprint\">\n        <tr>\n          <td style=\"width: 40px\">aa aa aa aa</td>\n          <td style=\"width: 40px\">aaaaaaaaaaa</td>\n          <td>This will take the rest of the width</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2, td_3 = row.children\n\n    assert table.width == 1000\n    assert td_1.width == 40\n    assert td_2.width == 11 * 16\n    assert td_3.width == 1000 - 40 - 11 * 16\n\n\n@assert_no_logs\ndef test_layout_table_auto_6():\n    page, = render_pages('''\n      <style>\n        @page { size: 100px 1000px; }\n      </style>\n      <table style=\"border-spacing: 1px; margin-right: 79px; font-size: 0\">\n        <tr>\n          <td><img src=pattern.png></td>\n          <td>\n            <img src=pattern.png> <img src=pattern.png>\n            <img src=pattern.png> <img src=pattern.png>\n            <img src=pattern.png> <img src=pattern.png>\n            <img src=pattern.png> <img src=pattern.png>\n            <img src=pattern.png>\n          </td>\n        </tr>\n        <tr>\n          <td></td>\n        </tr>\n      </table>\n    ''')\n    # Preferred minimum width is 2 * 4 + 3 * 1 = 11\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row1, row2 = row_group.children\n    td_11, td_12 = row1.children\n    td_21, = row2.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 0\n    assert td_11.position_x == td_21.position_x == 1  # spacing\n    assert td_11.width == td_21.width == 4  # minimum width\n    assert td_12.position_x == 6  # 1 + 5 + sp\n    assert td_12.width == 14  # available width\n    assert table.width == 21\n\n\n@assert_no_logs\ndef test_layout_table_auto_7():\n    page, = render_pages('''\n      <table style=\"border-spacing: 10px; margin: 5px\">\n        <colgroup>\n          <col style=\"width: 20px\" />\n        </colgroup>\n        <tr>\n          <td></td>\n          <td style=\"width: 40px\">a</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 5  # 0 + margin-left\n    assert td_1.position_x == 15  # 0 + border-spacing\n    assert td_1.width == 20\n    assert td_2.position_x == 45  # 15 + 20 + border-spacing\n    assert td_2.width == 40\n    assert table.width == 90  # 20 + 40 + 3 * border-spacing\n\n\n@assert_no_logs\ndef test_layout_table_auto_8():\n    page, = render_pages('''\n      <table style=\"border-spacing: 10px; width: 120px; margin: 5px;\n                    font-size: 0\">\n        <tr>\n          <td style=\"width: 20px\"><img src=pattern.png></td>\n          <td><img src=pattern.png style=\"width: 40px\"></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 5  # 0 + margin-left\n    assert td_1.position_x == 15  # 5 + border-spacing\n    assert td_1.width == 20  # fixed\n    assert td_2.position_x == 45  # 15 + 20 + border-spacing\n    assert td_2.width == 70  # 120 - 3 * border-spacing - 20\n    assert table.width == 120\n\n\n@assert_no_logs\ndef test_layout_table_auto_9():\n    page, = render_pages('''\n      <table style=\"border-spacing: 10px; width: 120px; margin: 5px\">\n        <tr>\n          <td style=\"width: 60px\"></td>\n          <td></td>\n        </tr>\n        <tr>\n          <td style=\"width: 50px\"></td>\n          <td style=\"width: 30px\"></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row_1, row_2 = row_group.children\n    td_1, td_2 = row_1.children\n    td_3, td_4 = row_2.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 5  # 0 + margin-left\n    assert td_1.position_x == 15  # 0 + border-spacing\n    assert td_3.position_x == 15\n    assert td_1.width == 60\n    assert td_2.width == 30\n    assert td_2.position_x == 85  # 15 + 60 + border-spacing\n    assert td_4.position_x == 85\n    assert td_3.width == 60\n    assert td_4.width == 30\n    assert table.width == 120  # 60 + 30 + 3 * border-spacing\n\n\n@assert_no_logs\ndef test_layout_table_auto_10():\n    page, = render_pages('''\n      <table style=\"border-spacing: 0; width: 14px; margin: 10px\">\n        <colgroup>\n          <col />\n          <col style=\"width: 6px\" />\n        </colgroup>\n        <tr>\n          <td><img src=pattern.png><img src=pattern.png></td>\n          <td style=\"width: 8px\"></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 10  # 0 + margin-left\n    assert td_1.position_x == 10\n    assert td_1.width == 6  # 14 - 8\n    assert td_2.position_x == 16  # 10 + 6\n    assert td_2.width == 8  # maximum of the minimum widths for the column\n    assert table.width == 14\n\n\n@assert_no_logs\ndef test_layout_table_auto_11():\n    page, = render_pages('''\n      <table style=\"border-spacing: 0\">\n        <tr>\n          <td style=\"width: 10px\"></td>\n          <td colspan=\"3\"></td>\n        </tr>\n        <tr>\n          <td colspan=\"2\" style=\"width: 22px\"></td>\n          <td style=\"width: 8px\"></td>\n          <td style=\"width: 8px\"></td>\n        </tr>\n        <tr>\n          <td></td>\n          <td></td>\n          <td colspan=\"2\"></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row1, row2, row3 = row_group.children\n    td_11, td_12 = row1.children\n    td_21, td_22, td_23 = row2.children\n    td_31, td_32, td_33 = row3.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 0\n    assert td_11.width == 10  # fixed\n    assert td_12.width == 28  # 38 - 10\n    assert td_21.width == 22  # fixed\n    assert td_22.width == 8  # fixed\n    assert td_23.width == 8  # fixed\n    assert td_31.width == 10  # same as first line\n    assert td_32.width == 12  # 22 - 10\n    assert td_33.width == 16  # 8 + 8 from second line\n    assert table.width == 38\n\n\n@assert_no_logs\ndef test_layout_table_auto_12():\n    page, = render_pages('''\n      <table style=\"border-spacing: 10px\">\n        <tr>\n          <td style=\"width: 10px\"></td>\n          <td colspan=\"3\"></td>\n        </tr>\n        <tr>\n          <td colspan=\"2\" style=\"width: 32px\"></td>\n          <td style=\"width: 8px\"></td>\n          <td style=\"width: 8px\"></td>\n        </tr>\n        <tr>\n          <td></td>\n          <td></td>\n          <td colspan=\"2\"></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row1, row2, row3 = row_group.children\n    td_11, td_12 = row1.children\n    td_21, td_22, td_23 = row2.children\n    td_31, td_32, td_33 = row3.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 0\n    assert td_11.width == 10  # fixed\n    assert td_12.width == 48  # 32 - 10 - sp + 2 * 8 + 2 * sp\n    assert td_21.width == 32  # fixed\n    assert td_22.width == 8  # fixed\n    assert td_23.width == 8  # fixed\n    assert td_31.width == 10  # same as first line\n    assert td_32.width == 12  # 32 - 10 - sp\n    assert td_33.width == 26  # 2 * 8 + sp\n    assert table.width == 88\n\n\n@assert_no_logs\ndef test_layout_table_auto_13():\n    # Regression test.\n    page, = render_pages('''\n      <table style=\"width: 30px\">\n        <tr>\n          <td colspan=2></td>\n          <td></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert td_1.width == 20  # 2 / 3 * 30\n    assert td_2.width == 10  # 1 / 3 * 30\n    assert table.width == 30\n\n\n@assert_no_logs\ndef test_layout_table_auto_14():\n    page, = render_pages('''\n      <table style=\"width: 20px\">\n        <col />\n        <col />\n        <tr>\n          <td></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, = row.children\n    assert td_1.width == 20\n    assert table.width == 20\n\n\n@assert_no_logs\ndef test_layout_table_auto_15():\n    page, = render_pages('''\n      <table style=\"width: 20px\">\n        <col />\n        <col />\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    column_group, = table.column_groups\n    column_1, column_2 = column_group.children\n    assert column_1.width == 0\n    assert column_2.width == 0\n\n\n@assert_no_logs\ndef test_layout_table_auto_16():\n    # Absolute table\n    page, = render_pages('''\n      <table style=\"width: 30px; position: absolute\">\n        <tr>\n          <td colspan=2></td>\n          <td></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert td_1.width == 20  # 2 / 3 * 30\n    assert td_2.width == 10  # 1 / 3 * 30\n    assert table.width == 30\n\n\n@assert_no_logs\ndef test_layout_table_auto_17():\n    # With border-collapse\n    page, = render_pages('''\n      <style>\n        /* Do not apply: */\n        colgroup, col, tbody, tr, td { margin: 1000px }\n      </style>\n      <table style=\"border-collapse: collapse; border: 10px solid;\n                    /* ignored with collapsed borders: */\n                    border-spacing: 10000px; padding: 1000px\">\n        <colgroup>\n          <col style=\"width: 30px\" />\n        </colgroup>\n        <tbody>\n          <tr>\n            <td style=\"padding: 2px\"></td>\n            <td style=\"width: 34px; padding: 10px; border: 2px solid\"></td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 0\n    assert table.border_left_width == 5  # half of the collapsed 10px border\n    assert td_1.position_x == 5  # border-spacing is ignored\n    assert td_1.margin_width() == 30  # as <col>\n    assert td_1.width == 20  # 30 - 5 (border-left) - 1 (border-right) - 2*2\n    assert td_2.position_x == 35\n    assert td_2.width == 34\n    assert td_2.margin_width() == 60  # 34 + 2*10 + 5 + 1\n    assert table.width == 90  # 30 + 60\n    assert table.margin_width() == 100  # 90 + 2*5 (border)\n\n\n@assert_no_logs\ndef test_layout_table_auto_18():\n    # Column widths as percentage\n    page, = render_pages('''\n      <table style=\"width: 200px\">\n        <colgroup>\n          <col style=\"width: 70%\" />\n          <col style=\"width: 30%\" />\n        </colgroup>\n        <tbody>\n          <tr>\n            <td>a</td>\n            <td>abc</td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert td_1.width == 140\n    assert td_2.width == 60\n    assert table.width == 200\n\n\n@assert_no_logs\ndef test_layout_table_auto_19():\n    # Column group width\n    page, = render_pages('''\n      <table style=\"width: 200px\">\n        <colgroup style=\"width: 100px\">\n          <col />\n          <col />\n        </colgroup>\n        <col style=\"width: 100px\" />\n        <tbody>\n          <tr>\n            <td>a</td>\n            <td>a</td>\n            <td>abc</td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2, td_3 = row.children\n    assert td_1.width == 100\n    assert td_2.width == 100\n    assert td_3.width == 100\n    assert table.width == 300\n\n\n@assert_no_logs\ndef test_layout_table_auto_20():\n    # Multiple column width\n    page, = render_pages('''\n      <table style=\"width: 200px\">\n        <colgroup>\n          <col style=\"width: 50px\" />\n          <col style=\"width: 30px\" />\n          <col />\n        </colgroup>\n        <tbody>\n          <tr>\n            <td>a</td>\n            <td>a</td>\n            <td>abc</td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2, td_3 = row.children\n    assert td_1.width == 50\n    assert td_2.width == 30\n    assert td_3.width == 120\n    assert table.width == 200\n\n\n@assert_no_logs\ndef test_layout_table_auto_21():\n    # Fixed-width table with column group with widths as percentages and pixels\n    page, = render_pages('''\n      <table style=\"width: 500px\">\n        <colgroup style=\"width: 100px\">\n          <col />\n          <col />\n        </colgroup>\n        <colgroup style=\"width: 30%\">\n          <col />\n          <col />\n        </colgroup>\n        <tbody>\n          <tr>\n            <td>a</td>\n            <td>a</td>\n            <td>abc</td>\n            <td>abc</td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2, td_3, td_4 = row.children\n    assert td_1.width == 100\n    assert td_2.width == 100\n    assert td_3.width == 150\n    assert td_4.width == 150\n    assert table.width == 500\n\n\n@assert_no_logs\ndef test_layout_table_auto_22():\n    # Auto-width table with column group with widths as percentages and pixels\n    page, = render_pages('''\n      <table>\n        <colgroup style=\"width: 10%\">\n          <col />\n          <col />\n        </colgroup>\n        <colgroup style=\"width: 200px\">\n          <col />\n          <col />\n        </colgroup>\n        <tbody>\n          <tr>\n            <td>a a</td>\n            <td>a b</td>\n            <td>a c</td>\n            <td>a d</td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2, td_3, td_4 = row.children\n    assert td_1.width == 50\n    assert td_2.width == 50\n    assert td_3.width == 200\n    assert td_4.width == 200\n    assert table.width == 500\n\n\n@assert_no_logs\ndef test_layout_table_auto_23():\n    # Wrong column group width\n    page, = render_pages('''\n      <table style=\"width: 200px\">\n        <colgroup style=\"width: 20%\">\n          <col />\n          <col />\n        </colgroup>\n        <tbody>\n          <tr>\n            <td>a</td>\n            <td>a</td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert td_1.width == 100\n    assert td_2.width == 100\n    assert table.width == 200\n\n\n@assert_no_logs\ndef test_layout_table_auto_24():\n    # Column width as percentage and cell width in pixels\n    page, = render_pages('''\n      <table style=\"width: 200px\">\n        <colgroup>\n          <col style=\"width: 70%\" />\n          <col />\n        </colgroup>\n        <tbody>\n          <tr>\n            <td>a</td>\n            <td style=\"width: 60px\">abc</td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert td_1.width == 140\n    assert td_2.width == 60\n    assert table.width == 200\n\n\n@assert_no_logs\ndef test_layout_table_auto_25():\n    # Column width and cell width as percentage\n    page, = render_pages('''\n      <div style=\"width: 400px\">\n        <table style=\"width: 50%\">\n          <colgroup>\n            <col style=\"width: 70%\" />\n            <col />\n          </colgroup>\n          <tbody>\n            <tr>\n              <td>a</td>\n              <td style=\"width: 30%\">abc</td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert td_1.width == 140\n    assert td_2.width == 60\n    assert table.width == 200\n\n\n@assert_no_logs\ndef test_layout_table_auto_26():\n    # Regression test for #307.\n    # Table with a cell larger than the table's max-width.\n    page, = render_pages('''\n      <table style=\"max-width: 300px\">\n        <td style=\"width: 400px\"></td>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_layout_table_auto_27():\n    # Table with a cell larger than the table's width.\n    page, = render_pages('''\n      <table style=\"width: 300px\">\n        <td style=\"width: 400px\"></td>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_layout_table_auto_28():\n    # Table with a cell larger than the table's width and max-width.\n    page, = render_pages('''\n      <table style=\"width: 300px; max-width: 350px\">\n        <td style=\"width: 400px\"></td>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_layout_table_auto_29():\n    # Table with a cell larger than the table's width and max-width.\n    page, = render_pages('''\n      <table style=\"width: 300px; max-width: 350px\">\n        <td style=\"padding: 50px\">\n          <div style=\"width: 300px\"></div>\n        </td>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_layout_table_auto_30():\n    # Table with a cell larger than the table's max-width.\n    page, = render_pages('''\n      <table style=\"max-width: 300px; margin: 100px\">\n        <td style=\"width: 400px\"></td>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_layout_table_auto_31():\n    # Test a table with column widths < table width < column width + spacing.\n    page, = render_pages('''\n      <table style=\"width: 300px; border-spacing: 2px\">\n        <td style=\"width: 299px\"></td>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_layout_table_auto_32():\n    # Table with a cell larger than the table's width.\n    page, = render_pages('''\n      <table style=\"width: 400px; margin: 100px\">\n        <td style=\"width: 500px\"></td>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    assert table_wrapper.margin_width() == 600  # 400 + 2 * 100\n\n\n@assert_no_logs\ndef test_layout_table_auto_33():\n    # Div with auto width containing a table with a min-width.\n    page, = render_pages('''\n      <div style=\"float: left\">\n        <table style=\"min-width: 400px; margin: 100px\">\n          <td></td>\n        </table>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    assert div.margin_width() == 600  # 400 + 2 * 100\n    assert table_wrapper.margin_width() == 600  # 400 + 2 * 100\n\n\n@assert_no_logs\ndef test_layout_table_auto_34():\n    # Div with auto width containing an empty table with a min-width.\n    page, = render_pages('''\n      <div style=\"float: left\">\n        <table style=\"min-width: 400px; margin: 100px\"></table>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    assert div.margin_width() == 600  # 400 + 2 * 100\n    assert table_wrapper.margin_width() == 600  # 400 + 2 * 100\n\n\n@assert_no_logs\ndef test_layout_table_auto_35():\n    # Div with auto width containing a table with a cell larger than the\n    # table's max-width.\n    page, = render_pages('''\n      <div style=\"float: left\">\n        <table style=\"max-width: 300px; margin: 100px\">\n          <td style=\"width: 400px\"></td>\n        </table>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    assert div.margin_width() == 600  # 400 + 2 * 100\n    assert table_wrapper.margin_width() == 600  # 400 + 2 * 100\n\n\n@assert_no_logs\ndef test_layout_table_auto_36():\n    # Regression test for #152.\n    page, = render_pages('''\n      <table>\n        <td style=\"width: 50%\">\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_layout_table_auto_37():\n    # Regression test for #305.\n    page, = render_pages('''\n      <table>\n        <tr>\n          <td>\n            <table>\n              <tr>\n                <th>Test</th>\n              </tr>\n              <tr>\n                <td style=\"min-width: 100%;\"></td>\n                <td style=\"width: 48px;\"></td>\n              </tr>\n            </table>\n          </td>\n        </tr>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_layout_table_auto_38():\n    page, = render_pages('''\n      <table>\n        <tr>\n          <td>\n            <table>\n              <tr>\n                <td style=\"width: 100%;\"></td>\n                <td style=\"width: 48px;\">\n                  <img src=\"icon.png\">\n                </td>\n              </tr>\n            </table>\n          </td>\n        </tr>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_layout_table_auto_39():\n    page, = render_pages('''\n      <table>\n        <tr>\n          <td>\n            <table style=\"display: inline-table\">\n              <tr>\n                <td style=\"width: 100%;\"></td>\n                <td></td>\n              </tr>\n            </table>\n          </td>\n        </tr>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_layout_table_auto_40():\n    # Regression test for #368.\n    # Check that white-space is used for the shrink-to-fit algorithm.\n    page, = render_pages('''\n      <table style=\"width: 0\">\n        <td style=\"font-family: weasyprint; white-space: nowrap\">a a</td>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    table, = table_wrapper.children\n    assert table.width == 16 * 3\n\n\n@assert_no_logs\ndef test_layout_table_auto_41():\n    # Cell width as percentage in auto-width table.\n    page, = render_pages('''\n      <div style=\"width: 100px\">\n        <table>\n          <tbody>\n            <tr>\n              <td>a a a a a a a a</td>\n              <td style=\"width: 30%\">a a a a a a a a</td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert td_1.width == 70\n    assert td_2.width == 30\n    assert table.width == 100\n\n\n@assert_no_logs\ndef test_layout_table_auto_42():\n    # Cell width as percentage in auto-width table.\n    page, = render_pages('''\n      <table style=\"font-family: weasyprint\">\n        <tbody>\n            <tr>\n              <td style=\"width: 70px\">aaa</td>\n              <td style=\"width: 25%\">aaa</td>\n            </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert td_2.width == 16 * 3  # Percentage column is set to max-width\n    assert td_1.width == (16 * 3) * 3  # Pixel column constraint is ignored\n    assert table.width == (16 * 3) * 4\n\n\n@assert_no_logs\ndef test_layout_table_auto_43():\n    # Cell width as percentage on colspan cell in auto-width table.\n    page, = render_pages('''\n      <div style=\"width: 100px\">\n        <table>\n          <tbody>\n            <tr>\n              <td>a a a a a a a a</td>\n              <td style=\"width: 30%\" colspan=2>a a a a a a a a</td>\n              <td>a a a a a a a a</td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2, td_3 = row.children\n    assert td_1.width == 35\n    assert td_2.width == 30\n    assert td_3.width == 35\n    assert table.width == 100\n\n\n@assert_no_logs\ndef test_layout_table_auto_44():\n    # Cells widths as percentages on normal and colspan cells.\n    page, = render_pages('''\n      <div style=\"width: 100px\">\n        <table>\n          <tbody>\n            <tr>\n              <td>a a a a a a a a</td>\n              <td style=\"width: 30%\" colspan=2>a a a a a a a a</td>\n              <td>a a a a a a a a</td>\n              <td style=\"width: 40%\">a a a a a a a a</td>\n              <td>a a a a a a a a</td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2, td_3, td_4, td_5 = row.children\n    assert td_1.width == 10\n    assert td_2.width == 30\n    assert td_3.width == 10\n    assert td_4.width == 40\n    assert td_5.width == 10\n    assert table.width == 100\n\n\n@assert_no_logs\ndef test_layout_table_auto_45():\n    # Cells widths as percentage on multiple lines.\n    page, = render_pages('''\n      <div style=\"width: 1000px\">\n        <table>\n          <tbody>\n            <tr>\n              <td>a a a a a a a a</td>\n              <td style=\"width: 30%\">a a a a a a a a</td>\n              <td>a a a a a a a a</td>\n              <td style=\"width: 40%\">a a a a a a a a</td>\n              <td>a a a a a a a a</td>\n            </tr>\n            <tr>\n              <td style=\"width: 31%\" colspan=2>a a a a a a a a</td>\n              <td>a a a a a a a a</td>\n              <td style=\"width: 42%\" colspan=2>a a a a a a a a</td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row_1, row_2 = row_group.children\n    td_11, td_12, td_13, td_14, td_15 = row_1.children\n    td_21, td_22, td_23 = row_2.children\n    assert td_11.width == 10  # 31% - 30%\n    assert td_12.width == 300  # 30%\n    assert td_13.width == 270  # 1000 - 31% - 42%\n    assert td_14.width == 400  # 40%\n    assert td_15.width == 20  # 42% - 2%\n    assert td_21.width == 310  # 31%\n    assert td_22.width == 270  # 1000 - 31% - 42%\n    assert td_23.width == 420  # 42%\n    assert table.width == 1000\n\n\n@assert_no_logs\ndef test_layout_table_auto_46():\n    page, = render_pages('''\n      <div style=\"position: absolute\">\n        <table style=\"margin: 50px; border: 20px solid black\">\n          <tr>\n            <td style=\"width: 200px; height: 200px\"></td>\n          </tr>\n        </table>\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td, = row.children\n    assert td.width == 200\n    assert table.width == 200\n    assert div.width == 340  # 200 + 2 * 50 + 2 * 20\n\n\n@assert_no_logs\ndef test_layout_table_auto_47():\n    # Regression test for #666.\n    page, = render_pages('''\n      <table style=\"font-family: weasyprint\">\n        <tr>\n          <td colspan=5>aaa</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td, = row.children\n    assert td.width == 48  # 3 * font-size\n\n\n@assert_no_logs\ndef test_layout_table_auto_48():\n    # Regression test for #685.\n    page, = render_pages('''\n      <table style=\"font-family: weasyprint; border-spacing: 100px;\n                    border-collapse: collapse\">\n        <tr>\n          <td colspan=5>aaa</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td, = row.children\n    assert td.width == 48  # 3 * font-size\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_layout_table_auto_49():\n    # Regression test for #685.\n    # See TODO in table_layout.group_layout.\n    page, = render_pages('''\n      <table style=\"font-family: weasyprint; border-spacing: 100px\">\n        <tr>\n          <td colspan=5>aaa</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td, = row.children\n    assert td.width == 48  # 3 * font-size\n\n\n@assert_no_logs\ndef test_layout_table_auto_50():\n    # Regression test for #685.\n    page, = render_pages('''\n      <table style=\"font-family: weasyprint; border-spacing: 5px\">\n       <tr><td>a</td><td>a</td><td>a</td><td>a</td><td>a</td></tr>\n       <tr>\n         <td colspan='5'>aaa aaa aaa aaa</td>\n       </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row_1, row_2 = row_group.children\n    for td in row_1.children:\n        assert td.width == 44  # (15 * font_size - 4 * sp) / 5\n    td_21, = row_2.children\n    assert td_21.width == 240  # 15 * font_size\n\n\n@assert_no_logs\ndef test_layout_table_auto_51():\n    # Regression test for #2174.\n    page, = render_pages('''\n      <table style=\"font-family: weasyprint; width: 100px\">\n        <tr>\n          <td style=\"width: 29.9999%\">a</td>\n          <td style=\"width: 70%\">a</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row_1, = row_group.children\n    td_1, td_2 = row_1.children\n    assert abs(td_1.width - 30) < 0.1\n    assert abs(td_2.width - 70) < 0.1\n\n\n@assert_no_logs\ndef test_layout_table_auto_52():\n    # Regression test for #2325.\n    page, = render_pages('''\n      <style>\n        @page { size: 20px }\n      </style>\n      <table style=\"font-family: weasyprint; border-spacing: 1px;\n                    font-size: 2px; line-height: 1\">\n        <tr>\n          <td><img src=pattern.png></td>\n          <td>\n            <span>foo</span>,\n            <span>foo</span>,\n            <span>foo</span>\n          </td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert table.width == 20\n    assert table_wrapper.position_x == 0\n    assert table.position_x == 0\n    assert td_1.position_x == 1  # spacing\n    assert td_1.width == 4  # image width\n    assert td_2.position_x == td_1.width + 2 * 1  # 2 * spacing\n    assert td_2.width == table.width - td_1.width - 3 * 1  # 3 * spacing\n    assert td_2.height == 3 * 2  # 3 lines * line height\n\n\n@assert_no_logs\n@pytest.mark.parametrize(\n    ('body_width', 'table_width', 'check_width', 'positions', 'widths'), [\n        ('500px', '230px', 220, [170, 5], [45, 155]),\n        ('530px', '100%', 520, [395, 5], [120, 380]),\n    ]\n)\ndef test_explicit_width_table_percent_rtl(body_width, table_width, check_width,\n                                          positions, widths):\n    page, = render_pages('''\n      <style>\n        body { width: %s }\n        table { width: %s; table-layout: fixed; direction: rtl;\n                border-collapse: collapse; font-size: 1px }\n        td, th { border: 10px solid }\n      </style>\n      <table style=\"\">\n        <col style=\"width: 25%%\"></col>\n        <col></col>\n        <tr>\n          <th>الاسم</th>\n          <th>العائلة</th>\n        </tr>\n        <tr>\n          <td>محمد يوسف</td>\n          <td>29</td>\n        </tr>\n      </table>\n    ''' % (body_width, table_width))\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    row_group, = table.children\n    row_1, row_2 = row_group.children\n\n    assert table.position_x == 0\n    assert table.width == check_width\n    assert [child.position_x for child in row_1.children] == positions\n    assert [child.position_x for child in row_2.children] == positions\n    assert [child.width for child in row_1.children] == widths\n    assert [child.width for child in row_2.children] == widths\n\n\n@assert_no_logs\ndef test_table_column_width_1():\n    source = '''\n      <style>\n        body { width: 20000px; margin: 0 }\n        table {\n          width: 10000px; margin: 0 auto; border-spacing: 100px 0;\n          table-layout: fixed\n        }\n        td { border: 10px solid; padding: 1px }\n      </style>\n      <table>\n        <col style=\"width: 10%\">\n        <tr>\n          <td style=\"width: 30%\" colspan=3>\n          <td>\n        </tr>\n        <tr>\n          <td>\n          <td>\n          <td>\n          <td>\n        </tr>\n        <tr>\n          <td>\n          <td colspan=12>This cell will be truncated to grid width\n          <td>This cell will be removed as it is beyond the grid width\n        </tr>\n      </table>\n    '''\n    with capture_logs() as logs:\n        page, = render_pages(source)\n    assert len(logs) == 1\n    assert logs[0].startswith('WARNING: This table row has more columns than '\n                              'the table, ignored 1 cell')\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    row_group, = table.children\n    first_row, second_row, third_row = row_group.children\n    cells = [first_row.children, second_row.children, third_row.children]\n    assert len(first_row.children) == 2\n    assert len(second_row.children) == 4\n    # Third cell here is completly removed.\n    assert len(third_row.children) == 2\n\n    assert body.position_x == 0\n    assert wrapper.position_x == 0\n    assert wrapper.margin_left == 5000\n    assert wrapper.content_box_x() == 5000  # auto margin-left\n    assert wrapper.width == 10000\n    assert table.position_x == 5000\n    assert table.width == 10000\n    assert row_group.position_x == 5100  # 5000 + border_spacing\n    assert row_group.width == 9800  # 10000 - 2*border-spacing\n    assert first_row.position_x == row_group.position_x\n    assert first_row.width == row_group.width\n\n    # This cell has colspan=3.\n    assert cells[0][0].position_x == 5100  # 5000 + border-spacing\n    # `width` on a cell sets the content width.\n    assert cells[0][0].width == 3000  # 30% of 10000px\n    assert cells[0][0].border_width() == 3022  # 3000 + borders + padding\n\n    # Second cell of the first line, but on the fourth and last column.\n    assert cells[0][1].position_x == 8222  # 5100 + 3022 + border-spacing\n    assert cells[0][1].border_width() == 6678  # 10000 - 3022 - 3*100\n    assert cells[0][1].width == 6656  # 6678 - borders - padding\n\n    assert cells[1][0].position_x == 5100  # 5000 + border-spacing\n    # `width` on a column sets the border width of cells.\n    assert cells[1][0].border_width() == 1000  # 10% of 10000px\n    assert cells[1][0].width == 978  # 1000 - borders - padding\n\n    assert cells[1][1].position_x == 6200  # 5100 + 1000 + border-spacing\n    assert cells[1][1].border_width() == 911  # (3022 - 1000 - 2*100) / 2\n    assert cells[1][1].width == 889  # 911 - borders - padding\n\n    assert cells[1][2].position_x == 7211  # 6200 + 911 + border-spacing\n    assert cells[1][2].border_width() == 911  # (3022 - 1000 - 2*100) / 2\n    assert cells[1][2].width == 889  # 911 - borders - padding\n\n    # Same as cells[0][1].\n    assert cells[1][3].position_x == 8222  # Also 7211 + 911 + border-spacing\n    assert cells[1][3].border_width() == 6678\n    assert cells[1][3].width == 6656\n\n    # Same as cells[1][0].\n    assert cells[2][0].position_x == 5100\n    assert cells[2][0].border_width() == 1000\n    assert cells[2][0].width == 978\n\n    assert cells[2][1].position_x == 6200  # Same as cells[1][1]\n    assert cells[2][1].border_width() == 8700  # 1000 - 1000 - 3*border-spacing\n    assert cells[2][1].width == 8678  # 8700 - borders - padding\n    assert cells[2][1].colspan == 3  # truncated to grid width\n\n\n@assert_no_logs\ndef test_table_column_width_2():\n    page, = render_pages('''\n      <style>\n        table { width: 1000px; border-spacing: 100px; table-layout: fixed }\n      </style>\n      <table>\n        <tr>\n          <td style=\"width: 50%\">\n          <td style=\"width: 60%\">\n          <td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    assert row.children[0].width == 500\n    assert row.children[1].width == 600\n    assert row.children[2].width == 0\n    assert table.width == 1500  # 500 + 600 + 4 * border-spacing\n\n\n@assert_no_logs\ndef test_table_column_width_3():\n    # Sum of columns width larger that the table width: increase the table width.\n    page, = render_pages('''\n      <style>\n        table { width: 1000px; border-spacing: 100px; table-layout: fixed }\n        td { width: 60% }\n      </style>\n      <table>\n        <tr>\n          <td>\n          <td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    cell_1, cell_2 = row.children\n    assert cell_1.width == 600  # 60% of 1000px\n    assert cell_2.width == 600\n    assert table.width == 1500  # 600 + 600 + 3*border-spacing\n    assert wrapper.width == table.width\n\n\n@assert_no_logs\ndef test_table_row_height_1():\n    page, = render_pages('''\n      <table style=\"width: 1000px; border-spacing: 0 100px;\n                    font: 20px/1em serif; margin: 3px; table-layout: fixed\">\n        <tr>\n          <td rowspan=0 style=\"height: 420px; vertical-align: top\"></td>\n          <td>X<br>X<br>X</td>\n          <td><table style=\"margin-top: 20px; border-spacing: 0\">\n            <tr><td>X</td></tr></table></td>\n          <td style=\"vertical-align: top\">X</td>\n          <td style=\"vertical-align: middle\">X</td>\n          <td style=\"vertical-align: bottom\">X</td>\n        </tr>\n        <tr>\n          <!-- cells with no text (no line boxes) is a corner case\n               in cell baselines -->\n          <td style=\"padding: 15px\"></td>\n          <td><div style=\"height: 10px\"></div></td>\n        </tr>\n        <tr></tr>\n        <tr>\n            <td style=\"vertical-align: bottom\"></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    row_group, = table.children\n\n    assert wrapper.position_y == 0\n    assert table.position_y == 3  # 0 + margin-top\n    assert table.height == 620  # sum of row heigths + 5*border-spacing\n    assert wrapper.height == table.height\n    assert row_group.position_y == 103  # 3 + border-spacing\n    assert row_group.height == 420  # 620 - 2*border-spacing\n    assert [row.height for row in row_group.children] == [\n        80, 30, 0, 10]\n    assert [row.position_y for row in row_group.children] == [\n        # cumulative sum of previous row heights and border-spacings\n        103, 283, 413, 513]\n    assert [[cell.height for cell in row.children]\n            for row in row_group.children] == [\n        [420, 60, 40, 20, 20, 20],\n        [0, 10],\n        [],\n        [0]\n    ]\n    assert [[cell.border_height() for cell in row.children]\n            for row in row_group.children] == [\n        [420, 80, 80, 80, 80, 80],\n        [30, 30],\n        [],\n        [10]\n    ]\n    # The baseline of the first row is at 40px because of the third column.\n    # The second column thus gets a top padding of 20px pushes the bottom\n    # to 80px.The middle is at 40px.\n    assert [[cell.padding_top for cell in row.children]\n            for row in row_group.children] == [\n        [0, 20, 0, 0, 30, 60],\n        [15, 5],\n        [],\n        [10]\n    ]\n    assert [[cell.padding_bottom for cell in row.children]\n            for row in row_group.children] == [\n        [0, 0, 40, 60, 30, 0],\n        [15, 15],\n        [],\n        [0]\n    ]\n    assert [[cell.position_y for cell in row.children]\n            for row in row_group.children] == [\n        [103, 103, 103, 103, 103, 103],\n        [283, 283],\n        [],\n        [513]\n    ]\n\n\n@assert_no_logs\ndef test_table_row_height_2():\n    # A cell box cannot extend beyond the last row box of a table.\n    page, = render_pages('''\n      <table style=\"border-spacing: 0\">\n        <tr style=\"height: 10px\">\n          <td rowspan=5></td>\n          <td></td>\n        </tr>\n        <tr style=\"height: 10px\">\n          <td></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    row_group, = table.children\n\n\n@assert_no_logs\ndef test_table_row_height_3():\n    # Regression test for #937.\n    page, = render_pages('''\n      <table style=\"border-spacing: 0; font-family: weasyprint;\n                    line-height: 20px\">\n        <tr><td>Table</td><td rowspan=\"2\"></td></tr>\n        <tr></tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    assert table.height == 20\n    row_group, = table.children\n    assert row_group.height == 20\n    row1, row2 = row_group.children\n    assert row1.height == 20\n    assert row2.height == 0\n\n\n@assert_no_logs\ndef test_table_row_height_4():\n    # A row cannot be shorter than the border-height of its tallest cell.\n    page, = render_pages('''\n      <table style=\"border-spacing: 0;\">\n        <tr style=\"height: 4px;\">\n          <td style=\"border: 1px solid; padding: 5px;\"></td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    row_group, = table.children\n    assert row_group.height == 12\n\n\n@assert_no_logs\ndef test_table_vertical_align(assert_pixels):\n    assert_pixels('''\n        rrrrrrrrrrrrrrrrrrrrrrrrrrrr\n        rBBBBBBBBBBBBBBBBBBBBBBBBBBr\n        rBrBB_BB_BB_BB_BBrrBBrrBB_Br\n        rBrBB_BB_BBrBBrBBrrBBrrBBrBr\n        rB_BBrBB_BBrBBrBBrrBBrrBBrBr\n        rB_BBrBB_BB_BB_BBrrBBrrBB_Br\n        rB_BB_BBrBB_BB_BB__BB__BB_Br\n        rB_BB_BBrBB_BB_BB__BB__BB_Br\n        rBBBBBBBBBBBBBBBBBBBBBBBBBBr\n        rrrrrrrrrrrrrrrrrrrrrrrrrrrr\n    ''', '''\n      <style>\n        @page { size: 28px 10px }\n        html { font-size: 1px; color: red }\n        body { margin: 0; width: 28px; height: 10px }\n        td {\n          width: 1em;\n          padding: 0 !important;\n          border: 1px solid blue;\n          line-height: 1em;\n          font-family: weasyprint;\n        }\n      </style>\n      <table style=\"border: 1px solid red; border-spacing: 0\">\n        <tr>\n          <!-- Test vertical-align: top, auto height -->\n          <td style=\"vertical-align: top\">o o</td>\n\n          <!-- Test vertical-align: middle, auto height -->\n          <td style=\"vertical-align: middle\">o o</td>\n\n          <!-- Test vertical-align: bottom, fixed useless height -->\n          <td style=\"vertical-align: bottom; height: 2em\">o o</td>\n\n          <!-- Test default vertical-align value (baseline),\n               fixed useless height -->\n          <td style=\"height: 5em\">o o</td>\n\n          <!-- Test vertical-align: baseline with baseline set by next cell,\n               auto height -->\n          <td style=\"vertical-align: baseline\">o o</td>\n\n          <!-- Set baseline height to 2px, auto height -->\n          <td style=\"vertical-align: baseline; font-size: 2em\">o o</td>\n\n          <!-- Test padding-bottom, fixed useless height,\n               set the height of the cells to 2 lines * 2em + 2px = 6px -->\n          <td style=\"vertical-align: baseline; height: 1em;\n                     font-size: 2em; padding-bottom: 2px !important\">\n            o o\n          </td>\n\n          <!-- Test padding-top, auto height -->\n          <td style=\"vertical-align: top; padding-top: 1em !important\">\n            o o\n          </td>\n        </tr>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_table_vertical_align_float():\n    # Regression test for #2216 and #2293.\n    page, = render_pages('''\n      <style>\n        @page { size: 100px 400px }\n        td { width: 50px; height: 100px; line-height: 0 }\n      </style>\n      <table>\n        <tr>\n          <td style=\"vertical-align: middle\">\n            <div style=\"float: left; height: 20px; width: 20px\"></div>\n            <div style=\"display: inline-block; height: 30px; width: 30px\"></div>\n          </td>\n          <td style=\"vertical-align: bottom\">\n            <div style=\"display: inline-block; height: 30px; width: 30px\"></div>\n            <div style=\"float: right; height: 20px; width: 20px\"></div>\n          </td>\n        </tr>\n        <tr>\n          <td style=\"vertical-align: middle\">\n            <div style=\"float: left; height: 20px; width: 20px\"></div>\n            <div style=\"display: inline-block; height: 10px; width: 10px\"></div>\n          </td>\n          <td style=\"vertical-align: bottom\">\n            <div style=\"display: inline-block; height: 10px; width: 10px\"></div>\n            <div style=\"float: right; height: 20px; width: 20px\"></div>\n          </td>\n        </tr>\n        <tr>\n          <td style=\"vertical-align: middle\">\n            <div style=\"display: inline-block; height: 10px; width: 10px\"></div>\n          </td>\n          <td style=\"vertical-align: bottom\">\n            <div style=\"display: inline-block; height: 10px; width: 10px\"></div>\n          </td>\n        </tr>\n        <tr>\n          <td style=\"vertical-align: middle\">\n            <div style=\"float: left; height: 20px; width: 20px\"></div>\n          </td>\n          <td style=\"vertical-align: bottom\">\n            <div style=\"float: right; height: 20px; width: 20px\"></div>\n          </td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    table, = wrapper.children\n    row_group, = table.children\n    row_1, row_2, row_3, row_4, = row_group.children\n\n    td_1, td_2 = row_1.children\n    div_1, line_wrapper = td_1.children\n    assert div_1.position_x == 0\n    assert div_1.position_y == (100 - 30) / 2\n    div_2 = line_wrapper.children[0].children[0]\n    assert div_2.position_x == 20\n    assert div_2.position_y == (100 - 30) / 2\n    line_box, = td_2.children\n    div_1, _, div_2 = line_box.children\n    assert div_1.position_x == 50\n    assert div_1.position_y == 100 - 30\n    assert div_2.position_x == 80\n    assert div_2.position_y == 100 - 30\n\n    td_1, td_2 = row_2.children\n    div_1, line_wrapper = td_1.children\n    assert div_1.position_x == 0\n    assert div_1.position_y == 100 + (100 - 20) / 2\n    div_2 = line_wrapper.children[0].children[0]\n    assert div_2.position_x == 20\n    assert div_2.position_y == 100 + (100 - 20) / 2\n    line_box, = td_2.children\n    div_1, _, div_2 = line_box.children\n    assert div_1.position_x == 50\n    assert div_1.position_y == 100 + (100 - 20)\n    assert div_2.position_x == 80\n    assert div_2.position_y == 100 + (100 - 20)\n\n    td_1, td_2 = row_3.children\n    line_box, = td_1.children\n    div, _ = line_box.children\n    assert div.position_x == 0\n    assert div.position_y == 200 + (100 - 10) / 2\n    div, = td_2.children\n    assert div.position_x == 50\n    assert div.position_y == 200 + (100 - 10)\n\n    td_1, td_2 = row_4.children\n    div, = td_1.children\n    assert div.position_x == 0\n    assert div.position_y == 300 + (100 - 20) / 2\n    div, = td_2.children\n    assert div.position_x == 80\n    assert div.position_y == 300 + (100 - 20)\n\n\n@assert_no_logs\ndef test_table_wrapper():\n    page, = render_pages('''\n      <style>\n        @page { size: 1000px }\n        table { width: 600px; height: 500px; table-layout: fixed;\n                  padding: 1px; border: 10px solid; margin: 100px; }\n      </style>\n      <table></table>\n    ''')\n    html, = page.children\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    assert body.width == 1000\n    assert wrapper.width == 600  # not counting borders or padding\n    assert wrapper.margin_left == 100\n    assert table.margin_width() == 600\n    assert table.width == 578  # 600 - 2*10 - 2*1, no margin\n    # box-sizing in the UA stylesheet makes `height: 500px` set this.\n    assert table.border_height() == 500\n    assert table.height == 478  # 500 - 2*10 - 2*1\n    assert table.margin_height() == 500  # no margin\n    assert wrapper.height == 500\n    assert wrapper.margin_height() == 700  # 500 + 2*100\n\n\n@assert_no_logs\ndef test_table_html_tag():\n    # Regression test.\n    page, = render_pages('<html style=\"display: table\">')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('html', 'rows', 'positions'), [\n    ('''\n      <style>\n        @page { size: 120px }\n        table { table-layout: fixed; width: 100% }\n        h1 { height: 30px }\n        td { height: 40px }\n      </style>\n      <h1>Dummy title</h1>\n      <table>\n        <tr><td>row 1</td></tr>\n        <tr><td>row 2</td></tr>\n\n        <tr><td>row 3</td></tr>\n        <tr><td>row 4</td></tr>\n        <tr><td>row 5</td></tr>\n\n        <tr><td style=\"height: 300px\"> <!-- overflow the page -->\n          row 6</td></tr>\n        <tr><td>row 7</td></tr>\n        <tr><td>row 8</td></tr>\n      </table>\n     ''',\n     [2, 3, 1, 2],\n     [30, 70, 0, 40, 80, 0, 0, 40]),\n    ('''\n      <style>\n        @page { size: 120px }\n        h1 { height: 30px}\n        td { height: 40px }\n        table { table-layout: fixed; width: 100%;\n                page-break-inside: avoid }\n      </style>\n      <h1>Dummy title</h1>\n      <table>\n        <tr><td>row 1</td></tr>\n        <tr><td>row 2</td></tr>\n        <tr><td>row 3</td></tr>\n        <tr><td>row 4</td></tr>\n     </table>\n     ''',\n     [0, 3, 1],\n     [0, 40, 80, 0]),\n    ('''\n      <style>\n        @page { size: 120px }\n        h1 { height: 30px}\n        td { height: 40px }\n        table { table-layout: fixed; width: 100%;\n                page-break-inside: avoid }\n      </style>\n      <h1>Dummy title</h1>\n      <table>\n        <tbody>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n\n        <tr><td>row 4</td></tr>\n      </table>\n     ''',\n     [0, 3, 1],\n     [0, 40, 80, 0]),\n    ('''\n      <style>\n        @page { size: 120px }\n        h1 { height: 30px}\n        td { height: 40px }\n        table { table-layout: fixed; width: 100% }\n      </style>\n      <h1>Dummy title</h1>\n      <table>\n        <tr><td>row 1</td></tr>\n\n        <tbody style=\"page-break-inside: avoid\">\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [1, 2],\n     [30, 0, 40]),\n    ('''\n      <style>\n        @page { size: 120px }\n        h1 { height: 30px}\n        td { line-height: 40px }\n        table { table-layout: fixed; width: 100% }\n      </style>\n      <h1>Dummy title</h1>\n      <table>\n        <tr><td>r1l1</td></tr>\n        <tr style=\"break-inside: avoid\"><td>r2l1<br>r2l2</td></tr>\n        <tr><td>r3l1</td></tr>\n      </table>\n     ''',\n     [1, 2],\n     [30, 0, 80]),\n    ('''\n      <style>\n        @page { size: 120px }\n        h1 { height: 30px}\n        td { line-height: 40px }\n        table { table-layout: fixed; width: 100% }\n      </style>\n      <h1>Dummy title</h1>\n      <table>\n        <tbody>\n          <tr><td>r1l1</td></tr>\n          <tr style=\"break-inside: avoid\"><td>r2l1<br>r2l2</td></tr>\n        </tbody>\n        <tr><td>r3l1</td></tr>\n      </table>\n     ''',\n     [1, 2],\n     [30, 0, 80]),\n    ('''\n      <style>\n        @page { size: 100px }\n        h1 { height: 30px }\n        td { line-height: 40px }\n        table { table-layout: fixed; width: 100% }\n        thead { display: table-row-group; break-after: avoid }\n        thead tr { break-after: avoid }\n      </style>\n      <h1>Dummy title</h1>\n      <table>\n        <thead>\n          <tr><td>r1l1</td></tr>\n        </thead>\n        <tbody>\n          <tr><td>r2l1</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [0, 2],\n     [0, 40]),\n])\ndef test_table_page_breaks(html, rows, positions):\n    pages = render_pages(html)\n    rows_per_page = []\n    rows_position_y = []\n    for i, page in enumerate(pages):\n        html, = page.children\n        body, = html.children\n        if i == 0:\n            body_children = body.children[1:]  # skip h1\n        else:\n            body_children = body.children\n        if not body_children:\n            rows_per_page.append(0)\n            continue\n        table_wrapper, = body_children\n        table, = table_wrapper.children\n        rows_in_this_page = 0\n        for group in table.children:\n            assert group.children, 'found an empty table group'\n            for row in group.children:\n                rows_in_this_page += 1\n                rows_position_y.append(row.position_y)\n                cell, = row.children\n        rows_per_page.append(rows_in_this_page)\n\n    assert rows_per_page == rows\n    assert rows_position_y == positions\n\n\n@assert_no_logs\ndef test_table_page_breaks_in_cell():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 120px }\n        h1 { height: 30px}\n        td { line-height: 40px }\n        table { table-layout: fixed; width: 100% }\n      </style>\n      <h1>Dummy title</h1>\n      <table>\n        <tr><td>r1c1l1</td><td>r1c2l1</td></tr>\n        <tr><td>r2c1l1</td><td style=\"break-inside: avoid\">r2c2l1<br>r2c2l2</td></tr>\n        <tr><td>r3c1l1</td><td>r3l1</td></tr>\n      </table>\n    ''')\n    html, = page1.children\n    body, = html.children\n    h1, table_wrapper = body.children\n    table, = table_wrapper.children\n    group, = table.children\n    row, = group.children\n    assert len(row.children) == 2\n\n    html, = page2.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    group, = table.children\n    row1, row2 = group.children\n    assert len(row1.children) == len(row2.children) == 2\n\n\n@assert_no_logs\ndef test_table_page_breaks_complex_1():\n    pages = render_pages('''\n      <style>\n        @page { size: 100px }\n      </style>\n      <h1 style=\"margin: 0; height: 30px\">Lipsum</h1>\n      <!-- Leave 70px on the first page: enough for the header or row1\n           but not both.  -->\n      <table style=\"border-spacing: 0; font-size: 5px\">\n        <thead>\n          <tr><td style=\"height: 20px\">Header</td></tr>\n        </thead>\n        <tbody>\n          <tr><td style=\"height: 60px\">Row 1</td></tr>\n          <tr><td style=\"height: 10px\">Row 2</td></tr>\n          <tr><td style=\"height: 50px\">Row 3</td></tr>\n          <tr><td style=\"height: 61px\">Row 4</td></tr>\n          <tr><td style=\"height: 90px\">Row 5</td></tr>\n        </tbody>\n        <tfoot>\n          <tr><td style=\"height: 20px\">Footer</td></tr>\n        </tfoot>\n      </table>\n    ''')\n    rows_per_page = []\n    for i, page in enumerate(pages):\n        groups = []\n        html, = page.children\n        body, = html.children\n        table_wrapper, = body.children\n        if i == 0:\n            assert table_wrapper.element_tag == 'h1'\n        else:\n            table, = table_wrapper.children\n            for group in table.children:\n                assert group.children, 'found an empty table group'\n                rows = []\n                for row in group.children:\n                    cell, = row.children\n                    line, = cell.children\n                    text, = line.children\n                    rows.append(text.text)\n                groups.append(rows)\n        rows_per_page.append(groups)\n    assert rows_per_page == [\n        [],\n        [['Header'], ['Row 1'], ['Footer']],\n        [['Header'], ['Row 2', 'Row 3'], ['Footer']],\n        [['Header'], ['Row 4']],\n        [['Row 5']]\n    ]\n\n\n@assert_no_logs\ndef test_table_page_breaks_complex_2():\n    pages = render_pages('''\n      <style>\n        @page { size: 250px }\n        td { height: 40px }\n        table { table-layout: fixed; width: 100%; break-before: avoid }\n      </style>\n      <table>\n        <thead>\n          <tr><td>head 1</td></tr>\n        </thead>\n        <tbody>\n          <tr><td>row 1 1</td></tr>\n          <tr><td>row 1 2</td></tr>\n          <tr><td>row 1 3</td></tr>\n        </tbody>\n        <tfoot>\n          <tr><td>foot 1</td></tr>\n        </tfoot>\n      </table>\n      <table>\n        <thead>\n          <tr><td>head 2</td></tr>\n        </thead>\n        <tbody>\n          <tr><td>row 2 1</td></tr>\n          <tr><td>row 2 2</td></tr>\n          <tr><td>row 2 3</td></tr>\n        </tbody>\n        <tfoot>\n          <tr><td>foot 2</td></tr>\n        </tfoot>\n      </table>\n     ''')\n    rows_per_page = []\n    for i, page in enumerate(pages):\n        groups = []\n        html, = page.children\n        body, = html.children\n        for table_wrapper in body.children:\n            table, = table_wrapper.children\n            for group in table.children:\n                assert group.children, 'found an empty table group'\n                rows = []\n                for row in group.children:\n                    cell, = row.children\n                    line, = cell.children\n                    text, = line.children\n                    rows.append(text.text)\n                groups.append(rows)\n        rows_per_page.append(groups)\n    assert rows_per_page == [\n        [['head 1'], ['row 1 1', 'row 1 2'], ['foot 1']],\n        [['head 1'], ['row 1 3'], ['foot 1'],\n         ['head 2'], ['row 2 1'], ['foot 2']],\n        [['head 2'], ['row 2 2', 'row 2 3'], ['foot 2']],\n    ]\n    # TODO: test positions, the place of footer on the first page is wrong.\n\n\n@assert_no_logs\ndef test_table_page_break_after():\n    page1, page2, page3, page4, page5, page6 = render_pages('''\n      <style>\n        @page { size: 1000px }\n        h1 { height: 30px}\n        td { height: 40px }\n        table { table-layout: fixed; width: 100% }\n      </style>\n      <h1>Dummy title</h1>\n      <table>\n\n        <tbody>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n        <tbody>\n          <tr style=\"break-after: page\"><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n        <tbody>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr style=\"break-after: page\"><td>row 3</td></tr>\n        </tbody>\n        <tbody style=\"break-after: right\">\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n        <tbody style=\"break-after: page\">\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n\n      </table>\n      <p>bla bla</p>\n     ''')\n    html, = page1.children\n    body, = html.children\n    h1, table_wrapper = body.children\n    table, = table_wrapper.children\n    table_group1, table_group2 = table.children\n    assert len(table_group1.children) == 3\n    assert len(table_group2.children) == 1\n\n    html, = page2.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    table_group1, table_group2 = table.children\n    assert len(table_group1.children) == 2\n    assert len(table_group2.children) == 3\n\n    html, = page3.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    table_group, = table.children\n    assert len(table_group.children) == 3\n\n    html, = page4.children\n    assert not html.children\n\n    html, = page5.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    table_group, = table.children\n    assert len(table_group.children) == 3\n\n    html, = page6.children\n    body, = html.children\n    p, = body.children\n    assert p.element_tag == 'p'\n\n\n@assert_no_logs\ndef test_table_page_break_before():\n    page1, page2, page3, page4, page5, page6 = render_pages('''\n      <style>\n        @page { size: 1000px }\n        h1 { height: 30px}\n        td { height: 40px }\n        table { table-layout: fixed; width: 100% }\n      </style>\n      <h1>Dummy title</h1>\n      <table>\n\n        <tbody>\n          <tr style=\"break-before: page\"><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n        <tbody>\n          <tr><td>row 1</td></tr>\n          <tr style=\"break-before: page\"><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n        <tbody>\n          <tr style=\"break-before: page\"><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n        <tbody>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n        <tbody style=\"break-before: left\">\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n\n      </table>\n      <p>bla bla</p>\n     ''')\n    html, = page1.children\n    body, = html.children\n    h1, = body.children\n    assert h1.element_tag == 'h1'\n\n    html, = page2.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    table_group1, table_group2 = table.children\n    assert len(table_group1.children) == 3\n    assert len(table_group2.children) == 1\n\n    html, = page3.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    table_group, = table.children\n    assert len(table_group.children) == 2\n\n    html, = page4.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    table_group1, table_group2 = table.children\n    assert len(table_group1.children) == 3\n    assert len(table_group2.children) == 3\n\n    html, = page5.children\n    assert not html.children\n\n    html, = page6.children\n    body, = html.children\n    table_wrapper, p = body.children\n    table, = table_wrapper.children\n    table_group, = table.children\n    assert len(table_group.children) == 3\n    assert p.element_tag == 'p'\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('html', 'rows'), [\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 26px }\n      </style>\n      <table>\n        <tbody>\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr style=\"break-before: avoid\"><td>row 2</td></tr>\n          <tr style=\"break-before: avoid\"><td>row 3</td></tr>\n        </tbody>\n      </table>\n    ''',\n     [1, 3]),\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 26px }\n      </style>\n      <table>\n        <tbody>\n          <tr><td>row 0</td></tr>\n          <tr style=\"break-after: avoid\"><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n          <tr style=\"break-before: avoid\"><td>row 3</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [1, 3]),\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 26px }\n      </style>\n      <table>\n        <tbody>\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr style=\"break-after: avoid\"><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [2, 2]),\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 26px }\n      </style>\n      <table>\n        <tbody>\n          <tr style=\"break-before: avoid\"><td>row 0</td></tr>\n          <tr style=\"break-before: avoid\"><td>row 1</td></tr>\n          <tr style=\"break-before: avoid\"><td>row 2</td></tr>\n          <tr style=\"break-before: avoid\"><td>row 3</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [3, 1]),\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 26px }\n      </style>\n      <table>\n        <tbody>\n          <tr style=\"break-after: avoid\"><td>row 0</td></tr>\n          <tr style=\"break-after: avoid\"><td>row 1</td></tr>\n          <tr style=\"break-after: avoid\"><td>row 2</td></tr>\n          <tr style=\"break-after: avoid\"><td>row 3</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [3, 1]),\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 26px }\n        p { height: 26px }\n      </style>\n      <p>wow p</p>\n      <table>\n        <tbody>\n          <tr style=\"break-after: avoid\"><td>row 0</td></tr>\n          <tr style=\"break-after: avoid\"><td>row 1</td></tr>\n          <tr style=\"break-after: avoid\"><td>row 2</td></tr>\n          <tr style=\"break-after: avoid\"><td>row 3</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [1, 3, 1]),\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 30px }\n      </style>\n      <table>\n        <tbody style=\"break-after: avoid\">\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n        </tbody>\n        <tbody>\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [2, 3, 1]),\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 30px }\n      </style>\n      <table>\n        <tbody>\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n        </tbody>\n        <tbody style=\"break-before: avoid\">\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [2, 3, 1]),\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 30px }\n      </style>\n      <table>\n        <tbody>\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n        </tbody>\n        <tbody>\n          <tr style=\"break-before: avoid\"><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [2, 3, 1]),\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 30px }\n      </style>\n      <table>\n        <tbody>\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr style=\"break-after: avoid\"><td>row 2</td></tr>\n        </tbody>\n        <tbody>\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [2, 3, 1]),\n    ('''\n      <style>\n        @page { size: 100px }\n        table { table-layout: fixed; width: 100% }\n        tr { height: 30px }\n      </style>\n      <table>\n        <tbody style=\"break-after: avoid\">\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr style=\"break-after: page\"><td>row 2</td></tr>\n        </tbody>\n        <tbody>\n          <tr><td>row 0</td></tr>\n          <tr><td>row 1</td></tr>\n          <tr><td>row 2</td></tr>\n        </tbody>\n      </table>\n     ''',\n     [3, 3]),\n])\ndef test_table_page_break_avoid(html, rows):\n    pages = render_pages(html)\n    assert len(pages) == len(rows)\n    rows_per_page = []\n    for page in pages:\n        html, = page.children\n        body, = html.children\n        if body.children[0].element_tag == 'p':\n            rows_per_page.append(len(body.children))\n            continue\n        else:\n            table_wrapper, = body.children\n        table, = table_wrapper.children\n        rows_in_this_page = 0\n        for group in table.children:\n            for row in group.children:\n                rows_in_this_page += 1\n        rows_per_page.append(rows_in_this_page)\n\n    assert rows_per_page == rows\n\n\n@assert_no_logs\ndef test_table_page_break_avoid_before_table():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 100px }\n        h1 { height: 30px }\n        td, p { font: 20px / 1 weasyprint }\n        tr { break-after: avoid }\n        table { table-layout: fixed; width: 100%; break-before: avoid }\n      </style>\n      <h1>Dummy title</h1>\n      <p>paragraph</p>\n      <table>\n        <tbody>\n          <tr><td>row 1</td></tr>\n        </tbody>\n        <tbody>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n      </table>\n     ''')\n    html, = page1.children\n    body, = html.children\n    h1, = body.children\n    assert h1.element_tag == 'h1'\n\n    html, = page2.children\n    body, = html.children\n    p, table_wrapper = body.children\n    assert p.element_tag == 'p'\n    assert table_wrapper.element_tag == 'table'\n\n\n@assert_no_logs\ndef test_table_page_break_avoid_before_tbody():\n    page1, page2 = render_pages('''\n      <style>\n        @page { size: 100px }\n        h1 { height: 30px }\n        td, p { height: 20px }\n        tr { break-after: avoid }\n        tbody { break-before: avoid }\n      </style>\n      <h1>Dummy title</h1>\n      <p>paragraph</p>\n      <table>\n        <tbody>\n          <tr><td>row 1</td></tr>\n        </tbody>\n        <tbody>\n          <tr><td>row 2</td></tr>\n          <tr><td>row 3</td></tr>\n        </tbody>\n      </table>\n     ''')\n    html, = page1.children\n    body, = html.children\n    h1, = body.children\n    assert h1.element_tag == 'h1'\n\n    html, = page2.children\n    body, = html.children\n    p, table_wrapper = body.children\n    assert p.element_tag == 'p'\n    assert table_wrapper.element_tag == 'table'\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('vertical_align', 'table_position_y'), [\n    ('top', 8),\n    ('bottom', 8),\n    ('baseline', 10),\n])\ndef test_inline_table_baseline(vertical_align, table_position_y):\n    # Check that inline table's baseline is its first row's baseline.\n    #\n    # Div text's baseline is at 18px from the top (10px because of the\n    # line-height, 8px as it's weasyprint.otf's baseline position).\n    #\n    # When a row has vertical-align: baseline cells, its baseline is its cell's\n    # baseline. The position of the table is thus 10px above the text's\n    # baseline.\n    #\n    # When a row has another value for vertical-align, the baseline is the\n    # bottom of the row. The first cell's text is aligned with the div's text,\n    # and the top of the table is thus 8px above the baseline.\n    page, = render_pages('''\n      <div style=\"font-family: weasyprint; font-size: 10px; line-height: 30px\">\n        abc\n        <table style=\"display: inline-table; border-collapse: collapse;\n                      line-height: 10px\">\n          <tr><td style=\"vertical-align: %s\">a</td></tr>\n          <tr><td>a</td></tr>\n        </table>\n        abc\n      </div>\n    ''' % vertical_align)\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line, = div.children\n    text1, table_wrapper, text2 = line.children\n    table, = table_wrapper.children\n    assert text1.position_y == text2.position_y == 0\n    assert table.height == 10 * 2\n    assert abs(table.position_y - table_position_y) < 0.1\n\n\n@assert_no_logs\ndef test_table_caption_margin_top():\n    page, = render_pages('''\n      <style>\n        table { margin: 20px; }\n        caption, h1, h2 { margin: 20px; height: 10px }\n        td { height: 10px }\n      </style>\n      <h1></h1>\n      <table>\n        <caption></caption>\n        <tr>\n          <td></td>\n        </tr>\n      </table>\n      <h2></h2>\n    ''')\n    html, = page.children\n    body, = html.children\n    h1, wrapper, h2 = body.children\n    caption, table = wrapper.children\n    tbody, = table.children\n    assert (h1.content_box_x(), h1.content_box_y()) == (20, 20)\n    assert (wrapper.content_box_x(), wrapper.content_box_y()) == (20, 50)\n    assert (caption.content_box_x(), caption.content_box_y()) == (40, 70)\n    assert (tbody.content_box_x(), tbody.content_box_y()) == (20, 100)\n    assert (h2.content_box_x(), h2.content_box_y()) == (20, 130)\n\n\n@assert_no_logs\ndef test_table_caption_margin_bottom():\n    page, = render_pages('''\n      <style>\n        table { margin: 20px; }\n        caption, h1, h2 { margin: 20px; height: 10px; caption-side: bottom }\n        td { height: 10px }\n      </style>\n      <h1></h1>\n      <table>\n        <caption></caption>\n        <tr>\n          <td></td>\n        </tr>\n      </table>\n      <h2></h2>\n    ''')\n    html, = page.children\n    body, = html.children\n    h1, wrapper, h2 = body.children\n    table, caption = wrapper.children\n    tbody, = table.children\n    assert (h1.content_box_x(), h1.content_box_y()) == (20, 20)\n    assert (wrapper.content_box_x(), wrapper.content_box_y()) == (20, 50)\n    assert (tbody.content_box_x(), tbody.content_box_y()) == (20, 50)\n    assert (caption.content_box_x(), caption.content_box_y()) == (40, 80)\n    assert (h2.content_box_x(), h2.content_box_y()) == (20, 130)\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('rows_expected', 'thead', 'tfoot', 'content'), [\n    ([[], ['Header', 'Footer']], 45, 45, '<p>content</p>'),\n    ([[], ['Header', 'Footer']], 85, 5, '<p>content</p>'),\n    ([['Header', 'Footer']], 30, 30, '<p>content</p>'),\n    ([[], ['Header']], 30, 110, '<p>content</p>'),\n    ([[], ['Header', 'Footer']], 30, 60, '<p>content</p>'),\n    ([[], ['Footer']], 110, 30, '<p>content</p>'),\n\n    # We try to render the header and footer on the same page, but it does not\n    # fit. So we try to render the header or the footer on the next one, but\n    # nothing fit either.\n    ([[], []], 110, 110, '<p>content</p>'),\n\n    ([['Header', 'Footer']], 30, 30, ''),\n    ([['Header']], 30, 110, ''),\n    ([['Header', 'Footer']], 30, 60, ''),\n    ([['Footer']], 110, 30, ''),\n    ([[]], 110, 110, ''),\n])\ndef test_table_empty_body(rows_expected, thead, tfoot, content):\n    html = '''\n      <style>\n        @page { size: 100px }\n        p { height: 20px }\n        thead th { height: %spx }\n        tfoot th { height: %spx }\n      </style>\n      %s\n      <table>\n        <thead><tr><th>Header</th></tr></thead>\n        <tfoot><tr><th>Footer</th></tr></tfoot>\n      </table>\n    ''' % (thead, tfoot, content)\n    pages = render_pages(html)\n    assert len(pages) == len(rows_expected)\n    for i, page in enumerate(pages):\n        rows = []\n        html, = page.children\n        body, = html.children\n        table_wrapper = body.children[-1]\n        if not table_wrapper.is_table_wrapper:\n            assert rows == rows_expected[i]\n            continue\n        table, = table_wrapper.children\n        for group in table.children:\n            for row in group.children:\n                cell, = row.children\n                line, = cell.children\n                text, = line.children\n                rows.append(text.text)\n        assert rows == rows_expected[i]\n\n\ndef test_table_group_break_inside_avoid_absolute():\n    # Regression test for #2134.\n    html = '''\n      <style>\n        @page { size: 5cm }\n        tbody { break-inside: avoid; line-height: 2cm }\n        div { position: absolute }\n      </style>\n      <table>\n        <tbody><tr><td>a</td></tr></tbody>\n        <tbody><tr>\n          <td><div>a<br>b</div></td>\n          <td>a<br>b</td>\n        </tr></tbody>\n      </table>\n    '''\n    page1, page2 = render_pages(html)\n\n    html, = page1.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    group, = table.children  # Only first group\n\n    html, = page2.children\n    body, = html.children  # No absolute div here\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    group, = table.children  # Only second group\n\n\ndef test_table_row_break_inside_avoid_absolute():\n    # Regression test for #2134.\n    html = '''\n      <style>\n        @page { size: 5cm }\n        tr { break-inside: avoid; line-height: 2cm }\n        div { position: absolute }\n      </style>\n      <table>\n        <tr><td>a</td></tr>\n        <tr>\n          <td><div>a<br>b</div></td>\n          <td>a<br>b</td>\n        </tr>\n      </table>\n    '''\n    page1, page2 = render_pages(html)\n\n    html, = page1.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    group, = table.children\n    row, = group.children  # Only first row\n\n    html, = page2.children\n    body, = html.children  # No absolute div here\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    group, = table.children\n    row, = group.children  # Only second row\n\n\ndef test_table_break_inside_avoid_absolute():\n    # Regression test for #2134.\n    html = '''\n      <style>\n        @page { size: 5cm }\n        body { line-height: 2cm }\n        table { break-inside: avoid }\n        div { position: absolute }\n      </style>\n      <p>text</p>\n      <table>\n        <tr>\n          <td><div>a<br>b</div></td>\n          <td>a<br>b</td>\n        </tr>\n      </table>\n    '''\n    page1, page2 = render_pages(html)\n\n    html, = page1.children\n    body, = html.children\n    p, = body.children\n    line, = p.children\n    text, = line.children\n    assert text.text == 'text'\n\n    html, = page2.children\n    body, = html.children  # No absolute div here\n\n\ndef test_table_break_children_margin():\n    # Regression test for #1254.\n    html = '''\n      <style>\n        @page { size: 100px }\n        p { height: 20px; margin-top: 50px }\n      </style>\n      <table>\n        <tr><td><p>Page1</p></td></tr>\n        <tr><td><p>Page2</p></td></tr>\n        <tr><td><p>Page3</p></td></tr>\n      </table>\n    '''\n    assert len(render_pages(html)) == 3\n\n\ndef test_table_td_break_inside_avoid():\n    # Regression test for #1547.\n    html = '''\n      <style>\n        @page { size: 4cm }\n        td { break-inside: avoid; line-height: 3cm }\n      </style>\n      <table>\n        <tr>\n          <td>\n            a<br>a\n          </td>\n        </tr>\n      </table>\n    '''\n    assert len(render_pages(html)) == 2\n\n\n@assert_no_logs\ndef test_table_bad_int_td_th_span():\n    page, = render_pages('''\n      <table>\n        <tr>\n          <td colspan=\"bad\"></td>\n          <td rowspan=\"23.4\"></td>\n        </tr>\n        <tr>\n          <th colspan=\"x\" rowspan=\"-2\"></th>\n          <th></th>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row_1, row_2 = row_group.children\n    td_1, td_2 = row_1.children\n    assert td_1.width == td_2.width\n    th_1, th_2 = row_2.children\n    assert th_1.width == th_2.width\n\n\n@assert_no_logs\ndef test_table_bad_int_col_span():\n    page, = render_pages('''\n      <table>\n        <colgroup>\n          <col span=\"bad\" style=\"width:25px\" />\n        </colgroup>\n        <tr>\n          <td>a</td>\n          <td>a</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert td_1.width == 25\n\n\n@assert_no_logs\ndef test_table_bad_int_colgroup_span():\n    page, = render_pages('''\n      <table>\n        <colgroup span=\"bad\" style=\"width:25px\">\n          <col />\n        </colgroup>\n        <tr>\n          <td>a</td>\n          <td>a</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    row, = row_group.children\n    td_1, td_2 = row.children\n    assert td_1.width == 25\n\n\n@assert_no_logs\ndef test_table_different_display():\n    # Test display attribute set on different table elements.\n    render_pages('''\n      <table style=\"font-size: 1px\">\n        <colgroup style=\"display: block\"><div>a</div></colgroup>\n        <col style=\"display: block\"><div>a</div></col>\n        <tr style=\"display: block\"><div>a</div></tr>\n        <td style=\"display: block\"><div>a</div></td>\n        <th style=\"display: block\"><div>a</div></th>\n        <thead>\n          <colgroup style=\"display: block\"><div>a</div></colgroup>\n          <col style=\"display: block\"><div>a</div></col>\n          <tr style=\"display: block\"><div>a</div></tr>\n          <td style=\"display: block\"><div>a</div></td>\n          <th style=\"display: block\"><div>a</div></th>\n        </thead>\n        <tbody>\n          <colgroup style=\"display: block\"><div>a</div></colgroup>\n          <col style=\"display: block\"><div>a</div></col>\n          <tr style=\"display: block\"><div>a</div></tr>\n          <td style=\"display: block\"><div>a</div></td>\n          <th style=\"display: block\"><div>a</div></th>\n        </tbody>\n        <tr>\n          <colgroup style=\"display: block\"><div>a</div></colgroup>\n          <col style=\"display: block\"><div>a</div></col>\n          <tr style=\"display: block\"><div>a</div></tr>\n          <td style=\"display: block\"><div>a</div></td>\n          <th style=\"display: block\"><div>a</div></th>\n        </tr>\n        <td>\n          <colgroup style=\"display: block\"><div>a</div></colgroup>\n          <col style=\"display: block\"><div>a</div></col>\n          <tr style=\"display: block\"><div>a</div></tr>\n          <td style=\"display: block\"><div>a</div></td>\n          <th style=\"display: block\"><div>a</div></th>\n        </td>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_min_width_with_overflow():\n    # Regression test for #1383.\n    page, = render_pages('''\n      <style>\n        table td { border: 1px solid black }\n        table.key-val tr td:nth-child(1) { min-width: 13em }\n      </style>\n\n      <table class=\"key-val\">\n        <tbody>\n          <tr>\n            <td>Normal Key 1</td>\n            <td>Normal Value 1</td>\n          </tr>\n          <tr>\n            <td>Normal Key 2</td>\n            <td>Normal Value 2</td>\n          </tr>\n        </tbody>\n      </table>\n\n      <table class=\"key-val\">\n        <tbody>\n          <tr>\n            <td>Short value</td>\n            <td>Works as expected</td>\n          </tr>\n          <tr>\n            <td>Long Value</td>\n            <td>Annoyingly breaks my table layout: Sed ut perspiciatis\n                unde omnis iste natus error sit voluptatem\n                accusantium doloremque laudantium, totam rem aperiam,\n                eaque ipsa quae ab illo inventore veritatis et quasi\n                architecto beatae vitae dicta sunt explicabo.\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper_1, table_wrapper_2 = body.children\n\n    table1, = table_wrapper_1.children\n    tbody1, = table1.children\n    tr1, tr2 = tbody1.children\n    table1_td1, table1_td2 = tr1.children\n\n    table2, = table_wrapper_2.children\n    tbody2, = table2.children\n    tr1, tr2 = tbody2.children\n    table2_td1, table2_td2 = tr1.children\n\n    assert table1_td1.min_width == table2_td1.min_width\n    assert table1_td1.width == table2_td1.width\n\n\n@assert_no_logs\ndef test_table_cell_max_width():\n    page, = render_pages('''\n      <style>\n        td {\n          text-overflow: ellipsis;\n          white-space: nowrap;\n          overflow: hidden;\n          max-width: 45px;\n        }\n      </style>\n      <table>\n        <tr>\n          <td>abcdefghijkl</td>\n        </tr>\n      </table>\n    ''')\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    tbody, = table.children\n    tr, = tbody.children\n    td, = tr.children\n    assert td.max_width == 45\n    assert td.width == 45\n\n\nblack = (0, 0, 0, 1)\nred = (1, 0, 0, 1)\ngreen = (0, 1, 0, 1)  # lime in CSS\nblue = (0, 0, 1, 1)\nyellow = (1, 1, 0, 1)\nblack_3 = ('solid', 3, black)\nred_1 = ('solid', 1, red)\nyellow_5 = ('solid', 5, yellow)\ngreen_5 = ('solid', 5, green)\ndashed_blue_5 = ('dashed', 5, blue)\n\n\n@assert_no_logs\ndef test_border_collapse_1():\n    html = parse_all('<table></table>')\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    assert isinstance(table, boxes.TableBox)\n    assert not hasattr(table, 'collapsed_border_grid')\n\n    grid = _get_grid('<table style=\"border-collapse: collapse\"></table>', 0, 0)\n    assert grid == ([], [])\n\n\n@assert_no_logs\ndef test_border_collapse_2():\n    vertical_borders, horizontal_borders = _get_grid('''\n      <style>td { border: 1px solid red }</style>\n      <table style=\"border-collapse: collapse; border: 3px solid black\">\n        <tr> <td>A</td> <td>B</td> </tr>\n        <tr> <td>C</td> <td>D</td> </tr>\n      </table>\n    ''', 2, 2)\n    assert vertical_borders == [\n        [black_3, red_1, black_3],\n        [black_3, red_1, black_3],\n    ]\n    assert horizontal_borders == [\n        [black_3, black_3],\n        [red_1, red_1],\n        [black_3, black_3],\n    ]\n\n\n@assert_no_logs\ndef test_border_collapse_3():\n    # hidden vs. none\n    vertical_borders, horizontal_borders = _get_grid('''\n      <style>table, td { border: 3px solid }</style>\n      <table style=\"border-collapse: collapse\">\n        <tr> <td>A</td> <td style=\"border-style: hidden\">B</td> </tr>\n        <tr> <td>C</td> <td style=\"border-style: none\">D</td> </tr>\n      </table>\n    ''', 2, 2)\n    assert vertical_borders == [\n        [black_3, None, None],\n        [black_3, black_3, black_3],\n    ]\n    assert horizontal_borders == [\n        [black_3, None],\n        [black_3, None],\n        [black_3, black_3],\n    ]\n\n\n@assert_no_logs\ndef test_border_collapse_4():\n    vertical_borders, horizontal_borders = _get_grid('''\n      <style>td { border: 1px solid red }</style>\n      <table style=\"border-collapse: collapse; border: 5px solid yellow\">\n        <col style=\"border: 3px solid black\" />\n        <tr> <td></td> <td></td> <td></td> </tr>\n        <tr> <td></td> <td style=\"border: 5px dashed blue\"></td>\n          <td style=\"border: 5px solid lime\"></td> </tr>\n        <tr> <td></td> <td></td> <td></td> </tr>\n        <tr> <td></td> <td></td> <td></td> </tr>\n      </table>\n    ''', 3, 4)\n    assert vertical_borders == [\n        [yellow_5, black_3, red_1, yellow_5],\n        [yellow_5, dashed_blue_5, green_5, green_5],\n        [yellow_5, black_3, red_1, yellow_5],\n        [yellow_5, black_3, red_1, yellow_5],\n    ]\n    assert horizontal_borders == [\n        [yellow_5, yellow_5, yellow_5],\n        [red_1, dashed_blue_5, green_5],\n        [red_1, dashed_blue_5, green_5],\n        [red_1, red_1, red_1],\n        [yellow_5, yellow_5, yellow_5],\n    ]\n\n\n@assert_no_logs\ndef test_border_collapse_5():\n    # rowspan and colspan\n    vertical_borders, horizontal_borders = _get_grid('''\n        <style>col, tr { border: 3px solid }</style>\n        <table style=\"border-collapse: collapse\">\n            <col /><col /><col />\n            <tr> <td rowspan=2></td> <td></td> <td></td> </tr>\n            <tr>                     <td colspan=2></td> </tr>\n        </table>\n    ''', 3, 2)\n    assert vertical_borders == [\n        [black_3, black_3, black_3, black_3],\n        [black_3, black_3, None, black_3],\n    ]\n    assert horizontal_borders == [\n        [black_3, black_3, black_3],\n        [None, black_3, black_3],\n        [black_3, black_3, black_3],\n    ]\n\n\n@assert_no_logs\ndef test_table_zero_width():\n    # Regression test for #2306.\n    page, = render_pages('''\n      <table style=\"font-family: weasyprint\">\n        <thead>\n          <tr>\n            <th colspan=\"2\">aaaaaaaaaaaaaaaaaa</th>\n          </tr>\n          <tr>\n            <th style=\"width:80px\"></th>\n            <th style=\"width:80px\">rrrrr</th>\n          </tr>\n        </thead>\n      </table>\n    ''')\n\n\n@assert_no_logs\ndef test_table_inline_atomic_nowrap():\n    # Regression test for #2661.\n    page, = render_pages('''\n      <div style=\"width: 0; font: 2px weasyprint; white-space: nowrap\">\n        <table>\n          <tr>\n            <td>a<img src=pattern.png></td>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    table_wrapper, = div.children\n    assert div.width == 0\n    assert table_wrapper.width == 6\n"
  },
  {
    "path": "tests/resources/acid2-reference.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\r\n<html>\r\n <head>\r\n  <title>The Second Acid Test (Reference Rendering)</title>\r\n  <style type=\"text/css\">\r\n   html { margin: 0; padding: 0; border: 0; overflow: hidden; background: white; }\r\n   body { margin: 0; padding: 0; border: 0; }\r\n   h2 { margin: 0; padding: 48px 0 36px 84px; border: 0; font: 24px/24px sans-serif; color: navy; }\r\n   p { margin: 0; padding: 0 0 0 72px; border: 0; }\r\n   img { vertical-align: top; margin: 0; padding: 0; border: 0; }\r\n  </style>\r\n </head>\r\n <body>\r\n  <h2>Hello&nbsp;World!</h2>\r\n  <p><a href=\"reference.png\"><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAACoCAMAAABDlVWGAAABUFBMVEUAAAABAQADAwAICAAIpQQOpwodHQAhISAmryIsLAAwsy1AQEBDQwBSUgBTwFBUVAB1dQCAgACBgQCCggCDgwCEhACFhQCGhgCJiQCJ1IeKigCLiwCMjACOjgCPjwCPj4+SkgCTkwCVlQCXlwCYmACbmwCdnQCdnZ2fnwCgoACiogCi3aClpQCmpgCpqQCrqwCsrACtrQCvrwCwsLCysgCzswC2tgC8vAC9vQC+vgDBwQDCwgDDwwDExADIyADJyQDKygDKysrLywDNzQDOzgDO7c3Q0ADS0gDT0wDU1ADW1gDX1wDa2gDb29vd3QDf3wDg4ADh4QDk5ADl5QDo6ADo9ujp6QDq6gDq6urr6wDs7ADt7QDv7wDw8ADx8QDy8gDz8wDz+vP19QD29gD39wD4+AD5+QD7+wD7+/v8/AD9/QD9/f3+/gD+/v7//wD///+VIlNwAAADWklEQVR42u3d6VPaQBgGcBBBkDNciiCIgKgRTxTPqijeiCeioohcAiH9/7+VduqMbVNIJruMGZ/96Mz77C/AZt/VDKq+yxgqiUPWXIACCiiggAIKKDXo/ybmJQ45FwAooIACCiiggJKFygHJuQBAAQUUUEABBZQeVBTuuVCTC6sVniWjJULLmeT6YUUutHK4nsyUKUJLmY0Zv2+t/Yo+xm1mk8lsiz+KxX2sqK35/DMbmRItaPVs0euw+4/rfMKq06hVKrVGZ02Ic/5RUT/22x3exbMqHWj1bH7U7po8Kb9EDH3G6G46vRs19hkiL92Zf1eUTyZd9tF5ISkBaHZlmGHCpw0+ohsI5Jqt9mjmAgO6SHfoPxWN0zDDDK9kaUArB6F29naFTxgGlt6KKdbjYVPFt6UBQ9d3X6Cist2+6tBBhQL0YXmEcS8U+EdrX+DtlrXotVq9hb19C/RZu6wowYrCgpsZWX4gD62fTzsZ7z7Hx3XGXJEd7P81BtlizqiLd4YKVnD7XsY5fV4nDq0d+e1M6JLjbZpoM2Xp/z0sqWZUY+sMFazgLkOM3X9UIw6tJscY11ye583q3Rarf59Wz7Z21ebOUOGK/JyLGUtWiUNLe0HGNdvONanSLY/2fVqtp5VWmTpDhSuqsy4muFciv5iOxx1DPz/8pKAPy0OO8WMKq/4iZnfHnsi99U8xtz12QQH6mnAwwWtyi+k6yDgSrzR2ppOpId9mg9TtqbHpG5o6obKF5rdCzokyR+aGz5UnnKGtPJ3uKbsajl1xZLZQ7ioWXs1SavNqNzvfCkIthvSmhOcL33ZuarQaZ+71riHQtElv89o/aty9chSPIg2BNlh64/wxihL0fdy/HyzuxR5FxFYQhvb+6Kwc6GfASf3jBKCAAgoooIACCqi4fGVAySbRgxKPogSlkUUDSimMOJReGlko1TiCybTzSAX3IJBIbm8S5cf2LFJmai8zv8YrqpzPqHJWvXLuo8rZmZSz1yune1JOP6qcDl85ZyblnELxCwhAAQUUUEAB/UxQMQ8W0LgYMXNJfgICUEABBRRQQAGV/RChitAQkw8ooIACCiiggJKF4hsLAAUUUEABBfTrQPFNhIACCiiggAL6daD4DwOAAgoooIACqljoDwseYUYsza58AAAAAElFTkSuQmCC\" alt=\"Follow this link to view the reference image, which should be rendered below the text &quot;Hello World!&quot; on the test page in the same way that this paragraph is rendered below that text on this page.\"></a></p>\r\n </body>\r\n</html>"
  },
  {
    "path": "tests/resources/acid2-test.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n<html>\n <head>\n  <title>The Second Acid Test</title>\n  <style type=\"text/css\">\n   /* section numbers refer to CSS2.1 */\n\n   /* page setup */\n   html { font: 12px sans-serif; margin: 0; padding: 0; overflow: hidden; /* hides scrollbars on viewport, see 11.1.1:3 */ background: white; color: red; }\n   body { margin: 0; padding: 0; }\n\n   /* introduction message */\n   .intro { font: 2em sans-serif; margin: 3.5em 2em; padding: 0.5em; border: solid thin; background: white; color: black; position: relative; z-index: 2; /* should cover the black and red bars that are fixed-positioned */ }\n   .intro * { font: inherit; margin: 0; padding: 0; }\n   .intro h1 { font-size: 1em; font-weight: bolder; margin: 0; padding: 0; }\n   .intro :link { color: blue; }\n   .intro :visited { color: purple; }\n\n   /* picture setup */\n   #top { margin: 100em 3em 0; padding: 2em 0 0 .5em; text-align: left; font: 2em/24px sans-serif; color: navy; white-space: pre; } /* \"Hello World!\" text */\n   .picture { position: relative; border: 1em solid transparent; margin: 0 0 100em 3em; } /* containing block for face */\n   .picture { background: red; } /* overriden by preferred stylesheet below */\n\n   /* top line of face (scalp): fixed positioning and min/max height/width */\n   .picture p { position: fixed; margin: 0; padding: 0; border: 0; top: 9em; left: 11em; width: 140%; max-width: 4em; height: 8px; min-height: 1em; max-height: 2mm; /* min-height overrides max-height, see 10.7 */ background: black; border-bottom: 0.5em yellow solid; }\n\n   /* bits that shouldn't be part of the top line (and shouldn't be visible at all): HTML parsing, \"+\" combinator, stacking order */\n   .picture p.bad { border-bottom: red solid; /* shouldn't matter, because the \"p + table + p\" rule below should match it too, thus hiding it */ }\n   .picture p + p { background: maroon; z-index: 1; } /* shouldn't match anything */\n   .picture p + table + p { margin-top: 3em; /* should end up under the absolutely positioned table below, and thus not be visible */ }\n\n   /* second line of face: attribute selectors, float positioning */\n   [class~=one].first.one { position: absolute; top: 0; margin: 36px 0 0 60px; padding: 0; border: black 2em; border-style: none solid; /* shrink wraps around float */ }\n   [class~=one][class~=first] [class=second\\ two][class=\"second two\"] { float: right; width: 48px; height: 12px; background: yellow; margin: 0; padding: 0; } /* only content of abs pos block */\n\n   /* third line of face: width and overflow */\n   .forehead { margin: 4em; width: 8em; border-left: solid black 1em; border-right: solid black 1em; background: red url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4%2F58BAAT%2FAf9jgNErAAAAAElFTkSuQmCC); /* that's a 1x1 yellow pixel PNG */ }\n   .forehead * { width: 12em; line-height: 1em; }\n\n   /* class selectors headache */\n   .two.error.two { background: maroon; } /* shouldn't match */\n   .forehead.error.forehead { background: red; } /* shouldn't match */\n   [class=second two] { background: red; } /* this should be ignored (invalid selector -- grammar says it only accepts IDENTs or STRINGs) */\n\n   /* fourth and fifth lines of face, with eyes: paint order test (see appendix E) and fixed backgrounds */\n   /* the two images are identical: 2-by-2 squares with the top left\n      and bottom right pixels set to yellow and the other two set to\n      transparent. Since they are offset by one pixel from each other,\n      the second one paints exactly over the transparent parts of the\n      first one, thus creating a solid yellow block. */\n   .eyes { position: absolute; top: 5em; left: 3em; margin: 0; padding: 0; background: red; }\n   #eyes-a { height: 0; line-height: 2em; text-align: right; } /* contents should paint top-most because they're inline */\n   #eyes-a object { display: inline; vertical-align: bottom; }\n   #eyes-a object[type] { width: 7.5em; height: 2.5em; } /* should have no effect since that object should fallback to being inline (height/width don't apply to inlines) */\n   #eyes-a object object object { border-right: solid 1em black; padding: 0 12px 0 11px; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAABnRSTlMAAAAAAABupgeRAAAABmJLR0QA%2FwD%2FAP%2BgvaeTAAAAEUlEQVR42mP4%2F58BCv7%2FZwAAHfAD%2FabwPj4AAAAASUVORK5CYII%3D) fixed 1px 0; }\n   #eyes-b { float: left; width: 10em; height: 2em; background: fixed url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAABnRSTlMAAAAAAABupgeRAAAABmJLR0QA%2FwD%2FAP%2BgvaeTAAAAEUlEQVR42mP4%2F58BCv7%2FZwAAHfAD%2FabwPj4AAAAASUVORK5CYII%3D); border-left: solid 1em black; border-right: solid 1em red; } /* should paint in the middle layer because it is a float */\n   #eyes-c { display: block; background: red; border-left: 2em solid yellow; width: 10em; height: 2em; } /* should paint bottom most because it is a block */\n\n   /* lines six to nine, with nose: auto margins */\n   .nose { float: left; margin: -2em 2em -1em; border: solid 1em black; border-top: 0; min-height: 80%; height: 60%; max-height: 3em; /* percentages become auto (see 10.5 and 10.7) and intrinsic height is more than 3em, so 3em wins */ padding: 0; width: 12em; }\n   .nose > div { padding: 1em 1em 3em; height: 0; background: yellow; }\n   .nose div div { width: 2em; height: 2em; background: red; margin: auto; }\n   .nose :hover div { border-color: blue; }\n   .nose div:hover :before { border-bottom-color: inherit; }\n   .nose div:hover :after { border-top-color: inherit; }\n   .nose div div:before { display: block; border-style: none solid solid; border-color: red yellow black yellow; border-width: 1em; content: ''; height: 0; }\n   .nose div    :after { display: block; border-style: solid solid none; border-color: black yellow red yellow; border-width: 1em; content: ''; height: 0; }\n\n   /* between lines nine and ten: margin collapsing with 'float' and 'clear' */\n   .empty { margin: 6.25em; height: 10%; /* computes to auto which makes it empty per 8.3.1:7 (own margins) */ }\n   .empty div { margin: 0 2em -6em 4em; }\n   .smile { margin: 5em 3em; clear: both; /* clearance is negative (see 8.3.1 and 9.5.1) */ }\n\n   /* line ten and eleven: containing block for abs pos */\n   .smile div { margin-top: 0.25em; background: black; width: 12em; height: 2em; position: relative; bottom: -1em; }\n   .smile div div { position: absolute; top: 0; right: 1em; width: auto; height: 0; margin: 0; border: yellow solid 1em; }\n\n   /* smile (over lines ten and eleven): backgrounds behind borders, inheritance of 'float', nested floats, negative heights */\n   .smile div div span { display: inline; margin: -1em 0 0 0; border: solid 1em transparent; border-style: none solid; float: right; background: black; height: 1em; }\n   .smile div div span em { float: inherit; border-top: solid yellow 1em; border-bottom: solid black 1em; } /* zero-height block; width comes from (zero-height) child. */\n   .smile div div span em strong { width: 6em; display: block; margin-bottom: -1em; /* should have no effect, since parent has top&bottom borders, so this margin doesn't collapse */ }\n\n   /* line twelve: line-height */\n   .chin { margin: -4em 4em 0; width: 8em; line-height: 1em; border-left: solid 1em black; border-right: solid 1em black; background: yellow url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAFSDNYfAAAAaklEQVR42u3XQQrAIAwAQeP%2F%2F6wf8CJBJTK9lnQ7FpHGaOurt1I34nfH9pMMZAZ8BwMGEvvh%2BBsJCAgICLwIOA8EBAQEBAQEBAQEBK79H5RfIQAAAAAAAAAAAAAAAAAAAAAAAAAAAID%2FABMSqAfj%2FsLmvAAAAABJRU5ErkJggg%3D%3D) /* 64x64 red square */ no-repeat fixed /* shouldn't be visible unless the smiley is moved to the top left of the viewport */; }\n   .chin div { display: inline; font: 2px/4px serif; }\n\n   /* line thirteen: cascade and selector tests */\n   .parser-container div { color: maroon; border: solid; color: orange; } /* setup */\n   div.parser-container * { border-color: black; /* overrides (implied) border-color on previous line */ } /* setup */\n   * div.parser { border-width: 0 2em; /* overrides (implied) declarations on earlier line */ } /* setup */\n\n   /* line thirteen continued: parser tests */\n   .parser { /* comment parsing test -- comment ends before the end of this line, the backslash should have no effect: \\*/ }\n   .parser { margin: 0 5em 1em; padding: 0 1em; width: 2em; height: 1em; error: \\}; background: yellow; } /* setup with parsing test */\n   * html .parser {  background: gray; }\n   \\.parser { padding: 2em; }\n   .parser { m\\argin: 2em; };\n   .parser { height: 3em; }\n   .parser { width: 200; }\n   .parser { border: 5em solid red ! error; }\n   .parser { background: red pink; }\n\n   /* line fourteen (last line of face): table */\n   ul { display: table; padding: 0; margin: -1em 7em 0; background: red; }\n   ul li { padding: 0; margin: 0; }\n   ul li.first-part { display: table-cell; height: 1em; width: 1em; background: black; }\n   ul li.second-part { display: table; height: 1em; width: 1em; background: black; } /* anonymous table cell wraps around this */\n   ul li.third-part { display: table-cell; height: 0.5em; /* gets stretched to fit row */ width: 1em; background: black; }\n   ul li.fourth-part { list-style: none; height: 1em; width: 1em; background: black; } /* anonymous table cell wraps around this */\n\n   /* bits that shouldn't appear: inline alignment in cells */\n   .image-height-test { height: 10px; overflow: hidden; font: 20em serif; } /* only the area between the top of the line box and the top of the image should be visible */\n   table { margin: 0; border-spacing: 0; }\n   td { padding: 0; }\n\n  </style>\n  <link rel=\"appendix stylesheet\" href=\"data:text/css,.picture%20%7B%20background%3A%20none%3B%20%7D\"> <!-- this stylesheet should be applied by default -->\n </head>\n <body>\n\n  <div class=\"intro\">\n   <h1>Standards compliant?</h1>\n   <p><a href=\"#top\">Take The Acid2 Test</a> and compare it to <a href=\"reference.html\">the reference rendering</a>.</p>\n  </div>\n  <h2 id=\"top\">Hello World!</h2>\n\n  <div class=\"picture\">\n   <p><table><tr><td></table><p class=\"bad\"> <!-- <table> closes <p> per the HTML4 DTD -->\n   <blockquote class=\"first one\"><address class=\"second two\"></address></blockquote>\n   <div class=\"forehead\"><div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</div></div>\n   <div class=\"eyes\"><div id=\"eyes-a\"><object data=\"data:application/x-unknown,ERROR\"><object data=\"./404\" type=\"text/html\"><object data=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAYCAYAAAFy7sgCAAAGsUlEQVRo3u2ZbWwcZxHHf3s%2B7LNbO3ZjXBtowprGODRX0qpNQCjmJKuVKhMl1P2AkCwhFOIKkCBSm9IXavGFKAixIAECwkmWo5MrhRI3Ub40IEwQgp6aIDg3Cd6eEqyIHEteah%2B1E69vhw%2BZtTaX8704ZzkKjHS6271nZ56ZZ%2BY%2F%2F%2BdZKF%2FCwYshx3EkkggLsD1v4FQkEZZYLCbAKyG9%2Ba9EIsG6hnUAf8x74K3aUC3j4%2BM54HcsR2oAIomwZOezkv%2FnSHpYNh%2BNCmAE7xv94zvFdd1bHsjMZmQkPSxAJP%2B%2FfuBLwK54PC7JZFKAVJmzXLBt2w%2FMvcDLwIb8QS8CeJ4nkURYIomw7J%2FYJ8BvSiiXptGGxWds2%2Fa9%2Bnaxh%2BYAD%2Bgt04NDgABTpQY2cvvSFLzw86gWeBVwC8SzlOSv2YeBPfmDBoBHgKmR9LBEEmHZfDTqGykqfkUE0nA78BzQGfSgUeP3wNeTXwXg7MwZDhw4UHL6ra2ti79%2FOvljgG8AZ4H64Lhm4MvAocxsRppGG%2FxcXihlwLIs6R%2FfKV2HO%2F26uA94pdDYUKUZUU7W1RQYXA98Gnhaf5%2FXWX0HeAHYoQonqa4sZSOsSWMCWeC9Yko%2BCQwBe4E6oNc0Tc91XTl1%2BaTsn9gnI%2Blhyc5nZWxsrBIkKSbl2tiic3tW53YDEwOKaoFBrcOfqKee53lG9xsPMjV784r%2F4lO%2FpPvyJ9iyZcuvFSaXK5XYeAZ4CDgGvB3MS4B54LQuWYPeuy4iRFsevsXqpuYoqVQKIH2bK1CuDQNo11o4XUzh%2FcDWYIe1LEtyuZx4niee54njOGKapgfsqlL%2Bl2OjEXg8nxrc1dJ0h3hbtL%2BGCtz7KPBF4CuBe9uB15VafE8hr9qylI3HgG8C2%2FK7VyHZoJj7MrBRm30qFotJMpkU27YlHo%2F7Ha5a%2BV%2FKRkSJ4KuKRLVLKapTjB1SzAVIjY2NSXY%2BKyPpYdk%2FsU9OXT4pruv6BdZbBQfKsVGnvWlIe1VB6VQO8JxC1vZYLCbZ%2BaxsPhpdZDyRRFhG0sPiOE6ldKBg2lRg4xF1YCDIIIKN7DGgD3gH%2BBXwejKZfPrs2tPs%2FvPN2bKuYR1nd7xLKBSSJeqoXKnERjPwNWAG%2BLn2rZuM%2B4Tpml6vaWlp4eLcxVusZq5lCgVgOVKJjRqdX86ffL4D5wIoZACnTpw4wRMdT96i%2FImOJxERAs4uVyqxUacF%2FPdiCj%2BjdRBRGFtwXVdG0sPSdbhTmkYbpH98p2RmM2JZlig1vl0GWo4NQ%2Fn%2Bs5pKRXfwjweaxy7TND3HcRZbfC6X8xVPVQlGy7WxVWlO5XRXFXm6EZmrQuSXYyPE3SiVoEhE6Wyr0u2rumO6zv%2B21AFdQAswC1wCMuUCXCmyWQus103Qg8qlDO0lxwOb%2Fl4FiK3AB3VS%2FuKKLtK%2FgbeAnwG%2FvUODuRw%2FFrR0H1UC75fwu8oJ%2FhFsW5VIG%2FBUgEIN6Y65O4AHu4Ap0zQ9y7LEcZyb9lRBUHQcRyzL8unZVBW5bFWAvAp%2BhDQ2g4F47dUYtlU6obXA54DnVdFLekjUGGifh4AFy7LEdV3xj3X9I66m0QZpGm2QrsOd0j%2B%2BU0bSw5KZzYjrun6HWlAd961i4FfCj0aN1Usau%2Bc1lmuXPFwvAEumUut7tQQvAb%2FXb%2FT0bCAej9cODg7yt%2Bm%2F8q2%2F7OUHZ76PnZ1k2p0mJzlykmPancbOTnL0whHs7CQfb%2B5mx2d3sH79%2BtCRI0c6FeaOr9ICrIQfLvA%2B8BGNXxi4R6HrisJVUWrxAVW2oMFf0Aczim8o3kV6enowDIPjF9%2Fk%2BMU3S3rrjzMMg56eHr%2BxP7qKFbASfojG6kpeDGs1tiW53RxwWT%2Bin5q8w4xpQK5evQpAR30H7ZH2khNvj7TTUd8BgD4rqmu1ZKX8qNeY%2BfHz4zlXDgT5E8tpCTUq7XSBC4Euv8227TV9fX1E73%2BYtvo27BmbS9cvFVTY3bSRFza9yOcf6Gfmygy7d%2B%2Fm%2FPnzF4DvrsBLhnJlJfwIKXxv1PheAE4qK6p4H9AGbNKTuhngBPBPXYRe4IemaT5kWZbR19fHNbmGnZ1k4r3U4glDR30Hm5qjbGjsImJEOHbsGHv27JFz5869o0eFq01Jq%2BmHAXwI6FFKagMTgHM7GzFDS%2BoeLSMv7zjzC9x4Y7gxFovVDAwMEI1GaWlpWSzRVCrFwYMH%2FXfxZ4AfAa8B%2F7lDaGg1%2FQgp43lfK0yqtRMuJa3ceKe5DfgYsCYAZ2ngD8CfAkzqTpW7xY%2F%2FSznyX%2FVeUb2kVmX4AAAAAElFTkSuQmCC\">ERROR</object></object></object></div><div id=\"eyes-b\"></div><div id=\"eyes-c\"></div></div> <!-- that's a PNG with 8bit alpha containing two eyes -->\n   <div class=\"nose\"><div><div></div></div></div>\n   <div class=\"empty\"><div></div></div>\n\n   <div class=\"smile\"><div><div><span><em><strong></strong></em></span></div></div></div>\n   <div class=\"chin\"><div>&nbsp;</div></div>\n   <div class=\"parser-container\"><div class=\"parser\"><!-- ->ERROR<!- --></div></div> <!-- two dashes is what delimits a comment, so the text \"->ERROR<!-\" earlier on this line is actually part of a comment -->\n   <ul>\n    <li class=\"first-part\"></li>\n    <li class=\"second-part\"></li>\n    <li class=\"third-part\"></li>\n    <li class=\"fourth-part\"></li>\n\n   </ul>\n   <div class=\"image-height-test\"><table><tr><td><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAFSDNYfAAAAaklEQVR42u3XQQrAIAwAQeP%2F%2F6wf8CJBJTK9lnQ7FpHGaOurt1I34nfH9pMMZAZ8BwMGEvvh%2BBsJCAgICLwIOA8EBAQEBAQEBAQEBK79H5RfIQAAAAAAAAAAAAAAAAAAAAAAAAAAAID%2FABMSqAfj%2FsLmvAAAAABJRU5ErkJggg%3D%3D\" alt=\"\"></td></tr></table></div>\n  </div>\n </body>\n</html>\n"
  },
  {
    "path": "tests/resources/doc1.html",
    "content": "<html>\n<head>\n    <meta http-equiv=Content-Type content=text/html;charset=utf-8>\n    <!-- currentColor means 'inherit' on color itself. -->\n    <link rel=stylesheet href=\"data:text/css;charset=ASCII,a%7Bcolor%3AcurrentColor%7D\">\n    <style media=print>\n        @import url(sub_directory/sheet1.css);\n        @import \"data:text/css;charset=utf-16le;base64,\\\n                    bABpAHsAYwBvAGwAbwByADoAcgBlAGQAfQA=\";\n        @import \"data:,ul {border-width: 1000px !important}\" screen;\n        @font-face { src: url(weasyprint.otf); font-family: weasyprint }\n        a:after {\n            content: \" [\" attr(href) \"]\";\n            border-style: solid;\n            border-top-width: inherit;\n            border-bottom-width: initial;\n        }\n        @page :first { margin-top: 5px }\n        ul {\n            border-style: none solid hidden;\n            border-width: thin thick 4px .25in;\n        }\n        body > h1:first-child {\n            background-image: url(logo_small.png);\n        }\n        h1 ~ p ~ ul a:after {\n            background: red;\n        }\n    </style>\n    <style type=text/NOT-css>\n        ul {\n            border-width: 1000px !important;\n        }\n    </style>\n    <style media=screen>\n        ul {\n            border-width: 1000px !important;\n        }\n    </style>\n    <link rel=not_stylesheet href=\"data:,ul {border-width: 1000px !important}\">\n</head>\n<body style=\"font-size: 20px\">\n    <h1 style=\"font-size: 2em\">WeasyPrint test document (with Ünicōde)</h1>\n    <p style=\"color: blue; font-size: x-large;\n              -weasy-link: attr(foo-link)  /* no such attribute */\">Hello</p>\n    <ul style=\"font-family: weasyprint; font-size: 1.25ex\">\n        <li style=\"font-size: 6pt; font-weight: bold\">\n            <a href=home.html\n                style=\"padding: 1px 2px 3px 4px; border: 42px solid;\n                       font-size: 300%; font-weight: bolder\">Home</a>\n        <li>…\n    </ul>\n    <div style=\"font-size: 2em\">\n      <span style=\"display: block; width: 10rem; height: 10em\">\n        <span style=\"font-size: 2rem\">WeasyPrint</span>\n      </span>\n    </div>\n</body>\n"
  },
  {
    "path": "tests/resources/latin1-test.css",
    "content": "h1::before {\n    content: \"Ilv Unicode\";\n    background-image: url(pattern.png)\n}\n"
  },
  {
    "path": "tests/resources/mini_ua.css",
    "content": "/* Minimal user-agent stylesheet */\np { margin: 1em 0px } /* 0px should be translated to 0*/\na { text-decoration: underline }\nh1 { font-weight: bolder }\n"
  },
  {
    "path": "tests/resources/sheet2.css",
    "content": "li {\n    margin-bottom: 3em; /* Should be masked*/\n    margin: 2em 0;\n    margin-left: 4em; /* Should not be masked*/\n}\n"
  },
  {
    "path": "tests/resources/sub_directory/sheet1.css",
    "content": "@import url(../sheet2.css) all;\np {\n    background: currentColor;\n}\n\n@media print {\n    ul {\n        /* 1ex == 0.8em for weasyprint.otf. */\n        margin: 2em 2.5ex;\n    }\n}\n@media screen {\n    ul {\n        border-width: 1000px !important;\n    }\n}\n"
  },
  {
    "path": "tests/resources/tests_ua.css",
    "content": "/* Simplified user-agent stylesheet for HTML5 in tests. */\n\n@font-face { src: url(weasyprint.otf); font-family: weasyprint }\n@page { background: white; bleed: 0; @footnote { margin: 0 } }\n\nhtml, body, div, h1, h2, h3, h4, ol, p, ul, hr, pre, section, article { display: block }\n\nbody      { orphans: 1; widows: 1 }\nli        { display: list-item }\nhead      { display: none }\npre       { white-space: pre }\nbr:before { content: '\\A'; white-space: pre-line }\nol        { list-style-type: decimal }\nol, ul    { counter-reset: list-item }\n\ntable     { display: table; box-sizing: border-box }\ntr        { display: table-row }\nthead     { display: table-header-group }\ntbody     { display: table-row-group }\ntfoot     { display: table-footer-group }\ncol       { display: table-column }\ncolgroup  { display: table-column-group }\ntd, th    { display: table-cell }\ncaption   { display: table-caption }\n\n*[lang]   { -weasy-lang: attr(lang) }\na[href]   { -weasy-link: attr(href) }\na[name]   { -weasy-anchor: attr(name) }\n*[id]     { -weasy-anchor: attr(id) }\nh1        { bookmark-level: 1; bookmark-label: content(text) }\nh2        { bookmark-level: 2; bookmark-label: content(text) }\nh3        { bookmark-level: 3; bookmark-label: content(text) }\nh4        { bookmark-level: 4; bookmark-label: content(text) }\nh5        { bookmark-level: 5; bookmark-label: content(text) }\nh6        { bookmark-level: 6; bookmark-label: content(text) }\n\n::marker  { font-variant-numeric: tabular-nums }\n\n::footnote-call   { content: counter(footnote) }\n::footnote-marker { content: counter(footnote) '.' }\n"
  },
  {
    "path": "tests/resources/user.css",
    "content": "html {\n    /* Reversed contrast */\n    color: white;\n    background-color: black;\n}\n"
  },
  {
    "path": "tests/resources/utf8-test.css",
    "content": "h1::before {\n    content: \"I løvë Unicode\";\n    background-image: url(pattern.png)\n}\n"
  },
  {
    "path": "tests/test_acid2.py",
    "content": "\"\"\"Check the famous Acid2 test.\"\"\"\n\nimport io\n\nfrom PIL import Image\n\nfrom weasyprint import CSS, HTML\n\nfrom .testing_utils import assert_no_logs, capture_logs, resource_path\n\n\n@assert_no_logs\ndef test_acid2(assert_pixels_equal):\n    # Reduce image size and avoid Ghostscript rounding problems\n    stylesheets = (CSS(string='@page { size: 500px 800px }'),)\n\n    def render(filename):\n        return HTML(resource_path(filename)).render(stylesheets=stylesheets)\n\n    with capture_logs():\n        # This is a copy of https://www.webstandards.org/files/acid2/test.html\n        document = render('acid2-test.html')\n        intro_page, test_page = document.pages\n        # Ignore the intro page: it is not in the reference\n        test_png = document.copy([test_page]).write_png()\n        test_pixels = Image.open(io.BytesIO(test_png)).get_flattened_data()\n\n    # This is a copy of https://www.webstandards.org/files/acid2/reference.html\n    ref_png = render('acid2-reference.html').write_png()\n    ref_image = Image.open(io.BytesIO(ref_png))\n    ref_pixels = ref_image.get_flattened_data()\n    width, height = ref_image.size\n\n    assert_pixels_equal(width, height, test_pixels, ref_pixels, tolerance=2)\n"
  },
  {
    "path": "tests/test_api.py",
    "content": "\"\"\"Test the public API.\"\"\"\n\nimport contextlib\nimport gzip\nimport io\nimport os\nimport re\nimport sys\nimport threading\nimport unicodedata\nimport wsgiref.simple_server\nimport zlib\nfrom base64 import b64encode\nfrom functools import partial\nfrom pathlib import Path\nfrom urllib.parse import urljoin, uses_relative\n\nimport pytest\nfrom PIL import Image\n\nfrom weasyprint import CSS, HTML, __main__, default_url_fetcher\nfrom weasyprint.pdf.anchors import resolve_links\n\nfrom .draw import parse_pixels\nfrom .testing_utils import FakeHTML, assert_no_logs, capture_logs, resource_path\n\nfrom weasyprint.urls import (  # isort:skip\n    FatalURLFetchingError, URLFetcher, URLFetcherResponse, URLFetchingError, path2url)\n\ntry:\n    # Available in Python 3.11+\n    from contextlib import chdir\nexcept ImportError:\n    # Backported from Python 3.11\n    from contextlib import AbstractContextManager\n\n    class chdir(AbstractContextManager):  # noqa: N801\n        def __init__(self, path):\n            self.path = path\n            self._old_cwd = []\n\n        def __enter__(self):\n            self._old_cwd.append(os.getcwd())\n            os.chdir(self.path)\n\n        def __exit__(self, *excinfo):\n            os.chdir(self._old_cwd.pop())\n\n\ndef _test_resource(class_, name, check, **kwargs):\n    \"\"\"Common code for testing the HTML and CSS classes.\"\"\"\n    absolute_path = resource_path(name)\n    absolute_filename = str(absolute_path)\n    url = path2url(absolute_path)\n    check(class_(absolute_path, **kwargs))\n    check(class_(absolute_filename, **kwargs))\n    check(class_(guess=absolute_path, **kwargs))\n    check(class_(guess=absolute_filename, **kwargs))\n    check(class_(filename=absolute_path, **kwargs))\n    check(class_(filename=absolute_filename, **kwargs))\n    check(class_(url, **kwargs))\n    check(class_(guess=url, **kwargs))\n    url = path2url(absolute_filename.encode())\n    check(class_(url=url, **kwargs))\n    with absolute_path.open('rb') as fd:\n        check(class_(fd, **kwargs))\n    with absolute_path.open('rb') as fd:\n        check(class_(guess=fd, **kwargs))\n    with absolute_path.open('rb') as fd:\n        check(class_(file_obj=fd, **kwargs))\n    content = absolute_path.read_bytes()\n    with chdir(Path(__file__).parent):\n        relative_path = Path('resources') / name\n        relative_filename = str(relative_path)\n        check(class_(relative_path, **kwargs))\n        check(class_(relative_filename, **kwargs))\n        kwargs.pop('base_url', None)\n        check(class_(string=content, base_url=relative_filename, **kwargs))\n        encoding = kwargs.pop('encoding', 'utf-8')\n        with absolute_path.open('r', encoding=encoding) as fd:\n            check(class_(file_obj=fd, **kwargs))\n        check(class_(\n            string=content.decode(encoding), base_url=relative_filename,\n            **kwargs))\n    with pytest.raises(TypeError):\n        class_(filename='foo', url='bar')\n\n\ndef _check_doc1(html, has_base_url=True):\n    \"\"\"Check that a parsed HTML document looks like resources/doc1.html\"\"\"\n    root = html.etree_element\n    assert root.tag == 'html'\n    assert [child.tag for child in root] == ['head', 'body']\n    _head, body = root\n    assert [child.tag for child in body] == ['h1', 'p', 'ul', 'div']\n    h1, p, ul, div = body\n    assert h1.text == 'WeasyPrint test document (with Ünicōde)'\n    if has_base_url:\n        url = urljoin(html.base_url, 'pattern.png')\n        assert url.startswith('file:')\n        assert url.endswith('tests/resources/pattern.png')\n    else:\n        assert html.base_url is None\n\n\ndef _run(args, stdin=b''):\n    stdin = io.BytesIO(stdin)\n    stdout = io.BytesIO()\n    HTML = partial(FakeHTML, force_uncompressed_pdf=False)  # noqa: N806\n    __main__.main(args.split(), stdin=stdin, stdout=stdout, HTML=HTML)\n    return stdout.getvalue()\n\n\ndef _gzip_compress(data):\n    file_obj = io.BytesIO()\n    gzip_file = gzip.GzipFile(fileobj=file_obj, mode='wb')\n    gzip_file.write(data)\n    gzip_file.close()\n    return file_obj.getvalue()\n\n\n@contextlib.contextmanager\ndef http_server():\n    handlers = {\n        '/gzip': lambda env: (\n            (_gzip_compress(b'<html test=ok>'), {'Content-Encoding': 'gzip'})\n            if 'gzip' in env.get('HTTP_ACCEPT_ENCODING', '') else\n            (b'<html test=accept-encoding-header-fail>', {})\n        ),\n        '/deflate': lambda env: (\n            (zlib.compress(b'<html test=ok>'), {'Content-Encoding': 'deflate'})\n            if 'deflate' in env.get('HTTP_ACCEPT_ENCODING', '') else\n            (b'<html test=accept-encoding-header-fail>', {})\n        ),\n        '/raw-deflate': lambda env: (\n            # Remove zlib header and checksum\n            (zlib.compress(b'<html test=ok>')[2:-4], {'Content-Encoding': 'deflate'})\n            if 'deflate' in env.get('HTTP_ACCEPT_ENCODING', '') else\n            (b'<html test=accept-encoding-header-fail>', {})\n        ),\n        '/redirect': lambda env: (b'', {'Location': '/gzip'}),\n        '/redirect-loop': lambda env: (b'', {'Location': '/redirect-loop-2'}),\n        '/redirect-loop-2': lambda env: (b'', {'Location': '/redirect-loop'}),\n    }\n\n    def wsgi_app(environ, start_response):\n        handler = handlers.get(environ['PATH_INFO'])\n        if handler:\n            response, headers = handler(environ)\n            status = '301 Moved Permanently' if 'Location' in headers else '200 OK'\n            headers = [(str(name), str(value)) for name, value in headers.items()]\n        else:  # pragma: no cover\n            status = '404 Not Found'\n            response = b''\n            headers = []\n        start_response(status, headers)\n        return [response]\n\n    # Port 0: let the OS pick an available port number\n    # https://stackoverflow.com/a/1365284/1162888\n    server = wsgiref.simple_server.make_server('127.0.0.1', 0, wsgi_app)\n    _host, port = server.socket.getsockname()\n    thread = threading.Thread(target=server.serve_forever)\n    thread.start()\n    try:\n        yield f'http://127.0.0.1:{port}'\n    finally:\n        server.shutdown()\n        thread.join()\n\n\nclass FakeFile:\n    def __init__(self):\n        self.chunks = []\n\n    def write(self, data):\n        self.chunks.append(bytes(data[:]))\n\n    def getvalue(self):\n        return b''.join(self.chunks)\n\n\ndef _png_size(png_bytes):\n    image = Image.open(io.BytesIO(png_bytes))\n    return image.width, image.height\n\n\ndef _round_meta(pages):\n    \"\"\"Eliminate errors of floating point arithmetic for metadata.\"\"\"\n    for page in pages:\n        anchors = page.anchors\n        for anchor_name, (x1, y1, x2, y2) in anchors.items():\n            anchors[anchor_name] = (\n                round(x1, 6), round(y1, 6),\n                round(x2, 6), round(y2, 6))\n        links = page.links\n        for i, link in enumerate(links):\n            link_type, target, rectangle, box = link\n            pos_x, pos_y, width, height = rectangle\n            link = (\n                link_type, target,\n                (round(pos_x, 6), round(pos_y, 6),\n                 round(width, 6), round(height, 6)),\n                box)\n            links[i] = link\n        bookmarks = page.bookmarks\n        for i, (level, label, (pos_x, pos_y), state) in enumerate(bookmarks):\n            bookmarks[i] = (\n                level, label, (round(pos_x, 6), round(pos_y, 6)), state)\n\n\n@assert_no_logs\ndef test_html_parsing():\n    \"\"\"Test the constructor for the HTML class.\"\"\"\n    _test_resource(FakeHTML, 'doc1.html', _check_doc1)\n    _test_resource(\n        FakeHTML, 'doc1_UTF-16BE.html', _check_doc1, encoding='UTF-16BE')\n\n    with chdir(Path(__file__).parent):\n        path = Path('resources') / 'doc1.html'\n        string = path.read_text('utf-8')\n        _test_resource(FakeHTML, 'doc1.html', _check_doc1, base_url=path)\n        _check_doc1(FakeHTML(string=string, base_url=path))\n        _check_doc1(FakeHTML(string=string), has_base_url=False)\n        string_with_base = string.replace(\n            '<meta', '<base href=\"resources/\"><meta')\n        _check_doc1(FakeHTML(string=string_with_base, base_url='.'))\n        string_with_no_base = string.replace('<meta', '<base><meta')\n        _check_doc1(FakeHTML(string=string_with_no_base), has_base_url=False)\n\n\n@assert_no_logs\ndef test_css_parsing():\n    \"\"\"Test the constructor for the CSS class.\"\"\"\n    def check_css(css):\n        \"\"\"Check that a parsed stylsheet looks like resources/utf8-test.css\"\"\"\n        # Using 'encoding' adds a CSSCharsetRule\n        h1_rule, = css.matcher.lower_local_name_selectors['h1']\n        assert h1_rule[3] == 'before'\n        assert h1_rule[4][0][0][0] == 'content'\n        assert h1_rule[4][0][0][1][0][1] == 'I løvë Unicode'\n        assert h1_rule[4][0][1][0] == 'background_image'\n        assert h1_rule[4][0][1][1][0][0] == 'url'\n        assert h1_rule[4][0][1][1][0][1].startswith('file:')\n        assert h1_rule[4][0][1][1][0][1].endswith('tests/resources/pattern.png')\n\n    _test_resource(CSS, 'utf8-test.css', check_css)\n    _test_resource(CSS, 'latin1-test.css', check_css, encoding='latin1')\n\n\ndef check_png_pattern(assert_pixels_equal, png_bytes, x2=False, blank=False,\n                      rotated=False):\n    if blank:\n        expected_pixels = '''\n            ________\n            ________\n            ________\n            ________\n            ________\n            ________\n            ________\n            ________\n        '''\n    elif x2:\n        expected_pixels = '''\n            ________________\n            ________________\n            ________________\n            ________________\n            ____rrBBBBBB____\n            ____rrBBBBBB____\n            ____BBBBBBBB____\n            ____BBBBBBBB____\n            ____BBBBBBBB____\n            ____BBBBBBBB____\n            ____BBBBBBBB____\n            ____BBBBBBBB____\n            ________________\n            ________________\n            ________________\n            ________________\n        '''\n    elif rotated:\n        expected_pixels = '''\n            ________\n            ________\n            __BBBB__\n            __BBBB__\n            __BBBB__\n            __rBBB__\n            ________\n            ________\n        '''\n    else:\n        expected_pixels = '''\n            ________\n            ________\n            __rBBB__\n            __BBBB__\n            __BBBB__\n            __BBBB__\n            ________\n            ________\n        '''\n    image = Image.open(io.BytesIO(png_bytes))\n    width, height, pixels = parse_pixels(expected_pixels)\n    assert_pixels_equal(width, height, image.get_flattened_data(), pixels)\n\n\n@assert_no_logs\ndef test_python_render(assert_pixels_equal, tmp_path):\n    \"\"\"Test rendering with the Python API.\"\"\"\n    base_url = str(resource_path('dummy.html'))\n    html_string = '<body><img src=pattern.png>'\n    css_string = '''\n        @page { margin: 2px; size: 8px }\n        body { margin: 0; font-size: 0 }\n        img { image-rendering: pixelated }\n\n        @media screen { img { transform: rotate(-90deg) } }\n    '''\n    html = FakeHTML(string=html_string, base_url=base_url)\n    css = CSS(string=css_string)\n\n    png_bytes = html.write_png(stylesheets=[css])\n    pdf_bytes = html.write_pdf(stylesheets=[css])\n    assert png_bytes.startswith(b'\\211PNG\\r\\n\\032\\n')\n    assert pdf_bytes.startswith(b'%PDF')\n    check_png_pattern(assert_pixels_equal, png_bytes)\n\n    png_file = FakeFile()\n    html.write_png(png_file, stylesheets=[css])\n    assert png_file.getvalue() == png_bytes\n    pdf_file = FakeFile()\n    html.write_pdf(pdf_file, stylesheets=[css])\n    assert pdf_file.getvalue().startswith(b'%PDF')\n\n    png_path = tmp_path / '1.png'\n    pdf_path = tmp_path / '1.pdf'\n    html.write_png(png_path, stylesheets=[css])\n    html.write_pdf(pdf_path, stylesheets=[css])\n    assert png_path.read_bytes() == png_bytes\n    assert pdf_path.read_bytes().startswith(b'%PDF')\n\n    png_path = tmp_path / '2.png'\n    pdf_path = tmp_path / '2.pdf'\n    with png_path.open('wb') as png_fd:\n        html.write_png(png_fd, stylesheets=[css])\n    with pdf_path.open('wb') as pdf_fd:\n        html.write_pdf(pdf_fd, stylesheets=[css])\n    assert png_path.read_bytes() == png_bytes\n    assert pdf_path.read_bytes().startswith(b'%PDF')\n\n    x2_png_bytes = html.write_png(stylesheets=[css], resolution=192)\n    check_png_pattern(assert_pixels_equal, x2_png_bytes, x2=True)\n\n    screen_css = CSS(string=css_string, media_type='screen')\n    rotated_png_bytes = html.write_png(stylesheets=[screen_css])\n    check_png_pattern(assert_pixels_equal, rotated_png_bytes, rotated=True)\n\n    assert FakeHTML(\n        string=html_string, base_url=base_url, media_type='screen'\n    ).write_png(\n        stylesheets=[io.BytesIO(css_string.encode())]\n    ) == rotated_png_bytes\n    assert FakeHTML(\n        string=f'<style>{css_string}</style>{html_string}',\n        base_url=base_url, media_type='screen'\n    ).write_png() == rotated_png_bytes\n\n\n@assert_no_logs\ndef test_unknown_options():\n    with capture_logs() as logs:\n        pdf_bytes = FakeHTML(string='test').write_pdf(zoom=2, unknown=True)\n    assert len(logs) == 1\n    assert 'unknown' in logs[0]\n    assert pdf_bytes\n\n\n@assert_no_logs\ndef test_command_line_render(tmp_path):\n    css = b'''\n        @page { margin: 2px; size: 8px }\n        @media screen { img { transform: rotate(-90deg) } }\n        body { margin: 0; font-size: 0 }\n    '''\n    html = b'<body><img src=pattern.png>'\n    combined = b'<style>' + css + b'</style>' + html\n    linked = b'<link rel=stylesheet href=style.css>' + html\n    not_optimized = b'<body>a<img src=\"not-optimized.jpg\">'\n\n    for name in ('pattern.png', 'not-optimized.jpg'):\n        pattern_bytes = resource_path(name).read_bytes()\n        (tmp_path / name).write_bytes(pattern_bytes)\n\n    with chdir(tmp_path):\n        # Reference\n        html_obj = FakeHTML(\n            string=combined, base_url='dummy.html',\n            force_uncompressed_pdf=False)\n        pdf_bytes = html_obj.write_pdf()\n        rotated_pdf_bytes = FakeHTML(\n            string=combined, base_url='dummy.html',\n            media_type='screen', force_uncompressed_pdf=False).write_pdf()\n\n        (tmp_path / 'no_css.html').write_bytes(html)\n        (tmp_path / 'combined.html').write_bytes(combined)\n        (tmp_path / 'combined-UTF-16BE.html').write_bytes(\n            combined.decode().encode('UTF-16BE'))\n        (tmp_path / 'linked.html').write_bytes(linked)\n        (tmp_path / 'not_optimized.html').write_bytes(not_optimized)\n        (tmp_path / 'style.css').write_bytes(css)\n\n        _run('combined.html out2.pdf')\n        assert (tmp_path / 'out2.pdf').read_bytes() == pdf_bytes\n\n        _run('combined-UTF-16BE.html out3.pdf --encoding UTF-16BE')\n        assert (tmp_path / 'out3.pdf').read_bytes() == pdf_bytes\n\n        _run(f'{(tmp_path / \"combined.html\")} out4.pdf')\n        assert (tmp_path / 'out4.pdf').read_bytes() == pdf_bytes\n\n        _run(f'{path2url(tmp_path / \"combined.html\")} out5.pdf')\n        assert (tmp_path / 'out5.pdf').read_bytes() == pdf_bytes\n\n        _run('linked.html --debug out6.pdf')  # test relative URLs\n        assert (tmp_path / 'out6.pdf').read_bytes() == pdf_bytes\n\n        _run('combined.html --verbose out7')\n        _run('combined.html --quiet out8')\n        assert (tmp_path / 'out7').read_bytes() == pdf_bytes\n        assert (tmp_path / 'out8').read_bytes() == pdf_bytes\n\n        _run('no_css.html out9.pdf')\n        _run('no_css.html out10.pdf -s style.css')\n        assert (tmp_path / 'out9.pdf').read_bytes() != pdf_bytes\n        assert (tmp_path / 'out10.pdf').read_bytes() == pdf_bytes\n\n        stdout = _run('combined.html -')\n        assert stdout == pdf_bytes\n\n        _run('- out11.pdf', stdin=combined)\n        assert (tmp_path / 'out11.pdf').read_bytes() == pdf_bytes\n\n        stdout = _run('- -', stdin=combined)\n        assert stdout == pdf_bytes\n\n        _run('combined.html out13.pdf --media-type screen')\n        _run('combined.html out12.pdf -m screen')\n        _run('linked.html out14.pdf -m screen')\n        assert (tmp_path / 'out12.pdf').read_bytes() == rotated_pdf_bytes\n        assert (tmp_path / 'out13.pdf').read_bytes() == rotated_pdf_bytes\n        assert (tmp_path / 'out14.pdf').read_bytes() == rotated_pdf_bytes\n\n        os.environ['SOURCE_DATE_EPOCH'] = '0'\n        _run('not_optimized.html out15.pdf')\n        _run('not_optimized.html out16.pdf --optimize-images')\n        _run('not_optimized.html out17.pdf --optimize-images -j 10')\n        _run('not_optimized.html out18.pdf --optimize-images -j 10 -D 1')\n        _run('not_optimized.html out19.pdf --hinting')\n        _run('not_optimized.html out20.pdf --full-fonts')\n        _run('not_optimized.html out21.pdf --full-fonts --uncompressed-pdf')\n        _run(f'not_optimized.html out22.pdf -c {tmp_path}')\n        assert (\n            len((tmp_path / 'out18.pdf').read_bytes()) <\n            len((tmp_path / 'out17.pdf').read_bytes()) <\n            len((tmp_path / 'out16.pdf').read_bytes()) <\n            len((tmp_path / 'out15.pdf').read_bytes()) <\n            len((tmp_path / 'out19.pdf').read_bytes()) <\n            len((tmp_path / 'out20.pdf').read_bytes()) <\n            len((tmp_path / 'out21.pdf').read_bytes()))\n        assert len({\n            (tmp_path / f'out{i}.pdf').read_bytes()\n            for i in (15, 22)}) == 1\n        os.environ.pop('SOURCE_DATE_EPOCH')\n\n        stdout = _run('combined.html --uncompressed-pdf -')\n        assert stdout.count(b'Filespec') == 0\n        stdout = _run('combined.html --uncompressed-pdf -')\n        assert stdout.count(b'Filespec') == 0\n        stdout = _run('-a pattern.png --uncompressed-pdf combined.html -')\n        assert stdout.count(b'Filespec') == 1\n        stdout = _run(\n            '-a style.css -a pattern.png --uncompressed-pdf combined.html -')\n        assert stdout.count(b'Filespec') == 2\n\n        _run('combined.html out23.pdf --timeout 30')\n        assert (tmp_path / 'out23.pdf').read_bytes() == pdf_bytes\n\n    subdirectory = tmp_path / 'subdirectory'\n    subdirectory.mkdir()\n    with chdir(subdirectory):\n        with capture_logs() as logs:\n            stdout = _run('- -', stdin=combined)\n        assert len(logs) == 1\n        assert logs[0].startswith('ERROR: Failed to load image')\n        assert stdout.startswith(b'%PDF')\n\n        with capture_logs() as logs:\n            stdout = _run('--base-url= - -', stdin=combined)\n        assert len(logs) == 1\n        assert logs[0].startswith(\n            'ERROR: Relative URI reference without a base URI')\n        assert stdout.startswith(b'%PDF')\n\n        stdout = _run('--base-url .. - -', stdin=combined)\n        assert stdout == pdf_bytes\n\n    with pytest.raises(SystemExit):\n        _run('--info')\n\n    with pytest.raises(SystemExit):\n        _run('--version')\n\n\n@pytest.mark.parametrize(('version', 'pdf_version'), [\n    ('1a', '1.4'),\n    ('2b', '1.7'),\n    ('3u', '1.7'),\n    ('4e', '2.0'),\n])\n@assert_no_logs\ndef test_pdfa(version, pdf_version):\n    stdout = _run(\n        f'--pdf-variant=pdf/a-{version} --uncompressed-pdf - -', b'<html lang=en>test')\n    assert f'PDF-{pdf_version}'.encode() in stdout\n    assert f'part=\"{version[0]}\"'.encode() in stdout\n\n\n@pytest.mark.parametrize(('version', 'pdf_version'), [\n    ('1a', '1.4'),\n    ('2b', '1.7'),\n    ('3u', '1.7'),\n    ('4e', '2.0'),\n])\n@assert_no_logs\ndef test_pdfa_compressed(version, pdf_version):\n    stdout = _run(f'--pdf-variant=pdf/a-{version} - -', b'<html lang=en>test')\n    assert f'PDF-{pdf_version}'.encode() in stdout\n\n\ndef test_pdfa1b_cidset():\n    stdout = _run('--pdf-variant=pdf/a-1b --uncompressed-pdf - -', b'test')\n    assert b'PDF-1.4' in stdout\n    assert b'CIDSet' in stdout\n\n\n@pytest.mark.parametrize(('version', 'pdf_version'), [\n    (1, '1.7'),\n    (2, '2.0'),\n])\n@assert_no_logs\ndef test_pdfua(version, pdf_version):\n    stdout = _run(\n        f'--pdf-variant=pdf/ua-{version} --uncompressed-pdf - -', b'<html lang=en>test')\n    assert f'PDF-{pdf_version}'.encode() in stdout\n    assert f'part=\"{version}\"'.encode() in stdout\n\n\n@pytest.mark.parametrize(('version', 'pdf_version'), [\n    (1, '1.7'),\n    (2, '2.0'),\n])\n@assert_no_logs\ndef test_pdfua_compressed(version, pdf_version):\n    stdout = _run(f'--pdf-variant=pdf/ua-{version} - -', b'<html lang=en>test')\n    assert f'PDF-{pdf_version}'.encode() in stdout\n\n\n@assert_no_logs\ndef test_pdf_tags():\n    stdout = _run('--pdf-tags --uncompressed-pdf - -', b'<html lang=en><article>test')\n    assert b'/StructTreeRoot' in stdout\n    assert b'/Art' in stdout\n\n\n@assert_no_logs\ndef test_pdf_identifier():\n    stdout = _run('--pdf-identifier=abc --uncompressed-pdf - -', b'test')\n    assert b'abc' in stdout\n\n\n@assert_no_logs\ndef test_pdf_version():\n    stdout = _run('--pdf-version=1.4 --uncompressed-pdf - -', b'test')\n    assert b'PDF-1.4' in stdout\n\n\n@assert_no_logs\ndef test_pdf_custom_metadata():\n    stdout = _run(\n        '--custom-metadata --uncompressed-pdf - -',\n        b'<meta name=key content=value />')\n    assert b'/key' in stdout\n    assert b'value' in stdout\n\n\n@assert_no_logs\ndef test_bad_pdf_custom_metadata():\n    stdout = _run(\n        '--custom-metadata --uncompressed-pdf - -',\n        '<meta name=é content=value />'.encode('latin1'))\n    assert b'value' not in stdout\n\n\n@assert_no_logs\ndef test_partial_pdf_custom_metadata():\n    stdout = _run(\n        '--custom-metadata --uncompressed-pdf - -',\n        '<meta name=a.b/céd0 content=value />'.encode('latin1'))\n    assert b'/abcd0' in stdout\n    assert b'value' in stdout\n\n\n@assert_no_logs\ndef test_pdf_srgb():\n    stdout = _run('--srgb --uncompressed-pdf - -', b'test')\n    assert b'sRGB' in stdout\n\n\n@assert_no_logs\ndef test_pdf_no_srgb():\n    stdout = _run('--uncompressed-pdf - -', b'test')\n    assert b'sRGB' not in stdout\n\n\n@assert_no_logs\ndef test_pdf_font_name():\n    # Regression test for #2396.\n    stdout = _run('--uncompressed-pdf - -', b'<div style=\"font-family:weasyprint\">test')\n    assert b'+weasyprint/' in stdout\n\n\n@assert_no_logs\ndef test_to_unicode():\n    # Regression test for #2388.\n    stdout = _run('--uncompressed-pdf --full-fonts - -', b'test')\n    matches = re.findall(b'(\\\\d+) beginbfchar', stdout)\n    assert matches\n    for match in matches:\n        assert int(match) <= 100\n\n\n@assert_no_logs\ndef test_to_unicode_rtl():\n    # Regression test for #378.\n    stdout = _run(\n        '--uncompressed-pdf -e utf-8 - -',\n        '<div style=\"font-family: weasyprint\">اب'.encode())\n    assert b'<00cf> <0627>' in stdout\n    assert b'<00d0> <0628>' in stdout\n\n\n@assert_no_logs\ndef test_no_redirect():\n    with http_server() as root_url:\n        _run(f'--no-http-redirect {root_url}/gzip -')\n        with pytest.raises(URLFetchingError):\n            _run(f'--no-http-redirect {root_url}/bad -')\n        with pytest.raises(URLFetchingError):\n            _run(f'--no-http-redirect {root_url}/redirect -')\n        with capture_logs() as logs:\n            _run(\n                '--no-http-redirect - -',\n                f'<link rel=stylesheet href=\"{root_url}/redirect\">'.encode())\n        assert len(logs) == 1\n        assert 'Failed to load stylesheet' in logs[0]\n\n\n@assert_no_logs\ndef test_fail_on_error():\n    with http_server() as root_url:\n        _run(f'--fail-on-http-error {root_url}/gzip -')\n        _run(f'--fail-on-http-error {root_url}/redirect -')\n        with pytest.raises(FatalURLFetchingError):\n            _run(f'--fail-on-http-error {root_url}/bad -')\n        with pytest.raises(FatalURLFetchingError):\n            _run(\n                '--fail-on-http-error - -',\n                f'<link rel=stylesheet href=\"{root_url}/bad\">'.encode())\n\n\n@assert_no_logs\ndef test_no_redirect_fail_on_error():\n    with http_server() as root_url:\n        _run(f'--no-http-redirect --fail-on-http-error {root_url}/gzip -')\n        with pytest.raises(FatalURLFetchingError):\n            _run(f'--no-http-redirect --fail-on-http-error {root_url}/redirect -')\n        with pytest.raises(FatalURLFetchingError):\n            _run(f'--no-http-redirect --fail-on-http-error {root_url}/bad -')\n        with pytest.raises(FatalURLFetchingError):\n            _run(\n                '--no-http-redirect --fail-on-http-error - -',\n                f'<link rel=stylesheet href=\"{root_url}/bad\">'.encode())\n\n\n@assert_no_logs\n@pytest.mark.parametrize('command', [\n    '- -',\n    '--allowed-protocols file - -',\n    '--allowed-protocols file,http - -',\n    '--allowed-protocols file,file - -',\n    '--allowed-protocols Http,File - -',\n])\ndef test_allowed_protocols(command):\n    _run(command, f'<img src=\"{path2url(resource_path(\"pattern.png\"))}\">'.encode())\n\n\n@assert_no_logs\n@pytest.mark.parametrize('command', [\n    '- -',\n    '--allowed-protocols data - -',\n    '--allowed-protocols File,Data - -',\n])\ndef test_allowed_protocols_data(command):\n    data = b64encode(resource_path('pattern.png').read_bytes()).decode()\n    _run(command, f'<img src=\"data:image/png;base64,{data}\">'.encode())\n\n\n@pytest.mark.parametrize('command', [\n    '--allowed-protocols http - -',\n    '--allowed-protocols http,https - -',\n    '--allowed-protocols filetest - -',\n    '--allowed-protocols \"\" - -',\n])\ndef test_disallowed_protocols(command):\n    with capture_logs() as logs:\n        _run(command, f'<img src=\"{path2url(resource_path(\"pattern.png\"))}\">'.encode())\n    assert len(logs) == 1\n    assert 'URI uses disallowed protocol' in logs[0]\n\n\n@assert_no_logs\ndef test_redirect_loop():\n    with http_server() as root_url:\n        with pytest.raises(URLFetchingError, match='infinite loop'):\n            _run(f'{root_url}/redirect-loop -')\n\n\n@pytest.mark.parametrize(('html', 'fields'), [\n    ('<input>', ['/Tx', '/V ()']),\n    ('<input value=\"\">', ['/Tx', '/V ()']),\n    ('<input type=\"checkbox\">', ['/Btn']),\n    ('<input type=\"radio\">',\n     ['/Btn', '/V /Off', '/AS /Off', '/Ff 49152']),\n    ('<input checked type=\"radio\" name=\"foo\" value=\"value\">',\n     ['/Btn', '/T (foo)', '/V /0', '/AS /0']),\n    ('<form><input type=\"radio\" name=\"foo\" value=\"v0\"></form>'\n     '<form><input checked type=\"radio\" name=\"foo\" value=\"v1\"></form>',\n     ['/Btn', '/AS /0', '/V /0', '/AS /Off', '/V /Off']),\n    ('<textarea></textarea>', ['/Tx', '/V ()']),\n    ('<select><option value=\"a\">A</option></select>', ['/Ch', '/Opt']),\n    ('<select>'\n     '<option value=\"a\">A</option>'\n     '<option value=\"b\" selected>B</option>'\n     '</select>', ['/Ch', '/Opt', '/V (b)']),\n    ('<select multiple>'\n     '<option value=\"a\">A</option>'\n     '<option value=\"b\" selected>B</option>'\n     '<option value=\"c\" selected>C</option>'\n     '</select>', ['/Ch', '/Opt', '[(b) (c)]']),\n])\ndef test_pdf_inputs(html, fields):\n    stdout = _run('--pdf-forms --uncompressed-pdf - -', html.encode())\n    assert b'AcroForm' in stdout\n    for field in fields:\n        assert field.encode() in stdout\n    stdout = _run('--uncompressed-pdf - -', html.encode())\n    assert b'AcroForm' not in stdout\n\n\n@pytest.mark.parametrize(('css', 'with_forms', 'without_forms'), [\n    ('appearance: auto', True, True),\n    ('appearance: none', False, False),\n    ('', True, False),\n])\ndef test_appearance(css, with_forms, without_forms):\n    html = f'<input style=\"{css}\">'.encode()\n    assert with_forms is (\n        b'AcroForm' in _run('--pdf-forms --uncompressed-pdf - -', html))\n    assert without_forms is (\n        b'AcroForm' in _run(' --uncompressed-pdf - -', html))\n\n\ndef test_appearance_non_input():\n    html = b'<div style=\"appearance: auto\">'\n    assert b'AcroForm' not in _run('--pdf-forms --uncompressed-pdf - -', html)\n\n\ndef test_reproducible():\n    os.environ['SOURCE_DATE_EPOCH'] = '0'\n    stdout1 = _run('- -', b'<body>a<img src=pattern.png>')\n    stdout2 = _run('- -', b'<body>a<img src=pattern.png>')\n    os.environ.pop('SOURCE_DATE_EPOCH')\n    assert stdout1 == stdout2\n\n\n@assert_no_logs\ndef test_unicode_filenames(assert_pixels_equal, tmp_path):\n    \"\"\"Test non-ASCII filenames both in Unicode or bytes form.\"\"\"\n    # Replicate pattern.png in CSS so that base_url does not matter.\n    html = b'''\n        <style>\n            @page { margin: 2px; size: 8px }\n            html { background: #00f; }\n            body { background: #f00; width: 1px; height: 1px }\n        </style>\n        <body>\n    '''\n    png_bytes = FakeHTML(string=html).write_png()\n    check_png_pattern(assert_pixels_equal, png_bytes)\n    unicode_filename = 'Unicödé'\n    if sys.platform.startswith('darwin'):  # pragma: no cover\n        unicode_filename = unicodedata.normalize('NFD', unicode_filename)\n\n    with chdir(tmp_path):\n        (tmp_path / unicode_filename).write_bytes(html)\n        bytes_file, = tuple(tmp_path.iterdir())\n        assert bytes_file.name == unicode_filename\n\n        assert FakeHTML(unicode_filename).write_png() == png_bytes\n        assert FakeHTML(bytes_file).write_png() == png_bytes\n\n        os.remove(unicode_filename)\n        assert not tuple(tmp_path.iterdir())\n\n        FakeHTML(string=html).write_png(unicode_filename)\n        assert bytes_file.read_bytes() == png_bytes\n\n\n@assert_no_logs\ndef test_low_level_api(assert_pixels_equal):\n    html = FakeHTML(string='<body>')\n    css = CSS(string='''\n        @page { margin: 2px; size: 8px }\n        html { background: #00f; }\n        body { background: #f00; width: 1px; height: 1px }\n    ''')\n    pdf_bytes = html.write_pdf(stylesheets=[css])\n    assert pdf_bytes.startswith(b'%PDF')\n\n    png_bytes = html.write_png(stylesheets=[css])\n    document = html.render(stylesheets=[css])\n    page, = document.pages\n    assert page.width == 8\n    assert page.height == 8\n    assert document.write_png() == png_bytes\n    assert document.copy([page]).write_png() == png_bytes\n\n    document = html.render(stylesheets=[css])\n    page, = document.pages\n    assert (page.width, page.height) == (8, 8)\n    png_bytes = document.write_png(resolution=192)\n    check_png_pattern(assert_pixels_equal, png_bytes, x2=True)\n\n    document = html.render(stylesheets=[css])\n    page, = document.pages\n    assert (page.width, page.height) == (8, 8)\n    # A resolution that is not multiple of 96:\n    assert _png_size(document.write_png(resolution=145.2)) == (12, 12)\n\n    document = FakeHTML(string='''\n        <style>\n            @page:first { size: 5px 10px } @page { size: 6px 4px }\n            p { page-break-before: always }\n        </style>\n        <p></p>\n        <p></p>\n    ''').render()\n    page_1, page_2 = document.pages\n    assert (page_1.width, page_1.height) == (5, 10)\n    assert (page_2.width, page_2.height) == (6, 4)\n\n    result = document.write_png()\n    # (Max of both widths, Sum of both heights)\n    assert _png_size(result) == (6, 14)\n    assert document.copy([page_1, page_2]).write_png() == result\n    assert _png_size(document.copy([page_1]).write_png()) == (5, 10)\n    assert _png_size(document.copy([page_2]).write_png()) == (6, 4)\n\n\n@pytest.mark.parametrize(('html', 'expected_by_page', 'expected_tree', 'round'), [\n    ('''\n        <style>h1, h2, h3, h4 { height: 10px }</style>\n        <h1>a</h1>\n        <h4 style=\"page-break-after: always\">b</h4>\n        <h3 style=\"position: relative; top: 2px; left: 3px\">c</h3>\n        <h2>d</h2>\n        <h1>e</h1>\n    ''', [\n        [(1, 'a', (0, 0), 'open'), (4, 'b', (0, 10), 'open')],\n        [(3, 'c', (3, 2), 'open'), (2, 'd', (0, 10), 'open'),\n         (1, 'e', (0, 20), 'open')],\n    ], [\n        ('a', (0, 0, 0), [\n            ('b', (0, 0, 10), [], 'open'),\n            ('c', (1, 3, 2), [], 'open'),\n            ('d', (1, 0, 10), [], 'open')], 'open'),\n        ('e', (1, 0, 20), [], 'open'),\n    ], False),\n    ('''\n        <style>\n            h1, h2, h3, span { height: 90px; margin: 0 0 10px 0 }\n        </style>\n        <h1>Title 1</h1>\n        <h1>Title 2</h1>\n        <h2 style=\"position: relative; left: 20px\">Title 3</h2>\n        <h2>Title 4</h2>\n        <h3>Title 5</h3>\n        <span style=\"display: block; page-break-before: always\"></span>\n        <h2>Title 6</h2>\n        <h1>Title 7</h1>\n        <h2>Title 8</h2>\n        <h3>Title 9</h3>\n        <h1>Title 10</h1>\n        <h2>Title 11</h2>\n    ''', [\n        [\n            (1, 'Title 1', (0, 0), 'open'),\n            (1, 'Title 2', (0, 100), 'open'),\n            (2, 'Title 3', (20, 200), 'open'),\n            (2, 'Title 4', (0, 300), 'open'),\n            (3, 'Title 5', (0, 400), 'open')\n        ], [\n            (2, 'Title 6', (0, 100), 'open'),\n            (1, 'Title 7', (0, 200), 'open'),\n            (2, 'Title 8', (0, 300), 'open'),\n            (3, 'Title 9', (0, 400), 'open'),\n            (1, 'Title 10', (0, 500), 'open'),\n            (2, 'Title 11', (0, 600), 'open')\n        ],\n    ], [\n        ('Title 1', (0, 0, 0), [], 'open'),\n        ('Title 2', (0, 0, 100), [\n            ('Title 3', (0, 20, 200), [], 'open'),\n            ('Title 4', (0, 0, 300), [\n                ('Title 5', (0, 0, 400), [], 'open')], 'open'),\n            ('Title 6', (1, 0, 100), [], 'open')], 'open'),\n        ('Title 7', (1, 0, 200), [\n            ('Title 8', (1, 0, 300), [\n                ('Title 9', (1, 0, 400), [], 'open')], 'open')], 'open'),\n        ('Title 10', (1, 0, 500), [\n            ('Title 11', (1, 0, 600), [], 'open')], 'open'),\n    ], False),\n    ('''\n        <style>* { height: 10px }</style>\n        <h2>A</h2> <p>depth 1</p>\n        <h4>B</h4> <p>depth 2</p>\n        <h2>C</h2> <p>depth 1</p>\n        <h3>D</h3> <p>depth 2</p>\n        <h4>E</h4> <p>depth 3</p>\n    ''', [[\n        (2, 'A', (0, 0), 'open'),\n        (4, 'B', (0, 20), 'open'),\n        (2, 'C', (0, 40), 'open'),\n        (3, 'D', (0, 60), 'open'),\n        (4, 'E', (0, 80), 'open'),\n    ]], [\n        ('A', (0, 0, 0), [\n            ('B', (0, 0, 20), [], 'open')], 'open'),\n        ('C', (0, 0, 40), [\n            ('D', (0, 0, 60), [\n                ('E', (0, 0, 80), [], 'open')], 'open')], 'open'),\n    ], False),\n    ('''\n        <style>* { height: 10px; font-size: 0 }</style>\n        <h2>A</h2> <p>h2 depth 1</p>\n        <h4>B</h4> <p>h4 depth 2</p>\n        <h3>C</h3> <p>h3 depth 2</p>\n        <h5>D</h5> <p>h5 depth 3</p>\n        <h1>E</h1> <p>h1 depth 1</p>\n        <h2>F</h2> <p>h2 depth 2</p>\n        <h2>G</h2> <p>h2 depth 2</p>\n        <h4>H</h4> <p>h4 depth 3</p>\n        <h1>I</h1> <p>h1 depth 1</p>\n    ''', [[\n        (2, 'A', (0, 0), 'open'),\n        (4, 'B', (0, 20), 'open'),\n        (3, 'C', (0, 40), 'open'),\n        (5, 'D', (0, 60), 'open'),\n        (1, 'E', (0, 70), 'open'),\n        (2, 'F', (0, 90), 'open'),\n        (2, 'G', (0, 110), 'open'),\n        (4, 'H', (0, 130), 'open'),\n        (1, 'I', (0, 150), 'open'),\n    ]], [\n        ('A', (0, 0, 0), [\n            ('B', (0, 0, 20), [], 'open'),\n            ('C', (0, 0, 40), [\n                ('D', (0, 0, 60), [], 'open')], 'open')], 'open'),\n        ('E', (0, 0, 70), [\n            ('F', (0, 0, 90), [], 'open'),\n            ('G', (0, 0, 110), [\n                ('H', (0, 0, 130), [], 'open')], 'open')], 'open'),\n        ('I', (0, 0, 150), [], 'open'),\n    ], False),\n    ('<h1>é', [\n        [(1, 'é', (0, 0), 'open')]\n    ], [\n        ('é', (0, 0, 0), [], 'open')\n    ], False),\n    ('''\n        <h1 style=\"transform: translateX(50px)\">!\n    ''', [\n        [(1, '!', (50, 0), 'open')]\n    ], [\n        ('!', (0, 50, 0), [], 'open')\n    ], False),\n    ('''\n        <style>\n          img { display: block; bookmark-label: attr(alt); bookmark-level: 1 }\n        </style>\n        <img src=\"%s\" alt=\"Chocolate\" />\n    ''' % path2url(resource_path('pattern.png')),\n     [[(1, 'Chocolate', (0, 0), 'open')]],\n     [('Chocolate', (0, 0, 0), [], 'open')], False),\n    ('''\n        <h1 style=\"transform-origin: 0 0;\n                   transform: rotate(90deg) translateX(50px)\">!\n    ''', [[(1, '!', (0, 50), 'open')]], [('!', (0, 0, 50), [], 'open')], True),\n    ('''\n        <body style=\"transform-origin: 0 0; transform: rotate(90deg)\">\n        <h1 style=\"transform: translateX(50px)\">!\n    ''', [[(1, '!', (0, 50), 'open')]], [('!', (0, 0, 50), [], 'open')], True),\n    ('''\n        <body>\n        <h1 style=\"width: 10px; line-height: 10px;\n                   transform: skew(45deg, 45deg)\">!\n    ''', [[(1, '!', (-5, -5), 'open')]], [('!', (0, -5, -5), [], 'open')],\n     True),\n])\n@assert_no_logs\ndef test_assert_bookmarks(html, expected_by_page, expected_tree, round):\n    document = FakeHTML(string=html).render()\n    if round:\n        _round_meta(document.pages)\n    assert [page.bookmarks for page in document.pages] == expected_by_page\n    assert document.make_bookmark_tree() == expected_tree\n\n\ndef simplify_links(links):\n    return [\n        (link_type, link_target, rectangle)\n        for link_type, link_target, rectangle, box in links]\n\n\ndef assert_links(html, links, anchors, resolved_links,\n                 base_url=resource_path('<inline HTML>'), warnings=(),\n                 round=False):\n    with capture_logs() as logs:\n        document = FakeHTML(string=html, base_url=base_url).render()\n        if round:\n            _round_meta(document.pages)\n        document_resolved_links = [\n            (simplify_links(page_links), page_anchors)\n            for page_links, page_anchors in resolve_links(document.pages)]\n    assert len(logs) == len(warnings)\n    for message, expected in zip(logs, warnings):\n        assert expected in message\n    document_links = [simplify_links(page.links) for page in document.pages]\n    document_anchors = [page.anchors for page in document.pages]\n    assert document_links == links\n    assert document_anchors == anchors\n    assert document_resolved_links == resolved_links\n\n\n@assert_no_logs\ndef test_links_1():\n    assert_links('''\n        <style>\n            body { font-size: 10px; line-height: 2; width: 200px }\n            p { height: 90px; margin: 0 0 10px 0 }\n            img { width: 30px; vertical-align: top }\n        </style>\n        <p><a href=\"https://weasyprint.org\"><img src=pattern.png></a></p>\n        <p style=\"padding: 0 10px\"><a\n            href=\"#lipsum\"><img style=\"border: solid 1px\"\n                                src=pattern.png></a></p>\n        <p id=hello>Hello, World</p>\n        <p id=lipsum>\n            <a style=\"display: block; page-break-before: always; height: 30px\"\n               href=\"#hel%6Co\"></a>\n        </p>\n    ''', [\n        [\n            ('external', 'https://weasyprint.org', (0, 0, 30, 20)),\n            ('external', 'https://weasyprint.org', (0, 0, 30, 30)),\n            ('internal', 'lipsum', (10, 100, 42, 120)),\n            ('internal', 'lipsum', (10, 100, 42, 132))\n        ],\n        [('internal', 'hello', (0, 0, 200, 30))],\n    ], [\n        {'hello': (0, 200, 200, 290)},\n        {'lipsum': (0, 0, 200, 90)}\n    ], [\n        (\n            [\n                ('external', 'https://weasyprint.org', (0, 0, 30, 20)),\n                ('external', 'https://weasyprint.org', (0, 0, 30, 30)),\n                ('internal', 'lipsum', (10, 100, 42, 120)),\n                ('internal', 'lipsum', (10, 100, 42, 132))\n            ],\n            [('hello', 0, 200)],\n        ),\n        (\n            [('internal', 'hello', (0, 0, 200, 30))],\n            [('lipsum', 0, 0)]),\n    ])\n\n\n@assert_no_logs\ndef test_links_2():\n    assert_links(\n        '''\n            <body style=\"width: 200px\">\n            <a href=\"../lipsum/é_%E9\" style=\"display: block; margin: 10px 5px\">\n        ''', [[('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9',\n                (5, 10, 195, 10))]],\n        [{}], [([('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9',\n                  (5, 10, 195, 10))], [])],\n        base_url='https://weasyprint.org/foo/bar/')\n\n\n@assert_no_logs\ndef test_links_3():\n    assert_links(\n        '''\n            <body style=\"width: 200px\">\n            <div style=\"display: block; margin: 10px 5px;\n                        -weasy-link: url(../lipsum/é_%E9)\">\n        ''', [[('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9',\n                (5, 10, 195, 10))]],\n        [{}], [([('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9',\n                  (5, 10, 195, 10))], [])],\n        base_url='https://weasyprint.org/foo/bar/')\n\n\n@assert_no_logs\ndef test_links_4():\n    # Relative URI reference without a base URI: allowed for links\n    assert_links(\n        '''\n            <body style=\"width: 200px\">\n            <a href=\"../lipsum\" style=\"display: block; margin: 10px 5px\">\n        ''', [[('external', '../lipsum', (5, 10, 195, 10))]], [{}],\n        [([('external', '../lipsum', (5, 10, 195, 10))], [])],\n        base_url=None)\n\n\n@assert_no_logs\ndef test_links_5():\n    # Relative URI reference without a base URI: not supported for -weasy-link\n    assert_links(\n        '''\n            <body style=\"width: 200px\">\n            <div style=\"-weasy-link: url(../lipsum);\n                        display: block; margin: 10px 5px\">\n        ''', [[]], [{}], [([], [])], base_url=None, warnings=[\n            'WARNING: Ignored `-weasy-link: url(../lipsum)` at 1:1, '\n            'Relative URI reference without a base URI'])\n\n\n@assert_no_logs\ndef test_links_6():\n    # Internal or absolute URI reference without a base URI: OK\n    assert_links(\n        '''\n            <body style=\"width: 200px\">\n            <a href=\"#lipsum\" id=\"lipsum\"\n                style=\"display: block; margin: 10px 5px\"></a>\n            <a href=\"https://weasyprint.org/\" style=\"display: block\"></a>\n        ''', [[\n            ('internal', 'lipsum', (5, 10, 195, 10)),\n            ('external', 'https://weasyprint.org/', (0, 10, 200, 10))]],\n        [{'lipsum': (5, 10, 195, 10)}],\n        [([('internal', 'lipsum', (5, 10, 195, 10)),\n           ('external', 'https://weasyprint.org/', (0, 10, 200, 10))],\n          [('lipsum', 5, 10)])],\n        base_url=None)\n\n\n@assert_no_logs\ndef test_links_7():\n    assert_links(\n        '''\n            <body style=\"width: 200px\">\n            <div style=\"-weasy-link: url(#lipsum);\n                        margin: 10px 5px\" id=\"lipsum\">\n        ''',\n        [[('internal', 'lipsum', (5, 10, 195, 10))]],\n        [{'lipsum': (5, 10, 195, 10)}],\n        [([('internal', 'lipsum', (5, 10, 195, 10))], [('lipsum', 5, 10)])],\n        base_url=None)\n\n\n@assert_no_logs\ndef test_links_8():\n    assert_links(\n        '''\n            <style> a { display: block; height: 15px } </style>\n            <body style=\"width: 200px\">\n                <a href=\"#lipsum\"></a>\n                <a href=\"#missing\" id=\"lipsum\"></a>\n        ''',\n        [[('internal', 'lipsum', (0, 0, 200, 15)),\n          ('internal', 'missing', (0, 15, 200, 30))]],\n        [{'lipsum': (0, 15, 200, 30)}],\n        [([('internal', 'lipsum', (0, 0, 200, 15))], [('lipsum', 0, 15)])],\n        base_url=None,\n        warnings=[\n            'ERROR: No anchor #missing for internal URI reference'])\n\n\n@assert_no_logs\ndef test_links_9():\n    assert_links(\n        '''\n            <body style=\"width: 100px; transform: translateY(100px)\">\n            <a href=\"#lipsum\" id=\"lipsum\" style=\"display: block; height: 20px;\n                transform: rotate(90deg) scale(2)\">\n        ''',\n        [[('internal', 'lipsum', (30, 10, 70, 210))]],\n        [{'lipsum': (70, 10, 30, 210)}],\n        [([('internal', 'lipsum', (30, 10, 70, 210))], [('lipsum', 70, 10)])],\n        round=True)\n\n\n@assert_no_logs\ndef test_links_10():\n    # Download for attachment\n    assert_links(\n        '''\n            <body style=\"width: 200px\">\n            <a rel=attachment href=\"pattern.png\" download=\"wow.png\"\n                style=\"display: block; margin: 10px 5px\">\n        ''', [[('attachment', 'pattern.png', (5, 10, 195, 10))]],\n        [{}], [([('attachment', 'pattern.png', (5, 10, 195, 10))], [])],\n        base_url=None)\n\n\n@assert_no_logs\ndef test_links_11():\n    # Attachment with missing href\n    assert_links(\n        '''\n            <body style=\"width: 200px\">\n            <a rel=attachment download=\"wow.png\"\n                style=\"display: block; margin: 10px 5px\">\n        ''', [[]], [{}], [([], [])], base_url=None)\n\n\n@assert_no_logs\ndef test_links_12():\n    # Absolute URI with no fragment and the same base URI: keep external URI\n    # Regression test for #1767.\n    assert_links(\n        '''\n            <body style=\"width: 200px\">\n            <a href=\"https://weasyprint.org\"\n               style=\"display: block; margin: 10px 5px\">\n        ''',\n        [[('external', 'https://weasyprint.org', (5, 10, 195, 10))]], [{}],\n        [([('external', 'https://weasyprint.org', (5, 10, 195, 10))], [])],\n        base_url='https://weasyprint.org')\n\n\n# Make relative URL references work with our custom URL scheme.\nuses_relative.append('weasyprint-custom')\n\n\n@pytest.mark.filterwarnings('ignore')\n@assert_no_logs\ndef test_deprecated_url_fetcher(assert_pixels_equal):\n    path = resource_path('pattern.png')\n    pattern_png = path.read_bytes()\n\n    def fetcher(url):\n        if url == 'weasyprint-custom:foo/%C3%A9_%e9_pattern':\n            return {'string': pattern_png, 'mime_type': 'image/png'}\n        elif url == 'weasyprint-custom:foo/bar.css':\n            return {\n                'string': 'body { background: url(é_%e9_pattern)',\n                'mime_type': 'text/css'}\n        elif url == 'weasyprint-custom:foo/bar.no':\n            return {\n                'string': 'body { background: red }',\n                'mime_type': 'text/no'}\n        else:\n            return default_url_fetcher(url)\n\n    base_url = str(resource_path('dummy.html'))\n    css = CSS(string='''\n        @page { size: 8px; margin: 2px }\n        body { margin: 0; font-size: 0 }\n    ''', base_url=base_url)\n\n    def test(html, blank=False):\n        html = FakeHTML(string=html, url_fetcher=fetcher, base_url=base_url)\n        check_png_pattern(\n            assert_pixels_equal, html.write_png(stylesheets=[css]),\n            blank=blank)\n\n    test('<body><img src=\"pattern.png\">')  # Test a \"normal\" URL\n    test(f'<body><img src=\"{path.as_uri()}\">')\n    test(f'<body><img src=\"{path.as_uri()}?ignored\">')\n    test('<body><img src=\"weasyprint-custom:foo/é_%e9_pattern\">')\n    test('<body style=\"background: url(weasyprint-custom:foo/é_%e9_pattern)\">')\n    test('<body><li style=\"list-style: inside '\n         'url(weasyprint-custom:foo/é_%e9_pattern)\">')\n    test('<link rel=stylesheet href=\"weasyprint-custom:foo/bar.css\"><body>')\n    test('<style>@import \"weasyprint-custom:foo/bar.css\";</style><body>')\n    test('<style>@import url(weasyprint-custom:foo/bar.css);</style><body>')\n    test('<style>@import url(\"weasyprint-custom:foo/bar.css\");</style><body>')\n\n    with capture_logs() as logs:\n        test('<body><img src=\"custom:foo/bar\">', blank=True)\n    assert len(logs) == 1\n    assert logs[0].startswith(\n        \"ERROR: Failed to load image at 'custom:foo/bar'\")\n\n    with capture_logs() as logs:\n        test(\n            '<link rel=stylesheet href=\"weasyprint-custom:foo/bar.css\">'\n            '<link rel=stylesheet href=\"weasyprint-custom:foo/bar.no\"><body>')\n    assert len(logs) == 1\n    assert logs[0].startswith('ERROR: Unsupported stylesheet type text/no')\n\n    def fetcher_2(url):\n        assert url == 'weasyprint-custom:%C3%A9_%e9.css'\n        return {'string': '', 'mime_type': 'text/css'}\n    FakeHTML(\n        string='<link rel=stylesheet href=\"weasyprint-custom:é_%e9.css\"><body',\n        url_fetcher=fetcher_2).render()\n\n\nclass Fetcher(URLFetcher):\n    def fetch(self, url, headers=None):\n        if url.startswith('fatal:'):\n            raise FatalURLFetchingError('Fatal error')\n        elif 'forbidden' in url:\n            raise FatalURLFetchingError('Forbidden URL')\n        elif url.startswith('bad:'):\n            raise ValueError('Bad protocol')\n        elif url == 'redirect:':\n            return self.fetch('weasyprint-custom:foo/bar.css')\n        elif url == 'weasyprint-custom:foo/%C3%A9_%e9_pattern':\n            pattern_png = resource_path('pattern.png').read_bytes()\n            return URLFetcherResponse(url, pattern_png, {'Content-Type': 'image/png'})\n        elif url == 'weasyprint-custom:foo/bar.css':\n            return URLFetcherResponse(\n                url, 'body { background: url(é_%e9_pattern)'.encode('iso-8859-15'),\n                {'Content-Type': 'text/css;charset=iso-8859-15'})\n        elif url == 'weasyprint-custom:foo/bar.no':\n            return URLFetcherResponse(\n                url, 'body { background: red }', {'Content-Type': 'text/no'})\n        else:\n            return super().fetch(url, headers)\n\n\n@pytest.mark.parametrize('html', [\n    '<body><img src=\"pattern.png\">',\n    f'<body><img src=\"{resource_path(\"pattern.png\").as_uri()}\">',\n    f'<body><img src=\"{resource_path(\"pattern.png\").as_uri()}?ignored\">',\n    '<body><img src=\"weasyprint-custom:foo/é_%e9_pattern\">',\n    '<body style=\"background: url(weasyprint-custom:foo/é_%e9_pattern)\">',\n    '<body><li style=\"list-style: inside url(weasyprint-custom:foo/é_%e9_pattern)\">',\n    '<link rel=stylesheet href=\"weasyprint-custom:foo/bar.css\"><body>',\n    '<link rel=stylesheet href=\"redirect:\"><body>',\n    '<style>@import \"weasyprint-custom:foo/bar.css\";</style><body>',\n    '<style>@import url(weasyprint-custom:foo/bar.css);</style><body>',\n    '<style>@import url(\"weasyprint-custom:foo/bar.css\");</style><body>',\n])\n@assert_no_logs\ndef test_url_fetcher(html, assert_pixels_equal):\n    base_url = str(resource_path('dummy.html'))\n    css = CSS(string='@page{size:8px;margin:2px} body{font-size:0}', base_url=base_url)\n    html = FakeHTML(string=html, url_fetcher=Fetcher(), base_url=base_url)\n    check_png_pattern(assert_pixels_equal, html.write_png(stylesheets=[css]))\n\n\n@assert_no_logs\ndef test_url_fetcher_default(assert_pixels_equal):\n    html = '<body><img src=\"pattern.png\">'\n    base_url = str(resource_path('dummy.html'))\n    css = CSS(string='@page{size:8px;margin:2px} body{font-size:0}', base_url=base_url)\n    html = FakeHTML(string=html, url_fetcher=URLFetcher(), base_url=base_url)\n    check_png_pattern(assert_pixels_equal, html.write_png(stylesheets=[css]))\n\n\n@assert_no_logs\ndef test_url_fetcher_bad_image(assert_pixels_equal):\n    string = '<body><img src=\"custom:foo/bar\">'\n    css = CSS(string='@page { size: 8px; margin: 2px } body { font-size: 0 }')\n    with capture_logs() as logs:\n        html = FakeHTML(string=string, url_fetcher=Fetcher())\n        png = html.write_png(stylesheets=[css])\n    assert len(logs) == 1\n    assert logs[0].startswith(\"ERROR: Failed to load image at 'custom:foo/bar'\")\n    check_png_pattern(assert_pixels_equal, png, blank=True)\n\n\n@assert_no_logs\ndef test_url_fetcher_bad_stylesheet(assert_pixels_equal):\n    string = (\n        '<link rel=stylesheet href=\"weasyprint-custom:foo/bar.css\">'\n        '<link rel=stylesheet href=\"weasyprint-custom:foo/bar.no\"><body>')\n    with capture_logs() as logs:\n        FakeHTML(string=string, url_fetcher=Fetcher()).write_png()\n    assert len(logs) == 1\n    assert logs[0].startswith('ERROR: Unsupported stylesheet type text/no')\n\n\n@assert_no_logs\ndef test_url_fetcher_non_fatal_error(assert_pixels_equal):\n    string = '<link rel=stylesheet href=\"bad:foo/bar.no\"><body>'\n    with capture_logs() as logs:\n        FakeHTML(string=string, url_fetcher=Fetcher()).write_png()\n    assert len(logs) == 1\n    assert 'Bad protocol' in logs[0]\n\n\n@assert_no_logs\ndef test_url_fetcher_fatal_error(assert_pixels_equal):\n    string = '<link rel=stylesheet href=\"fatal:foo/bar.no\"><body>'\n    with pytest.raises(FatalURLFetchingError, match='Fatal error'):\n        FakeHTML(string=string, url_fetcher=Fetcher()).write_png()\n\n\n@assert_no_logs\ndef test_url_fetcher_default_fail_on_errors(assert_pixels_equal):\n    html = '<body><img src=\"unknown.png\">'\n    base_url = str(resource_path('dummy.html'))\n    url_fetcher = URLFetcher(fail_on_errors=True)\n    with pytest.raises(FatalURLFetchingError, match='Error fetching'):\n        FakeHTML(string=html, url_fetcher=url_fetcher, base_url=base_url).write_png()\n\n\ndef assert_meta(html, **meta):\n    meta.setdefault('title', None)\n    meta.setdefault('authors', [])\n    meta.setdefault('keywords', [])\n    meta.setdefault('generator', None)\n    meta.setdefault('description', None)\n    meta.setdefault('created', None)\n    meta.setdefault('modified', None)\n    meta.setdefault('attachments', [])\n    meta.setdefault('lang', None)\n    meta.setdefault('custom', {})\n    meta.setdefault('xmp_metadata', [])\n    assert vars(FakeHTML(string=html).render().metadata) == meta\n\n\n@assert_no_logs\ndef test_html_meta_1():\n    assert_meta('<body>')\n\n\n@assert_no_logs\ndef test_html_meta_2():\n    assert_meta(\n        '''\n            <html lang=\"en\"><head>\n            <meta name=author content=\"I Me &amp; Myself\">\n            <meta name=author content=\"Smith, John\">\n            <title>Test document</title>\n            <h1>Another title</h1>\n            <meta name=generator content=\"Human after all\">\n            <meta name=generator content=\"Human\">\n            <meta name=dummy content=ignored>\n            <meta name=dummy>\n            <meta content=ignored>\n            <meta>\n            <meta name=keywords content=\"html ,\\tcss,\n                                         pdf,css\">\n            <meta name=dcterms.created content=2011-04>\n            <meta name=dcterms.created content=2011-05>\n            <meta name=dcterms.modified content=2013>\n            <meta name=keywords content=\"Python; pydyf\">\n            <meta name=description content=\"Blah… \">\n            <meta name=description content=\"*Oh-no/\">\n            <meta name=dcterms.modified content=2012>\n            </head></html>\n        ''',\n        authors=['I Me & Myself', 'Smith, John'],\n        title='Test document',\n        generator='Human after all',\n        keywords=['html', 'css', 'pdf', 'Python; pydyf'],\n        description='Blah… ',\n        created='2011-04',\n        modified='2013',\n        lang='en',\n        custom={'dummy': 'ignored'})\n\n\n@assert_no_logs\ndef test_html_meta_3():\n    assert_meta(\n        '''\n            <title>One</title>\n            <meta name=Author>\n            <title>Two</title>\n            <title>Three</title>\n            <meta name=author content=Me>\n        ''',\n        title='One',\n        authors=['', 'Me'])\n\n\n@assert_no_logs\ndef test_html_meta_4():\n    with capture_logs() as logs:\n        assert_meta(\n            '''\n                <meta name=dcterms.created content=wrong>\n                <meta name=author content=Me>\n                <title>Title</title>\n            ''',\n            title='Title',\n            authors=['Me'])\n    assert len(logs) == 1\n    assert 'Invalid date' in logs[0]\n\n\n@assert_no_logs\ndef test_http():\n    with http_server() as root_url:\n        assert HTML(f'{root_url}/gzip').etree_element.get('test') == 'ok'\n        assert HTML(f'{root_url}/deflate').etree_element.get('test') == 'ok'\n        assert HTML(f'{root_url}/raw-deflate').etree_element.get('test') == 'ok'\n        assert HTML(f'{root_url}/redirect').etree_element.get('test') == 'ok'\n\n        url_fetcher = URLFetcher()\n        HTML(f'{root_url}/redirect', url_fetcher=url_fetcher).render()\n        with capture_logs() as logs:\n            HTML(\n                string=f'<link rel=stylesheet href=\"{root_url}/redirect\">',\n                url_fetcher=url_fetcher).render()\n        assert len(logs) == 1\n        assert 'Unsupported stylesheet' in logs[0]\n\n        url_fetcher = URLFetcher(allow_redirects=False)\n        with pytest.raises(URLFetchingError, match='301'):\n            HTML(f'{root_url}/redirect', url_fetcher=url_fetcher).render()\n        with capture_logs() as logs:\n            HTML(\n                string=f'<link rel=stylesheet href=\"{root_url}/redirect\">',\n                url_fetcher=url_fetcher).render()\n        assert len(logs) == 1\n        assert '301' in logs[0]\n\n        url_fetcher = URLFetcher(fail_on_errors=True)\n        HTML(f'{root_url}/redirect', url_fetcher=url_fetcher).render()\n        with pytest.raises(FatalURLFetchingError, match='Error fetching'):\n            HTML(\n                string=f'<img src=\"{root_url}/bad\">',\n                url_fetcher=url_fetcher).render()\n\n        url_fetcher = URLFetcher(allow_redirects=False, fail_on_errors=True)\n        with pytest.raises(FatalURLFetchingError, match='Error fetching'):\n            HTML(f'{root_url}/redirect', url_fetcher=url_fetcher).render()\n        with pytest.raises(FatalURLFetchingError, match='Error fetching'):\n            HTML(\n                string=f'<link rel=stylesheet href=\"{root_url}/redirect\">',\n                url_fetcher=url_fetcher).render()\n\n\n@assert_no_logs\ndef test_page_copy_relative():\n    # Regression test for #1473.\n    document = FakeHTML(string='<div style=\"position: relative\">a').render()\n    duplicated_pages = document.copy([*document.pages, *document.pages])\n    pngs = duplicated_pages.write_png(split_images=True)\n    assert pngs[0] == pngs[1]\n"
  },
  {
    "path": "tests/test_boxes.py",
    "content": "\"\"\"Test that the \"before layout\" box tree is correctly constructed.\"\"\"\n\nimport pytest\n\nfrom weasyprint.css import get_all_computed_styles\nfrom weasyprint.formatting_structure import boxes, build\nfrom weasyprint.layout.page import PageType, set_page_type_computed_styles\n\nfrom .testing_utils import (  # isort:skip\n    FakeHTML, assert_no_logs, assert_tree, capture_logs, parse, parse_all,\n    render_pages)\n\n\n@assert_no_logs\ndef test_box_tree():\n    assert_tree(parse('<p>'), [('p', 'Block', [])])\n    assert_tree(parse('''\n      <style>\n        span { display: inline-block }\n      </style>\n      <p>Hello <em>World <img src=\"pattern.png\"><span>L</span></em>!</p>'''), [\n          ('p', 'Block', [\n            ('p', 'Text', 'Hello '),\n            ('em', 'Inline', [\n                ('em', 'Text', 'World '),\n                ('img', 'InlineReplaced', '<replaced>'),\n                ('span', 'InlineBlock', [\n                    ('span', 'Text', 'L')])]),\n            ('p', 'Text', '!')])])\n\n\n@assert_no_logs\ndef test_html_entities():\n    for quote in ['\"', '&quot;', '&#x22;', '&#34;']:\n        assert_tree(parse(f'<p>{quote}abc{quote}'), [\n            ('p', 'Block', [\n                ('p', 'Text', '\"abc\"')])])\n\n\n@assert_no_logs\ndef test_inline_in_block_1():\n    source = '<div>Hello, <em>World</em>!\\n<p>Lipsum.</p></div>'\n    expected = [\n        ('div', 'Block', [\n            ('div', 'Block', [\n                ('div', 'Line', [\n                    ('div', 'Text', 'Hello, '),\n                    ('em', 'Inline', [\n                        ('em', 'Text', 'World')]),\n                    ('div', 'Text', '!\\n')])]),\n            ('p', 'Block', [\n                ('p', 'Line', [\n                    ('p', 'Text', 'Lipsum.')])])])]\n    box = parse(source)\n    box = build.inline_in_block(box)\n    assert_tree(box, expected)\n\n\n@assert_no_logs\ndef test_inline_in_block_2():\n    source = '<div><p>Lipsum.</p>Hello, <em>World</em>!\\n</div>'\n    expected = [\n        ('div', 'Block', [\n            ('p', 'Block', [\n                ('p', 'Line', [\n                    ('p', 'Text', 'Lipsum.')])]),\n            ('div', 'Block', [\n                ('div', 'Line', [\n                    ('div', 'Text', 'Hello, '),\n                    ('em', 'Inline', [\n                        ('em', 'Text', 'World')]),\n                    ('div', 'Text', '!\\n')])])])]\n    box = parse(source)\n    box = build.inline_in_block(box)\n    assert_tree(box, expected)\n\n\n@assert_no_logs\ndef test_inline_in_block_3():\n    # Absolutes are left in the lines to get their static position later.\n    source = '''<p>Hello <em style=\"position:absolute;\n                                    display: block\">World</em>!</p>'''\n    expected = [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p', 'Text', 'Hello '),\n                ('em', 'Block', [\n                    ('em', 'Line', [\n                        ('em', 'Text', 'World')])]),\n                ('p', 'Text', '!')])])]\n    box = parse(source)\n    box = build.inline_in_block(box)\n    assert_tree(box, expected)\n    box = build.block_in_inline(box)\n    assert_tree(box, expected)\n\n\n@assert_no_logs\ndef test_inline_in_block_4():\n    # Floats are pull to the top of their containing blocks\n    source = '<p>Hello <em style=\"float: left\">World</em>!</p>'\n    box = parse(source)\n    box = build.inline_in_block(box)\n    box = build.block_in_inline(box)\n    assert_tree(box, [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p', 'Text', 'Hello '),\n                ('em', 'Block', [\n                    ('em', 'Line', [\n                        ('em', 'Text', 'World')])]),\n                ('p', 'Text', '!')])])])\n\n\n@assert_no_logs\ndef test_block_in_inline():\n    box = parse('''\n      <style>\n        p { display: inline-block; }\n        span, i { display: block; }\n      </style>\n      <p>Lorem <em>ipsum <strong>dolor <span>sit</span>\n      <span>amet,</span></strong><span><em>conse<i>''')\n    box = build.inline_in_block(box)\n    assert_tree(box, [\n        ('body', 'Line', [\n            ('p', 'InlineBlock', [\n                ('p', 'Line', [\n                    ('p', 'Text', 'Lorem '),\n                    ('em', 'Inline', [\n                        ('em', 'Text', 'ipsum '),\n                        ('strong', 'Inline', [\n                            ('strong', 'Text', 'dolor '),\n                            ('span', 'Block', [  # This block is \"pulled up\"\n                                ('span', 'Line', [\n                                    ('span', 'Text', 'sit')])]),\n                            ('strong', 'Text', '\\n      '),\n                            ('span', 'Block', [  # This block is \"pulled up\"\n                                ('span', 'Line', [\n                                    ('span', 'Text', 'amet,')])])]),\n                        ('span', 'Block', [  # This block is \"pulled up\"\n                            ('span', 'Line', [\n                                ('em', 'Inline', [\n                                    ('em', 'Text', 'conse'),\n                                    ('i', 'Block', [])])])])])])])])])\n\n    box = build.block_in_inline(box)\n    assert_tree(box, [\n        ('body', 'Line', [\n            ('p', 'InlineBlock', [\n                ('p', 'Block', [\n                    ('p', 'Line', [\n                        ('p', 'Text', 'Lorem '),\n                        ('em', 'Inline', [\n                            ('em', 'Text', 'ipsum '),\n                            ('strong', 'Inline', [\n                                ('strong', 'Text', 'dolor ')])])])]),\n                ('span', 'Block', [\n                    ('span', 'Line', [\n                        ('span', 'Text', 'sit')])]),\n                ('p', 'Block', [\n                    ('p', 'Line', [\n                        ('em', 'Inline', [\n                            ('strong', 'Inline', [\n                                ('strong', 'Text', '\\n      ')])])])]),\n                ('span', 'Block', [\n                    ('span', 'Line', [\n                        ('span', 'Text', 'amet,')])]),\n\n                ('p', 'Block', [\n                    ('p', 'Line', [\n                        ('em', 'Inline', [\n                            ('strong', 'Inline', [])])])]),\n                ('span', 'Block', [\n                    ('span', 'Block', [\n                        ('span', 'Line', [\n                            ('em', 'Inline', [\n                                ('em', 'Text', 'conse')])])]),\n                    ('i', 'Block', []),\n                    ('span', 'Block', [\n                        ('span', 'Line', [\n                            ('em', 'Inline', [])])])]),\n                ('p', 'Block', [\n                    ('p', 'Line', [\n                        ('em', 'Inline', [])])])])])])\n\n\n@assert_no_logs\ndef test_styles():\n    box = parse('''\n      <style>\n        span { display: block; }\n        * { margin: 42px }\n        html { color: blue }\n      </style>\n      <p>Lorem <em>ipsum <strong>dolor <span>sit</span>\n        <span>amet,</span></strong><span>consectetur</span></em></p>''')\n    box = build.inline_in_block(box)\n    box = build.block_in_inline(box)\n\n    descendants = list(box.descendants())\n    assert len(descendants) == 31\n    assert descendants[0] == box\n\n    for child in descendants:\n        # All boxes inherit the color\n        assert child.style['color'] == (0, 0, 1, 1)  # blue\n        # Only non-anonymous boxes have margins\n        assert child.style['margin_top'] in ((0, 'px'), (42, 'px'))\n\n\n@assert_no_logs\ndef test_whitespace():\n    assert_tree(parse_all('''\n      <p>Lorem \\t\\r\\n  ipsum\\t<strong>  dolor\n        <img src=pattern.png> sit\n        <span style=\"position: absolute\"></span> <em> amet </em>\n        consectetur</strong>.</p>\n      <pre>\\t  foo\\n</pre>\n      <pre style=\"white-space: pre-wrap\">\\t  foo\\n</pre>\n      <pre style=\"white-space: pre-line\">\\t  foo\\n</pre>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p', 'Text', 'Lorem ipsum '),\n                ('strong', 'Inline', [\n                    ('strong', 'Text', 'dolor '),\n                    ('img', 'InlineReplaced', '<replaced>'),\n                    ('strong', 'Text', ' sit '),\n                    ('span', 'Block', []),\n                    ('em', 'Inline', [\n                        ('em', 'Text', 'amet ')]),\n                    ('strong', 'Text', 'consectetur')]),\n                ('p', 'Text', '.')])]),\n        ('pre', 'Block', [\n            ('pre', 'Line', [\n                # pre\n                ('pre', 'Text', '\\t  foo\\n')])]),\n        ('pre', 'Block', [\n            ('pre', 'Line', [\n                # pre-wrap\n                ('pre', 'Text', '\\t  foo\\n')])]),\n        ('pre', 'Block', [\n            ('pre', 'Line', [\n                # pre-line\n                ('pre', 'Text', 'foo\\n')])])])\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('page_type', 'top', 'right', 'bottom', 'left'), [\n    (PageType('left', False, '', 0, ()), 20, 3, 3, 10),\n    (PageType('right', False, '', 0, ()), 20, 10, 3, 3),\n    (PageType('left', False, '', 1, ()), 10, 3, 3, 10),\n    (PageType('right', False, '', 1, ()), 10, 10, 3, 3),\n    (PageType('right', False, 'name', 1, (('name', 0),)), 5, 10, 3, 15),\n    (PageType('right', False, 'name', 1, (('name', 1),)), 5, 10, 4, 15),\n    (PageType('right', False, 'name', 2, (('name', 0),)), 5, 10, 1, 15),\n    (PageType('right', False, 'name', 8, (('name', 8),)), 5, 10, 2, 15),\n])\ndef test_page_style(page_type, top, right, bottom, left):\n    document = FakeHTML(string='''\n      <style>\n        @page { margin: 3px }\n        @page name { margin-left: 15px; margin-top: 5px }\n        @page :nth(3) { margin-bottom: 1px }\n        @page :nth(5n+4) { margin-bottom: 2px }\n        @page :nth(2 of name) { margin-bottom: 4px }\n        @page :first { margin-top: 20px }\n        @page :right { margin-right: 10px; margin-top: 10px }\n        @page :left { margin-left: 10px; margin-top: 10px }\n      </style>\n    ''')\n    style_for = get_all_computed_styles(document)\n\n    # Force the generation of the style for this page type as it's generally\n    # only done during the rendering.\n    set_page_type_computed_styles(page_type, document, style_for)\n\n    style = style_for(page_type)\n    assert style['margin_top'] == (top, 'px')\n    assert style['margin_right'] == (right, 'px')\n    assert style['margin_bottom'] == (bottom, 'px')\n    assert style['margin_left'] == (left, 'px')\n\n\n@assert_no_logs\ndef test_images_1():\n    with capture_logs() as logs:\n        result = parse_all('''\n          <p><img src=pattern.png\n            /><img alt=\"No src\"\n            /><img src=inexistent.jpg alt=\"Inexistent src\" /></p>\n        ''')\n    assert len(logs) == 1\n    assert 'ERROR: Failed to load image' in logs[0]\n    assert 'inexistent.jpg' in logs[0]\n    assert_tree(result, [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('img', 'InlineReplaced', '<replaced>'),\n                ('img', 'Inline', [\n                    ('img', 'Text', 'No src')]),\n                ('img', 'Inline', [\n                    ('img', 'Text', 'Inexistent src')])])])])\n\n\n@assert_no_logs\ndef test_images_2():\n    with capture_logs() as logs:\n        result = parse_all('<p><img src=pattern.png alt=\"No base_url\">',\n                           base_url=None)\n    assert len(logs) == 1\n    assert 'ERROR: Relative URI reference without a base URI' in logs[0]\n    assert_tree(result, [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('img', 'Inline', [\n                    ('img', 'Text', 'No base_url')])])])])\n\n\n@assert_no_logs\ndef test_tables_1():\n    # Rules in https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes\n\n    # Rule 1.3\n    # Also table model: https://www.w3.org/TR/CSS21/tables.html#model\n    assert_tree(parse_all('''\n      <style>\n        x-table { display: table }\n        x-tr { display: table-row }\n        x-td, x-th { display: table-cell }\n        x-thead { display: table-header-group }\n        x-tfoot { display: table-footer-group }\n        x-col { display: table-column }\n        x-caption { display: table-caption }\n      </style>\n      <x-table>\n        <x-tr>\n          <x-th>foo</x-th>\n          <x-th>bar</x-th>\n        </x-tr>\n        <x-tfoot></x-tfoot>\n        <x-thead><x-th></x-th></x-thead>\n        <x-caption style=\"caption-side: bottom\"></x-caption>\n        <x-thead></x-thead>\n        <x-col></x-col>\n        <x-caption>top caption</x-caption>\n        <x-tr>\n          <x-td>baz</x-td>\n        </x-tr>\n      </x-table>\n    '''), [\n        ('x-table', 'Block', [\n            ('x-caption', 'TableCaption', [\n                ('x-caption', 'Line', [\n                    ('x-caption', 'Text', 'top caption')])]),\n            ('x-table', 'Table', [\n                ('x-table', 'TableColumnGroup', [\n                    ('x-col', 'TableColumn', [])]),\n                ('x-thead', 'TableRowGroup', [\n                    ('x-thead', 'TableRow', [\n                        ('x-th', 'TableCell', [])])]),\n                ('x-table', 'TableRowGroup', [\n                    ('x-tr', 'TableRow', [\n                        ('x-th', 'TableCell', [\n                            ('x-th', 'Line', [\n                                ('x-th', 'Text', 'foo')])]),\n                        ('x-th', 'TableCell', [\n                            ('x-th', 'Line', [\n                                ('x-th', 'Text', 'bar')])])])]),\n                ('x-thead', 'TableRowGroup', []),\n                ('x-table', 'TableRowGroup', [\n                    ('x-tr', 'TableRow', [\n                        ('x-td', 'TableCell', [\n                            ('x-td', 'Line', [\n                                ('x-td', 'Text', 'baz')])])])]),\n                ('x-tfoot', 'TableRowGroup', [])]),\n            ('x-caption', 'TableCaption', [])])])\n\n\n@assert_no_logs\ndef test_tables_2():\n    # Rules 1.4 and 3.1\n    assert_tree(parse_all('''\n      <span style=\"display: table-cell\">foo</span>\n      <span style=\"display: table-cell\">bar</span>\n    '''), [\n        ('body', 'Block', [\n            ('body', 'Table', [\n                ('body', 'TableRowGroup', [\n                    ('body', 'TableRow', [\n                        ('span', 'TableCell', [\n                            ('span', 'Line', [\n                                ('span', 'Text', 'foo')])]),\n                        ('span', 'TableCell', [\n                            ('span', 'Line', [\n                                ('span', 'Text', 'bar')])])])])])])])\n\n\n@assert_no_logs\ndef test_tables_3():\n    # https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes\n    # Rules 1.1 and 1.2\n    # Rule XXX (not in the spec): column groups have at least one column child\n    assert_tree(parse_all('''\n      <span style=\"display: table-column-group\">\n        1\n        <em style=\"display: table-column\">\n          2\n          <strong>3</strong>\n        </em>\n        <strong>4</strong>\n      </span>\n      <ins style=\"display: table-column-group\"></ins>\n    '''), [\n        ('body', 'Block', [\n            ('body', 'Table', [\n                ('span', 'TableColumnGroup', [\n                    ('em', 'TableColumn', [])]),\n                ('ins', 'TableColumnGroup', [\n                    ('ins', 'TableColumn', [])])])])])\n\n\n@assert_no_logs\ndef test_tables_4():\n    # Rules 2.1 then 2.3\n    assert_tree(parse_all('<x-table style=\"display:table\">foo <div></div></x-table>'), [\n        ('x-table', 'Block', [\n            ('x-table', 'Table', [\n                ('x-table', 'TableRowGroup', [\n                    ('x-table', 'TableRow', [\n                        ('x-table', 'TableCell', [\n                            ('x-table', 'Block', [\n                                ('x-table', 'Line', [\n                                    ('x-table', 'Text', 'foo ')])]),\n                            ('div', 'Block', [])])])])])])])\n\n\n@assert_no_logs\ndef test_tables_5():\n    # Rule 2.2\n    assert_tree(parse_all('<x-thead style=\"display: table-header-group\"><div></div>'\n                          '<x-td style=\"display: table-cell\"></x-td></x-thead>'), [\n        ('body', 'Block', [\n            ('body', 'Table', [\n                ('x-thead', 'TableRowGroup', [\n                    ('x-thead', 'TableRow', [\n                        ('x-thead', 'TableCell', [\n                            ('div', 'Block', [])]),\n                        ('x-td', 'TableCell', [])])])])])])\n\n\n@assert_no_logs\ndef test_tables_6():\n    # Rule 3.2\n    assert_tree(parse_all('<span><x-tr style=\"display: table-row\"></x-tr></span>'), [\n        ('body', 'Line', [\n            ('span', 'Inline', [\n                ('span', 'InlineBlock', [\n                    ('span', 'InlineTable', [\n                        ('span', 'TableRowGroup', [\n                            ('x-tr', 'TableRow', [])])])])])])])\n\n\n@assert_no_logs\ndef test_tables_7():\n    # Rule 3.1\n    # Also, rule 1.3 does not apply: whitespace before and after is preserved\n    assert_tree(parse_all('''\n      <span>\n        <em style=\"display: table-cell\"></em>\n        <em style=\"display: table-cell\"></em>\n      </span>\n    '''), [\n        ('body', 'Line', [\n            ('span', 'Inline', [\n                # Whitespace is preserved in table handling, then collapsed\n                # into a single space.\n                ('span', 'Text', ' '),\n                ('span', 'InlineBlock', [\n                    ('span', 'InlineTable', [\n                        ('span', 'TableRowGroup', [\n                            ('span', 'TableRow', [\n                                ('em', 'TableCell', []),\n                                ('em', 'TableCell', [])])])])]),\n                ('span', 'Text', ' ')])])])\n\n\n@assert_no_logs\ndef test_tables_8():\n    # Rule 3.2\n    assert_tree(parse_all(\n        '<x-tr style=\"display: table-row\"></x-tr>\\t'\n        '<x-tr style=\"display: table-row\"></x-tr>'\n    ), [\n        ('body', 'Block', [\n            ('body', 'Table', [\n                ('body', 'TableRowGroup', [\n                    ('x-tr', 'TableRow', []),\n                    ('x-tr', 'TableRow', [])])])])])\n\n\n@assert_no_logs\ndef test_tables_9():\n    assert_tree(parse_all(\n        '<x-col style=\"display: table-column\"></x-col>\\n'\n        '<x-colgroup style=\"display: table-column-group\"></x-colgroup>'\n    ), [\n        ('body', 'Block', [\n            ('body', 'Table', [\n                ('body', 'TableColumnGroup', [\n                    ('x-col', 'TableColumn', [])]),\n                ('x-colgroup', 'TableColumnGroup', [\n                    ('x-colgroup', 'TableColumn', [])])])])])\n\n\n@assert_no_logs\ndef test_table_style():\n    html = parse_all('<table style=\"margin: 1px; padding: 2px\"></table>')\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    assert isinstance(wrapper, boxes.BlockBox)\n    assert isinstance(table, boxes.TableBox)\n    assert wrapper.style['margin_top'] == (1, 'px')\n    assert wrapper.style['padding_top'] == (0, 'px')\n    assert table.style['margin_top'] == (0, 'px')\n    assert table.style['padding_top'] == (2, 'px')\n\n\n@assert_no_logs\ndef test_column_style():\n    html = parse_all('''\n      <table>\n        <col span=3 style=\"width: 10px\"></col>\n        <col span=2></col>\n      </table>\n    ''')\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    colgroup, = table.column_groups\n    widths = [col.style['width'] for col in colgroup.children]\n    assert widths == [(10, 'px'), (10, 'px'), (10, 'px'), 'auto', 'auto']\n    assert [col.grid_x for col in colgroup.children] == [0, 1, 2, 3, 4]\n    # copies, not the same box object\n    assert colgroup.children[0] is not colgroup.children[1]\n\n\n@assert_no_logs\ndef test_nested_grid_x():\n    html = parse_all('''\n      <table>\n        <col span=2></col>\n        <colgroup span=2></colgroup>\n        <colgroup>\n          <col></col>\n          <col span=2></col>\n        </colgroup>\n        <col></col>\n      </table>\n    ''')\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    grid = [(colgroup.grid_x, [col.grid_x for col in colgroup.children])\n            for colgroup in table.column_groups]\n    assert grid == [(0, [0, 1]), (2, [2, 3]), (4, [4, 5, 6]), (7, [7])]\n\n\n@assert_no_logs\ndef test_colspan_rowspan_1():\n    # +---+---+---+\n    # | A | B | C | X\n    # +---+---+---+\n    # | D |     E | X\n    # +---+---+   +---+\n    # |  F ...|   |   |   <-- overlap\n    # +---+---+---+   +\n    # | H | X   X | G |\n    # +---+---+   +   +\n    # | I | J | X |   |\n    # +---+---+   +---+\n\n    # X: empty cells\n    html = parse_all('''\n      <table>\n        <tr>\n          <td>A <td>B <td>C\n        </tr>\n        <tr>\n          <td>D <td colspan=2 rowspan=2>E\n        </tr>\n        <tr>\n          <td colspan=2>F <td rowspan=0>G\n        </tr>\n        <tr>\n          <td>H\n        </tr>\n        <tr>\n          <td>I <td>J\n        </tr>\n      </table>\n    ''')\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    group, = table.children\n    assert [[c.grid_x for c in row.children] for row in group.children] == [\n        [0, 1, 2],\n        [0, 1],\n        [0, 3],\n        [0],\n        [0, 1],\n    ]\n    assert [[c.colspan for c in row.children] for row in group.children] == [\n        [1, 1, 1],\n        [1, 2],\n        [2, 1],\n        [1],\n        [1, 1],\n    ]\n    assert [[c.rowspan for c in row.children] for row in group.children] == [\n        [1, 1, 1],\n        [1, 2],\n        [1, 3],\n        [1],\n        [1, 1],\n    ]\n\n\n@assert_no_logs\ndef test_colspan_rowspan_2():\n    # A cell box cannot extend beyond the last row box of a table.\n    html = parse_all('''\n        <table>\n            <tr>\n                <td rowspan=5></td>\n                <td></td>\n            </tr>\n            <tr>\n                <td></td>\n            </tr>\n        </table>\n    ''')\n    body, = html.children\n    wrapper, = body.children\n    table, = wrapper.children\n    group, = table.children\n    assert [[c.grid_x for c in row.children] for row in group.children] == [\n        [0, 1],\n        [1],\n    ]\n    assert [[c.colspan for c in row.children] for row in group.children] == [\n        [1, 1],\n        [1],\n    ]\n    assert [[c.rowspan for c in row.children] for row in group.children] == [\n        [2, 1],  # Not 5\n        [1],\n    ]\n\n\n@assert_no_logs\ndef test_before_after_1():\n    assert_tree(parse_all('''\n      <style>\n        p:before { content: normal }\n        div:before { content: none }\n        section::before { color: black }\n      </style>\n      <p></p>\n      <div></div>\n      <section></section>\n    '''), [\n        # No content in pseudo-element, no box generated\n        ('p', 'Block', []),\n        ('div', 'Block', []),\n        ('section', 'Block', [])])\n\n\n@assert_no_logs\ndef test_before_after_2():\n    assert_tree(parse_all('''\n      <style>\n        p:before { content: 'a' 'b' }\n        p::after { content: 'd' 'e' }\n      </style>\n      <p> c </p>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('p::before', 'Inline', [\n                    ('p::before', 'Text', 'ab')]),\n                ('p', 'Text', ' c '),\n                ('p::after', 'Inline', [\n                    ('p::after', 'Text', 'de')])])])])\n\n\n@assert_no_logs\ndef test_before_after_3():\n    assert_tree(parse_all('''\n      <style>\n        a[href]:before { content: '[' attr(href) '] ' }\n      </style>\n      <p><a href=\"some url\">some text</a></p>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('a', 'Inline', [\n                    ('a::before', 'Inline', [\n                        ('a::before', 'Text', '[some url] ')]),\n                    ('a', 'Text', 'some text')])])])])\n\n\n@assert_no_logs\ndef test_before_after_4():\n    assert_tree(parse_all('''\n      <style>\n        body { quotes: '«' '»' '“' '”' }\n        q:before { content: open-quote ' '}\n        q:after { content: ' ' close-quote }\n      </style>\n      <p><q>Lorem ipsum <q>dolor</q> sit amet</q></p>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('q', 'Inline', [\n                    ('q::before', 'Inline', [\n                        ('q::before', 'Text', '« ')]),\n                    ('q', 'Text', 'Lorem ipsum '),\n                    ('q', 'Inline', [\n                        ('q::before', 'Inline', [\n                            ('q::before', 'Text', '“ ')]),\n                        ('q', 'Text', 'dolor'),\n                        ('q::after', 'Inline', [\n                            ('q::after', 'Text', ' ”')])]),\n                    ('q', 'Text', ' sit amet'),\n                    ('q::after', 'Inline', [\n                        ('q::after', 'Text', ' »')])])])])])\n\n\n@assert_no_logs\ndef test_before_after_5():\n    # Regression test.\n    with capture_logs() as logs:\n        assert_tree(parse_all('''\n          <style>\n            p:before {\n              content: 'a' url(pattern.png) 'b';\n              /* Invalid, ignored in favor of the one above. */\n              content: some-function(nested-function(something));\n            }\n          </style>\n          <p>c</p>\n        '''), [\n            ('p', 'Block', [\n                ('p', 'Line', [\n                    ('p::before', 'Inline', [\n                        ('p::before', 'Text', 'a'),\n                        ('p::before', 'InlineReplaced', '<replaced>'),\n                        ('p::before', 'Text', 'b')]),\n                    ('p', 'Text', 'c')])])])\n    assert len(logs) == 1\n    assert 'nested-function(' in logs[0]\n    assert 'invalid value' in logs[0]\n\n\n@assert_no_logs\ndef test_quotes_auto():\n    assert_tree(parse_all('''\n      <style>\n        body { quotes: auto }\n        q:before { content: open-quote ' '}\n        q:after { content: ' ' close-quote }\n      </style>\n      <p><q>Lorem ipsum <q>dolor</q> sit amet</q></p>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('q', 'Inline', [\n                    ('q::before', 'Inline', [\n                        ('q::before', 'Text', '“ ')]),\n                    ('q', 'Text', 'Lorem ipsum '),\n                    ('q', 'Inline', [\n                        ('q::before', 'Inline', [\n                            ('q::before', 'Text', '‘ ')]),\n                        ('q', 'Text', 'dolor'),\n                        ('q::after', 'Inline', [\n                            ('q::after', 'Text', ' ’')])]),\n                    ('q', 'Text', ' sit amet'),\n                    ('q::after', 'Inline', [\n                        ('q::after', 'Text', ' ”')])])])])])\n\n\n@assert_no_logs\ndef test_quotes_none():\n    assert_tree(parse_all('''\n      <style>\n        body { quotes: none }\n        q:before { content: open-quote ' '}\n        q:after { content: ' ' close-quote }\n      </style>\n      <p><q>Lorem ipsum <q>dolor</q> sit amet</q></p>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('q', 'Inline', [\n                    ('q::before', 'Inline', [\n                        ('q::before', 'Text', ' ')]),\n                    ('q', 'Text', 'Lorem ipsum '),\n                    ('q', 'Inline', [\n                        ('q::before', 'Inline', [\n                            ('q::before', 'Text', ' ')]),\n                        ('q', 'Text', 'dolor'),\n                        ('q::after', 'Inline', [\n                            ('q::after', 'Text', ' ')])]),\n                    ('q', 'Text', ' sit amet'),\n                    ('q::after', 'Inline', [\n                        ('q::after', 'Text', ' ')])])])])])\n\n\n@assert_no_logs\ndef test_quotes_lang():\n    assert_tree(parse_all('''\n      <style>\n        q:before { content: open-quote ' '}\n        q:after { content: ' ' close-quote }\n      </style>\n      <p lang=\"fr\"><q>Lorem ipsum <q>dolor</q> sit amet</q></p>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('q', 'Inline', [\n                    ('q::before', 'Inline', [\n                        ('q::before', 'Text', '« ')]),\n                    ('q', 'Text', 'Lorem ipsum '),\n                    ('q', 'Inline', [\n                        ('q::before', 'Inline', [\n                            ('q::before', 'Text', '« ')]),\n                        ('q', 'Text', 'dolor'),\n                        ('q::after', 'Inline', [\n                            ('q::after', 'Text', ' »')])]),\n                    ('q', 'Text', ' sit amet'),\n                    ('q::after', 'Inline', [\n                        ('q::after', 'Text', ' »')])])])])])\n\n\n@assert_no_logs\ndef test_quotes_lang_alternate():\n    assert_tree(parse_all('''\n      <style>\n        q:before { content: open-quote ' '}\n        q:after { content: ' ' close-quote }\n      </style>\n      <p lang=\"fr_CH\"><q>Lorem ipsum <q>dolor</q> sit amet</q></p>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('q', 'Inline', [\n                    ('q::before', 'Inline', [\n                        ('q::before', 'Text', '« ')]),\n                    ('q', 'Text', 'Lorem ipsum '),\n                    ('q', 'Inline', [\n                        ('q::before', 'Inline', [\n                            ('q::before', 'Text', '‹ ')]),\n                        ('q', 'Text', 'dolor'),\n                        ('q::after', 'Inline', [\n                            ('q::after', 'Text', ' ›')])]),\n                    ('q', 'Text', ' sit amet'),\n                    ('q::after', 'Inline', [\n                        ('q::after', 'Text', ' »')])])])])])\n\n\n@assert_no_logs\ndef test_quotes_lang_parent():\n    assert_tree(parse_all('''\n      <style>\n        q:before { content: open-quote ' '}\n        q:after { content: ' ' close-quote }\n      </style>\n      <p lang=\"fr_CH_alt\"><q>Lorem ipsum <q>dolor</q> sit amet</q></p>\n    '''), [\n        ('p', 'Block', [\n            ('p', 'Line', [\n                ('q', 'Inline', [\n                    ('q::before', 'Inline', [\n                        ('q::before', 'Text', '« ')]),\n                    ('q', 'Text', 'Lorem ipsum '),\n                    ('q', 'Inline', [\n                        ('q::before', 'Inline', [\n                            ('q::before', 'Text', '‹ ')]),\n                        ('q', 'Text', 'dolor'),\n                        ('q::after', 'Inline', [\n                            ('q::after', 'Text', ' ›')])]),\n                    ('q', 'Text', ' sit amet'),\n                    ('q::after', 'Inline', [\n                        ('q::after', 'Text', ' »')])])])])])\n\n\n@assert_no_logs\ndef test_margin_boxes():\n    page_1, page_2 = render_pages('''\n      <style>\n        @page {\n          /* Make the page content area only 10px high and wide,\n             so every word in <p> end up on a page of its own. */\n          size: 30px;\n          margin: 10px;\n          @top-center { content: \"Title\" }\n        }\n        @page :first {\n          @bottom-left { content: \"foo\" }\n          @bottom-left-corner { content: \"baz\" }\n        }\n      </style>\n      <p>lorem ipsum\n    ''')\n    assert page_1.children[0].element_tag == 'html'\n    assert page_2.children[0].element_tag == 'html'\n\n    margin_boxes_1 = [box.at_keyword for box in page_1.children[1:]]\n    margin_boxes_2 = [box.at_keyword for box in page_2.children[1:]]\n    assert margin_boxes_1 == ['@top-center', '@bottom-left',\n                              '@bottom-left-corner']\n    assert margin_boxes_2 == ['@top-center']\n\n    html, top_center = page_2.children\n    line_box, = top_center.children\n    text_box, = line_box.children\n    assert text_box.text == 'Title'\n\n\n@assert_no_logs\ndef test_margin_box_string_set_1():\n    # Test that both pages get string in the `bottom-center` margin box\n    page_1, page_2 = render_pages('''\n      <style>\n        @page {\n          @bottom-center { content: string(text_header) }\n        }\n        p {\n          string-set: text_header content();\n        }\n        .page {\n          page-break-before: always;\n        }\n      </style>\n      <p>first assignment</p>\n      <div class=\"page\"></div>\n    ''')\n\n    html, bottom_center = page_2.children\n    line_box, = bottom_center.children\n    text_box, = line_box.children\n    assert text_box.text == 'first assignment'\n\n    html, bottom_center = page_1.children\n    line_box, = bottom_center.children\n    text_box, = line_box.children\n    assert text_box.text == 'first assignment'\n\n\n@assert_no_logs\ndef test_margin_box_string_set_2():\n    def simple_string_set_test(content_val, extra_style=''):\n        page_1, = render_pages('''\n          <style>\n            @page {\n              @top-center { content: string(text_header) }\n            }\n            p {\n              string-set: text_header content(%s);\n            }\n            %s\n          </style>\n          <p>first assignment</p>\n        ''' % (content_val, extra_style))\n\n        html, top_center = page_1.children\n        line_box, = top_center.children\n        text_box, = line_box.children\n        if content_val in ('before', 'after'):\n            assert text_box.text == 'pseudo'\n        else:\n            assert text_box.text == 'first assignment'\n\n    # Test each accepted value of `content()` as an arguemnt to `string-set`\n    for value in ('', 'text', 'before', 'after'):\n        if value in ('before', 'after'):\n            extra_style = 'p:%s{content: \"pseudo\"}' % value\n            simple_string_set_test(value, extra_style)\n        else:\n            simple_string_set_test(value)\n\n\n@assert_no_logs\ndef test_margin_box_string_set_3():\n    # Test `first` (default value) ie. use the first assignment on the page\n    page_1, = render_pages('''\n      <style>\n        @page {\n          @top-center { content: string(text_header, first) }\n        }\n        p {\n          string-set: text_header content();\n        }\n      </style>\n      <p>first assignment</p>\n      <p>Second assignment</p>\n    ''')\n\n    html, top_center = page_1.children\n    line_box, = top_center.children\n    text_box, = line_box.children\n    assert text_box.text == 'first assignment'\n\n\n@assert_no_logs\ndef test_margin_box_string_set_4():\n    # test `first-except` ie. exclude from page on which value is assigned\n    page_1, page_2 = render_pages('''\n      <style>\n        @page {\n          @top-center { content: string(header_nofirst, first-except) }\n        }\n        p{\n          string-set: header_nofirst content();\n        }\n        .page{\n          page-break-before: always;\n        }\n      </style>\n      <p>first_excepted</p>\n      <div class=\"page\"></div>\n    ''')\n    html, top_center = page_1.children\n    assert len(top_center.children) == 0\n\n    html, top_center = page_2.children\n    line_box, = top_center.children\n    text_box, = line_box.children\n    assert text_box.text == 'first_excepted'\n\n\n@assert_no_logs\ndef test_margin_box_string_set_5():\n    # Test `last` ie. use the most-recent assignment\n    page_1, = render_pages('''\n      <style>\n        @page {\n          @top-center { content: string(header_last, last) }\n        }\n        p {\n          string-set: header_last content();\n        }\n      </style>\n      <p>String set</p>\n      <p>Second assignment</p>\n    ''')\n\n    html, top_center = page_1.children[:2]\n    line_box, = top_center.children\n\n    text_box, = line_box.children\n    assert text_box.text == 'Second assignment'\n\n\n@assert_no_logs\ndef test_margin_box_string_set_6():\n    # Test multiple complex string-set values\n    page_1, = render_pages('''\n      <style>\n        @page {\n          @top-center { content: string(text_header, first) }\n          @bottom-center { content: string(text_footer, last) }\n        }\n        html { counter-reset: a }\n        body { counter-increment: a }\n        ul { counter-reset: b }\n        li {\n          counter-increment: b;\n          string-set:\n            text_header content(before) \"-\" content() \"-\" content(after)\n                        counter(a, upper-roman) '.' counters(b, '|'),\n            text_footer content(before) '-' attr(class)\n                        counters(b, '|') \"/\" counter(a, upper-roman);\n        }\n        li:before { content: 'before!' }\n        li:after { content: 'after!' }\n        li:last-child:before { content: 'before!last' }\n        li:last-child:after { content: 'after!last' }\n      </style>\n      <ul>\n        <li class=\"firstclass\">first\n        <li>\n          <ul>\n            <li class=\"secondclass\">second\n    ''')\n\n    html, top_center, bottom_center = page_1.children\n    top_line_box, = top_center.children\n    top_text_box, = top_line_box.children\n    assert top_text_box.text == 'before!-first-after!I.1'\n    bottom_line_box, = bottom_center.children\n    bottom_text_box, = bottom_line_box.children\n    assert bottom_text_box.text == 'before!last-secondclass2|1/I'\n\n\ndef test_margin_box_string_set_7():\n    # Regression test for #722.\n    page_1, = render_pages('''\n      <style>\n        img { string-set: left attr(alt) }\n        img + img { string-set: right attr(alt) }\n        @page { @top-left  { content: '[' string(left)  ']' }\n                @top-right { content: '{' string(right) '}' } }\n      </style>\n      <img src=pattern.png alt=\"Chocolate\">\n      <img src=no_such_file.png alt=\"Cake\">\n    ''')\n\n    html, top_left, top_right = page_1.children\n    left_line_box, = top_left.children\n    left_text_box, = left_line_box.children\n    assert left_text_box.text == '[Chocolate]'\n    right_line_box, = top_right.children\n    right_text_box, = right_line_box.children\n    assert right_text_box.text == '{Cake}'\n\n\n@assert_no_logs\ndef test_margin_box_string_set_8():\n    # Regression test for #726.\n    page_1, page_2, page_3 = render_pages('''\n      <style>\n        @page { @top-left  { content: '[' string(left) ']' } }\n        p { page-break-before: always }\n        .initial { string-set: left 'initial' }\n        .empty   { string-set: left ''        }\n        .space   { string-set: left ' '       }\n      </style>\n\n      <p class=\"initial\">Initial</p>\n      <p class=\"empty\">Empty</p>\n      <p class=\"space\">Space</p>\n    ''')\n    html, top_left = page_1.children\n    left_line_box, = top_left.children\n    left_text_box, = left_line_box.children\n    assert left_text_box.text == '[initial]'\n\n    html, top_left = page_2.children\n    left_line_box, = top_left.children\n    left_text_box, = left_line_box.children\n    assert left_text_box.text == '[]'\n\n    html, top_left = page_3.children\n    left_line_box, = top_left.children\n    left_text_box, = left_line_box.children\n    assert left_text_box.text == '[ ]'\n\n\n@assert_no_logs\ndef test_margin_box_string_set_9():\n    # Regression test for #827.\n    # Test that named strings are case-sensitive.\n    page_1, = render_pages('''\n      <style>\n        @page {\n          @top-center {\n            content: string(text_header, first)\n                     ' ' string(TEXT_header, first)\n          }\n        }\n        p { string-set: text_header content() }\n        div { string-set: TEXT_header content() }\n      </style>\n      <p>first assignment</p>\n      <div>second assignment</div>\n    ''')\n\n    html, top_center = page_1.children\n    line_box, = top_center.children\n    text_box, = line_box.children\n    assert text_box.text == 'first assignment second assignment'\n\n\n@assert_no_logs\ndef test_margin_box_string_set_10():\n    page_1, page_2, page_3, page_4 = render_pages('''\n      <style>\n        @page { @top-left  { content: '[' string(p, start) ']' } }\n        p { string-set: p content(); page-break-after: always }\n      </style>\n      <article></article>\n      <p>1</p>\n      <article></article>\n      <p>2</p>\n      <p>3</p>\n      <article></article>\n    ''')\n    html, top_left = page_1.children\n    left_line_box, = top_left.children\n    left_text_box, = left_line_box.children\n    assert left_text_box.text == '[]'\n\n    html, top_left = page_2.children\n    left_line_box, = top_left.children\n    left_text_box, = left_line_box.children\n    assert left_text_box.text == '[1]'\n\n    html, top_left = page_3.children\n    left_line_box, = top_left.children\n    left_text_box, = left_line_box.children\n    assert left_text_box.text == '[3]'\n\n    html, top_left = page_4.children\n    left_line_box, = top_left.children\n    left_text_box, = left_line_box.children\n    assert left_text_box.text == '[3]'\n\n\n@assert_no_logs\ndef test_page_counters():\n    \"\"\"Test page-based counters.\"\"\"\n    pages = render_pages('''\n      <style>\n        @page {\n          /* Make the page content area only 10px high and wide,\n             so every word in <p> end up on a page of its own. */\n          size: 30px;\n          margin: 10px;\n          @bottom-center {\n            content: \"Page \" counter(page) \" of \" counter(pages) \".\";\n          }\n        }\n      </style>\n      <p>lorem ipsum dolor\n    ''')\n    for page_number, page in enumerate(pages, 1):\n        html, bottom_center = page.children\n        line_box, = bottom_center.children\n        text_box, = line_box.children\n        assert text_box.text == f'Page {page_number} of 3.'\n\n\n@assert_no_logs\n@pytest.mark.parametrize('html', [\n    '<html style=\"display: none\">',\n    '<html style=\"display: none\">abc',\n    '<html style=\"display: none\"><p>abc',\n    '<body style=\"display: none\"><p>abc',\n])\ndef test_display_none_root(html):\n    box = parse_all(html)\n    assert box.style['display'] == ('block', 'flow')\n    assert not box.children\n"
  },
  {
    "path": "tests/test_fonts.py",
    "content": "\"\"\"Test the fonts features.\"\"\"\n\nfrom .testing_utils import assert_no_logs, render_pages\n\n\n@assert_no_logs\ndef test_font_face():\n    page, = render_pages('''\n      <style>\n        body { font-family: weasyprint }\n      </style>\n      <span>abc</span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    assert line.width == 3 * 16\n\n\n@assert_no_logs\ndef test_kerning_default():\n    # Kerning and ligatures are on by default\n    page, = render_pages('''\n      <style>\n        body { font-family: weasyprint }\n      </style>\n      <span>kk</span><span>liga</span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span1, span2 = line.children\n    assert span1.width == 1.5 * 16\n    assert span2.width == 1.5 * 16\n\n\n@assert_no_logs\ndef test_ligatures_word_space():\n    # Regression test for #1469.\n    # Kerning and ligatures are on for text with increased word spacing.\n    page, = render_pages('''\n      <style>\n        body { font-family: weasyprint; word-spacing: 1em; width: 10em }\n      </style>\n      aa liga aa''')\n    html, = page.children\n    body, = html.children\n    assert len(body.children) == 1\n\n\n@assert_no_logs\ndef test_kerning_deactivate():\n    # Deactivate kerning\n    page, = render_pages('''\n      <style>\n        @font-face {\n          src: url(weasyprint.otf);\n          font-family: no-kern;\n          font-feature-settings: 'kern' 0;\n        }\n        @font-face {\n          src: url(weasyprint.otf);\n          font-family: kern;\n        }\n        span:nth-child(1) { font-family: kern }\n        span:nth-child(2) { font-family: no-kern }\n      </style>\n      <span>kk</span><span>kk</span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span1, span2 = line.children\n    assert span1.width == 1.5 * 16\n    assert span2.width == 2 * 16\n\n\n@assert_no_logs\ndef test_kerning_ligature_deactivate():\n    # Deactivate kerning and ligatures\n    page, = render_pages('''\n      <style>\n        @font-face {\n          src: url(weasyprint.otf);\n          font-family: no-kern-liga;\n          font-feature-settings: 'kern' off;\n          font-variant: no-common-ligatures;\n        }\n        @font-face {\n          src: url(weasyprint.otf);\n          font-family: kern-liga;\n        }\n        span:nth-child(1) { font-family: kern-liga }\n        span:nth-child(2) { font-family: no-kern-liga }\n      </style>\n      <span>kk liga</span><span>kk liga</span>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span1, span2 = line.children\n    assert span1.width == (1.5 + 1 + 1.5) * 16\n    assert span2.width == (2 + 1 + 4) * 16\n\n\n@assert_no_logs\ndef test_font_face_descriptors():\n    page, = render_pages(\n        '''\n        <style>\n          @font-face {\n            src: url(weasyprint.otf);\n            font-family: weasyprint-variant;\n            font-variant: sub\n                          discretionary-ligatures\n                          oldstyle-nums\n                          slashed-zero;\n          }\n          span { font-family: weasyprint-variant }\n        </style>'''\n        '<span>kk</span>'\n        '<span>subs</span>'\n        '<span>dlig</span>'\n        '<span>onum</span>'\n        '<span>zero</span>')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    kern, subs, dlig, onum, zero = line.children\n    assert kern.width == 1.5 * 16\n    assert subs.width == 1.5 * 16\n    assert dlig.width == 1.5 * 16\n    assert onum.width == 1.5 * 16\n    assert zero.width == 1.5 * 16\n\n\n@assert_no_logs\ndef test_woff_simple():\n    page, = render_pages(\n      '''\n      <style>\n        @font-face {\n          src: url(weasyprint.otf);\n          font-family: weasyprint-otf;\n        }\n        @font-face {\n          src: url(weasyprint.woff);\n          font-family: weasyprint-woff;\n        }\n        @font-face {\n          src: url(weasyprint.woff);\n          font-family: weasyprint-woff-cached;\n        }\n        span:nth-child(1) { font-family: weasyprint-otf }\n        span:nth-child(2) { font-family: weasyprint-woff }\n        span:nth-child(3) { font-family: weasyprint-woff-cached }\n        span:nth-child(4) { font-family: sans }\n      </style>'''\n      '<span>woff font</span>'\n      '<span>woff font</span>'\n      '<span>woff font</span>'\n      '<span>woff font</span>')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    span1, span2, span3, span4 = line.children\n    # otf font matches woff font\n    assert span1.width == span2.width\n    # otf font matches woff font loaded from cache\n    assert span1.width == span3.width\n    # the default font does not match the loaded fonts\n    assert span1.width != span4.width\n"
  },
  {
    "path": "tests/test_pdf.py",
    "content": "\"\"\"Test PDF-related code, including metadata, bookmarks and hyperlinks.\"\"\"\n\nimport hashlib\nimport io\nimport re\nfrom codecs import BOM_UTF16_BE\n\nimport pytest\n\nfrom weasyprint import Attachment\nfrom weasyprint.document import Document, DocumentMetadata\nfrom weasyprint.text.fonts import FontConfiguration\nfrom weasyprint.urls import path2url\n\nfrom .testing_utils import FakeHTML, assert_no_logs, capture_logs, resource_path\n\n# Top and right positions in points, rounded to the default float precision of\n# 6 digits, a rendered by pydyf\nTOP = round(297 * 72 / 25.4, 6)\nRIGHT = round(210 * 72 / 25.4, 6)\n\n\n@assert_no_logs\n@pytest.mark.parametrize('zoom', [1, 1.5, 0.5])\ndef test_page_size_zoom(zoom):\n    pdf = FakeHTML(string='<style>@page{size:3in 4in').write_pdf(zoom=zoom)\n    width, height = int(216 * zoom), int(288 * zoom)\n    assert f'/MediaBox [0 0 {width} {height}]'.encode() in pdf\n\n\n@assert_no_logs\ndef test_bookmarks_1():\n    pdf = FakeHTML(string='''\n      <h1>a</h1>  #\n      <h4>b</h4>  ####\n      <h3>c</h3>  ###\n      <h2>d</h2>  ##\n      <h1>e</h1>  #\n    ''').write_pdf()\n    # a\n    # |_ b\n    # |_ c\n    # L_ d\n    # e\n    assert re.findall(b'/Count ([0-9-]*)', pdf)[-1] == b'5'\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [\n        b'a', b'b', b'c', b'd', b'e']\n\n\n@assert_no_logs\ndef test_bookmarks_2():\n    pdf = FakeHTML(string='<body>').write_pdf()\n    assert b'Outlines' not in pdf\n\n\n@assert_no_logs\ndef test_bookmarks_3():\n    pdf = FakeHTML(string='<h1>a nbsp…</h1>').write_pdf()\n    assert re.findall(b'/Title <(\\\\w*)>', pdf) == [\n        b'feff006100a0006e0062007300702026']\n\n\n@assert_no_logs\ndef test_bookmarks_4():\n    pdf = FakeHTML(string='''\n      <style>\n        h1, h2, h3, span { height: 90pt; margin: 0 0 10pt 0 }\n      </style>\n      <h1>1</h1>\n      <h1>2</h1>\n      <h2 style=\"position: relative; left: 20pt\">3</h2>\n      <h2>4</h2>\n      <h3>5</h3>\n      <span style=\"display: block; page-break-before: always\"></span>\n      <h2>6</h2>\n      <h1>7</h1>\n      <h2>8</h2>\n      <h3>9</h3>\n      <h1>10</h1>\n      <h2>11</h2>\n    ''').write_pdf()\n    # 1\n    # 2\n    # |_ 3\n    # |_ 4\n    # |  L_ 5\n    # L_ 6\n    # 7\n    # L_ 8\n    #    L_ 9\n    # 10\n    # L_ 11\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [\n        str(i).encode() for i in range(1, 12)]\n    counts = re.findall(b'/Count ([0-9-]*)', pdf)\n    counts.pop(0)  # Page count\n    outlines = counts.pop()\n    assert outlines == b'11'\n    assert counts == [\n        b'0', b'4', b'0', b'1', b'0', b'0', b'2', b'1', b'0', b'1', b'0']\n\n\n@assert_no_logs\ndef test_bookmarks_5():\n    pdf = FakeHTML(string='''\n      <h2>1</h2> level 1\n      <h4>2</h4> level 2\n      <h2>3</h2> level 1\n      <h3>4</h3> level 2\n      <h4>5</h4> level 3\n    ''').write_pdf()\n    # 1\n    # L_ 2\n    # 3\n    # L_ 4\n    #    L_ 5\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [\n        str(i).encode() for i in range(1, 6)]\n    counts = re.findall(b'/Count ([0-9-]*)', pdf)\n    counts.pop(0)  # Page count\n    outlines = counts.pop()\n    assert outlines == b'5'\n    assert counts == [b'1', b'0', b'2', b'1', b'0']\n\n\n@assert_no_logs\ndef test_bookmarks_6():\n    pdf = FakeHTML(string='''\n      <h2>1</h2> h2 level 1\n      <h4>2</h4> h4 level 2\n      <h3>3</h3> h3 level 2\n      <h5>4</h5> h5 level 3\n      <h1>5</h1> h1 level 1\n      <h2>6</h2> h2 level 2\n      <h2>7</h2> h2 level 2\n      <h4>8</h4> h4 level 3\n      <h1>9</h1> h1 level 1\n    ''').write_pdf()\n    # 1\n    # |_ 2\n    # L_ 3\n    #    L_ 4\n    # 5\n    # |_ 6\n    # L_ 7\n    #    L_ 8\n    # 9\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [\n        str(i).encode() for i in range(1, 10)]\n    counts = re.findall(b'/Count ([0-9-]*)', pdf)\n    counts.pop(0)  # Page count\n    outlines = counts.pop()\n    assert outlines == b'9'\n    assert counts == [b'3', b'0', b'1', b'0', b'3', b'0', b'1', b'0', b'0']\n\n\n@assert_no_logs\ndef test_bookmarks_7():\n    # Reference for the next test. zoom=1\n    pdf = FakeHTML(string='<h2>a</h2>').write_pdf()\n\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [b'a']\n    dest, = re.findall(b'/Dest \\\\[(.*)\\\\]', pdf)\n    y = round(float(dest.strip().split()[-2]))\n\n    pdf = FakeHTML(string='<h2>a</h2>').write_pdf(zoom=1.5)\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [b'a']\n    dest, = re.findall(b'/Dest \\\\[(.*)\\\\]', pdf)\n    assert round(float(dest.strip().split()[-2])) == 1.5 * y\n\n\n@assert_no_logs\ndef test_bookmarks_8():\n    pdf = FakeHTML(string='''\n      <h1>a</h1>\n      <h2>b</h2>\n      <h3>c</h3>\n      <h2 style=\"bookmark-state: closed\">d</h2>\n      <h3>e</h3>\n      <h4>f</h4>\n      <h1>g</h1>\n    ''').write_pdf()\n    # a\n    # |_ b\n    # |  |_ c\n    # |_ d (closed)\n    # |  |_ e\n    # |     |_ f\n    # g\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [\n        b'a', b'b', b'c', b'd', b'e', b'f', b'g']\n    counts = re.findall(b'/Count ([0-9-]*)', pdf)\n    counts.pop(0)  # Page count\n    outlines = counts.pop()\n    assert outlines == b'5'\n    assert counts == [b'3', b'1', b'0', b'-2', b'1', b'0', b'0']\n\n\n@assert_no_logs\ndef test_bookmarks_9():\n    pdf = FakeHTML(string='''\n      <h1 style=\"bookmark-label: 'h1 on page ' counter(page)\">a</h1>\n    ''').write_pdf()\n    counts = re.findall(b'/Count ([0-9-]*)', pdf)\n    outlines = counts.pop()\n    assert outlines == b'1'\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [b'h1 on page 1']\n\n\n@assert_no_logs\ndef test_bookmarks_10():\n    pdf = FakeHTML(string='''\n      <style>\n      div:before, div:after {\n         content: '';\n         bookmark-level: 1;\n         bookmark-label: 'x';\n      }\n      </style>\n      <div>a</div>\n    ''').write_pdf()\n    # x\n    # x\n    counts = re.findall(b'/Count ([0-9-]*)', pdf)\n    outlines = counts.pop()\n    assert outlines == b'2'\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [b'x', b'x']\n\n\n@assert_no_logs\ndef test_bookmarks_11():\n    pdf = FakeHTML(string='''\n      <div style=\"display:inline; white-space:pre;\n       bookmark-level:1; bookmark-label:'a'\">\n      a\n      a\n      a\n      </div>\n      <div style=\"bookmark-level:1; bookmark-label:'b'\">\n        <div>b</div>\n        <div style=\"break-before:always\">c</div>\n      </div>\n    ''').write_pdf()\n    # a\n    # b\n    counts = re.findall(b'/Count ([0-9-]*)', pdf)\n    outlines = counts.pop()\n    assert outlines == b'2'\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [b'a', b'b']\n\n\n@assert_no_logs\ndef test_bookmarks_12():\n    pdf = FakeHTML(string='''\n      <div style=\"bookmark-level:1; bookmark-label:contents\">a</div>\n    ''').write_pdf()\n    # a\n    counts = re.findall(b'/Count ([0-9-]*)', pdf)\n    outlines = counts.pop()\n    assert outlines == b'1'\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [b'a']\n\n\n@assert_no_logs\ndef test_bookmarks_13():\n    pdf = FakeHTML(string='''\n      <div style=\"bookmark-level:1; bookmark-label:contents;\n                  text-transform:uppercase\">a</div>\n    ''').write_pdf()\n    # a\n    counts = re.findall(b'/Count ([0-9-]*)', pdf)\n    outlines = counts.pop()\n    assert outlines == b'1'\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [b'a']\n\n\n@assert_no_logs\ndef test_bookmarks_14():\n    pdf = FakeHTML(string='''\n      <h1>a</h1>\n      <h1> b c d </h1>\n      <h1> e\n             f </h1>\n      <h1> g <span> h </span> i </h1>\n    ''').write_pdf()\n    assert re.findall(b'/Count ([0-9-]*)', pdf)[-1] == b'4'\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [\n        b'a', b'b c d', b'e f', b'g h i']\n\n\n@assert_no_logs\ndef test_bookmarks_15():\n    # Regression test for #1815.\n    pdf = FakeHTML(string='''\n      <style>@page { size: 10pt 10pt }</style>\n      <h1>a</h1>\n    ''').write_pdf()\n    assert re.findall(b'/Count ([0-9-]*)', pdf)[-1] == b'1'\n    assert re.findall(b'/Title \\\\((.*)\\\\)', pdf) == [b'a']\n    assert b'/XYZ 0 10 0' in pdf\n\n\n@assert_no_logs\ndef test_links_none():\n    pdf = FakeHTML(string='<body>').write_pdf()\n    assert b'Annots' not in pdf\n\n\n@assert_no_logs\ndef test_links():\n    pdf = FakeHTML(string='''\n      <style>\n        body { margin: 0; font-size: 10pt; line-height: 2 }\n        p { display: block; height: 90pt; margin: 0 0 10pt 0 }\n        img { width: 30pt; vertical-align: top }\n      </style>\n      <p><a href=\"https://weasyprint.org\"><img src=pattern.png></a></p>\n      <p style=\"padding: 0 10pt\"><a\n         href=\"#lipsum\"><img style=\"border: solid 1pt\"\n                             src=pattern.png></a></p>\n      <p id=hello>Hello, World</p>\n      <p id=lipsum>\n        <a style=\"display: block; page-break-before: always; height: 30pt\"\n           href=\"#hel%6Co\"></a>a\n      </p>\n    ''', base_url=resource_path('<inline HTML>')).write_pdf()\n\n    uris = re.findall(b'/URI \\\\((.*)\\\\)', pdf)\n    types = re.findall(b'/S (/\\\\w*)', pdf)\n    subtypes = re.findall(b'/Subtype (/\\\\w*)', pdf)\n    rects = [\n        [float(number) for number in match.split()] for match in re.findall(\n            b'/Rect \\\\[([\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+)\\\\]', pdf)]\n\n    # 30pt wide (like the image), 20pt high (like line-height)\n    assert uris.pop(0) == b'https://weasyprint.org'\n    assert subtypes.pop(0) == b'/Link'\n    assert types.pop(0) == b'/URI'\n    assert rects.pop(0) == [0, TOP, 30, TOP - 20]\n\n    # The image itself: 30*30pt\n    assert uris.pop(0) == b'https://weasyprint.org'\n    assert subtypes.pop(0) == b'/Link'\n    assert types.pop(0) == b'/URI'\n    assert rects.pop(0) == [0, TOP, 30, TOP - 30]\n\n    # 32pt wide (image + 2 * 1pt of border), 20pt high\n    assert subtypes.pop(0) == b'/Link'\n    assert b'/Dest (lipsum)' in pdf\n    link = re.search(\n        b'\\\\(lipsum\\\\) \\\\[\\\\d+ 0 R /XYZ ([\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+)]',\n        pdf).group(1)\n    assert [float(number) for number in link.split()] == [0, TOP, 0]\n    assert rects.pop(0) == [10, TOP - 100, 10 + 32, TOP - 100 - 20]\n\n    # The image itself: 32*32pt\n    assert subtypes.pop(0) == b'/Link'\n    assert rects.pop(0) == [10, TOP - 100, 10 + 32, TOP - 100 - 32]\n\n    # 100% wide (block), 30pt high\n    assert subtypes.pop(0) == b'/Link'\n    assert b'/Dest (hello)' in pdf\n    link = re.search(\n        b'\\\\(hello\\\\) \\\\[\\\\d+ 0 R /XYZ ([\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+)]',\n        pdf).group(1)\n    assert [float(number) for number in link.split()] == [0, TOP - 200, 0]\n    assert rects.pop(0) == [0, TOP, RIGHT, TOP - 30]\n\n\n@assert_no_logs\ndef test_sorted_links():\n    # Regression test for #1352.\n    pdf = FakeHTML(string='''\n      <p id=\"zzz\">zzz</p>\n      <p id=\"aaa\">aaa</p>\n      <a href=\"#zzz\">z</a>\n      <a href=\"#aaa\">a</a>\n    ''', base_url=resource_path('<inline HTML>')).write_pdf()\n    assert b'(zzz) [' in pdf.split(b'(aaa) [')[-1]\n\n\n@assert_no_logs\ndef test_relative_links_no_height():\n    # 100% wide (block), 0pt high\n    pdf = FakeHTML(\n        string='<a href=\"../lipsum\" style=\"display: block\"></a>a',\n        base_url='https://weasyprint.org/foo/bar/').write_pdf()\n    assert b'/S /URI\\n/URI (https://weasyprint.org/foo/lipsum)'\n    assert f'/Rect [0 {TOP} {RIGHT} {TOP}]'.encode() in pdf\n\n\n@assert_no_logs\ndef test_relative_links_missing_base():\n    # Relative URI reference without a base URI\n    pdf = FakeHTML(\n        string='<a href=\"../lipsum\" style=\"display: block\"></a>a',\n        base_url=None).write_pdf()\n    assert b'/S /URI\\n/URI (../lipsum)'\n    assert f'/Rect [0 {TOP} {RIGHT} {TOP}]'.encode() in pdf\n\n\n@assert_no_logs\ndef test_relative_links_missing_base_link():\n    # Relative URI reference without a base URI: not supported for -weasy-link\n    with capture_logs() as logs:\n        pdf = FakeHTML(\n            string='<div style=\"-weasy-link: url(../lipsum)\">',\n            base_url=None).write_pdf()\n    assert b'/Annots' not in pdf\n    assert len(logs) == 1\n    assert 'WARNING: Ignored `-weasy-link: url(../lipsum)`' in logs[0]\n    assert 'Relative URI reference without a base URI' in logs[0]\n\n\n@assert_no_logs\ndef test_relative_links_internal():\n    # Internal URI reference without a base URI: OK\n    pdf = FakeHTML(\n        string='<a href=\"#lipsum\" id=\"lipsum\" style=\"display: block\"></a>a',\n        base_url=None).write_pdf()\n    assert b'/Dest (lipsum)' in pdf\n    link = re.search(\n        b'\\\\(lipsum\\\\) \\\\[\\\\d+ 0 R /XYZ ([\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+)]',\n        pdf).group(1)\n    assert [float(number) for number in link.split()] == [0, TOP, 0]\n    rect = re.search(\n        b'/Rect \\\\[([\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+)\\\\]',\n        pdf).group(1)\n    assert [float(number) for number in rect.split()] == [0, TOP, RIGHT, TOP]\n\n\n@assert_no_logs\ndef test_relative_links_anchors():\n    pdf = FakeHTML(\n        string='<div style=\"-weasy-link: url(#lipsum)\" id=\"lipsum\"></div>a',\n        base_url=None).write_pdf()\n    assert b'/Dest (lipsum)' in pdf\n    link = re.search(\n        b'\\\\(lipsum\\\\) \\\\[\\\\d+ 0 R /XYZ ([\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+)]',\n        pdf).group(1)\n    assert [float(number) for number in link.split()] == [0, TOP, 0]\n    rect = re.search(\n        b'/Rect \\\\[([\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+)\\\\]',\n        pdf).group(1)\n    assert [float(number) for number in rect.split()] == [0, TOP, RIGHT, TOP]\n\n\n@assert_no_logs\ndef test_relative_links_different_base():\n    pdf = FakeHTML(\n        string='<a href=\"/test/lipsum\"></a>a',\n        base_url='https://weasyprint.org/foo/bar/').write_pdf()\n    assert b'https://weasyprint.org/test/lipsum' in pdf\n\n\n@assert_no_logs\ndef test_relative_links_same_base():\n    pdf = FakeHTML(\n        string='<a id=\"test\" href=\"/foo/bar/#test\"></a>a',\n        base_url='https://weasyprint.org/foo/bar/').write_pdf()\n    assert b'/Dest (test)' in pdf\n\n\n@assert_no_logs\ndef test_missing_links():\n    with capture_logs() as logs:\n        pdf = FakeHTML(string='''\n          <style> a { display: block; height: 15pt } </style>\n          <a href=\"#lipsum\"></a>\n          <a href=\"#missing\" id=\"lipsum\"></a>\n          <a href=\"\"></a>a\n        ''', base_url=None).write_pdf()\n    assert b'/Dest (lipsum)' in pdf\n    assert len(logs) == 1\n    link = re.search(\n        b'\\\\(lipsum\\\\) \\\\[\\\\d+ 0 R /XYZ ([\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+)]',\n        pdf).group(1)\n    assert [float(number) for number in link.split()] == [0, TOP - 15, 0]\n    rect = re.search(\n        b'/Rect \\\\[([\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+ [\\\\d\\\\.]+)\\\\]',\n        pdf).group(1)\n    assert [float(number) for number in rect.split()] == [\n        0, TOP, RIGHT, TOP - 15]\n    assert 'ERROR: No anchor #missing for internal URI reference' in logs[0]\n\n\n@assert_no_logs\ndef test_anchor_multiple_pages():\n    pdf = FakeHTML(string='''\n      <style> a { display: block; break-after: page } </style>\n      <div id=\"lipsum\">\n        <a href=\"#lipsum\"></a>\n        <a href=\"#lipsum\"></a>\n        <a href=\"#lipsum\"></a>\n      </div>\n    ''', base_url=None).write_pdf()\n    first_page, = re.findall(b'/Kids \\\\[(\\\\d+) 0 R', pdf)\n    assert b'/Names [(lipsum) [' + first_page in pdf\n\n\n@assert_no_logs\ndef test_embed_gif():\n    assert b'/Filter /DCTDecode' not in FakeHTML(\n        base_url=resource_path('dummy.html'),\n        string='<img src=\"pattern.gif\">').write_pdf()\n\n\n@assert_no_logs\ndef test_embed_jpeg():\n    # JPEG-encoded image, embedded in PDF:\n    assert b'/Filter /DCTDecode' in FakeHTML(\n        base_url=resource_path('dummy.html'),\n        string='<img src=\"blue.jpg\">').write_pdf()\n\n\n@assert_no_logs\ndef test_embed_image_once():\n    # Image repeated multiple times, embedded once\n    assert FakeHTML(\n        base_url=resource_path('dummy.html'),\n        string='''\n          <img src=\"blue.jpg\">\n          <div style=\"background: url(blue.jpg)\"></div>\n          <img src=\"blue.jpg\">\n          <div style=\"background: url(blue.jpg) no-repeat\"></div>\n        ''').write_pdf().count(b'/Filter /DCTDecode') == 1\n\n\n@assert_no_logs\ndef test_embed_images_from_pages():\n    page1, = FakeHTML(\n        base_url=resource_path('dummy.html'),\n        string='<img src=\"blue.jpg\">').render().pages\n    page2, = FakeHTML(\n        base_url=resource_path('dummy.html'),\n        string='<img src=\"not-optimized.jpg\">').render().pages\n    document = Document(\n        (page1, page2), metadata=DocumentMetadata(),\n        font_config=FontConfiguration(), color_profiles={},\n        url_fetcher=None).write_pdf()\n    assert document.count(b'/Filter /DCTDecode') == 2\n\n\n@assert_no_logs\ndef test_document_info():\n    pdf = FakeHTML(string='''\n      <meta name=author content=\"I Me &amp; Myself\">\n      <title>Test document</title>\n      <h1>Another title</h1>\n      <meta name=generator content=\"Human after all\">\n      <meta name=keywords content=\"html ,\\tcss,\n                                   pdf,css\">\n      <meta name=description content=\"Blah… \">\n      <meta name=dcterms.created content=2011-04-21T23:00:00Z>\n      <meta name=dcterms.modified content=2013-07-21T23:46+01:00>\n    ''').write_pdf()\n    assert b'/Author (I Me & Myself)' in pdf\n    assert b'/Title (Test document)' in pdf\n    assert (\n        b'/Creator <feff00480075006d0061006e00a00061'\n        b'006600740065007200a00061006c006c>') in pdf\n    assert b'/Keywords (html, css, pdf)' in pdf\n    assert b'/Subject <feff0042006c0061006820260020>' in pdf\n    assert b'/CreationDate (D:20110421230000Z)' in pdf\n    assert b\"/ModDate (D:20130721234600+01'00)\" in pdf\n\n\n@assert_no_logs\ndef test_embedded_files_attachments(tmp_path):\n    absolute_tmp_path = tmp_path / 'some_file.txt'\n    absolute_data = b'12345678'\n    absolute_tmp_path.write_bytes(absolute_data)\n    absolute_url = path2url(absolute_tmp_path)\n    assert absolute_url.startswith('file://')\n\n    relative_tmp_path = tmp_path / 'äöü.txt'\n    relative_data = b'abcdefgh'\n    relative_tmp_path.write_bytes(relative_data)\n\n    pdf = FakeHTML(\n        string=f'''\n          <title>Test document</title>\n          <meta charset=\"utf-8\">\n          <link\n            rel=\"attachment\"\n            title=\"some file attachment äöü\"\n            href=\"data:,hi%20there\">\n          <link rel=\"attachment\" href=\"{absolute_url}\">\n          <link rel=\"attachment\" href=\"{relative_tmp_path.name}\">\n          <h1>Heading 1</h1>\n          <h2>Heading 2</h2>\n        ''',\n        base_url=tmp_path,\n    ).write_pdf(\n        attachments=[\n            Attachment('data:,oob attachment', description='Hello'),\n            'data:,raw URL',\n            io.BytesIO(b'file like obj')\n        ]\n    )\n    assert f'<{hashlib.md5(b\"hi there\").hexdigest()}>'.encode() in pdf\n    assert b'/F (attachment.bin)' in pdf\n    assert b'/UF (attachment.bin)' in pdf\n    name = BOM_UTF16_BE + 'some file attachment äöü'.encode('utf-16-be')\n    assert b'/Desc <' + name.hex().encode() + b'>' in pdf\n\n    assert hashlib.md5(absolute_data).hexdigest().encode() in pdf\n    assert absolute_tmp_path.name.encode() in pdf\n\n    assert hashlib.md5(relative_data).hexdigest().encode() in pdf\n    name = BOM_UTF16_BE + 'some file attachment äöü'.encode('utf-16-be')\n    assert b'/Desc <' + name.hex().encode() + b'>' in pdf\n\n    assert hashlib.md5(b'oob attachment').hexdigest().encode() in pdf\n    assert b'/Desc (Hello)' in pdf\n    assert hashlib.md5(b'raw URL').hexdigest().encode() in pdf\n    assert hashlib.md5(b'file like obj').hexdigest().encode() in pdf\n\n    assert b'/EmbeddedFiles' in pdf\n    assert b'/Outlines' in pdf\n\n\n@assert_no_logs\ndef test_attachments_data():\n    pdf = FakeHTML(string='''\n      <title>Test document 2</title>\n      <meta charset=\"utf-8\">\n      <link rel=\"attachment\" href=\"data:,some data\">\n    ''').write_pdf()\n    md5 = f'<{hashlib.md5(b\"some data\").hexdigest()}>'.encode()\n    assert md5 in pdf\n    assert b'EmbeddedFiles' in pdf\n\n\n@assert_no_logs\ndef test_attachments_data_with_anchor():\n    pdf = FakeHTML(string='''\n      <title>Test document 2</title>\n      <meta charset=\"utf-8\">\n      <link rel=\"attachment\" href=\"data:,some data\">\n      <h1 id=\"title\">Title</h1>\n      <a href=\"#title\">example</a>\n    ''').write_pdf()\n    md5 = f'<{hashlib.md5(b\"some data\").hexdigest()}>'.encode()\n    assert md5 in pdf\n    assert b'EmbeddedFiles' in pdf\n\n\n@assert_no_logs\ndef test_attachments_no_href():\n    with capture_logs() as logs:\n        pdf = FakeHTML(string='''\n          <title>Test document 2</title>\n          <meta charset=\"utf-8\">\n          <link rel=\"attachment\">\n        ''').write_pdf()\n    assert b'Names' not in pdf\n    assert b'Outlines' not in pdf\n    assert len(logs) == 1\n    assert 'Missing href' in logs[0]\n\n\n@assert_no_logs\ndef test_attachments_none():\n    pdf = FakeHTML(string='''\n      <title>Test document 3</title>\n      <meta charset=\"utf-8\">\n      <h1>Heading</h1>\n    ''').write_pdf()\n    assert b'Names' not in pdf\n    assert b'Outlines' in pdf\n\n\n@assert_no_logs\ndef test_attachments_none_empty():\n    pdf = FakeHTML(string='''\n      <title>Test document 3</title>\n      <meta charset=\"utf-8\">\n    ''').write_pdf()\n    assert b'Names' not in pdf\n    assert b'Outlines' not in pdf\n\n\n@assert_no_logs\ndef test_annotations():\n    pdf = FakeHTML(string='''\n      <title>Test document</title>\n      <meta charset=\"utf-8\">\n      <a\n        rel=\"attachment\"\n        href=\"data:,some data\"\n        download>A link that lets you download an attachment</a>\n    ''').write_pdf()\n\n    assert hashlib.md5(b'some data').hexdigest().encode() in pdf\n    assert b'/FileAttachment' in pdf\n    assert b'/EmbeddedFiles' not in pdf\n\n\n@pytest.mark.parametrize(('style', 'media', 'bleed', 'trim'), [\n    ('bleed: 30pt; size: 10pt',\n     [-30, -30, 40, 40],\n     [-10, -10, 20, 20],\n     [0, 0, 10, 10]),\n    ('bleed: 15pt 3pt 6pt 18pt; size: 12pt 15pt',\n     [-18, -15, 15, 21],\n     [-10, -10, 15, 21],\n     [0, 0, 12, 15]),\n])\n@assert_no_logs\ndef test_bleed(style, media, bleed, trim):\n    pdf = FakeHTML(string='''\n      <title>Test document</title>\n      <style>@page { %s }</style>\n      <body>test\n    ''' % style).write_pdf()\n    assert f'/MediaBox {str(media).replace(\",\", \"\")}'.encode() in pdf\n    assert f'/BleedBox {str(bleed).replace(\",\", \"\")}'.encode() in pdf\n    assert f'/TrimBox {str(trim).replace(\",\", \"\")}'.encode() in pdf\n\n\n@assert_no_logs\ndef test_default_rdf_metadata():\n    pdf_document = FakeHTML(string='<body>test</body>').render()\n\n    pdf_document.metadata.title = None\n\n    pdf_bytes = pdf_document.write_pdf(\n        pdf_variant='pdf/a-3b', pdf_identifier=b'example-bytes', uncompressed_pdf=True)\n    assert b'<rdf:RDF xmlns:pdf=\"http://ns.adobe.com/pdf/1.3/\"' in pdf_bytes\n\n\n@assert_no_logs\ndef test_custom_rdf_metadata():\n    def generate_rdf_metadata(*args, **kwargs):\n        return b'TEST_METADATA'\n\n    pdf_document = FakeHTML(string='<body>test</body>').render()\n\n    pdf_document.metadata.title = None\n    pdf_document.metadata.generate_rdf_metadata = generate_rdf_metadata\n\n    pdf_bytes = pdf_document.write_pdf(\n        pdf_variant='pdf/a-3b', pdf_identifier=b'example-bytes', uncompressed_pdf=True)\n    assert b'TEST_METADATA' in pdf_bytes\n\n\n@assert_no_logs\ndef test_font_descent_ascent():\n    pdf = FakeHTML(string='''\n      <html style=\"font-family: weasyprint\">abc\n    ''').write_pdf()\n    assert b'/Descent -200' in pdf\n    assert b'/Ascent 800' in pdf\n\n\n@assert_no_logs\ndef test_pdf_tags_inline_table():\n    # Regression test for #2601.\n    FakeHTML(string='''\n      <html lang=\"en\"><table style=\"display: inline\"><td>abc\n    ''').write_pdf(pdf_tags=True)\n"
  },
  {
    "path": "tests/test_presentational_hints.py",
    "content": "\"\"\"Test the HTML presentational hints.\"\"\"\n\nfrom weasyprint import CSS, HTML\n\nfrom .testing_utils import BASE_URL, assert_no_logs\n\nPH_TESTING_CSS = CSS(string='''\n@page {margin: 0; size: 1000px 1000px}\nbody {margin: 0}\n''')\n\n\n@assert_no_logs\ndef test_no_ph():\n    # Test both CSS and non-CSS rules\n    document = HTML(string='''\n      <hr size=100 />\n      <table align=right width=100><td>0</td></table>\n    ''').render(stylesheets=[PH_TESTING_CSS])\n    page, = document.pages\n    html, = page._page_box.children\n    body, = html.children\n    hr, table = body.children\n    assert hr.border_height() != 100\n    assert table.position_x == 0\n\n\n@assert_no_logs\ndef test_ph_page():\n    document = HTML(string='''\n      <body marginheight=2 topmargin=3 leftmargin=5\n            bgcolor=red text=blue />\n    ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)\n    page, = document.pages\n    html, = page._page_box.children\n    body, = html.children\n    assert body.margin_top == 2\n    assert body.margin_bottom == 2\n    assert body.margin_left == 5\n    assert body.margin_right == 0\n    assert body.style['background_color'] == (1, 0, 0, 1)\n    assert body.style['color'] == (0, 0, 1, 1)\n\n\n@assert_no_logs\ndef test_ph_flow():\n    document = HTML(string='''\n      <pre wrap></pre>\n      <center></center>\n      <div align=center></div>\n      <div align=middle></div>\n      <div align=left></div>\n      <div align=right></div>\n      <div align=justify></div>\n    ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)\n    page, = document.pages\n    html, = page._page_box.children\n    body, = html.children\n    pre, center, div1, div2, div3, div4, div5 = body.children\n    assert pre.style['white_space'] == 'pre-wrap'\n    assert center.style['text_align_all'] == 'center'\n    assert div1.style['text_align_all'] == 'center'\n    assert div2.style['text_align_all'] == 'center'\n    assert div3.style['text_align_all'] == 'left'\n    assert div4.style['text_align_all'] == 'right'\n    assert div5.style['text_align_all'] == 'justify'\n\n\n@assert_no_logs\ndef test_ph_phrasing():\n    document = HTML(string='''\n      <br clear=left>\n      <br clear=right />\n      <br clear=both />\n      <br clear=all />\n      <font color=red face=weasyprint size=7></font>\n      <Font size=4></Font>\n      <font size=+5 ></font>\n      <font size=-5 ></font>\n    ''', base_url=BASE_URL).render(\n        stylesheets=[PH_TESTING_CSS], presentational_hints=True)\n    page, = document.pages\n    html, = page._page_box.children\n    body, = html.children\n    line1, line2, line3, line4, line5 = body.children\n    br1, = line1.children\n    br2, = line2.children\n    br3, = line3.children\n    br4, = line4.children\n    font1, font2, font3, font4 = line5.children\n    assert br1.style['clear'] == 'left'\n    assert br2.style['clear'] == 'right'\n    assert br3.style['clear'] == 'both'\n    assert br4.style['clear'] == 'both'\n    assert font1.style['color'] == (1, 0, 0, 1)\n    assert font1.style['font_family'] == ('weasyprint',)\n    assert font1.style['font_size'] == 1.5 * 2 * 16\n    assert font2.style['font_size'] == 6 / 5 * 16\n    assert font3.style['font_size'] == 1.5 * 2 * 16\n    assert font4.style['font_size'] == 8 / 9 * 16\n\n\n@assert_no_logs\ndef test_ph_lists():\n    document = HTML(string='''\n      <ol>\n        <li type=A></li>\n        <li type=1></li>\n        <li type=a></li>\n        <li type=i></li>\n        <li type=I></li>\n      </ol>\n      <ul>\n        <li type=circle></li>\n        <li type=disc></li>\n        <li type=square></li>\n      </ul>\n    ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)\n    page, = document.pages\n    html, = page._page_box.children\n    body, = html.children\n    ol, ul = body.children\n    oli1, oli2, oli3, oli4, oli5 = ol.children\n    uli1, uli2, uli3 = ul.children\n    assert oli1.style['list_style_type'] == 'upper-alpha'\n    assert oli2.style['list_style_type'] == 'decimal'\n    assert oli3.style['list_style_type'] == 'lower-alpha'\n    assert oli4.style['list_style_type'] == 'lower-roman'\n    assert oli5.style['list_style_type'] == 'upper-roman'\n    assert uli1.style['list_style_type'] == 'circle'\n    assert uli2.style['list_style_type'] == 'disc'\n    assert uli3.style['list_style_type'] == 'square'\n\n\n@assert_no_logs\ndef test_ph_lists_types():\n    document = HTML(string='''\n      <ol type=A></ol>\n      <ol type=1></ol>\n      <ol type=a></ol>\n      <ol type=i></ol>\n      <ol type=I></ol>\n      <ul type=circle></ul>\n      <ul type=disc></ul>\n      <ul type=square></ul>\n    ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)\n    page, = document.pages\n    html, = page._page_box.children\n    body, = html.children\n    ol1, ol2, ol3, ol4, ol5, ul1, ul2, ul3 = body.children\n    assert ol1.style['list_style_type'] == 'upper-alpha'\n    assert ol2.style['list_style_type'] == 'decimal'\n    assert ol3.style['list_style_type'] == 'lower-alpha'\n    assert ol4.style['list_style_type'] == 'lower-roman'\n    assert ol5.style['list_style_type'] == 'upper-roman'\n    assert ul1.style['list_style_type'] == 'circle'\n    assert ul2.style['list_style_type'] == 'disc'\n    assert ul3.style['list_style_type'] == 'square'\n\n\n@assert_no_logs\ndef test_ph_tables():\n    document = HTML(string='''\n      <table align=left rules=none></table>\n      <table align=right rules=groups></table>\n      <table align=center rules=rows></table>\n      <table border=10 cellspacing=3 bordercolor=green>\n        <thead>\n          <tr>\n            <th valign=top></th>\n          </tr>\n        </thead>\n        <tr>\n          <td nowrap><h1 align=right></h1><p align=center></p></td>\n        </tr>\n        <tr>\n        </tr>\n        <tfoot align=justify>\n          <tr>\n            <td></td>\n          </tr>\n        </tfoot>\n      </table>\n    ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)\n    page, = document.pages\n    html, = page._page_box.children\n    body, = html.children\n    wrapper1, wrapper2, wrapper3, wrapper4, = body.children\n    assert wrapper1.style['float'] == 'left'\n    assert wrapper2.style['float'] == 'right'\n    assert wrapper3.style['margin_left'] == 'auto'\n    assert wrapper3.style['margin_right'] == 'auto'\n    assert wrapper1.children[0].style['border_left_style'] == 'hidden'\n    assert wrapper1.style['border_collapse'] == 'collapse'\n    assert wrapper2.children[0].style['border_left_style'] == 'hidden'\n    assert wrapper2.style['border_collapse'] == 'collapse'\n    assert wrapper3.children[0].style['border_left_style'] == 'hidden'\n    assert wrapper3.style['border_collapse'] == 'collapse'\n\n    table4, = wrapper4.children\n    assert table4.style['border_top_style'] == 'outset'\n    assert table4.style['border_top_width'] == 10\n    assert table4.style['border_spacing'] == (3, 3)\n    r, g, b, a = table4.style['border_left_color']\n    assert g > r\n    assert g > b\n    head_group, rows_group, foot_group = table4.children\n    head, = head_group.children\n    th, = head.children\n    assert th.style['vertical_align'] == 'top'\n    line1, line2 = rows_group.children\n    td, = line1.children\n    assert td.style['white_space'] == 'nowrap'\n    assert td.style['border_top_width'] == 1\n    assert td.style['border_top_style'] == 'inset'\n    h1, p = td.children\n    assert h1.style['text_align_all'] == 'right'\n    assert p.style['text_align_all'] == 'center'\n    foot, = foot_group.children\n    tr, = foot.children\n    assert tr.style['text_align_all'] == 'justify'\n\n\n@assert_no_logs\ndef test_ph_hr():\n    document = HTML(string='''\n      <hr align=left>\n      <hr align=right />\n      <hr align=both color=red />\n      <hr align=center noshade size=10 />\n      <hr align=all size=8 width=100 />\n    ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)\n    page, = document.pages\n    html, = page._page_box.children\n    body, = html.children\n    hr1, hr2, hr3, hr4, hr5 = body.children\n    assert hr1.margin_left == 0\n    assert hr1.style['margin_right'] == 'auto'\n    assert hr2.style['margin_left'] == 'auto'\n    assert hr2.margin_right == 0\n    assert hr3.style['margin_left'] == 'auto'\n    assert hr3.style['margin_right'] == 'auto'\n    assert hr3.style['color'] == (1, 0, 0, 1)\n    assert hr4.style['margin_left'] == 'auto'\n    assert hr4.style['margin_right'] == 'auto'\n    assert hr4.border_height() == 10\n    assert hr4.style['border_top_width'] == 5\n    assert hr5.border_height() == 8\n    assert hr5.height == 6\n    assert hr5.width == 100\n    assert hr5.style['border_top_width'] == 1\n\n\n@assert_no_logs\ndef test_ph_embedded():\n    document = HTML(string='''\n      <object data=\"data:image/svg+xml,<svg></svg>\"\n              align=top hspace=10 vspace=20></object>\n      <img src=\"data:image/svg+xml,<svg></svg>\" alt=text\n              align=right width=10 height=20 />\n      <embed src=\"data:image/svg+xml,<svg></svg>\" align=texttop />\n    ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True)\n    page, = document.pages\n    html, = page._page_box.children\n    body, = html.children\n    line, = body.children\n    object_, text1, img, embed, text2 = line.children\n    assert embed.style['vertical_align'] == 'text-top'\n    assert object_.style['vertical_align'] == 'top'\n    assert object_.margin_top == 20\n    assert object_.margin_left == 10\n    assert img.style['float'] == 'right'\n    assert img.width == 10\n    assert img.height == 20\n"
  },
  {
    "path": "tests/test_stacking.py",
    "content": "\"\"\"Test CSS stacking contexts.\"\"\"\n\nimport pytest\n\nfrom weasyprint.stacking import StackingContext\n\nfrom .testing_utils import assert_no_logs, render_pages, serialize\n\nz_index_source = '''\n  <style>\n    @page { size: 10px }\n    div, div * { width: 10px; height: 10px; position: absolute }\n    article { background: red; z-index: %s }\n    section { background: blue; z-index: %s }\n    nav { background: lime; z-index: %s }\n  </style>\n  <div>\n    <article></article>\n    <section></section>\n    <nav></nav>\n  </div>'''\n\n\ndef flatten_blocks_and_cells(blocks_and_cells):\n    for block, blocks_and_cells in blocks_and_cells.items():\n        yield block.element_tag\n        yield from flatten_blocks_and_cells(blocks_and_cells)\n\n\ndef serialize_stacking(context):\n    return (\n        context.box.element_tag,\n        list(flatten_blocks_and_cells(context.blocks_and_cells)),\n        [serialize_stacking(context) for context in context.zero_z_contexts])\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('source', 'contexts'), [\n    ('''\n      <p id=lorem></p>\n      <div style=\"position: relative\">\n        <p id=lipsum></p>\n      </div>''',\n     ('html', ['body', 'p'], [('div', ['p'], [])])),\n    ('''\n      <div style=\"position: relative\">\n        <p style=\"position: relative\"></p>\n      </div>''',\n     ('html', ['body'], [('div', [], []), ('p', [], [])])),\n])\ndef test_nested(source, contexts):\n    page, = render_pages(source)\n    html, = page.children\n    assert serialize_stacking(StackingContext.from_box(html, page)) == contexts\n\n\n@assert_no_logs\ndef test_image_contexts():\n    page, = render_pages('''\n      <body>Some text: <img style=\"position: relative\" src=pattern.png>''')\n    html, = page.children\n    context = StackingContext.from_box(html, page)\n    # The image is *not* in this context:\n    assert serialize([context.box]) == [\n        ('html', 'Block', [\n            ('body', 'Block', [\n                ('body', 'Line', [\n                    ('body', 'Text', 'Some text: ')])])])]\n    # ... but in a sub-context:\n    assert serialize(c.box for c in context.zero_z_contexts) == [\n        ('img', 'InlineReplaced', '<replaced>')]\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('z_indexes', 'color'), [\n    ((3, 2, 1), 'R'),\n    ((1, 2, 3), 'G'),\n    ((1, 2, -3), 'B'),\n    ((1, 2, 'auto'), 'B'),\n    ((-1, 'auto', -2), 'B'),\n])\ndef test_z_index(assert_pixels, z_indexes, color):\n    assert_pixels('\\n'.join([color * 10] * 10), z_index_source % z_indexes)\n"
  },
  {
    "path": "tests/test_text.py",
    "content": "\"\"\"Test the text layout.\"\"\"\n\nimport pytest\n\nfrom weasyprint.css import InitialStyle\nfrom weasyprint.formatting_structure.build import capitalize\nfrom weasyprint.text.fonts import FontConfiguration\nfrom weasyprint.text.line_break import split_first_line\n\nfrom .testing_utils import MONO_FONTS, SANS_FONTS, assert_no_logs, render_pages\n\n\ndef make_text(text, width=None, **style):\n    \"\"\"Wrapper for split_first_line() creating a style dict.\"\"\"\n    new_style = InitialStyle(FontConfiguration())\n    new_style['font_family'] = MONO_FONTS.split(',')\n    new_style.update(style)\n    return split_first_line(\n        text, new_style, context=None, max_width=width,\n        justification_spacing=0)\n\n\n@assert_no_logs\ndef test_line_content():\n    for width, remaining in [(100, 'text for test'),\n                             (45, 'is a text for test')]:\n        text = 'This is a text for test'\n        _, length, resume_index, _, _, _ = make_text(\n            text, width, font_family=SANS_FONTS.split(','), font_size=19)\n        assert text[resume_index:] == remaining\n        assert length + 1 == resume_index  # +1 for the removed trailing space\n\n\n@assert_no_logs\ndef test_line_with_any_width():\n    _, _, _, width_1, _, _ = make_text('some text')\n    _, _, _, width_2, _, _ = make_text('some text some text')\n    assert width_1 < width_2\n\n\n@assert_no_logs\ndef test_line_breaking():\n    string = 'Thïs is a text for test'\n\n    # These two tests do not really rely on installed fonts\n    _, _, resume_index, _, _, _ = make_text(string, 90, font_size=1)\n    assert resume_index is None\n\n    _, _, resume_index, _, _, _ = make_text(string, 90, font_size=100)\n    assert string.encode()[resume_index:].decode() == 'is a text for test'\n\n    _, _, resume_index, _, _, _ = make_text(\n        string, 100, font_family=SANS_FONTS.split(','), font_size=19)\n    assert string.encode()[resume_index:].decode() == 'text for test'\n\n\n@assert_no_logs\ndef test_line_breaking_rtl():\n    string = 'لوريم ايبسوم دولا'\n\n    # These two tests do not really rely on installed fonts\n    _, _, resume_index, _, _, _ = make_text(string, 90, font_size=1)\n    assert resume_index is None\n\n    _, _, resume_index, _, _, _ = make_text(string, 90, font_size=100)\n    assert string.encode()[resume_index:].decode() == 'ايبسوم دولا'\n\n\n@assert_no_logs\ndef test_line_breaking_nbsp():\n    # Regression test for #1561.\n    page, = render_pages('''\n      <style>\n        body { font-family: weasyprint; width: 7.5em }\n      </style>\n      <body>a <span>b</span> c d&nbsp;<span>ef\n    ''')\n    html, = page.children\n    body, = html.children\n    line_1, line_2 = body.children\n    assert line_1.children[0].text == 'a '\n    assert line_1.children[1].children[0].text == 'b'\n    assert line_1.children[2].text == ' c'\n    assert line_2.children[0].text == 'd\\xa0'\n    assert line_2.children[1].children[0].text == 'ef'\n\n\n@assert_no_logs\ndef test_text_dimension():\n    string = 'This is a text for test. This is a test for text.py'\n    _, _, _, width_1, height_1, _ = make_text(string, 200, font_size=12)\n\n    _, _, _, width_2, height_2, _ = make_text(string, 200, font_size=20)\n    assert width_1 * height_1 < width_2 * height_2\n\n\n@assert_no_logs\ndef test_text_font_size_zero():\n    page, = render_pages('''\n      <style>\n        p { font-size: 0; }\n      </style>\n      <p>test font size zero</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line, = paragraph.children\n    # zero-sized text boxes are removed\n    assert not line.children\n    assert line.height == 0\n    assert paragraph.height == 0\n\n\n@assert_no_logs\ndef test_text_font_size_very_small():\n    # Regression test for #1499.\n    page, = render_pages('''\n      <style>\n        p { font-size: 0.00000001px }\n      </style>\n      <p>test font size zero</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line, = paragraph.children\n    assert line.height < 0.001\n    assert paragraph.height < 0.001\n\n\n@assert_no_logs\ndef test_text_spaced_inlines():\n    page, = render_pages('''\n      <p>start <i><b>bi1</b> <b>bi2</b></i> <b>b1</b> end</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line, = paragraph.children\n    start, i, space, b, end = line.children\n    assert start.text == 'start '\n    assert space.text == ' '\n    assert space.width > 0\n    assert end.text == ' end'\n\n    bi1, space, bi2 = i.children\n    bi1, = bi1.children\n    bi2, = bi2.children\n    assert bi1.text == 'bi1'\n    assert space.text == ' '\n    assert space.width > 0\n    assert bi2.text == 'bi2'\n\n    b1, = b.children\n    assert b1.text == 'b1'\n\n\n@assert_no_logs\ndef test_text_align_left():\n    # <-------------------->  page, body\n    #     +-----+\n    # +---+     |\n    # |   |     |\n    # +---+-----+\n\n    # ^   ^     ^          ^\n    # x=0 x=40  x=100      x=200\n    page, = render_pages('''\n      <style>\n        @page { size: 200px }\n      </style>\n      <body>\n        <img src=\"pattern.png\" style=\"width: 40px\"\n        ><img src=\"pattern.png\" style=\"width: 60px\">''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    img_1, img_2 = line.children\n    # initial value for text-align: left (in ltr text)\n    assert img_1.position_x == 0\n    assert img_2.position_x == 40\n\n\n@assert_no_logs\ndef test_text_align_right():\n    # <-------------------->  page, body\n    #                +-----+\n    #            +---+     |\n    #            |   |     |\n    #            +---+-----+\n\n    # ^          ^   ^     ^\n    # x=0        x=100     x=200\n    #                x=140\n    page, = render_pages('''\n      <style>\n        @page { size: 200px }\n        body { text-align: right }\n      </style>\n      <body>\n        <img src=\"pattern.png\" style=\"width: 40px\"\n        ><img src=\"pattern.png\" style=\"width: 60px\">''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    img_1, img_2 = line.children\n    assert img_1.position_x == 100  # 200 - 60 - 40\n    assert img_2.position_x == 140  # 200 - 60\n\n\n@assert_no_logs\ndef test_text_align_center():\n    # <-------------------->  page, body\n    #           +-----+\n    #       +---+     |\n    #       |   |     |\n    #       +---+-----+\n\n    # ^     ^   ^     ^\n    # x=    x=50     x=150\n    #           x=90\n    page, = render_pages('''\n      <style>\n        @page { size: 200px }\n        body { text-align: center }\n      </style>\n      <body>\n        <img src=\"pattern.png\" style=\"width: 40px\"\n        ><img src=\"pattern.png\" style=\"width: 60px\">''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    img_1, img_2 = line.children\n    assert img_1.position_x == 50\n    assert img_2.position_x == 90\n\n\n@assert_no_logs\ndef test_text_align_justify():\n    page, = render_pages('''\n      <style>\n        @page { size: 300px 1000px }\n        body { text-align: justify }\n      </style>\n      <p><img src=\"pattern.png\" style=\"width: 40px\">\n        <strong>\n          <img src=\"pattern.png\" style=\"width: 60px\">\n          <img src=\"pattern.png\" style=\"width: 10px\">\n          <img src=\"pattern.png\" style=\"width: 100px\"\n        ></strong><img src=\"pattern.png\" style=\"width: 290px\"\n        ><!-- Last image will be on its own line. -->''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line_1, line_2 = paragraph.children\n    image_1, space_1, strong = line_1.children\n    image_2, space_2, image_3, space_3, image_4 = strong.children\n    image_5, = line_2.children\n    assert space_1.text == ' '\n    assert space_2.text == ' '\n    assert space_3.text == ' '\n\n    assert image_1.position_x == 0\n    assert space_1.position_x == 40\n    assert strong.position_x == 70\n    assert image_2.position_x == 70\n    assert space_2.position_x == 130\n    assert image_3.position_x == 160\n    assert space_3.position_x == 170\n    assert image_4.position_x == 200\n    assert strong.width == 230\n\n    assert image_5.position_x == 0\n\n\n@assert_no_logs\ndef test_text_align_justify_all():\n    page, = render_pages('''\n      <style>\n        @page { size: 300px 1000px }\n        body { text-align: justify-all }\n      </style>\n      <p><img src=\"pattern.png\" style=\"width: 40px\">\n        <strong>\n          <img src=\"pattern.png\" style=\"width: 60px\">\n          <img src=\"pattern.png\" style=\"width: 10px\">\n          <img src=\"pattern.png\" style=\"width: 100px\"\n        ></strong><img src=\"pattern.png\" style=\"width: 200px\">\n        <img src=\"pattern.png\" style=\"width: 10px\">''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line_1, line_2 = paragraph.children\n    image_1, space_1, strong = line_1.children\n    image_2, space_2, image_3, space_3, image_4 = strong.children\n    image_5, space_4, image_6 = line_2.children\n    assert space_1.text == ' '\n    assert space_2.text == ' '\n    assert space_3.text == ' '\n    assert space_4.text == ' '\n\n    assert image_1.position_x == 0\n    assert space_1.position_x == 40\n    assert strong.position_x == 70\n    assert image_2.position_x == 70\n    assert space_2.position_x == 130\n    assert image_3.position_x == 160\n    assert space_3.position_x == 170\n    assert image_4.position_x == 200\n    assert strong.width == 230\n\n    assert image_5.position_x == 0\n    assert space_4.position_x == 200\n    assert image_6.position_x == 290\n\n\n@assert_no_logs\ndef test_text_align_all_last():\n    page, = render_pages('''\n      <style>\n        @page { size: 300px 1000px }\n        body { text-align-all: justify; text-align-last: right }\n      </style>\n      <p><img src=\"pattern.png\" style=\"width: 40px\">\n        <strong>\n          <img src=\"pattern.png\" style=\"width: 60px\">\n          <img src=\"pattern.png\" style=\"width: 10px\">\n          <img src=\"pattern.png\" style=\"width: 100px\"\n        ></strong><img src=\"pattern.png\" style=\"width: 200px\"\n        ><img src=\"pattern.png\" style=\"width: 10px\">''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line_1, line_2 = paragraph.children\n    image_1, space_1, strong = line_1.children\n    image_2, space_2, image_3, space_3, image_4 = strong.children\n    image_5, image_6 = line_2.children\n    assert space_1.text == ' '\n    assert space_2.text == ' '\n    assert space_3.text == ' '\n\n    assert image_1.position_x == 0\n    assert space_1.position_x == 40\n    assert strong.position_x == 70\n    assert image_2.position_x == 70\n    assert space_2.position_x == 130\n    assert image_3.position_x == 160\n    assert space_3.position_x == 170\n    assert image_4.position_x == 200\n    assert strong.width == 230\n\n    assert image_5.position_x == 90\n    assert image_6.position_x == 290\n\n\n@assert_no_logs\ndef test_text_align_not_enough_space():\n    page, = render_pages('''\n      <style>\n        p { text-align: center; width: 0 }\n        span { display: inline-block }\n      </style>\n      <p><span>aaaaaaaaaaaaaaaaaaaaaaaaaa</span></p>''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    span, = paragraph.children\n    assert span.position_x == 0\n\n\n@assert_no_logs\ndef test_text_align_justify_no_space():\n    # single-word line (zero spaces)\n    page, = render_pages('''\n      <style>\n        body { text-align: justify; width: 50px }\n      </style>\n      <p>Supercalifragilisticexpialidocious bar</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line_1, line_2 = paragraph.children\n    text, = line_1.children\n    assert text.position_x == 0\n\n\n@assert_no_logs\ndef test_text_align_justify_text_indent():\n    # text-indent\n    page, = render_pages('''\n      <style>\n        @page { size: 300px 1000px }\n        body { text-align: justify }\n        p { text-indent: 3px }\n      </style>\n      <p><img src=\"pattern.png\" style=\"width: 40px\">\n        <strong>\n          <img src=\"pattern.png\" style=\"width: 60px\">\n          <img src=\"pattern.png\" style=\"width: 10px\">\n          <img src=\"pattern.png\" style=\"width: 100px\"\n        ></strong><img src=\"pattern.png\" style=\"width: 290px\"\n        ><!-- Last image will be on its own line. -->''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line_1, line_2 = paragraph.children\n    image_1, space_1, strong = line_1.children\n    image_2, space_2, image_3, space_3, image_4 = strong.children\n    image_5, = line_2.children\n    assert space_1.text == ' '\n    assert space_2.text == ' '\n    assert space_3.text == ' '\n\n    assert image_1.position_x == 3\n    assert space_1.position_x == 43\n    assert strong.position_x == 72\n    assert image_2.position_x == 72\n    assert space_2.position_x == 132\n    assert image_3.position_x == 161\n    assert space_3.position_x == 171\n    assert image_4.position_x == 200\n    assert strong.width == 228\n\n    assert image_5.position_x == 0\n\n\n@assert_no_logs\ndef test_text_align_justify_no_break_between_children():\n    # Test justification when line break happens between two inline children\n    # that must stay together.\n    # Regression test for #637.\n    page, = render_pages('''\n      <style>\n        p { text-align: justify; font-family: weasyprint; width: 7em }\n      </style>\n      <p>\n        <span>a</span>\n        <span>b</span>\n        <span>bla</span><span>,</span>\n        <span>b</span>\n      </p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line_1, line_2 = paragraph.children\n\n    span_1, space_1, span_2, space_2 = line_1.children\n    assert span_1.position_x == 0\n    assert span_2.position_x == 6 * 16  # 1 character + 5 spaces\n    assert line_1.width == 7 * 16  # 7em\n\n    span_1, span_2, space_1, span_3, space_2 = line_2.children\n    assert span_1.position_x == 0\n    assert span_2.position_x == 3 * 16  # 3 characters\n    assert span_3.position_x == 5 * 16  # (3 + 1) characters + 1 space\n\n\n@pytest.mark.parametrize('text', [\n    'Lorem ipsum dolor<em>sit amet</em>',\n    'Lorem ipsum <em>dolorsit</em> amet',\n    'Lorem ipsum <em></em>dolorsit amet',\n    'Lorem ipsum<em> </em>dolorsit amet',\n    'Lorem ipsum<em> dolorsit</em> amet',\n    'Lorem ipsum <em>dolorsit </em>amet',\n])\n@assert_no_logs\ndef test_word_spacing(text):\n    # Regression test.\n    page, = render_pages('''\n      <style></style> <!-- element.text is None -->\n      <body><strong>Lorem ipsum dolor<em>sit amet</em></strong>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    strong_1, = line.children\n\n    page, = render_pages('''\n      <style>strong { word-spacing: 11px }</style>\n      <body><strong>%s</strong>''' % text)\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    strong_2, = line.children\n\n    assert strong_2.width - strong_1.width == 33\n\n\n@assert_no_logs\ndef test_letter_spacing_1():\n    page, = render_pages('''\n        <body><strong>Supercalifragilisticexpialidocious</strong>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    strong_1, = line.children\n\n    page, = render_pages('''\n        <style>strong { letter-spacing: 11px }</style>\n        <body><strong>Supercalifragilisticexpialidocious</strong>''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    strong_2, = line.children\n    assert strong_2.width - strong_1.width == 34 * 11\n\n    # an embedded tag should not affect the single-line letter spacing\n    page, = render_pages(\n        '<style>strong { letter-spacing: 11px }</style>'\n        '<body><strong>Supercali<span>fragilistic</span>expialidocious'\n        '</strong>')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    strong_3, = line.children\n    assert strong_3.width == strong_2.width\n\n    # duplicate wrapped lines should also have same overall width\n    # Note work-around for word-wrap bug (issue #163) by marking word\n    # as an inline-block\n    page, = render_pages(\n        '<style>'\n        '  strong {'\n        '    letter-spacing: 11px;'\n        f'    max-width: {strong_3.width * 1.5}px'\n        '}'\n        '  span { display: inline-block }'\n        '</style>'\n        '<body><strong>'\n        '  <span>Supercali<i>fragilistic</i>expialidocious</span> '\n        '  <span>Supercali<i>fragilistic</i>expialidocious</span>'\n        '</strong>')\n    html, = page.children\n    body, = html.children\n    line1, line2 = body.children\n    assert line1.children[0].width == line2.children[0].width\n    assert line1.children[0].width == strong_2.width\n\n\n@pytest.mark.parametrize('spacing', ['word-spacing', 'letter-spacing'])\n@assert_no_logs\ndef test_spacing_ex(spacing):\n    # Regression test.\n    render_pages(f'<div style=\"{spacing}: 2ex\">abc def')\n\n\n@pytest.mark.parametrize('indent', ['12px', '6%'])\n@assert_no_logs\ndef test_text_indent(indent):\n    page, = render_pages('''\n        <style>\n            @page { size: 220px }\n            body { margin: 10px; text-indent: %(indent)s }\n        </style>\n        <p>Some text that is long enough that it take at least three line,\n           but maybe more.\n    ''' % {'indent': indent})\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    lines = paragraph.children\n    text_1, = lines[0].children\n    text_2, = lines[1].children\n    text_3, = lines[2].children\n    assert text_1.position_x == 22  # 10px margin-left + 12px indent\n    assert text_2.position_x == 10  # No indent\n    assert text_3.position_x == 10  # No indent\n\n\n@assert_no_logs\ndef test_text_indent_inline():\n    # Regression test for #1000.\n    page, = render_pages('''\n        <style>\n            p { display: inline-block; text-indent: 1em; font-family: weasyprint }\n        </style>\n        <p><span>text\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line, = paragraph.children\n    assert line.width == (4 + 1) * 16\n\n\n@pytest.mark.parametrize('indent', ['12px', '6%'])\n@assert_no_logs\ndef test_text_indent_multipage(indent):\n    # Regression test for #706.\n    pages = render_pages('''\n        <style>\n            @page { size: 220px 1.5em; margin: 0 }\n            body { margin: 10px; text-indent: %(indent)s }\n        </style>\n        <p>Some text that is long enough that it take at least three line,\n           but maybe more.\n    ''' % {'indent': indent})\n    page = pages.pop(0)\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line, = paragraph.children\n    text, = line.children\n    assert text.position_x == 22  # 10px margin-left + 12px indent\n\n    page = pages.pop(0)\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line, = paragraph.children\n    text, = line.children\n    assert text.position_x == 10  # No indent\n\n\n@assert_no_logs\ndef test_hyphenate_character_1():\n    page, = render_pages(\n        '<html style=\"width: 5em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        'hyphenate-character: \\'!\\'\" lang=fr>'\n        'hyphénation')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) > 1\n    assert lines[0].children[0].text.endswith('!')\n    full_text = ''.join(line.children[0].text for line in lines)\n    assert full_text.replace('!', '') == 'hyphénation'\n\n\n@assert_no_logs\ndef test_hyphenate_character_2():\n    page, = render_pages(\n        '<html style=\"width: 5em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        'hyphenate-character: \\'à\\'\" lang=fr>'\n        'hyphénation')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) > 1\n    assert lines[0].children[0].text.endswith('à')\n    full_text = ''.join(line.children[0].text for line in lines)\n    assert full_text.replace('à', '') == 'hyphénation'\n\n\n@assert_no_logs\ndef test_hyphenate_character_3():\n    page, = render_pages(\n        '<html style=\"width: 5em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        'hyphenate-character: \\'ù ù\\'\" lang=fr>'\n        'hyphénation')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) > 1\n    assert lines[0].children[0].text.endswith('ù ù')\n    full_text = ''.join(line.children[0].text for line in lines)\n    assert full_text.replace(' ', '').replace('ù', '') == 'hyphénation'\n\n\n@assert_no_logs\ndef test_hyphenate_character_4():\n    page, = render_pages(\n        '<html style=\"width: 5em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        'hyphenate-character: \\'\\'\" lang=fr>'\n        'hyphénation')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) > 1\n    full_text = ''.join(line.children[0].text for line in lines)\n    assert full_text == 'hyphénation'\n\n\n@assert_no_logs\ndef test_hyphenate_character_5():\n    page, = render_pages(\n        '<html style=\"width: 5em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        'hyphenate-character: \\'———\\'\" lang=fr>'\n        'hyphénation')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) > 1\n    assert lines[0].children[0].text.endswith('———')\n    full_text = ''.join(line.children[0].text for line in lines)\n    assert full_text.replace('—', '') == 'hyphénation'\n\n\n@assert_no_logs\n@pytest.mark.parametrize('i', (range(1, len('hyphénation'))))\ndef test_hyphenate_manual_1(i):\n    for hyphenate_character in ('!', 'ù ù'):\n        word = f'{\"hyphénation\"[:i]}\\xad{\"hyphénation\"[i:]}'\n        page, = render_pages(\n            '<html style=\"width: 5em; font-family: weasyprint\">'\n            '<body style=\"hyphens: manual;'\n            f'  hyphenate-character: \\'{hyphenate_character}\\'\"'\n            f'  lang=fr>{word}')\n        html, = page.children\n        body, = html.children\n        lines = body.children\n        assert len(lines) == 2\n        assert lines[0].children[0].text.endswith(hyphenate_character)\n        full_text = ''.join(\n            child.text for line in lines for child in line.children)\n        assert full_text.replace(hyphenate_character, '') == word\n\n\n@assert_no_logs\n@pytest.mark.parametrize('i', (range(1, len('hy phénation'))))\ndef test_hyphenate_manual_2(i):\n    for hyphenate_character in ('!', 'ù ù'):\n        word = f'{\"hy phénation\"[:i]}\\xad{\"hy phénation\"[i:]}'\n        page, = render_pages(\n            '<html style=\"width: 5em; font-family: weasyprint\">'\n            '<body style=\"hyphens: manual;'\n            f'  hyphenate-character: \\'{hyphenate_character}\\'\"'\n            f'  lang=fr>{word}')\n        html, = page.children\n        body, = html.children\n        lines = body.children\n        assert len(lines) in (2, 3)\n        full_text = ''.join(\n            child.text for line in lines for child in line.children)\n        full_text = full_text.replace(hyphenate_character, '')\n        if lines[0].children[0].text.endswith(hyphenate_character):\n            assert full_text == word\n        else:\n            assert lines[0].children[0].text.rstrip('\\xad').endswith('y')\n            if len(lines) == 3:\n                assert lines[1].children[0].text.rstrip('\\xad').endswith(\n                    hyphenate_character)\n\n\n@assert_no_logs\ndef test_hyphenate_manual_3():\n    # Automatic hyphenation opportunities within a word must be ignored if the\n    # word contains a conditional hyphen, in favor of the conditional\n    # hyphen(s).\n    page, = render_pages(\n        '<html style=\"width: 0.1em\" lang=\"en\">'\n        '<body style=\"hyphens: auto\">in&shy;lighten&shy;lighten&shy;in')\n    html, = page.children\n    body, = html.children\n    line_1, line_2, line_3, line_4 = body.children\n    assert line_1.children[0].text == 'in\\xad‐'\n    assert line_2.children[0].text == 'lighten\\xad‐'\n    assert line_3.children[0].text == 'lighten\\xad‐'\n    assert line_4.children[0].text == 'in'\n\n\n@assert_no_logs\ndef test_hyphenate_manual_4():\n    # Regression test for #1878.\n    page, = render_pages(\n        '<html style=\"width: 0.1em\" lang=\"en\">'\n        '<body style=\"hyphens: auto\">test&shy;')\n    html, = page.children\n    body, = html.children\n    line_1, = body.children\n    # TODO: should not end with an hyphen\n    # assert line_1.children[0].text == 'test\\xad'\n\n\n@assert_no_logs\ndef test_hyphenate_limit_zone_1():\n    page, = render_pages(\n        '<html style=\"width: 12em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        'hyphenate-limit-zone: 0\" lang=fr>'\n        'mmmmm hyphénation')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) == 2\n    assert lines[0].children[0].text.endswith('‐')\n    full_text = ''.join(line.children[0].text for line in lines)\n    assert full_text.replace('‐', '') == 'mmmmm hyphénation'\n\n\n@assert_no_logs\ndef test_hyphenate_limit_zone_2():\n    page, = render_pages(\n        '<html style=\"width: 12em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        'hyphenate-limit-zone: 9em\" lang=fr>'\n        'mmmmm hyphénation')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) > 1\n    assert lines[0].children[0].text.endswith('mm')\n    full_text = ''.join(line.children[0].text for line in lines)\n    assert full_text == 'mmmmmhyphénation'\n\n\n@assert_no_logs\ndef test_hyphenate_limit_zone_3():\n    page, = render_pages(\n        '<html style=\"width: 12em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        'hyphenate-limit-zone: 5%\" lang=fr>'\n        'mmmmm hyphénation')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) == 2\n    assert lines[0].children[0].text.endswith('‐')\n    full_text = ''.join(line.children[0].text for line in lines)\n    assert full_text.replace('‐', '') == 'mmmmm hyphénation'\n\n\n@assert_no_logs\ndef test_hyphenate_limit_zone_4():\n    page, = render_pages(\n        '<html style=\"width: 12em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        'hyphenate-limit-zone: 95%\" lang=fr>'\n        'mmmmm hyphénation')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) > 1\n    assert lines[0].children[0].text.endswith('mm')\n    full_text = ''.join(line.children[0].text for line in lines)\n    assert full_text == 'mmmmmhyphénation'\n\n\ndef test_hyphen_nbsp():\n    # Regression test for #1817.\n    page, = render_pages('''\n      <style>\n        body {width: 20px; font: 2px weasyprint}\n      </style>\n      <body style=\"hyphens: auto;\" lang=\"en\">\n        <span>this&nbsp;hyphenation\n    ''')\n    html, = page.children\n    body, = html.children\n    line1, line2 = body.children\n    assert line1.children[0].children[0].text == 'this hy‐'\n    assert line2.children[0].children[0].text == 'phenation'\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('css', 'result'), [\n    ('auto', 2),\n    ('auto auto 0', 2),\n    ('0 0 0', 2),\n    ('4 4 auto', 1),\n    ('6 2 4', 2),\n    ('auto 1 auto', 2),\n    ('7 auto auto', 1),\n    ('6 auto auto', 2),\n    ('5 2', 2),\n    ('3', 2),\n    ('2 4 6', 1),\n    ('auto 4', 1),\n    ('auto 2', 2),\n])\ndef test_hyphenate_limit_chars(css, result):\n    page, = render_pages(\n        '<html style=\"width: 1em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        f'hyphenate-limit-chars: {css}\" lang=en>'\n        'hyphen')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) == result\n\n\n@assert_no_logs\n@pytest.mark.parametrize('css', [\n    # light·en\n    '3 3 3',  # 'en' is shorter than 3\n    '3 6 2',  # 'light' is shorter than 6\n    '8',  # 'lighten' is shorter than 8\n])\ndef test_hyphenate_limit_chars_punctuation(css):\n    # Regression test for #109.\n    page, = render_pages(\n        '<html style=\"width: 1em; font-family: weasyprint\">'\n        '<body style=\"hyphens: auto;'\n        f'hyphenate-limit-chars: {css}\" lang=en>'\n        '..lighten..')\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    assert len(lines) == 1\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('wrap', 'text', 'test', 'full_text'), [\n    ('anywhere', 'aaaaaaaa', lambda a: a > 1, 'aaaaaaaa'),\n    ('break-word', 'aaaaaaaa', lambda a: a > 1, 'aaaaaaaa'),\n    ('normal', 'aaaaaaaa', lambda a: a == 1, 'aaaaaaaa'),\n    ('break-word', 'hyphenations', lambda a: a > 3,\n     'hy\\u2010phen\\u2010ations'),\n    ('break-word', 'A splitted word.  An hyphenated word.',\n     lambda a: a > 8, 'Asplittedword.Anhy\\u2010phen\\u2010atedword.'),\n])\ndef test_overflow_wrap(wrap, text, test, full_text):\n    page, = render_pages('''\n      <style>\n        body {width: 80px; overflow: hidden; font-family: weasyprint}\n        span {overflow-wrap: %s}\n      </style>\n      <body style=\"hyphens: auto;\" lang=\"en\">\n        <span>%s\n    ''' % (wrap, text))\n    html, = page.children\n    body, = html.children\n    lines = []\n    for line in body.children:\n        box, = line.children\n        text_box, = box.children\n        lines.append(text_box.text)\n    lines_full_text = ''.join(line for line in lines)\n    assert test(len(lines))\n    assert full_text == lines_full_text\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('span_css', 'expected_lines'), [\n    # overflow-wrap: anywhere and break-word are only allowed to break a word\n    # \"if there are no otherwise-acceptable break points in the line\", which\n    # means they should not split a word if it fits cleanly into the next line.\n    # This can be done accidentally if it is in its own inline element.\n    ('overflow-wrap: anywhere', ['aaa', 'bbb']),\n    ('overflow-wrap: break-word', ['aaa', 'bbb']),\n\n    # On the other hand, word-break: break-all mandates a break anywhere at the\n    # end of a line, even if the word could fit cleanly onto the next line.\n    ('word-break: break-all', ['aaa b', 'bb']),\n])\ndef test_wrap_overflow_word_break(span_css, expected_lines):\n    page, = render_pages('''\n      <style>\n        body {width: 80px; overflow: hidden; font-family: weasyprint}\n        span {%s}\n      </style>\n      <body>\n        <span>aaa </span><span>bbb\n    ''' % span_css)\n    html, = page.children\n    body, = html.children\n    lines = body.children\n    lines = []\n    for line in body.children:\n        line_text = ''\n        for span_box in line.children:\n            line_text += span_box.children[0].text\n        lines.append(line_text)\n    assert lines == expected_lines\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('wrap', 'text', 'body_width', 'expected_width'), [\n    ('anywhere', 'aaaaaa', 10, 20),\n    ('anywhere', 'aaaaaa', 40, 40),\n    ('break-word', 'aaaaaa', 40, 120),\n    ('normal', 'aaaaaa', 40, 120),\n])\ndef test_overflow_wrap_2(wrap, text, body_width, expected_width):\n    page, = render_pages('''\n      <style>\n        body {width: %dpx; font-family: weasyprint; font-size: 20px}\n        table {overflow-wrap: %s}\n      </style>\n      <table><tr><td>%s''' % (body_width, wrap, text))\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    tr, = row_group.children\n    td, = tr.children\n    assert td.width == expected_width\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('wrap', 'text', 'body_width', 'expected_width'), [\n    ('anywhere', 'aaaaaa', 10, 20),\n    ('anywhere', 'aaaaaa', 40, 40),\n    ('break-word', 'aaaaaa', 40, 120),\n    ('normal', 'abcdef', 40, 120),\n])\ndef test_overflow_wrap_trailing_space(wrap, text, body_width, expected_width):\n    page, = render_pages('''\n      <style>\n        body {width: %dpx; font-family: weasyprint; font-size: 20px}\n        table {overflow-wrap: %s}\n      </style>\n      <table><tr><td>%s ''' % (body_width, wrap, text))\n    html, = page.children\n    body, = html.children\n    table_wrapper, = body.children\n    table, = table_wrapper.children\n    row_group, = table.children\n    tr, = row_group.children\n    td, = tr.children\n    assert td.width == expected_width\n\n\ndef test_overflow_wrap_no_break_on_space():\n    # Regression test for #1817.\n    page, = render_pages('''\n      <style>\n        body {width: 11px; font-family: weasyprint; font-size: 2px;\n              overflow-wrap: anywhere}\n      </style>.jpg, .png''')\n    html, = page.children\n    body, = html.children\n    line1, line2 = body.children\n    text1, = line1.children\n    assert text1.text == '.jpg,'\n    text2, = line2.children\n    assert text2.text == '.png'\n\n\ndef test_line_break_before_trailing_space():\n    # Regression test for #1852.\n    page, = render_pages('''\n        <p style=\"display: inline-block\">test\\u2028 </p>a\n        <p style=\"display: inline-block\">test\\u2028</p>a\n    ''')\n    html, = page.children\n    body, = html.children\n    line, = body.children\n    p1, space1, p2, space2 = line.children\n    assert p1.width == p2.width\n\n\ndef test_line_break_before_nested_trailing_space():\n    # Regression test for #2448.\n    page, = render_pages('''\n    <div style=\"font: 2px weasyprint; width: 6px\">\n      AA <span><b><i>BBB</i></b> </span></div>\n    ''')\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    assert len(div.children) == 2\n\n\ndef white_space_lines(width, space):\n    page, = render_pages('''\n      <style>\n        body { font-size: 100px; width: %dpx }\n        span { white-space: %s }\n      </style>\n      <body><span>This +    \\n    is text''' % (width, space))\n    html, = page.children\n    body, = html.children\n    return body.children\n\n\n@assert_no_logs\ndef test_white_space_1():\n    line1, line2, line3, line4 = white_space_lines(1, 'normal')\n    box1, = line1.children\n    text1, = box1.children\n    assert text1.text == 'This'\n    box2, = line2.children\n    text2, = box2.children\n    assert text2.text == '+'\n    box3, = line3.children\n    text3, = box3.children\n    assert text3.text == 'is'\n    box4, = line4.children\n    text4, = box4.children\n    assert text4.text == 'text'\n\n\n@assert_no_logs\ndef test_white_space_2():\n    line1, line2 = white_space_lines(1, 'pre')\n    box1, = line1.children\n    text1, = box1.children\n    assert text1.text == 'This +    '\n    box2, = line2.children\n    text2, = box2.children\n    assert text2.text == '    is text'\n\n\n@assert_no_logs\ndef test_white_space_3():\n    line1, = white_space_lines(1, 'nowrap')\n    box1, = line1.children\n    text1, = box1.children\n    assert text1.text == 'This + is text'\n\n\n@assert_no_logs\ndef test_white_space_4():\n    line1, line2, line3, line4, line5 = white_space_lines(1, 'pre-wrap')\n    box1, = line1.children\n    text1, = box1.children\n    assert text1.text == 'This '\n    box2, = line2.children\n    text2, = box2.children\n    assert text2.text == '+    '\n    box3, = line3.children\n    text3, = box3.children\n    assert text3.text == '    '\n    box4, = line4.children\n    text4, = box4.children\n    assert text4.text == 'is '\n    box5, = line5.children\n    text5, = box5.children\n    assert text5.text == 'text'\n\n\n@assert_no_logs\ndef test_white_space_5():\n    line1, line2, line3, line4 = white_space_lines(1, 'pre-line')\n    box1, = line1.children\n    text1, = box1.children\n    assert text1.text == 'This'\n    box2, = line2.children\n    text2, = box2.children\n    assert text2.text == '+'\n    box3, = line3.children\n    text3, = box3.children\n    assert text3.text == 'is'\n    box4, = line4.children\n    text4, = box4.children\n    assert text4.text == 'text'\n\n\n@assert_no_logs\ndef test_white_space_6():\n    line1, = white_space_lines(1000000, 'normal')\n    box1, = line1.children\n    text1, = box1.children\n    assert text1.text == 'This + is text'\n\n\n@assert_no_logs\ndef test_white_space_7():\n    line1, line2 = white_space_lines(1000000, 'pre')\n    box1, = line1.children\n    text1, = box1.children\n    assert text1.text == 'This +    '\n    box2, = line2.children\n    text2, = box2.children\n    assert text2.text == '    is text'\n\n\n@assert_no_logs\ndef test_white_space_8():\n    line1, = white_space_lines(1000000, 'nowrap')\n    box1, = line1.children\n    text1, = box1.children\n    assert text1.text == 'This + is text'\n\n\n@assert_no_logs\ndef test_white_space_9():\n    line1, line2 = white_space_lines(1000000, 'pre-wrap')\n    box1, = line1.children\n    text1, = box1.children\n    assert text1.text == 'This +    '\n    box2, = line2.children\n    text2, = box2.children\n    assert text2.text == '    is text'\n\n\n@assert_no_logs\ndef test_white_space_10():\n    line1, line2 = white_space_lines(1000000, 'pre-line')\n    box1, = line1.children\n    text1, = box1.children\n    assert text1.text == 'This +'\n    box2, = line2.children\n    text2, = box2.children\n    assert text2.text == 'is text'\n\n\n@assert_no_logs\ndef test_white_space_11():\n    # Regression test for #813.\n    page, = render_pages('''\n      <style>\n        pre { width: 0 }\n      </style>\n      <body><pre>This<br/>is text''')\n    html, = page.children\n    body, = html.children\n    pre, = body.children\n    line1, line2 = pre.children\n    text1, box = line1.children\n    assert text1.text == 'This'\n    assert box.element_tag == 'br'\n    text2, = line2.children\n    assert text2.text == 'is text'\n\n\n@assert_no_logs\ndef test_white_space_12():\n    # Regression test for #813.\n    page, = render_pages('''\n      <style>\n        pre { width: 0 }\n      </style>\n      <body><pre>This is <span>lol</span> text''')\n    html, = page.children\n    body, = html.children\n    pre, = body.children\n    line1, = pre.children\n    text1, span, text2 = line1.children\n    assert text1.text == 'This is '\n    assert span.element_tag == 'span'\n    assert text2.text == ' text'\n\n\n@assert_no_logs\n@pytest.mark.parametrize(('value', 'width'), [\n    (8, 144),  # (2 + (8 - 1)) * 16\n    (4, 80),  # (2 + (4 - 1)) * 16\n    ('3em', 64),  # (2 + (3 - 1)) * 16\n    ('25px', 41),  # 2 * 16 + 25 - 1 * 16\n    # (0, 32),  # See Layout.set_tabs\n])\ndef test_tab_size(value, width):\n    page, = render_pages('''\n      <style>\n        pre { tab-size: %s; font-family: weasyprint }\n      </style>\n      <pre>a&#9;a</pre>\n    ''' % value)\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line, = paragraph.children\n    assert line.width == width\n\n\n@assert_no_logs\ndef test_text_transform():\n    page, = render_pages('''\n      <style>\n        p { text-transform: capitalize }\n        p+p { text-transform: uppercase }\n        p+p+p { text-transform: lowercase }\n        p+p+p+p { text-transform: full-width }\n        p+p+p+p+p { text-transform: none }\n      </style>\n<p>hé lO1</p><p>hé lO1</p><p>hé lO1</p><p>hé lO1</p><p>hé lO1</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    p1, p2, p3, p4, p5 = body.children\n    line1, = p1.children\n    text1, = line1.children\n    assert text1.text == 'Hé LO1'\n    line2, = p2.children\n    text2, = line2.children\n    assert text2.text == 'HÉ LO1'\n    line3, = p3.children\n    text3, = line3.children\n    assert text3.text == 'hé lo1'\n    line4, = p4.children\n    text4, = line4.children\n    assert text4.text == '\\uff48é\\u3000\\uff4c\\uff2f\\uff11'\n    line5, = p5.children\n    text5, = line5.children\n    assert text5.text == 'hé lO1'\n\n\n@assert_no_logs\n@pytest.mark.parametrize(\n    ('lang', 'lowercase', 'uppercase'), [\n        ('az', 'ı i', 'I İ'),\n        ('tr', 'ı i', 'I İ'),\n        ('el', 'καλημέρα αύριο θεϊκό ευφυΐα Νεράιδα',\n         'ΚΑΛΗΜΕΡΑ ΑΥΡΙΟ ΘΕΪΚΟ ΕΥΦΥΪΑ ΝΕΡΑΪΔΑ'),\n])\ndef test_text_transform_lang_uppercase(lang, lowercase, uppercase):\n    page, = render_pages(f'''\n      <html lang=\"{lang}\">\n        <p style=\"text-transform: uppercase\">{lowercase}\n    ''')\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    line, = p.children\n    text, = line.children\n    assert text.text == uppercase\n\n\n@assert_no_logs\n@pytest.mark.parametrize(\n    ('lang', 'uppercase', 'lowercase'), [\n        ('az', 'İ I', 'i ı'),\n        ('tr', 'İ I', 'i ı'),\n        ('lt', 'Ì Í Ĩ', 'i̇̀ i̇́ i̇̃'),\n])\ndef test_text_transform_lang_lowercase(lang, uppercase, lowercase):\n    page, = render_pages(f'''\n      <html lang=\"{lang}\">\n        <p style=\"text-transform: lowercase\">{uppercase}\n    ''')\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    line, = p.children\n    text, = line.children\n    assert text.text == lowercase\n\n\n@assert_no_logs\n@pytest.mark.parametrize(\n    ('original', 'transformed', 'lang_code'), [\n        ('abc def ghi', 'Abc Def Ghi', None),\n        ('AbC def ghi', 'AbC Def Ghi', None),\n        ('I’m SO cool', 'I’m SO Cool', None),\n        ('Wow.wow!wow', 'Wow.wow!wow', None),\n        ('!now not tomorrow', '!Now Not Tomorrow', None),\n        ('SUPER cool', 'SUPER Cool', None),\n        ('i 😻 non‑breaking characters', 'I 😻 Non‑breaking Characters', None),\n        ('3lite 3lite', '3lite 3lite', None),\n        ('one/two/three', 'One/two/three', None),\n        ('supernatural,super', 'Supernatural,super', None),\n        ('éternel αιώνια', 'Éternel Αιώνια', None),\n        ('great ijland', 'Great Ijland', 'fr'),\n        ('great ijland', 'Great IJland', 'nl'),\n        ('great ijland', 'Great İjland', 'az'),\n    ]\n)\ndef test_text_transform_capitalize(original, transformed, lang_code):\n    # Results are different for different browsers, we almost get the same\n    # results as Firefox, that’s good enough!\n    assert capitalize(original, lang_code) == transformed\n\n\n@assert_no_logs\ndef test_text_floating_pre_line():\n    # Regression test for #610.\n    page, = render_pages('''\n      <div style=\"float: left; white-space: pre-line\">This is\n      oh this end </div>\n    ''')\n\n\n@assert_no_logs\n@pytest.mark.parametrize(\n    ('leader', 'content'), [\n        ('dotted', '.'),\n        ('solid', '_'),\n        ('space', ' '),\n        ('\" .-\"', ' .-'),\n    ]\n)\ndef test_leader_content(leader, content):\n    page, = render_pages('''\n      <style>div::after { content: leader(%s) }</style>\n      <div></div>\n    ''' % leader)\n    html, = page.children\n    body, = html.children\n    div, = body.children\n    line, = div.children\n    after, = line.children\n    inline, = after.children\n    assert inline.children[0].text == content\n\n\n@pytest.mark.xfail\n@assert_no_logs\ndef test_max_lines():\n    page, = render_pages('''\n      <style>\n        @page {size: 10px 10px;}\n        p {\n          font-family: weasyprint;\n          font-size: 2px;\n          max-lines: 2;\n        }\n      </style>\n      <p>\n        abcd efgh ijkl\n      </p>\n    ''')\n    html, = page.children\n    body, = html.children\n    p1, p2 = body.children\n    line1, line2 = p1.children\n    line3, = p2.children\n    text1, = line1.children\n    text2, = line2.children\n    text3, = line3.children\n    assert text1.text == 'abcd'\n    assert text2.text == 'efgh'\n    assert text3.text == 'ijkl'\n\n\n@assert_no_logs\ndef test_continue():\n    page, = render_pages('''\n      <style>\n        @page {size: 10px 4px;}\n        div {\n          continue: discard;\n          font-family: weasyprint;\n          font-size: 2px;\n        }\n      </style>\n      <div>\n        abcd efgh ijkl\n      </div>\n    ''')\n    html, = page.children\n    body, = html.children\n    p, = body.children\n    line1, line2 = p.children\n    text1, = line1.children\n    text2, = line2.children\n    assert text1.text == 'abcd'\n    assert text2.text == 'efgh'\n\n\n@assert_no_logs\ndef test_first_letter_line_height():\n    page, = render_pages('''\n      <style>\n        p { font-family: weasyprint }\n        p:first-letter { line-height: 1; font-size: 20px }\n      </style>\n      <p>abc\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line, = paragraph.children\n    first_letter, _ = line.children\n    first_letter_text, = first_letter.children\n    assert first_letter_text.text == 'a'\n    assert first_letter_text.height == 20\n\n\n@assert_no_logs\ndef test_first_letter_text_transform():\n    # Regression test for #1906.\n    page, = render_pages('''\n      <style>\n        p::first-letter { text-transform: uppercase }\n      </style>\n      <p>abc\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line, = paragraph.children\n    first_letter, next_text = line.children\n    first_letter_text, = first_letter.children\n    assert first_letter_text.text == 'A'\n    assert next_text.text == 'bc'\n\n\n@assert_no_logs\ndef test_first_letter_nested():\n    page, = render_pages('''\n      <style>\n        body::first-letter { font-style: italic }\n        p::first-letter { font-weight: 700 }\n      </style>\n      <p>abc</p>\n      <p>abc</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    p1, p2 = body.children\n    line, = p1.children\n    first_letter, next_text = line.children\n    first_letter_text, = first_letter.children\n    assert first_letter_text.style['font_style'] == 'italic'\n    assert first_letter_text.style['font_weight'] == 700\n    assert next_text.style['font_style'] == 'normal'\n    assert next_text.style['font_weight'] == 400\n    line, = p2.children\n    first_letter, next_text = line.children\n    first_letter_text, = first_letter.children\n    assert first_letter_text.style['font_style'] == 'normal'\n    assert first_letter_text.style['font_weight'] == 700\n    assert next_text.style['font_style'] == 'normal'\n    assert next_text.style['font_weight'] == 400\n\n\n@assert_no_logs\ndef test_first_line_line_height():\n    page, = render_pages('''\n      <style>\n        p { font-family: weasyprint }\n        p:first-line { line-height: 1; font-size: 20px }\n      </style>\n      <p>abc<br>def\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line1, line2 = paragraph.children\n    first_line_box, = line1.children\n    first_line_text, br = first_line_box.children\n    assert first_line_text.text == 'abc'\n    assert first_line_text.height == 20\n\n\n@assert_no_logs\ndef test_first_line_text_transform():\n    page, = render_pages('''\n      <style>\n        p::first-line { text-transform: uppercase }\n      </style>\n      <p>abc<br>def\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line1, line2 = paragraph.children\n    first_line_box, = line1.children\n    first_line_text, br = first_line_box.children\n    assert first_line_text.text == 'ABC'\n    next_line_text, = line2.children\n    assert next_line_text.text == 'def'\n\n\n@assert_no_logs\ndef test_first_line_nested():\n    page, = render_pages('''\n      <style>\n        body::first-line { font-style: italic }\n        p::first-line { font-weight: 700 }\n      </style>\n      <p>abc<br>def</p>\n      <p>abc<br>def</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    p1, p2 = body.children\n    line1, line2 = p1.children\n    first_line_box, = line1.children\n    first_line_text, br = first_line_box.children\n    assert first_line_text.style['font_style'] == 'italic'\n    assert first_line_text.style['font_weight'] == 700\n    next_line_text, = line2.children\n    assert next_line_text.style['font_style'] == 'normal'\n    assert next_line_text.style['font_weight'] == 400\n    line1, line2 = p2.children\n    first_line_box, = line1.children\n    first_line_text, br = first_line_box.children\n    assert first_line_text.style['font_style'] == 'normal'\n    assert first_line_text.style['font_weight'] == 700\n    next_line_text, = line2.children\n    assert next_line_text.style['font_style'] == 'normal'\n    assert next_line_text.style['font_weight'] == 400\n\n\n@assert_no_logs\ndef test_first_line_first_letter():\n    page, = render_pages('''\n      <style>\n        p::first-letter { font-style: italic }\n        p::first-line { font-weight: 700 }\n      </style>\n      <p>abc<br>def</p>\n    ''')\n    html, = page.children\n    body, = html.children\n    paragraph, = body.children\n    line1, line2 = paragraph.children\n    first_line_box, = line1.children\n    first_letter_box, first_line_text, br = first_line_box.children\n    first_letter_text, = first_letter_box.children\n    assert first_letter_text.style['font_style'] == 'italic'\n    assert first_letter_text.style['font_weight'] == 700\n    assert first_line_text.style['font_style'] == 'normal'\n    assert first_line_text.style['font_weight'] == 700\n    next_line_text, = line2.children\n    assert next_line_text.style['font_style'] == 'normal'\n    assert next_line_text.style['font_weight'] == 400\n"
  },
  {
    "path": "tests/test_unicode.py",
    "content": "\"\"\"Test various unicode texts and filenames.\"\"\"\n\nfrom weasyprint.urls import ensure_url\n\nfrom .draw import document_to_pixels, html_to_pixels\nfrom .testing_utils import FakeHTML, assert_no_logs, resource_path\n\n\n@assert_no_logs\ndef test_unicode(assert_pixels_equal, tmp_path):\n    text = 'I løvë Unicode'\n    style = '''\n      @page { size: 200px 50px }\n      p { color: blue }\n    '''\n    expected_width, expected_height, expected_lines = html_to_pixels(f'''\n      <style>{style}</style>\n      <p><img src=\"pattern.png\"> {text}</p>\n    ''')\n\n    stylesheet = tmp_path / 'style.css'\n    image = tmp_path / 'pattern.png'\n    html = tmp_path / 'doc.html'\n    stylesheet.write_text(style, 'utf-8')\n    image.write_bytes(resource_path('pattern.png').read_bytes())\n    html_content = f'''\n      <link rel=stylesheet href=\"{ensure_url(str(stylesheet))}\">\n      <p><img src=\"{ensure_url(str(image))}\"> {text}</p>\n    '''\n    html.write_text(html_content, 'utf-8')\n\n    document = FakeHTML(html, encoding='utf-8')\n    width, height, lines = document_to_pixels(document)\n    assert (expected_width, expected_height) == (width, height)\n    assert_pixels_equal(width, height, lines, expected_lines)\n"
  },
  {
    "path": "tests/test_url.py",
    "content": "\"\"\"Test URLs.\"\"\"\n\nimport re\n\nimport pytest\n\nfrom .testing_utils import FakeHTML, capture_logs, resource_path\n\n\n@pytest.mark.parametrize(('url', 'base_url'), [\n    ('https://weasyprint.org]', resource_path('<inline HTML>')),\n    ('https://weasyprint.org]', 'https://weasyprint.org]'),\n    ('https://weasyprint.org/', 'https://weasyprint.org]'),\n])\ndef test_malformed_url_link(url, base_url):\n    \"\"\"Test malformed URLs.\"\"\"\n    with capture_logs() as logs:\n        pdf = FakeHTML(\n            string=f'<p><a href=\"{url}\">My Link</a></p>',\n            base_url=base_url).write_pdf()\n\n    assert len(logs) == 1\n    assert 'Malformed' in logs[0]\n    assert ']' in logs[0]\n\n    uris = re.findall(b'/URI \\\\((.*)\\\\)', pdf)\n    types = re.findall(b'/S (/\\\\w*)', pdf)\n    subtypes = re.findall(b'/Subtype (/\\\\w*)', pdf)\n\n    assert uris.pop(0) == url.encode()\n    assert subtypes.pop(0) == b'/Link'\n    assert types.pop(0) == b'/URI'\n"
  },
  {
    "path": "tests/testing_utils.py",
    "content": "\"\"\"Helpers for tests.\"\"\"\n\nimport functools\nimport sys\nfrom pathlib import Path\n\nfrom weasyprint import CSS, DEFAULT_OPTIONS, HTML, images\nfrom weasyprint.css import get_all_computed_styles\nfrom weasyprint.css.counters import CounterStyle\nfrom weasyprint.css.targets import TargetCollector\nfrom weasyprint.formatting_structure import boxes, build\nfrom weasyprint.html import HTML5_UA_STYLESHEET\nfrom weasyprint.logger import capture_logs\nfrom weasyprint.text.fonts import FontConfiguration\nfrom weasyprint.urls import path2url\n\n# Lists of fonts with many variants (including condensed)\nif sys.platform.startswith('win'):  # pragma: no cover\n    SANS_FONTS = 'DejaVu Sans, Arial Nova, Arial, sans'\n    MONO_FONTS = 'Courier New, Courier, monospace'\nelse:  # pragma: no cover\n    SANS_FONTS = 'DejaVu Sans, sans'\n    MONO_FONTS = 'DejaVu Sans Mono, monospace'\n\nPROPER_CHILDREN = {\n    # Children can be of *any* type in *one* of the lists.\n    boxes.BlockContainerBox: ((boxes.BlockLevelBox,), (boxes.LineBox,)),\n    boxes.LineBox: ((boxes.InlineLevelBox,),),\n    boxes.InlineBox: ((boxes.InlineLevelBox,),),\n    boxes.TableBox: ((\n        boxes.TableCaptionBox, boxes.TableColumnGroupBox, boxes.TableColumnBox,\n        boxes.TableRowGroupBox, boxes.TableRowBox),),\n    boxes.InlineTableBox: ((\n        boxes.TableCaptionBox, boxes.TableColumnGroupBox, boxes.TableColumnBox,\n        boxes.TableRowGroupBox, boxes.TableRowBox),),\n    boxes.TableColumnGroupBox: ((boxes.TableColumnBox,),),\n    boxes.TableRowGroupBox: ((boxes.TableRowBox,),),\n    boxes.TableRowBox: ((boxes.TableCellBox,),),\n}\n\n\nclass FakeHTML(HTML):\n    \"\"\"Like weasyprint.HTML, but with a lighter UA stylesheet.\"\"\"\n    def __init__(self, *args, force_uncompressed_pdf=True, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._force_uncompressed_pdf = force_uncompressed_pdf\n\n    def _ua_stylesheets(self, forms=False):\n        return [\n            TEST_UA_STYLESHEET if stylesheet == HTML5_UA_STYLESHEET\n            else stylesheet for stylesheet in super()._ua_stylesheets(forms)]\n\n    def render(self, font_config=None, *args, **kwargs):\n        return super().render(TEST_UA_FONT_CONFIG, *args, **kwargs)\n\n    def write_pdf(self, target=None, zoom=1, finisher=None, **options):\n        # Override function to force the generation of uncompressed PDFs\n        if self._force_uncompressed_pdf:\n            options['uncompressed_pdf'] = True\n        return super().write_pdf(target, zoom, finisher, **options)\n\n\ndef resource_path(name):\n    \"\"\"Return the absolute path of the resource called ``name``.\"\"\"\n    return Path(__file__).parent / 'resources' / name\n\n\n# Dummy filename, but in the right directory.\nBASE_URL = path2url(resource_path('<test>'))\n# Default stylesheet for tests.\nTEST_UA_FONT_CONFIG = FontConfiguration()\nTEST_UA_STYLESHEET = CSS(resource_path('tests_ua.css'), font_config=TEST_UA_FONT_CONFIG)\n\n\ndef assert_no_logs(function):\n    \"\"\"Decorator that asserts that nothing is logged in a function.\"\"\"\n    @functools.wraps(function)\n    def wrapper(*args, **kwargs):\n        with capture_logs() as logs:\n            try:\n                function(*args, **kwargs)\n            except Exception:  # pragma: no cover\n                if logs:\n                    print(f'{len(logs)} errors logged:', file=sys.stderr)  # noqa: T201\n                    for message in logs:\n                        print(message, file=sys.stderr)  # noqa: T201\n                raise\n            else:\n                if logs:  # pragma: no cover\n                    for message in logs:\n                        print(message, file=sys.stderr)  # noqa: T201\n                    raise AssertionError(f'{len(logs)} errors logged')\n    return wrapper\n\n\ndef serialize(box_list):\n    \"\"\"Transform a box list into a structure easier to compare for testing.\"\"\"\n    return [(\n        box.element_tag,\n        type(box).__name__[:-3],\n        # All concrete boxes are either text, replaced, column or parent.\n        (box.text if isinstance(box, boxes.TextBox)\n            else '<replaced>' if isinstance(box, boxes.ReplacedBox)\n            else serialize(\n                getattr(box, 'column_groups', ()) + tuple(box.children))))\n            for box in box_list]\n\n\ndef tree_position(box_list, matcher):\n    \"\"\"Return a list identifying the first matching box's tree position.\n\n    Given a list of Boxes, this function returns a list containing the first\n    (depth-first) Box that the matcher function identifies. This list can then\n    be compared to another similarly-obtained list to assert that one Box is in\n    the document tree before or after another.\n\n    box_list: a list of Box objects, possibly PageBoxes\n    matcher: a function that takes a Box and returns truthy when it matches\n\n    \"\"\"\n    for i, box in enumerate(box_list):\n        if matcher(box):\n            return [i]\n        elif hasattr(box, 'children'):\n            position = tree_position(box.children, matcher)\n            if position:\n                return [i, *position]\n\n\ndef _parse_base(html_content, base_url=BASE_URL):\n    document = FakeHTML(string=html_content, base_url=base_url)\n    counter_style = CounterStyle()\n    style_for = get_all_computed_styles(document, counter_style=counter_style)\n    get_image_from_uri = functools.partial(\n        images.get_image_from_uri, cache={}, url_fetcher=document.url_fetcher,\n        options=DEFAULT_OPTIONS)\n    target_collector = TargetCollector()\n    footnotes = []\n    return (\n        document.etree_element, style_for, get_image_from_uri, base_url,\n        target_collector, counter_style, footnotes)\n\n\ndef parse(html_content):\n    \"\"\"Parse some HTML, apply stylesheets and transform to boxes.\"\"\"\n    box, = build.element_to_box(*_parse_base(html_content))\n    return box\n\n\ndef parse_all(html_content, base_url=BASE_URL):\n    \"\"\"Like parse() but also run all corrections on boxes.\"\"\"\n    box = build.build_formatting_structure(*_parse_base(\n        html_content, base_url))\n    _sanity_checks(box)\n    return box\n\n\ndef render_pages(html_content):\n    \"\"\"Lay out a document and return a list of PageBox objects.\"\"\"\n    return [\n        page._page_box for page in\n        FakeHTML(string=html_content, base_url=BASE_URL).render().pages]\n\n\ndef assert_tree(box, expected):\n    \"\"\"Check the box tree equality.\n\n    The obtained result is prettified in the message in case of failure.\n\n    box: a Box object, starting with <html> and <body> blocks.\n    expected: a list of serialized <body> children as returned by to_lists().\n\n    \"\"\"\n    assert box.element_tag == 'html'\n    assert isinstance(box, boxes.BlockBox)\n    assert len(box.children) == 1\n\n    box = box.children[0]\n    assert isinstance(box, boxes.BlockBox)\n    assert box.element_tag == 'body'\n\n    assert serialize(box.children) == expected\n\n\ndef _sanity_checks(box):\n    \"\"\"Check that the rules regarding boxes are met.\n\n    This is not required and only helps debugging.\n\n    - A block container can contain either only block-level boxes or\n      only line boxes;\n    - Line boxes and inline boxes can only contain inline-level boxes.\n\n    \"\"\"\n    if not isinstance(box, boxes.ParentBox):\n        return\n\n    acceptable_types_lists = None  # raises when iterated\n    for class_ in type(box).mro():  # pragma: no cover\n        if class_ in PROPER_CHILDREN:\n            acceptable_types_lists = PROPER_CHILDREN[class_]\n            break\n\n    assert any(\n        all(isinstance(child, acceptable_types) or\n            not child.is_in_normal_flow()\n            for child in box.children)\n        for acceptable_types in acceptable_types_lists\n    ), (box, box.children)\n\n    for child in box.children:\n        _sanity_checks(child)\n"
  },
  {
    "path": "weasyprint/__init__.py",
    "content": "\"\"\"The Awesome Document Factory.\n\nThe public API is what is accessible from this \"root\" packages without\nimporting sub-modules.\n\n\"\"\"\n\nfrom datetime import datetime\nfrom os.path import getctime, getmtime\nfrom pathlib import Path\nfrom urllib.parse import urljoin\n\nimport cssselect2\nimport tinycss2\nimport tinyhtml5\n\nVERSION = __version__ = '68.1'\n\n#: Default values for command-line and Python API rendering options. See\n#: :func:`__main__.main` to learn more about specific options for\n#: command-line.\n#:\n#: :param list stylesheets:\n#:     An optional list of user stylesheets. The list can include\n#:     are :class:`CSS` objects, filenames, URLs, or file-like\n#:     objects. (See :ref:`Stylesheet Origins`.)\n#: :param str media_type:\n#:     Media type to use for @media.\n#: :param list attachments:\n#:     A list of additional file attachments for the generated PDF\n#:     document or :obj:`None`. The list's elements are\n#:     :class:`Attachment` objects, filenames, URLs or file-like objects.\n#: :param bytes pdf_identifier:\n#:     A bytestring used as PDF file identifier.\n#: :param str pdf_variant:\n#:     A PDF variant name.\n#: :param str pdf_version:\n#:     A PDF version number.\n#: :param bool pdf_forms:\n#:     Whether PDF forms have to be included.\n#: :param bool pdf_tags:\n#:     Whether PDF should be tagged for accessibility.\n#: :param bool uncompressed_pdf:\n#:     Whether PDF content should be compressed.\n#: :param bool custom_metadata:\n#:     Whether custom HTML metadata should be stored in the generated PDF.\n#: :param bool presentational_hints:\n#:     Whether HTML presentational hints are followed.\n#: :param bool srgb:\n#:     Whether sRGB color profile should be included and set as default for\n#:     device-dependant RGB colors.\n#: :param bool optimize_images:\n#:     Whether size of embedded images should be optimized, with no quality\n#:     loss.\n#: :param int jpeg_quality:\n#:     JPEG quality between 0 (worst) to 95 (best).\n#: :param int dpi:\n#:     Maximum resolution of images embedded in the PDF.\n#: :param bool full_fonts:\n#:     Whether unmodified font files should be embedded when possible.\n#: :param bool hinting:\n#:     Whether hinting information should be kept in embedded fonts.\n#: :type cache: :obj:`dict`, :class:`pathlib.Path` or :obj:`str`\n#: :param cache:\n#:     A dictionary used to cache images in memory, or a folder path where\n#:     images are temporarily stored.\nDEFAULT_OPTIONS = {\n    'stylesheets': None,\n    'attachments': None,\n    'attachment_relationships': None,\n    'pdf_identifier': None,\n    'pdf_variant': None,\n    'pdf_version': None,\n    'pdf_forms': None,\n    'pdf_tags': False,\n    'uncompressed_pdf': False,\n    'xmp_metadata': None,\n    'custom_metadata': False,\n    'presentational_hints': False,\n    'srgb': False,\n    'optimize_images': False,\n    'jpeg_quality': None,\n    'dpi': None,\n    'full_fonts': False,\n    'hinting': False,\n    'cache': None,\n}\n\n__all__ = [\n    'CSS', 'DEFAULT_OPTIONS', 'HTML', 'VERSION', 'Attachment', 'Document', 'Page',\n    '__version__', 'default_url_fetcher']\n\n\n# Import after setting the version, as the version is used in other modules\nfrom .urls import URLFetcher, default_url_fetcher, select_source  # noqa: I001, E402\nfrom .logger import LOGGER, PROGRESS_LOGGER  # noqa: E402\n# Some imports are at the end of the file (after the CSS class)\n# to work around circular imports.\n\n\ndef _find_base_url(html_document, fallback_base_url):\n    \"\"\"Return the base URL for the document.\n\n    See https://www.w3.org/TR/html5/urls.html#document-base-url\n\n    \"\"\"\n    first_base_element = next(iter(html_document.iter('base')), None)\n    if first_base_element is not None:\n        href = first_base_element.get('href', '').strip()\n        if href:\n            return urljoin(fallback_base_url, href)\n    return fallback_base_url\n\n\nclass HTML:\n    \"\"\"HTML document parsed by tinyhtml5.\n\n    You can just create an instance with a positional argument:\n    ``doc = HTML(something)``\n    The class will try to guess if the input is a filename, an absolute URL,\n    or a :term:`file object`.\n\n    Alternatively, use **one** named argument so that no guessing is involved:\n\n    :type filename: str or pathlib.Path\n    :param filename:\n        A filename, relative to the current directory, or absolute.\n    :param str url:\n        An absolute, fully qualified URL.\n    :type file_obj: :term:`file object`\n    :param file_obj:\n        Any object with a ``read`` method.\n    :param str string:\n        A string of HTML source.\n\n    Specifying multiple inputs is an error:\n    ``HTML(filename=\"foo.html\", url=\"localhost://bar.html\")``\n    will raise a :obj:`TypeError`.\n\n    You can also pass optional named arguments:\n\n    :param str encoding:\n        Force the source character encoding.\n    :type base_url: str or pathlib.Path\n    :param base_url:\n        The base used to resolve relative URLs (e.g. in\n        ``<img src=\"../foo.png\">``). If not provided, try to use the input\n        filename, URL, or ``name`` attribute of\n        :term:`file objects <file object>`.\n    :type url_fetcher: :term:`callable`\n    :param url_fetcher:\n        An instance of :class:`urls.URLFetcher`. (See :ref:`URL Fetchers`.)\n    :param str media_type:\n        The media type to use for ``@media``. Defaults to ``'print'``.\n        **Note:** In some cases like ``HTML(string=foo)`` relative URLs will be\n        invalid if ``base_url`` is not provided.\n\n    \"\"\"\n    def __init__(self, guess=None, filename=None, url=None, file_obj=None,\n                 string=None, encoding=None, base_url=None,\n                 url_fetcher=None, media_type='print'):\n        PROGRESS_LOGGER.info(\n            'Step 1 - Fetching and parsing HTML - %s',\n            guess or filename or url or\n            getattr(file_obj, 'name', 'HTML string'))\n        if isinstance(base_url, Path):\n            base_url = str(base_url)\n        if url_fetcher is None:\n            url_fetcher = URLFetcher()\n        result = select_source(\n            guess, filename, url, file_obj, string, base_url, url_fetcher)\n        with result as (file_obj, base_url, protocol_encoding, _):\n            kwargs = {'namespace_html_elements': False}\n            if protocol_encoding is not None:\n                kwargs['transport_encoding'] = protocol_encoding\n            if encoding is not None:\n                kwargs['override_encoding'] = encoding\n            result = tinyhtml5.parse(file_obj, **kwargs)\n        self.base_url = _find_base_url(result, base_url)\n        self.url_fetcher = url_fetcher\n        self.media_type = media_type\n        self.wrapper_element = cssselect2.ElementWrapper.from_html_root(\n            result, content_language=None)\n        self.etree_element = self.wrapper_element.etree_element\n\n    def _ua_stylesheets(self, forms=False):\n        if forms:\n            return [HTML5_UA_STYLESHEET, HTML5_UA_FORM_STYLESHEET]\n        return [HTML5_UA_STYLESHEET]\n\n    def _ua_counter_style(self):\n        return [HTML5_UA_COUNTER_STYLE.copy()]\n\n    def _ph_stylesheets(self):\n        return [HTML5_PH_STYLESHEET]\n\n    def render(self, font_config=None, counter_style=None, color_profiles=None,\n               **options):\n        \"\"\"Lay out and paginate the document, but do not (yet) export it.\n\n        This returns a :class:`document.Document` object which provides\n        access to individual pages and various meta-data.\n        See :meth:`write_pdf` to get a PDF directly.\n\n        :type font_config: :class:`text.fonts.FontConfiguration`\n        :param font_config:\n            A font configuration handling ``@font-face`` rules.\n        :type counter_style: :class:`css.counters.CounterStyle`\n        :param counter_style:\n            A dictionary storing ``@counter-style`` rules.\n        :param options:\n            The ``options`` parameter includes by default the\n            :data:`DEFAULT_OPTIONS` values.\n        :returns: A :class:`document.Document` object.\n\n        \"\"\"\n        for unknown in set(options) - set(DEFAULT_OPTIONS):\n            LOGGER.warning('Unknown rendering option: %s.', unknown)\n        new_options = DEFAULT_OPTIONS.copy()\n        new_options.update(options)\n        options = new_options\n        return Document._render(\n            self, font_config, counter_style, color_profiles, options)\n\n    def write_pdf(self, target=None, zoom=1, finisher=None,\n                  font_config=None, counter_style=None, color_profiles=None, **options):\n        \"\"\"Render the document to a PDF file.\n\n        This is a shortcut for calling :meth:`render`, then\n        :meth:`Document.write_pdf() <document.Document.write_pdf>`.\n\n        :type target:\n            :class:`str`, :class:`pathlib.Path` or :term:`file object`\n        :param target:\n            A filename where the PDF file is generated, a file object, or\n            :obj:`None`.\n        :param float zoom:\n            The zoom factor in PDF units per CSS units.  **Warning**:\n            All CSS units are affected, including physical units like\n            ``cm`` and named sizes like ``A4``.  For values other than\n            1, the physical CSS units will thus be \"wrong\".\n        :type finisher: :term:`callable`\n        :param finisher:\n            A finisher function or callable that accepts the document and a\n            :class:`pydyf.PDF` object as parameters. Can be passed to perform\n            post-processing on the PDF right before the trailer is written.\n        :type font_config: :class:`text.fonts.FontConfiguration`\n        :param font_config:\n            A font configuration handling ``@font-face`` rules.\n        :type counter_style: :class:`css.counters.CounterStyle`\n        :param counter_style:\n            A dictionary storing ``@counter-style`` rules.\n        :param options:\n            The ``options`` parameter includes by default the\n            :data:`DEFAULT_OPTIONS` values.\n        :returns:\n            The PDF as :obj:`bytes` if ``target`` is not provided or\n            :obj:`None`, otherwise :obj:`None` (the PDF is written to\n            ``target``).\n\n        \"\"\"\n        new_options = DEFAULT_OPTIONS.copy()\n        new_options.update(options)\n        options = new_options\n        return (\n            self.render(font_config, counter_style, color_profiles, **options)\n            .write_pdf(target, zoom, finisher, **options))\n\n\nclass CSS:\n    \"\"\"CSS stylesheet parsed by tinycss2.\n\n    An instance is created in the same way as :class:`HTML`, with the same\n    arguments.\n\n    An additional argument called ``font_config`` must be provided to handle\n    ``@font-face`` rules. The same ``text.fonts.FontConfiguration`` object\n    must be used for different ``CSS`` objects applied to the same document.\n\n    ``CSS`` objects have no public attributes or methods. They are only meant\n    to be used in the :meth:`HTML.write_pdf` and :meth:`HTML.render` methods\n    of :class:`HTML` objects.\n\n    \"\"\"\n    def __init__(self, guess=None, filename=None, url=None, file_obj=None, string=None,\n                 encoding=None, base_url=None, url_fetcher=None, _check_mime_type=False,\n                 media_type='print', font_config=None, counter_style=None,\n                 color_profiles=None, matcher=None, page_rules=None, layers=None,\n                 layer=None):\n        PROGRESS_LOGGER.info(\n            'Step 2 - Fetching and parsing CSS - %s',\n            filename or url or getattr(file_obj, 'name', 'CSS string'))\n        if url_fetcher is None:\n            url_fetcher = URLFetcher()\n        result = select_source(\n            guess, filename, url, file_obj, string, base_url=base_url,\n            url_fetcher=url_fetcher, check_css_mime_type=_check_mime_type)\n        with result as (file_obj, base_url, protocol_encoding, mime_type):\n            css = file_obj.read()\n            if isinstance(css, str):\n                stylesheet = tinycss2.parse_stylesheet(css)\n            else:\n                stylesheet, _ = tinycss2.parse_stylesheet_bytes(\n                    css, environment_encoding=encoding,\n                    protocol_encoding=protocol_encoding)\n        self.base_url = base_url\n        self.matcher = matcher or cssselect2.Matcher()\n        self.page_rules = [] if page_rules is None else page_rules\n        self.layers = [] if layers is None else layers\n        counter_style = {} if counter_style is None else counter_style\n        color_profiles = {} if color_profiles is None else color_profiles\n        preprocess_stylesheet(\n            media_type, base_url, stylesheet, url_fetcher, self.matcher,\n            self.page_rules, self.layers, font_config, counter_style, color_profiles,\n            layer=layer)\n\n\nclass Attachment:\n    \"\"\"File attachment for a PDF document.\n\n    An instance is created in the same way as :class:`HTML`, except that the\n    HTML specific arguments (``encoding`` and ``media_type``) are not\n    supported.\n\n    :param str name:\n        The name of the attachment to be included in the PDF document.\n        May be :obj:`None`.\n    :param str description:\n        A description of the attachment to be included in the PDF document.\n        May be :obj:`None`.\n    :type created: :obj:`datetime.datetime`\n    :param created:\n        Creation date and time. Default is current date and time.\n    :type modified: :obj:`datetime.datetime`\n    :param modified:\n        Modification date and time. Default is current date and time.\n    :param str relationship:\n        A string that represents the relationship between the attachment and\n        the PDF it is embedded in. Default is 'Unspecified', other common\n        values are defined in ISO-32000-2:2020, 7.11.3.\n\n    \"\"\"\n    def __init__(self, guess=None, filename=None, url=None, file_obj=None,\n                 string=None, base_url=None, url_fetcher=None, name=None,\n                 description=None, created=None, modified=None,\n                 relationship='Unspecified'):\n        if url_fetcher is None:\n            url_fetcher = URLFetcher()\n        self.source = select_source(\n            guess, filename, url, file_obj, string, base_url=base_url,\n            url_fetcher=url_fetcher)\n        self.name = name\n        self.description = description\n        self.relationship = relationship\n        self.md5 = None\n\n        if created is None:\n            if filename:\n                created = datetime.fromtimestamp(getctime(filename))\n            else:\n                created = datetime.now()\n        if modified is None:\n            if filename:\n                modified = datetime.fromtimestamp(getmtime(filename))\n            else:\n                modified = datetime.now()\n        self.created = created\n        self.modified = modified\n\n\n# Work around circular imports.\nfrom .css import preprocess_stylesheet  # noqa: I001, E402\nfrom .html import (  # noqa: E402\n    HTML5_UA_COUNTER_STYLE, HTML5_UA_STYLESHEET, HTML5_UA_FORM_STYLESHEET,\n    HTML5_PH_STYLESHEET)\nfrom .document import Document, Page  # noqa: E402\n"
  },
  {
    "path": "weasyprint/__main__.py",
    "content": "\"\"\"Command-line interface to WeasyPrint.\"\"\"\n\nimport argparse\nimport logging\nimport platform\nimport sys\n\nimport pydyf\n\nfrom . import DEFAULT_OPTIONS, HTML, LOGGER, __version__\nfrom .pdf import VARIANTS\nfrom .text.ffi import pango\nfrom .urls import URLFetcher\n\n\nclass PrintInfo(argparse.Action):\n    def __call__(*_, **__):\n        # TODO: ignore check at block-level when available.\n        # https://github.com/astral-sh/ruff/issues/3711\n        uname = platform.uname()\n        print('System:', uname.system)  # noqa: T201\n        print('Machine:', uname.machine)  # noqa: T201\n        print('Version:', uname.version)  # noqa: T201\n        print('Release:', uname.release)  # noqa: T201\n        print()  # noqa: T201\n        print('WeasyPrint version:', __version__)  # noqa: T201\n        print('Python version:', sys.version.split()[0])  # noqa: T201\n        print('Pydyf version:', pydyf.__version__)  # noqa: T201\n        print('Pango version:', pango.pango_version())  # noqa: T201\n        sys.exit()\n\n\nclass Parser(argparse.ArgumentParser):\n    def __init__(self, *args, **kwargs):\n        self._groups = {None: {}}\n        super().__init__(*args, **kwargs)\n\n    def add_argument(self, *args, _group_name=None, **kwargs):\n        if _group_name is None:\n            super().add_argument(*args, **kwargs)\n        key = args[-1].lstrip('-')\n        kwargs['flags'] = args\n        kwargs['positional'] = args[-1][0] != '-'\n        self._groups[_group_name][key] = kwargs\n\n    def add_argument_group(self, name, *args, **kwargs):\n        group = super().add_argument_group(name, *args, **kwargs)\n        self._groups[name] = {}\n        def add_argument(*args, **kwargs):\n            group._add_argument(*args, **kwargs)\n            self.add_argument(*args, _group_name=name, **kwargs)\n        group._add_argument = group.add_argument\n        group.add_argument = add_argument\n        return group\n\n    @property\n    def docstring(self):\n        self._groups[None].pop('help')\n        data = []\n        for group, arguments in self._groups.items():\n            if not arguments:\n                continue\n            if group:\n                data.append(f'{group[0].title()}{group[1:]}\\n')\n                data.append(f'{\"~\" * len(group)}\\n\\n')\n            for key, args in arguments.items():\n                data.append('.. option:: ')\n                action = args.get('action', 'store')\n                for flag in args['flags']:\n                    data.append(flag)\n                    if not args['positional'] and action in ('store', 'append'):\n                        data.append(f' <{key}>')\n                    data.append(', ')\n                data[-1] = '\\n\\n'\n                data.append(f'  {args[\"help\"][0].upper()}{args[\"help\"][1:]}.\\n\\n')\n                if 'choices' in args:\n                    choices = ', '.join(args['choices'])\n                    data.append(f'  Possible choices: {choices}.\\n\\n')\n                if action == 'append':\n                    data.append('  This option can be passed multiple times.\\n\\n')\n        return ''.join(data)\n\n\nPARSER = Parser(prog='weasyprint', description='Render web pages to PDF.')\nPARSER.add_argument('input', help='URL or filename of the HTML input, or - for stdin')\nPARSER.add_argument('output', help='filename where output is written, or - for stdout')\nPARSER.add_argument(\n    '-i', '--info', action=PrintInfo, nargs=0, help='print system information and exit')\nPARSER.add_argument(\n    '--version', action='version', version=f'WeasyPrint version {__version__}',\n    help='print WeasyPrint’s version number and exit')\n\ngroup = PARSER.add_argument_group('rendering options')\ngroup.add_argument(\n    '-s', '--stylesheet', action='append', dest='stylesheets',\n    help='URL or filename for a user CSS stylesheet')\ngroup.add_argument(\n    '-a', '--attachment', action='append', dest='attachments',\n    help='URL or filename of a file to attach to the PDF document')\ngroup.add_argument(\n    '--attachment-relationship', action='append', dest='attachment_relationships',\n    help='Relationship of the attachment file to attach to the PDF')\ngroup.add_argument('--pdf-identifier', help='PDF file identifier')\ngroup.add_argument('--pdf-variant', choices=VARIANTS, help='PDF variant to generate')\ngroup.add_argument('--pdf-version', help='PDF version number')\ngroup.add_argument('--pdf-forms', action='store_true', help='include PDF forms')\ngroup.add_argument('--pdf-tags', action='store_true', help='tag PDF for accessibility')\ngroup.add_argument(\n    '--uncompressed-pdf', action='store_true',\n    help='do not compress PDF content, mainly for debugging purpose')\ngroup.add_argument(\n    '--xmp-metadata', action='append',\n    help='URL or filename of a file to include into the XMP metadata')\ngroup.add_argument(\n    '--custom-metadata', action='store_true',\n    help='include custom HTML meta tags in PDF metadata')\ngroup.add_argument(\n    '-p', '--presentational-hints', action='store_true',\n    help='follow HTML presentational hints')\ngroup.add_argument('--srgb', action='store_true', help='include sRGB color profile')\ngroup.add_argument(\n    '--optimize-images', action='store_true',\n    help='optimize size of embedded images with no quality loss')\ngroup.add_argument(\n    '-j', '--jpeg-quality', type=int,\n    help='JPEG quality between 0 (worst) to 95 (best)')\ngroup.add_argument(\n    '-D', '--dpi', type=int,\n    help='set maximum resolution of images embedded in the PDF')\ngroup.add_argument(\n    '--full-fonts', action='store_true',\n    help='embed unmodified font files when possible')\ngroup.add_argument(\n    '--hinting', action='store_true', help='keep hinting information in embedded fonts')\ngroup.add_argument(\n    '-c', '--cache-folder', dest='cache',\n    help='store cache on disk instead of memory, folder is '\n    'created if needed and cleaned after the PDF is generated')\n\ngroup = PARSER.add_argument_group('HTML options')\ngroup.add_argument('-e', '--encoding', help='force the input character encoding')\ngroup.add_argument(\n    '-m', '--media-type', help='media type to use for @media, defaults to print',\n    default='print')\ngroup.add_argument(\n    '-u', '--base-url',\n    help='base for relative URLs in the HTML input, defaults to the '\n    'input’s own filename or URL or the current directory for stdin')\n\ngroup = PARSER.add_argument_group('URL fetcher options')\ngroup.add_argument(\n    '-t', '--timeout', type=int, help='set timeout in seconds for HTTP requests')\ngroup.add_argument(\n    '--allowed-protocols', dest='allowed_protocols',\n    help='only authorize comma-separated list of protocols for fetching URLs')\ngroup.add_argument(\n    '--no-http-redirects', action='store_true', help='do not follow HTTP redirects')\ngroup.add_argument(\n    '--fail-on-http-errors', action='store_true',\n    help='abort document rendering on any HTTP error')\n\ngroup = PARSER.add_argument_group('command-line logging options')\ngroup.add_argument(\n    '-v', '--verbose', action='store_true',\n    help='show warnings and information messages')\ngroup.add_argument(\n    '-d', '--debug', action='store_true', help='show debugging messages')\ngroup.add_argument('-q', '--quiet', action='store_true', help='hide logging messages')\n\nPARSER.set_defaults(**DEFAULT_OPTIONS)\n\n\ndef main(argv=None, stdout=None, stdin=None, HTML=HTML):  # noqa: N803\n    \"\"\"The ``weasyprint`` program takes at least two arguments:\n\n    .. code-block:: sh\n\n        weasyprint [options] <input> <output>\n\n    \"\"\"\n    args = PARSER.parse_args(argv)\n\n    if args.input == '-':\n        source = stdin or sys.stdin.buffer\n        if args.base_url is None:\n            args.base_url = '.'  # current directory\n        elif args.base_url == '':\n            args.base_url = None  # no base URL\n    else:\n        source = args.input\n\n    if args.output == '-':\n        output = stdout or sys.stdout.buffer\n    else:\n        output = args.output\n\n    fetcher_args = {}\n    if args.timeout is not None:\n        fetcher_args['timeout'] = args.timeout\n    if args.allowed_protocols is not None:\n        fetcher_args['allowed_protocols'] = {\n            protocol.strip().lower() for protocol in args.allowed_protocols.split(',')}\n    if args.no_http_redirects:\n        fetcher_args['allow_redirects'] = False\n    if args.fail_on_http_errors:\n        fetcher_args['fail_on_errors'] = True\n    url_fetcher = URLFetcher(**fetcher_args)\n\n    options = {\n        key: value for key, value in vars(args).items() if key in DEFAULT_OPTIONS}\n\n    # Default to logging to stderr.\n    if args.debug:\n        LOGGER.setLevel(logging.DEBUG)\n    elif args.verbose:\n        LOGGER.setLevel(logging.INFO)\n    if not args.quiet:\n        handler = logging.StreamHandler()\n        if args.debug:\n            # Add extra information when debug logging\n            handler.setFormatter(\n                logging.Formatter(\n                    '%(levelname)s: %(filename)s:%(lineno)d '\n                    '(%(funcName)s): %(message)s'))\n        else:\n            handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))\n        LOGGER.addHandler(handler)\n\n    html = HTML(\n        source, base_url=args.base_url, encoding=args.encoding,\n        media_type=args.media_type, url_fetcher=url_fetcher)\n    html.write_pdf(output, **options)\n\n\nmain.__doc__ += '\\n\\n' + PARSER.docstring\n\n\nif __name__ == '__main__':  # pragma: no cover\n    main()\n"
  },
  {
    "path": "weasyprint/anchors.py",
    "content": "\"\"\"Find anchors, links, bookmarks and inputs in documents.\"\"\"\n\nimport math\n\nfrom .formatting_structure import boxes\nfrom .layout.percent import percentage\nfrom .matrix import Matrix\n\n\ndef rectangle_aabb(matrix, pos_x, pos_y, width, height):\n    \"\"\"Apply a transformation matrix to an axis-aligned rectangle.\n\n    Return its axis-aligned bounding box as ``(x1, y1, x2, y2)``.\n\n    \"\"\"\n    if not matrix:\n        return pos_x, pos_y, pos_x + width, pos_y + height\n    transform_point = matrix.transform_point\n    x1, y1 = transform_point(pos_x, pos_y)\n    x2, y2 = transform_point(pos_x + width, pos_y)\n    x3, y3 = transform_point(pos_x, pos_y + height)\n    x4, y4 = transform_point(pos_x + width, pos_y + height)\n    box_x1 = min(x1, x2, x3, x4)\n    box_y1 = min(y1, y2, y3, y4)\n    box_x2 = max(x1, x2, x3, x4)\n    box_y2 = max(y1, y2, y3, y4)\n    return box_x1, box_y1, box_x2, box_y2\n\n\ndef gather_anchors(box, anchors, links, bookmarks, forms, parent_matrix=None,\n                   parent_form=None):\n    \"\"\"Gather anchors and other data related to specific positions in PDF.\n\n    Currently finds anchors, links, bookmarks and forms.\n\n    \"\"\"\n    # Get box transformation matrix.\n    # \"Transforms apply to block-level and atomic inline-level elements,\n    #  but do not apply to elements which may be split into\n    #  multiple inline-level boxes.\"\n    # https://www.w3.org/TR/css-transforms-1/#introduction\n    if box.style['transform'] and not isinstance(box, boxes.InlineBox):\n        border_width = box.border_width()\n        border_height = box.border_height()\n        origin_x, origin_y = box.style['transform_origin']\n        offset_x = percentage(origin_x, box.style, border_width)\n        offset_y = percentage(origin_y, box.style, border_height)\n        origin_x = box.border_box_x() + offset_x\n        origin_y = box.border_box_y() + offset_y\n\n        matrix = Matrix(e=origin_x, f=origin_y)\n        for name, args in box.style['transform']:\n            a, b, c, d, e, f = 1, 0, 0, 1, 0, 0\n            if name == 'scale':\n                a, d = args\n            elif name == 'rotate':\n                a = d = math.cos(args)\n                b = math.sin(args)\n                c = -b\n            elif name == 'translate':\n                e = percentage(args[0], box.style, border_width)\n                f = percentage(args[1], box.style, border_height)\n            elif name == 'skew':\n                b, c = math.tan(args[1]), math.tan(args[0])\n            else:\n                assert name == 'matrix'\n                a, b, c, d, e, f = args\n            matrix = Matrix(a, b, c, d, e, f) @ matrix\n        box.transformation_matrix = (\n            Matrix(e=-origin_x, f=-origin_y) @ matrix)\n        if parent_matrix:\n            matrix = box.transformation_matrix @ parent_matrix\n        else:\n            matrix = box.transformation_matrix\n    else:\n        matrix = parent_matrix\n\n    bookmark_label = box.bookmark_label\n    if box.style['bookmark_level'] == 'none':\n        bookmark_level = None\n    else:\n        bookmark_level = box.style['bookmark_level']\n    state = box.style['bookmark_state']\n    link = box.style['link']\n    anchor_name = box.style['anchor']\n    has_bookmark = bookmark_label and bookmark_level\n    # 'link' is inherited but redundant on text boxes\n    has_link = link and not isinstance(box, (boxes.TextBox, boxes.LineBox))\n    # In case of duplicate IDs, only the first is an anchor.\n    has_anchor = anchor_name and anchor_name not in anchors\n    is_input = box.is_input()\n\n    if box.is_form():\n        parent_form = box.element\n        if parent_form not in forms:\n            forms[parent_form] = []\n\n    if has_bookmark or has_link or has_anchor or is_input:\n        if is_input:\n            pos_x, pos_y = box.content_box_x(), box.content_box_y()\n            width, height = box.width, box.height\n        else:\n            pos_x, pos_y, width, height = box.hit_area()\n        if has_link or is_input:\n            rectangle = rectangle_aabb(matrix, pos_x, pos_y, width, height)\n        if has_link:\n            token_type, link = link\n            assert token_type == 'url'\n            link_type, target = link\n            assert isinstance(target, str)\n            if link_type == 'external' and box.is_attachment():\n                link_type = 'attachment'\n            links.append((link_type, target, rectangle, box))\n        if is_input:\n            forms[parent_form].append((box.element, box.style, rectangle))\n        if has_bookmark:\n            if matrix:\n                pos_x, pos_y = matrix.transform_point(pos_x, pos_y)\n            bookmark = (bookmark_level, bookmark_label, (pos_x, pos_y), state)\n            bookmarks.append(bookmark)\n        if has_anchor:\n            pos_x1, pos_y1, pos_x2, pos_y2 = pos_x, pos_y, pos_x + width, pos_y + height\n            if matrix:\n                pos_x1, pos_y1 = matrix.transform_point(pos_x1, pos_y1)\n                pos_x2, pos_y2 = matrix.transform_point(pos_x2, pos_y2)\n            anchors[anchor_name] = (pos_x1, pos_y1, pos_x2, pos_y2)\n\n    for child in box.all_children():\n        gather_anchors(child, anchors, links, bookmarks, forms, matrix, parent_form)\n\n\ndef make_page_bookmark_tree(page, skipped_levels, last_by_depth,\n                            previous_level, page_number, matrix):\n    \"\"\"Make a tree of all bookmarks in a given page.\"\"\"\n    for level, label, (point_x, point_y), state in page.bookmarks:\n        if level > previous_level:\n            # Example: if the previous bookmark is a <h2>, the next\n            # depth \"should\" be for <h3>. If now we get a <h6> we’re\n            # skipping two levels: append 6 - 3 - 1 = 2\n            skipped_levels.append(level - previous_level - 1)\n        else:\n            temp = level\n            while temp < previous_level:\n                temp += 1 + skipped_levels.pop()\n            if temp > previous_level:\n                # We remove too many \"skips\", add some back:\n                skipped_levels.append(temp - previous_level - 1)\n\n        previous_level = level\n        depth = level - sum(skipped_levels)\n        assert depth == len(skipped_levels)\n        assert depth >= 1\n\n        children = []\n        point_x, point_y = matrix.transform_point(point_x, point_y)\n        subtree = (label, (page_number, point_x, point_y), children, state)\n        last_by_depth[depth - 1].append(subtree)\n        del last_by_depth[depth:]\n        last_by_depth.append(children)\n    return previous_level\n"
  },
  {
    "path": "weasyprint/css/__init__.py",
    "content": "\"\"\"Find and apply CSS.\n\nThis module takes care of steps 3 and 4 of “CSS 2.1 processing model”: Retrieve\nstylesheets associated with a document and annotate every element with a value\nfor every CSS property.\n\nhttps://www.w3.org/TR/CSS21/intro.html#processing-model\n\nThis module does this in more than two steps. The\n:func:`get_all_computed_styles` function does everything, but it is itsef based\non other functions in this module.\n\n\"\"\"\n\nimport math\nfrom collections import namedtuple\nfrom itertools import groupby\nfrom logging import DEBUG, WARNING\nfrom math import inf\n\nimport cssselect2\nimport tinycss2\nimport tinycss2.ast\nimport tinycss2.nth\nfrom PIL.ImageCms import ImageCmsProfile\n\nfrom .. import CSS\nfrom ..logger import LOGGER, PROGRESS_LOGGER\nfrom ..text.fonts import FontConfiguration\nfrom ..urls import URLFetchingError, fetch, get_url_attribute, url_join\nfrom . import counters, media_queries\nfrom .computed_values import COMPUTER_FUNCTIONS, PHYSICAL_FUNCTIONS\nfrom .functions import Function, check_math, check_var\nfrom .properties import INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES, ZERO_PIXELS\nfrom .units import ANGLE_UNITS, LENGTH_UNITS, RELATIVE_UNITS, to_pixels, to_radians\nfrom .validation import preprocess_declarations\nfrom .validation.descriptors import preprocess_descriptors\nfrom .validation.properties import validate_non_shorthand\n\nfrom .tokens import (  # isort:skip\n    E, MINUS_INFINITY, NAN, PI, PLUS_INFINITY, InvalidValues, Pending, PercentageInMath,\n    RelativeLengthInMath, get_angle, get_url, remove_whitespace, split_on_comma,\n    tokenize)\n\n# Reject anything not in here:\nPSEUDO_ELEMENTS = (\n    None, 'before', 'after', 'marker', 'first-line', 'first-letter',\n    'footnote-call', 'footnote-marker')\n\nPageSelectorType = namedtuple(\n    'PageSelectorType', ['side', 'blank', 'first', 'index', 'name'])\n\n\nclass StyleFor:\n    \"\"\"Convenience function to get the computed styles for an element.\"\"\"\n    def __init__(self, html, sheets, presentational_hints, font_config,\n                 target_collector):\n        # keys: (element, pseudo_element_type)\n        #    element: an ElementTree Element or the '@page' string\n        #    pseudo_element_type: a string such as 'first' (for @page) or\n        #        'after', or None for normal elements\n        # values: dicts of\n        #     keys: property name as a string\n        #     values: (values, weight)\n        #         values: a PropertyValue-like object\n        #         weight: values with a greater weight take precedence, see\n        #             https://www.w3.org/TR/CSS21/cascade.html#cascading-order\n        self._cascaded_styles = cascaded_styles = {}\n\n        # keys: (element, pseudo_element_type), like cascaded_styles\n        # values: style dict objects:\n        #     keys: property name as a string\n        #     values: a PropertyValue-like object\n        self._computed_styles = {}\n\n        # Set when the first page is created, used for viewport-based units.\n        self.initial_page_sizes = {'box': None, 'area': None}\n\n        self._sheets = sheets\n        self.font_config = font_config\n\n        PROGRESS_LOGGER.info('Step 3 - Applying CSS')\n        layer_order = inf\n        for specificity, attributes in find_style_attributes(\n                html.etree_element, presentational_hints, html.base_url):\n            element, declarations, base_url = attributes\n            style = cascaded_styles.setdefault((element, None), {})\n            for name, values, importance in preprocess_declarations(\n                    base_url, declarations):\n                precedence = declaration_precedence('author', importance)\n                weight = (precedence, layer_order, specificity)\n                old_weight = style.get(name, (None, None))[1]\n                if old_weight is None or old_weight <= weight:\n                    style[name] = values, weight\n\n        # First, add declarations and set computed styles for \"real\" elements\n        # *in tree order*. Tree order is important so that parents have\n        # computed styles before their children, for inheritance.\n\n        # Iterate on all elements, even if there is no cascaded style for them.\n        for element in html.wrapper_element.iter_subtree():\n            for sheet, origin, sheet_specificity in sheets:\n                # Add declarations for matched elements\n                for selector in sheet.matcher.match(element):\n                    specificity, order, pseudo_type, (declarations, layer) = selector\n                    layer_order = inf if layer is None else sheet.layers.index(layer)\n                    specificity = sheet_specificity or specificity\n                    style = cascaded_styles.setdefault(\n                        (element.etree_element, pseudo_type), {})\n                    for name, values, importance in declarations:\n                        precedence = declaration_precedence(origin, importance)\n                        weight = (precedence, layer_order, specificity)\n                        old_weight = style.get(name, (None, None))[1]\n                        if old_weight is None or old_weight <= weight:\n                            style[name] = values, weight\n            parent = element.parent.etree_element if element.parent else None\n            self.set_computed_styles(\n                element.etree_element, root=html.etree_element, parent=parent,\n                base_url=html.base_url, target_collector=target_collector)\n\n        # Then computed styles for pseudo elements, in any order.\n        # Pseudo-elements inherit from their associated element so they come\n        # last. Do them in a second pass as there is no easy way to iterate\n        # on the pseudo-elements for a given element with the current structure\n        # of cascaded_styles. (Keys are (element, pseudo_type) tuples.)\n\n        # Only iterate on pseudo-elements that have cascaded styles. (Others\n        # might as well not exist.)\n        for element, pseudo_type in cascaded_styles:\n            if pseudo_type:\n                self.set_computed_styles(\n                    element, pseudo_type=pseudo_type,\n                    # The pseudo-element inherits from the element.\n                    root=html.etree_element, parent=element,\n                    base_url=html.base_url, target_collector=target_collector)\n\n        # Clear the cascaded styles, we don't need them anymore. Keep the\n        # dictionary, it is used later for page margins.\n        self._cascaded_styles.clear()\n\n    def __call__(self, element, pseudo_type=None):\n        if style := self._computed_styles.get((element, pseudo_type)):\n            if 'table' in style['display'] and style['border_collapse'] == 'collapse':\n                # Padding does not apply.\n                for side in ('top', 'bottom', 'left', 'right'):\n                    style[f'padding_{side}'] = ZERO_PIXELS\n            if len(style['display']) == 1:\n                display, = style['display']\n                if display.startswith('table-') and display != 'table-caption':\n                    # Margins do not apply.\n                    for side in ('top', 'bottom', 'left', 'right'):\n                        style[f'margin_{side}'] = ZERO_PIXELS\n        return style\n\n    def set_computed_styles(self, element, parent, root=None, pseudo_type=None,\n                            base_url=None, target_collector=None):\n        \"\"\"Set the computed values of styles to ``element``.\n\n        Take the properties left by ``apply_style_rule`` on an element or\n        pseudo-element and assign computed values with respect to the cascade,\n        declaration priority (ie. ``!important``) and selector specificity.\n\n        \"\"\"\n        cascaded_styles = self.get_cascaded_styles()\n        computed_styles = self.get_computed_styles()\n        if element == root and pseudo_type is None:\n            assert parent is None\n            parent_style = None\n            root_style = InitialStyle(self.font_config)\n        else:\n            assert parent is not None\n            parent_style = computed_styles[parent, None]\n            root_style = computed_styles[root, None]\n\n        cascaded = cascaded_styles.get((element, pseudo_type), {})\n        computed = computed_styles[element, pseudo_type] = ComputedStyle(\n            parent_style, cascaded, element, pseudo_type, root_style, base_url,\n            self.font_config, self.initial_page_sizes)\n        if target_collector and computed['anchor']:\n            target_collector.collect_anchor(computed['anchor'])\n\n    def add_page_declarations(self, page_type):\n        # TODO: use real layer order.\n        layer_order = None\n        for sheet, origin, sheet_specificity in self._sheets:\n            for _rule, selector_list, declarations in sheet.page_rules:\n                for selector in selector_list:\n                    specificity, pseudo_type, page_selector_type = selector\n                    if self._page_type_match(page_selector_type, page_type):\n                        specificity = sheet_specificity or specificity\n                        style = self._cascaded_styles.setdefault(\n                            (page_type, pseudo_type), {})\n                        for name, values, importance in declarations:\n                            precedence = declaration_precedence(origin, importance)\n                            weight = (precedence, layer_order, specificity)\n                            old_weight = style.get(name, (None, None))[1]\n                            if old_weight is None or old_weight <= weight:\n                                style[name] = values, weight\n\n    def get_cascaded_styles(self):\n        return self._cascaded_styles\n\n    def get_computed_styles(self):\n        return self._computed_styles\n\n    @staticmethod\n    def _page_type_match(page_selector_type, page_type):\n        if page_selector_type.side not in (None, page_type.side):\n            return False\n        if page_selector_type.blank not in (None, page_type.blank):\n            return False\n        if page_selector_type.first not in (None, page_type.index == 0):\n            return False\n        if page_selector_type.name not in (None, page_type.name):\n            return False\n        if page_selector_type.index is not None:\n            a, b, name = page_selector_type.index\n            if name is None:\n                index = page_type.index\n                offset = index + 1 - b\n                return offset == 0 if a == 0 else (offset / a >= 0 and not offset % a)\n            if name != page_type.name:\n                return False\n            for group_name, index in page_type.groups:\n                if name != group_name:\n                    continue\n                offset = index + 1 - b\n                if (offset == 0 if a == 0 else (offset / a >= 0 and not offset % a)):\n                    return True\n            return False\n        return True\n\n\ndef get_child_text(element):\n    \"\"\"Return the text directly in the element, not descendants.\"\"\"\n    content = [element.text] if element.text else []\n    for child in element:\n        if child.tail:\n            content.append(child.tail)\n    return ''.join(content)\n\n\ndef text_decoration(key, value, parent_value, cascaded):\n    # The text-decoration-* properties are not inherited but propagated\n    # using specific rules.\n    # See https://drafts.csswg.org/css-text-decor-3/#line-decoration\n    # TODO: these rules don’t follow the specification.\n    text_properties = (\n        'text_decoration_color', 'text_decoration_style', 'text_decoration_thickness')\n    if key in text_properties:\n        if not cascaded:\n            value = parent_value\n    elif key == 'text_decoration_line':\n        if parent_value != 'none':\n            if value == 'none':\n                value = parent_value\n            else:\n                value = value | parent_value\n    return value\n\n\ndef find_stylesheets(wrapper_element, device_media_type, url_fetcher, base_url,\n                     font_config, counter_style, color_profiles, page_rules, layers):\n    \"\"\"Yield the stylesheets in ``element_tree``.\n\n    The output order is the same as the source order.\n\n    \"\"\"\n    from ..html import element_has_link_type\n\n    for wrapper in wrapper_element.query_all('style', 'link'):\n        element = wrapper.etree_element\n        mime_type = element.get('type', 'text/css').split(';', 1)[0].strip()\n        # Only keep 'type/subtype' from 'type/subtype ; param1; param2'.\n        if mime_type != 'text/css':\n            continue\n        media_attr = element.get('media', '').strip() or 'all'\n        media = [media_type.strip() for media_type in media_attr.split(',')]\n        if not media_queries.evaluate_media_query(media, device_media_type):\n            continue\n        if element.tag == 'style':\n            # Content is text that is directly in the <style> element, not its\n            # descendants\n            content = get_child_text(element)\n            # ElementTree should give us either unicode or ASCII-only\n            # bytestrings, so we don't need `encoding` here.\n            css = CSS(\n                string=content, base_url=base_url,\n                url_fetcher=url_fetcher, media_type=device_media_type,\n                font_config=font_config, counter_style=counter_style,\n                page_rules=page_rules, color_profiles=color_profiles, layers=layers)\n            yield css\n        elif element.tag == 'link' and element.get('href'):\n            if not element_has_link_type(element, 'stylesheet') or \\\n                    element_has_link_type(element, 'alternate'):\n                continue\n            href = get_url_attribute(element, 'href', base_url)\n            if href is not None:\n                try:\n                    yield CSS(\n                        url=href, url_fetcher=url_fetcher, media_type=device_media_type,\n                        font_config=font_config, counter_style=counter_style,\n                        color_profiles=color_profiles, page_rules=page_rules,\n                        layers=layers, _check_mime_type=True)\n                except URLFetchingError as exception:\n                    LOGGER.error('Failed to load stylesheet at %s: %s', href, exception)\n                    LOGGER.debug('Error while loading stylesheet:', exc_info=exception)\n\n\ndef find_style_attributes(tree, presentational_hints=False, base_url=None):\n    \"\"\"Yield ``specificity, (element, declaration, base_url)`` rules.\n\n    Rules from \"style\" attribute are returned with specificity\n    ``(1, 0, 0)``.\n\n    If ``presentational_hints`` is ``True``, rules from presentational hints\n    are returned with specificity ``(0, 0, 0)``.\n\n    \"\"\"\n    def check_style_attribute(element, style_attribute):\n        declarations = tinycss2.parse_blocks_contents(style_attribute)\n        return element, declarations, base_url\n\n    for element in tree.iter():\n        specificity = (1, 0, 0)\n        style_attribute = element.get('style')\n        if style_attribute:\n            yield specificity, check_style_attribute(element, style_attribute)\n        if not presentational_hints:\n            continue\n        specificity = (0, 0, 0)\n        if element.tag == 'body':\n            # TODO: we should check the container frame element\n            for part, position in (\n                    ('height', 'top'), ('height', 'bottom'),\n                    ('width', 'left'), ('width', 'right')):\n                style_attribute = None\n                for prop in (f'margin{part}', f'{position}margin'):\n                    if element.get(prop):\n                        style_attribute = f'margin-{position}:{element.get(prop)}px'\n                        break\n                if style_attribute:\n                    yield specificity, check_style_attribute(element, style_attribute)\n            if element.get('background'):\n                style_attribute = f'background-image:url({element.get(\"background\")})'\n                yield specificity, check_style_attribute(element, style_attribute)\n            if element.get('bgcolor'):\n                style_attribute = f'background-color:{element.get(\"bgcolor\")}'\n                yield specificity, check_style_attribute(element, style_attribute)\n            if element.get('text'):\n                style_attribute = f'color:{element.get(\"text\")}'\n                yield specificity, check_style_attribute(element, style_attribute)\n            # TODO: we should support link, vlink, alink\n        elif element.tag == 'center':\n            yield specificity, check_style_attribute(element, 'text-align:center')\n        elif element.tag == 'div':\n            align = element.get('align', '').lower()\n            if align == 'middle':\n                yield specificity, check_style_attribute(element, 'text-align:center')\n            elif align in ('center', 'left', 'right', 'justify'):\n                yield specificity, check_style_attribute(element, f'text-align:{align}')\n        elif element.tag == 'font':\n            if element.get('color'):\n                yield specificity, check_style_attribute(\n                    element, f'color:{element.get(\"color\")}')\n            if element.get('face'):\n                yield specificity, check_style_attribute(\n                    element, f'font-family:{element.get(\"face\")}')\n            if element.get('size'):\n                size = element.get('size').strip()\n                relative_plus = size.startswith('+')\n                relative_minus = size.startswith('-')\n                if relative_plus or relative_minus:\n                    size = size[1:].strip()\n                try:\n                    size = int(size)\n                except ValueError:\n                    LOGGER.warning('Invalid value for size: %s', size)\n                else:\n                    font_sizes = {\n                        1: 'x-small',\n                        2: 'small',\n                        3: 'medium',\n                        4: 'large',\n                        5: 'x-large',\n                        6: 'xx-large',\n                        7: '48px',  # 1.5 * xx-large\n                    }\n                    if relative_plus:\n                        size += 3\n                    elif relative_minus:\n                        size -= 3\n                    size = max(1, min(7, size))\n                    yield specificity, check_style_attribute(\n                        element, f'font-size:{font_sizes[size]}')\n        elif element.tag == 'table':\n            if element.get('cellspacing'):\n                yield specificity, check_style_attribute(\n                    element, f'border-spacing:{element.get(\"cellspacing\")}px')\n            if element.get('cellpadding'):\n                cellpadding = element.get('cellpadding')\n                if cellpadding.isdigit():\n                    cellpadding += 'px'\n                # TODO: don't match subtables cells\n                for subelement in element.iter():\n                    if subelement.tag in ('td', 'th'):\n                        yield specificity, check_style_attribute(\n                            subelement,\n                            f'padding-left:{cellpadding};'\n                            f'padding-right:{cellpadding};'\n                            f'padding-top:{cellpadding};'\n                            f'padding-bottom:{cellpadding};')\n            if element.get('hspace'):\n                hspace = element.get('hspace')\n                if hspace.isdigit():\n                    hspace += 'px'\n                yield specificity, check_style_attribute(\n                    element, f'margin-left:{hspace};margin-right:{hspace}')\n            if element.get('vspace'):\n                vspace = element.get('vspace')\n                if vspace.isdigit():\n                    vspace += 'px'\n                yield specificity, check_style_attribute(\n                    element, f'margin-top:{vspace};margin-bottom:{vspace}')\n            if element.get('width'):\n                style_attribute = f'width:{element.get(\"width\")}'\n                if element.get('width').isdigit():\n                    style_attribute += 'px'\n                yield specificity, check_style_attribute(element, style_attribute)\n            if element.get('height'):\n                style_attribute = f'height:{element.get(\"height\")}'\n                if element.get('height').isdigit():\n                    style_attribute += 'px'\n                yield specificity, check_style_attribute(element, style_attribute)\n            if element.get('background'):\n                style_attribute = (\n                    f'background-image:url({element.get(\"background\")})')\n                yield specificity, check_style_attribute(element, style_attribute)\n            if element.get('bgcolor'):\n                style_attribute = f'background-color:{element.get(\"bgcolor\")}'\n                yield specificity, check_style_attribute(element, style_attribute)\n            if element.get('bordercolor'):\n                style_attribute = f'border-color:{element.get(\"bordercolor\")}'\n                yield specificity, check_style_attribute(element, style_attribute)\n            if element.get('border'):\n                style_attribute = f'border-width:{element.get(\"border\")}px'\n                yield specificity, check_style_attribute(element, style_attribute)\n        elif element.tag in ('tr', 'td', 'th', 'thead', 'tbody', 'tfoot'):\n            align = element.get('align', '').lower()\n            # TODO: we should align descendants too\n            if align == 'middle':\n                yield specificity, check_style_attribute(\n                    element, 'text-align:center')\n            elif align in ('center', 'left', 'right', 'justify'):\n                yield specificity, check_style_attribute(element, f'text-align:{align}')\n            if element.get('background'):\n                style_attribute = f'background-image:url({element.get(\"background\")})'\n                yield specificity, check_style_attribute(element, style_attribute)\n            if element.get('bgcolor'):\n                style_attribute = f'background-color:{element.get(\"bgcolor\")}'\n                yield specificity, check_style_attribute(element, style_attribute)\n            if element.tag in ('tr', 'td', 'th'):\n                if element.get('height'):\n                    style_attribute = f'height:{element.get(\"height\")}'\n                    if element.get('height').isdigit():\n                        style_attribute += 'px'\n                    yield specificity, check_style_attribute(element, style_attribute)\n                if element.tag in ('td', 'th'):\n                    if element.get('width'):\n                        style_attribute = f'width:{element.get(\"width\")}'\n                        if element.get('width').isdigit():\n                            style_attribute += 'px'\n                        yield specificity, check_style_attribute(\n                            element, style_attribute)\n        elif element.tag == 'caption':\n            align = element.get('align', '').lower()\n            # TODO: we should align descendants too\n            if align == 'middle':\n                yield specificity, check_style_attribute(element, 'text-align:center')\n            elif align in ('center', 'left', 'right', 'justify'):\n                yield specificity, check_style_attribute(element, f'text-align:{align}')\n        elif element.tag == 'col':\n            if element.get('width'):\n                style_attribute = f'width:{element.get(\"width\")}'\n                if element.get('width').isdigit():\n                    style_attribute += 'px'\n                yield specificity, check_style_attribute(element, style_attribute)\n        elif element.tag == 'hr':\n            size = 0\n            if element.get('size'):\n                try:\n                    size = int(element.get('size'))\n                except ValueError:\n                    LOGGER.warning('Invalid value for size: %s', size)\n            if (element.get('color'), element.get('noshade')) != (None, None):\n                if size >= 1:\n                    yield specificity, check_style_attribute(\n                        element, f'border-width:{size / 2}px')\n            elif size == 1:\n                yield specificity, check_style_attribute(\n                    element, 'border-bottom-width:0')\n            elif size > 1:\n                yield specificity, check_style_attribute(\n                    element, f'height:{size - 2}px')\n            if element.get('width'):\n                style_attribute = f'width:{element.get(\"width\")}'\n                if element.get('width').isdigit():\n                    style_attribute += 'px'\n                yield specificity, check_style_attribute(element, style_attribute)\n            if element.get('color'):\n                yield specificity, check_style_attribute(\n                    element, f'color:{element.get(\"color\")}')\n        elif element.tag in (\n                'iframe', 'applet', 'embed', 'img', 'input', 'object',\n                '{http://www.w3.org/2000/svg}svg'):\n            if (element.tag != 'input' or\n                    element.get('type', '').lower() == 'image'):\n                align = element.get('align', '').lower()\n                if align in ('middle', 'center'):\n                    # TODO: middle and center values are wrong\n                    yield specificity, check_style_attribute(\n                        element, 'vertical-align:middle')\n                if element.get('hspace'):\n                    hspace = element.get('hspace')\n                    if hspace.isdigit():\n                        hspace += 'px'\n                    yield specificity, check_style_attribute(\n                        element, f'margin-left:{hspace};margin-right:{hspace}')\n                if element.get('vspace'):\n                    vspace = element.get('vspace')\n                    if vspace.isdigit():\n                        vspace += 'px'\n                    yield specificity, check_style_attribute(\n                        element, f'margin-top:{vspace};margin-bottom:{vspace}')\n                # TODO: img seems to be excluded for width and height, but a\n                # lot of W3C tests rely on this attribute being applied to img\n                if element.get('width'):\n                    style_attribute = f'width:{element.get(\"width\")}'\n                    if element.get('width').isdigit():\n                        style_attribute += 'px'\n                    yield specificity, check_style_attribute(element, style_attribute)\n                if element.get('height'):\n                    style_attribute = f'height:{element.get(\"height\")}'\n                    if element.get('height').isdigit():\n                        style_attribute += 'px'\n                    yield specificity, check_style_attribute(element, style_attribute)\n                if element.tag in ('img', 'object', 'input'):\n                    if element.get('border'):\n                        yield specificity, check_style_attribute(\n                            element,\n                            f'border-width:{element.get(\"border\")}px;'\n                            f'border-style:solid')\n        elif element.tag == 'ol':\n            # From https://www.w3.org/TR/css-lists-3/#ua-stylesheet\n            if element.get('start'):\n                yield specificity, check_style_attribute(\n                    element,\n                    f'counter-reset:list-item {element.get(\"start\")};'\n                    'counter-increment:list-item -1')\n        elif element.tag == 'li':\n            # From https://www.w3.org/TR/css-lists-3/#ua-stylesheet\n            if element.get('value'):\n                yield specificity, check_style_attribute(\n                    element,\n                    f'counter-reset:list-item {element.get(\"value\")};'\n                    'counter-increment:none')\n\n\ndef declaration_precedence(origin, importance):\n    \"\"\"Return the precedence for a declaration.\n\n    Precedence values have no meaning unless compared to each other.\n\n    Acceptable values for ``origin`` are the strings ``'author'``, ``'user'``\n    and ``'user agent'``.\n\n    \"\"\"\n    # See https://www.w3.org/TR/CSS21/cascade.html#cascading-order\n    if origin == 'user agent':\n        return 1\n    elif origin == 'user' and not importance:\n        return 2\n    elif origin == 'author' and not importance:\n        return 3\n    elif origin == 'author':  # and importance\n        return 4\n    else:\n        assert origin == 'user'  # and importance\n        return 5\n\n\ndef resolve_var(computed, token, parent_style, known_variables=None):\n    \"\"\"Return token with resolved CSS variables.\"\"\"\n    if not check_var(token):\n        return\n\n    if known_variables is None:\n        known_variables = set()\n\n    if token.type == '() block' or token.lower_name != 'var':\n        items = []\n        token_items = token.arguments if token.type == 'function' else token.content\n        for i, argument in enumerate(token_items):\n            if argument.type in ('function', '() block'):\n                resolved = resolve_var(\n                    computed, argument, parent_style, known_variables.copy())\n                items.extend((argument,) if resolved is None else resolved)\n            else:\n                items.append(argument)\n        if token.type == '() block':\n            token = tinycss2.ast.ParenthesesBlock(\n                token.source_line, token.source_column, items)\n        else:\n            token = tinycss2.ast.FunctionBlock(\n                token.source_line, token.source_column, token.name, items)\n        return resolve_var(computed, token, parent_style, known_variables) or (token,)\n\n    function = Function(token)\n    arguments = function.split_comma(single_tokens=False, trailing=True)\n    if not arguments or len(arguments[0]) != 1:\n        return []\n    variable_name = arguments[0][0].value.replace('-', '_')  # first arg is name\n    if variable_name in known_variables:\n        return []  # endless recursion\n    else:\n        known_variables.add(variable_name)\n    default = arguments[1] if len(arguments) > 1 else []\n    computed_value = []\n    for value in (computed[variable_name] or default):\n        resolved = resolve_var(computed, value, parent_style, known_variables.copy())\n        computed_value.extend((value,) if resolved is None else resolved)\n    return computed_value\n\n\ndef _resolve_calc_sum(computed, tokens, property_name, refer_to):\n    groups = [[]]\n    for token in tokens:\n        if token.type == 'literal' and token.value in '+-':\n            groups.append(token.value)\n            groups.append([])\n        elif token.type == '() block':\n            content = remove_whitespace(token.content)\n            result = _resolve_calc_sum(computed, content, property_name, refer_to)\n            if result is None:\n                return\n            groups[-1].append(result)\n        else:\n            groups[-1].append(token)\n\n    value, sign, unit = 0, '+', None\n    exception = None\n    while groups:\n        if sign is None:\n            sign = groups.pop(0)\n            assert sign in '+-'\n        else:\n            group = groups.pop(0)\n            assert group\n            assert isinstance(group, list)\n            try:\n                product = _resolve_calc_product(\n                    computed, group, property_name, refer_to)\n            except RelativeLengthInMath as relative_exception:\n                # RelativeLengthInMath raised, assume that we got pixels and continue to\n                # find if we have to raise PercentageInMath first.\n                if unit == '%':\n                    raise PercentageInMath\n                exception = relative_exception\n                unit = 'px'\n                sign = None\n                continue\n            else:\n                if product is None:\n                    return\n            if product.type == 'dimension':\n                if unit is None:\n                    unit = product.unit.lower()\n                elif unit == '%':\n                    raise PercentageInMath\n                elif unit != product.unit.lower():\n                    return\n            elif product.type == 'percentage':\n                if refer_to is not None:\n                    product.value = product.value / 100 * refer_to\n                    unit = 'px'\n                else:\n                    if unit is None or unit == '%':\n                        unit = '%'\n                    else:\n                        raise PercentageInMath\n            if sign == '+':\n                value += product.value\n            else:\n                value -= product.value\n            sign = None\n\n    # Raise RelativeLengthInMath, only if we didn’t raise PercentageInMath before.\n    if exception:\n        raise exception\n\n    return tokenize(value, unit=unit)\n\n\ndef _resolve_calc_product(computed, tokens, property_name, refer_to):\n    groups = [[]]\n    for token in tokens:\n        if token.type == 'literal' and token.value in '*/':\n            groups.append(token.value)\n            groups.append([])\n        elif token.type == 'number':\n            groups[-1].append(token)\n        elif token.type == 'dimension' and token.unit.lower() in LENGTH_UNITS:\n            if computed is None and token.unit.lower() in RELATIVE_UNITS:\n                raise RelativeLengthInMath\n            pixels = to_pixels(token, computed, property_name)\n            groups[-1].append(tokenize(pixels, unit='px'))\n        elif token.type == 'dimension' and token.unit.lower() in ANGLE_UNITS:\n            groups[-1].append(tokenize(to_radians(token), unit='rad'))\n        elif token.type == 'percentage':\n            groups[-1].append(tokenize(token.value, unit='%'))\n        elif token.type == 'ident':\n            groups[-1].append(token)\n        else:\n            return\n\n    value, sign, unit = 1, '*', None\n    while groups:\n        if sign is None:\n            sign = groups.pop(0)\n            assert sign in '*/'\n        else:\n            group = groups.pop(0)\n            assert isinstance(group, list)\n            calc = _resolve_calc_value(computed, group)\n            if calc is None:\n                return\n            if calc.type == 'dimension':\n                if unit is None or unit == '%':\n                    unit = calc.unit.lower()\n                else:\n                    return\n            elif calc.type == 'percentage':\n                if unit is None:\n                    unit = '%'\n            if sign == '*':\n                value *= calc.value\n            else:\n                value /= calc.value\n            sign = None\n\n    return tokenize(value, unit=unit)\n\n\ndef _resolve_calc_value(computed, tokens):\n    if len(tokens) == 1:\n        token, = tokens\n        if token.type in ('number', 'dimension', 'percentage'):\n            return token\n        elif token.type == 'ident':\n            if token.lower_value == 'e':\n                return E\n            elif token.lower_value == 'pi':\n                return PI\n            elif token.lower_value == 'infinity':\n                return PLUS_INFINITY\n            elif token.lower_value == '-infinity':\n                return MINUS_INFINITY\n            elif token.lower_value == 'nan':\n                return NAN\n\n\ndef resolve_math(token, computed=None, property_name=None, refer_to=None):\n    \"\"\"Return token with resolved math functions.\n\n    Raise, in order of priority, ``PercentageInMath`` if percentages are mixed with\n    other values with no ``refer_to`` size, or ``RelativeLengthInMath`` if no\n    ``computed`` style is available to get font / viewport size.\n\n    ``PercentageInMath`` has to be raised before ``RelativeLengthInMath`` so that it can\n    be used to discard validation of properties that don’t accept percentages.\n\n    \"\"\"\n    if not check_math(token):\n        return\n\n    args = []\n    function = Function(token)\n    if function.name is None:\n        return\n    for part in function.split_comma(single_tokens=False):\n        args.append([])\n        for arg in part:\n            if check_math(arg):\n                arg = resolve_math(arg, computed, property_name, refer_to)\n                if arg is None:\n                    return\n            args[-1].append(arg)\n\n    if function.name == 'calc':\n        result = _resolve_calc_sum(computed, args[0], property_name, refer_to)\n        if result is None:\n            return\n        else:\n            return tokenize(result)\n\n    elif function.name in ('min', 'max'):\n        target_value = target_token = unit = None\n        for tokens in args:\n            token = _resolve_calc_sum(computed, tokens, property_name, refer_to)\n            if token is None:\n                return\n            if token.type == 'percentage':\n                if refer_to is None:\n                    if unit in ('px', ''):\n                        raise PercentageInMath\n                    unit = '%'\n                    value = token\n                else:\n                    unit = 'px'\n                    token = value = tokenize(token.value / 100 * refer_to, unit='px')\n            elif token.type == 'number':\n                if unit == '%':\n                    raise PercentageInMath\n                elif unit == 'px':\n                    return\n                unit = ''\n                value = tokenize(token.value, unit='px')\n            else:\n                if unit == '%':\n                    raise PercentageInMath\n                elif unit == '':\n                    return\n                unit = 'px'\n                value = tokenize(to_pixels(token, computed, property_name), unit='px')\n            update_condition = (\n                target_value is None or\n                (function.name == 'min' and value.value < target_value.value) or\n                (function.name == 'max' and value.value > target_value.value))\n            if update_condition:\n                target_value, target_token = value, token\n        return tokenize(target_token)\n\n    elif function.name == 'round':\n        strategy, multiple = 'nearest', 1\n        if len(args) == 1:\n            number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)\n        elif len(args) == 2:\n            strategies = ('nearest', 'up', 'down', 'to-zero')\n            if len(args[0]) == 1 and args[0][0].value in strategies:\n                strategy = args[0][0].value\n                number_token = _resolve_calc_sum(\n                    computed, args[1], property_name, refer_to)\n                if number_token is None:\n                    return\n            else:\n                number_token = _resolve_calc_sum(\n                    computed, args[0], property_name, refer_to)\n                multiple_token = _resolve_calc_sum(\n                    computed, args[1], property_name, refer_to)\n                if None in (number_token, multiple_token):\n                    return\n                if number_token.type != multiple_token.type:\n                    return\n                multiple = multiple_token.value\n        elif len(args) == 3:\n            strategy = args[0][0].value\n            number_token = _resolve_calc_sum(computed, args[1], property_name, refer_to)\n            multiple_token = _resolve_calc_sum(\n                computed, args[2], property_name, refer_to)\n            if None in (number_token, multiple_token):\n                return\n            if number_token.type != multiple_token.type:\n                return\n            multiple = multiple_token.value\n        if strategy == 'nearest':\n            # TODO: always round x.5 to +inf, see\n            # https://drafts.csswg.org/css-values-4/#combine-integers.\n            function = round\n        elif strategy == 'up':\n            function = math.ceil\n        elif strategy == 'down':\n            function = math.floor\n        elif strategy == 'to-zero':\n            function = math.floor if number_token.value > 0 else math.ceil\n        else:\n            return\n        return tokenize(number_token, lambda x: function(x / multiple) * multiple)\n\n    elif function.name in ('mod', 'rem'):\n        number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)\n        parameter_token = _resolve_calc_sum(computed, args[1], property_name, refer_to)\n        if None in (number_token, parameter_token):\n            return\n        if number_token.type != parameter_token.type:\n            return\n        number = number_token.value\n        parameter = parameter_token.value\n        value = number % parameter\n        if function.name == 'rem' and number * parameter < 0:\n            value += abs(parameter)\n        return tokenize(number_token, lambda x: value)\n\n    elif function.name in ('sin', 'cos', 'tan'):\n        number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)\n        if number_token is None:\n            return\n        if number_token.type == 'number':\n            angle = number_token.value\n        elif (angle := get_angle(number_token)) is None:\n            return\n        value = getattr(math, function.name)(angle)\n        return tokenize(value)\n\n    elif function.name in ('asin', 'acos', 'atan'):\n        number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)\n        if number_token is None or number_token.type != 'number':\n            return\n        try:\n            value = getattr(math, function.name)(number_token.value)\n        except ValueError:\n            return\n        return tokenize(value, unit='rad')\n\n    elif function.name == 'atan2':\n        y_token, x_token = [\n            _resolve_calc_sum(computed, arg, property_name, refer_to) for arg in args]\n        if None in (y_token, x_token):\n            return\n        if {y_token.type, x_token.type} != {'number'}:\n            return\n        y, x = y_token.value, x_token.value\n        return tokenize(math.atan2(y, x), unit='rad')\n\n    elif function.name == 'clamp':\n        pixels_list = []\n        unit = None\n        for tokens in args:\n            token = _resolve_calc_sum(computed, tokens, property_name, refer_to)\n            if token is None:\n                return\n            if token.type == 'percentage':\n                if refer_to is None:\n                    if unit == 'px':\n                        raise PercentageInMath\n                    unit = '%'\n                    value = token\n                else:\n                    unit = 'px'\n                    token = tokenize(token.value / 100 * refer_to, unit='px')\n            else:\n                if unit == '%':\n                    raise PercentageInMath\n                unit = 'px'\n                pixels = to_pixels(token, computed, property_name)\n                value = tokenize(pixels, unit='px')\n            pixels_list.append(value)\n        min_token, token, max_token = pixels_list\n        if token.value < min_token.value:\n            token = min_token\n        if token.value > max_token.value:\n            token = max_token\n        return tokenize(token)\n\n    elif function.name == 'pow':\n        number_token, power_token = [\n            _resolve_calc_sum(computed, arg, property_name, refer_to) for arg in args]\n        if None in (number_token, power_token):\n            return\n        if {number_token.type, power_token.type} != {'number'}:\n            return\n        return tokenize(number_token, lambda x: x ** power_token.value)\n\n    elif function.name == 'sqrt':\n        number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)\n        if number_token is None or number_token.type != 'number':\n            return\n        return tokenize(number_token, lambda x: x ** 0.5)\n\n    elif function.name == 'hypot':\n        resolved = [\n            _resolve_calc_sum(computed, tokens, property_name, refer_to)\n            for tokens in args]\n        if None in resolved:\n            return\n        value = math.hypot(*[token.value for token in resolved])\n        return tokenize(resolved[0], lambda x: value)\n\n    elif function.name == 'log':\n        number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)\n        if number_token is None or number_token.type != 'number':\n            return\n        if len(args) == 2:\n            base_token = _resolve_calc_sum(computed, args[1], property_name, refer_to)\n            if base_token is None or base_token.type != 'number':\n                return\n            base = base_token.value\n        else:\n            base = math.e\n        return tokenize(number_token, lambda x: math.log(x, base))\n\n    elif function.name == 'exp':\n        number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)\n        if number_token is None or number_token.type != 'number':\n            return\n        return tokenize(number_token, math.exp)\n\n    elif function.name == 'abs':\n        number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)\n        if number_token is None:\n            return\n        return tokenize(number_token, abs)\n\n    elif function.name == 'sign':\n        number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)\n        if number_token is None:\n            return\n        return tokenize(\n            number_token.value, lambda x: 0 if x == 0 else 1 if x > 0 else -1)\n\n    arguments = []\n    for i, argument in enumerate(token.arguments):\n        if argument.type == 'function':\n            result = resolve_math(argument, computed, property_name, refer_to)\n            if result is None:\n                return\n            arguments.append(result)\n        else:\n            arguments.append(argument)\n    token = tinycss2.ast.FunctionBlock(\n        token.source_line, token.source_column, token.name, arguments)\n    return resolve_math(token, computed, property_name, refer_to) or token\n\n\nclass InitialStyle(dict):\n    \"\"\"Dummy computed style used to store initial values.\"\"\"\n    def __init__(self, font_config):\n        self.parent_style = None\n        self.specified = self\n        self.cache = {}\n        self.font_config = font_config\n\n    def __missing__(self, key):\n        value = self[key] = INITIAL_VALUES[key]\n        return value\n\n\nclass AnonymousStyle(dict):\n    \"\"\"Computed style used for anonymous boxes.\"\"\"\n    def __init__(self, parent_style):\n        # border-*-style is none, so border-width computes to zero.\n        # Other than that, properties that would need computing are\n        # border-*-color, but they do not apply.\n        self.update({\n            'border_top_width': 0,\n            'border_bottom_width': 0,\n            'border_left_width': 0,\n            'border_right_width': 0,\n            'outline_width': 0,\n        })\n        self.parent_style = parent_style\n        self.is_root_element = False\n        self.specified = self\n        self.cache = parent_style.cache\n        self.font_config = parent_style.font_config\n\n    def copy(self):\n        copy = AnonymousStyle(self.parent_style)\n        copy.update(self)\n        return copy\n\n    def __missing__(self, key):\n        if key in INHERITED or key[:2] == '__':\n            value = self[key] = self.parent_style[key]\n        elif key == 'page':\n            # page is not inherited but taken from the ancestor if 'auto'\n            value = self[key] = self.parent_style[key]\n        elif key[:16] == 'text_decoration_':\n            value = self[key] = text_decoration(\n                key, INITIAL_VALUES[key], self.parent_style[key], cascaded=False)\n        else:\n            value = INITIAL_VALUES[key]\n            if key in INITIAL_NOT_COMPUTED:\n                # Value not computed yet: compute.\n                value = self[key] = COMPUTER_FUNCTIONS[key](self, key, value)\n            else:\n                # The value is the same as when computed.\n                self[key] = value\n        return value\n\n\nclass ComputedStyle(dict):\n    \"\"\"Computed style used for non-anonymous boxes.\"\"\"\n    def __init__(self, parent_style, cascaded, element, pseudo_type,\n                 root_style, base_url, font_config, initial_page_sizes):\n        self.specified = {}\n        self.parent_style = parent_style\n        self.cascaded = cascaded\n        self.is_root_element = parent_style is None\n        self.element = element\n        self.pseudo_type = pseudo_type\n        self.root_style = root_style\n        self.base_url = base_url\n        self.font_config = font_config\n        self.initial_page_sizes = initial_page_sizes\n        self.cache = parent_style.cache if parent_style else {}\n\n    def copy(self):\n        copy = ComputedStyle(\n            self.parent_style, self.cascaded, self.element, self.pseudo_type,\n            self.root_style, self.base_url, self.font_config, self.initial_page_sizes)\n        copy.update(self)\n        copy.specified = self.specified.copy()\n        return copy\n\n    def __missing__(self, key):\n        if key == 'float':\n            # Set specified value for position, needed for computed value.\n            self['position']\n        elif key == 'display':\n            # Set specified value for float, needed for computed value.\n            self['float']\n\n        parent_style = self.parent_style\n\n        if key in self.cascaded:\n            # Property defined in cascaded properties.\n            value, weight = self.cascaded[key]\n            pending = isinstance(value, Pending)\n        else:\n            # Property not defined in cascaded properties, define as inherited\n            # or initial value.\n            if key in INHERITED or key[:2] == '__':\n                value = 'inherit'\n            else:\n                value = 'initial'\n            weight = (0, 0, (0, 0, 0))\n            pending = False\n\n        if logical_function := PHYSICAL_FUNCTIONS.get(key):\n            # TODO: use writing-mode and text-orientation.\n            logical_key = logical_function(block='ttb', inline=self['direction'])\n            if logical_key in self.cascaded:\n                logical_value, logical_weight = self.cascaded[logical_key]\n                if logical_weight >= weight:\n                    value = logical_value\n                    pending = isinstance(value, Pending)\n\n        if value == 'inherit' and parent_style is None:\n            # On the root element, 'inherit' from initial values\n            value = 'initial'\n\n        if pending:\n            # Property with pending values, validate them.\n            solved_tokens = []\n            for token in value.tokens:\n                tokens = resolve_var(self, token, parent_style)\n                if tokens is None:\n                    solved_tokens.append(token)\n                else:\n                    solved_tokens.extend(tokens)\n            original_key = key.replace('_', '-')\n            try:\n                value = value.solve(solved_tokens, original_key)\n            except InvalidValues:\n                if key in INHERITED and parent_style is not None:\n                    # Values in parent_style are already computed.\n                    self[key] = value = parent_style[key]\n                else:\n                    value = INITIAL_VALUES[key]\n                    if key not in INITIAL_NOT_COMPUTED:\n                        # The value is the same as when computed.\n                        self[key] = value\n\n        if value == 'initial':\n            value = [] if key[:2] == '__' else INITIAL_VALUES[key]\n            if key not in INITIAL_NOT_COMPUTED:\n                # The value is the same as when computed.\n                self[key] = value\n        elif value == 'inherit':\n            # Values in parent_style are already computed.\n            self[key] = value = parent_style[key]\n\n        if key[:16] == 'text_decoration_' and parent_style is not None:\n            # Text decorations are not inherited but propagated. See\n            # https://www.w3.org/TR/css-text-decor-3/#line-decoration.\n            if key in COMPUTER_FUNCTIONS:\n                value = COMPUTER_FUNCTIONS[key](self, key, value)\n            self[key] = text_decoration(\n                key, value, parent_style[key], key in self.cascaded)\n        elif key == 'page' and value == 'auto':\n            # The page property does not inherit. However, if the page value on\n            # an element is auto, then its used value is the value specified on\n            # its nearest ancestor with a non-auto value. When specified on the\n            # root element, the used value for auto is the empty string. See\n            # https://www.w3.org/TR/css-page-3/#using-named-pages.\n            value = '' if parent_style is None else parent_style['page']\n            if key in self:\n                del self[key]\n        elif key in ('position', 'float', 'display'):\n            # Save specified values to define computed values for these\n            # specific properties. See\n            # https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo.\n            self.specified[key] = value\n\n        if check_math(value):\n            function = value\n            solved_tokens = []\n            try:\n                try:\n                    token = resolve_math(function, self, key)\n                except PercentageInMath:\n                    solved_tokens.append(function)\n                else:\n                    if token is None:\n                        raise Exception\n                    solved_tokens.append(token)\n                original_key = key.replace('_', '-')\n                value = validate_non_shorthand(solved_tokens, original_key)[0][1]\n            except Exception:\n                LOGGER.warning(\n                    'Invalid math function at %d:%d: %s',\n                    function.source_line, function.source_column, function.serialize())\n                if key in INHERITED and parent_style is not None:\n                    # Values in parent_style are already computed.\n                    self[key] = value = parent_style[key]\n                else:\n                    value = INITIAL_VALUES[key]\n                    if key not in INITIAL_NOT_COMPUTED:\n                        # The value is the same as when computed.\n                        self[key] = value\n\n        if key in self:\n            # Value already computed and saved: return.\n            return self[key]\n\n        if key in COMPUTER_FUNCTIONS:\n            # Value not computed yet: compute.\n            value = COMPUTER_FUNCTIONS[key](self, key, value)\n\n        self[key] = value\n        return value\n\n\nclass ColorProfile:\n    def __init__(self, file_object, descriptors):\n        self.src = descriptors['src'][1]\n        self.renderingintent = descriptors['rendering-intent']\n        self.components = descriptors['components']\n        self._profile = ImageCmsProfile(file_object)\n\n    @property\n    def name(self):\n        return (\n            self._profile.profile.model or\n            self._profile.profile.profile_description)\n\n    @property\n    def content(self):\n        return self._profile.tobytes()\n\n\ndef _add_layer(layer, layers):\n    \"\"\"Add layer to list of layers, handling order.\"\"\"\n    index = None\n    parts = layer.split('.')\n    full_layer = ''\n    for part in parts:\n        if full_layer:\n            full_layer += '.'\n        full_layer += part\n        if full_layer in layers:\n            index = layers.index(full_layer)\n            continue\n        if index is None:\n            layers.append(full_layer)\n            index = len(layers) - 1\n        else:\n            layers.insert(index, full_layer)\n            index -= 1\n\n\ndef computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None,\n                           root_style=None, base_url=None,\n                           target_collector=None):\n    \"\"\"Get a dict of computed style mixed from parent and cascaded styles.\"\"\"\n    if not cascaded and parent_style is not None:\n        return AnonymousStyle(parent_style)\n\n\ndef _parse_layer(tokens):\n    \"\"\"Parse tokens representing a layer name.\"\"\"\n    if not tokens:\n        return\n    new_layer = ''\n    last_dot = True\n    for token in tokens:\n        if token.type == 'ident' and last_dot:\n            new_layer += token.value\n            last_dot = False\n        elif token.type == 'literal' and token.value == '.' and not last_dot:\n            new_layer += '.'\n            last_dot = True\n        else:\n            return\n    if not last_dot:\n        return new_layer\n\n\ndef parse_color_profile_name(prelude):\n    tokens = list(remove_whitespace(prelude))\n\n    if len(tokens) != 1:\n        return\n\n    token = tokens[0]\n    if token.type != 'ident':\n        return\n\n    if token.value.startswith('--') or token.value == 'device-cmyk':\n        return token.value\n\n\ndef parse_page_selectors(rule):\n    \"\"\"Parse a page selector rule.\n\n    Return a list of page data if the rule is correctly parsed. Page data are a\n    dict containing:\n\n    - 'side' ('left', 'right' or None),\n    - 'blank' (True or None),\n    - 'first' (True or None),\n    - 'index' (page number or None),\n    - 'name' (page name string or None), and\n    - 'specificity' (list of numbers).\n\n    Return ``None` if something went wrong while parsing the rule.\n\n    \"\"\"\n    # See https://drafts.csswg.org/css-page-3/#syntax-page-selector\n\n    tokens = list(remove_whitespace(rule.prelude))\n    page_data = []\n\n    # TODO: Specificity is probably wrong, should clean and test that.\n    if not tokens:\n        page_data.append({\n            'side': None, 'blank': None, 'first': None, 'index': None,\n            'name': None, 'specificity': [0, 0, 0]})\n        return page_data\n\n    while tokens:\n        types = {\n            'side': None, 'blank': None, 'first': None, 'index': None,\n            'name': None, 'specificity': [0, 0, 0]}\n\n        if tokens[0].type == 'ident':\n            token = tokens.pop(0)\n            types['name'] = token.value\n            types['specificity'][0] = 1\n\n        if len(tokens) == 1:\n            return None\n        elif not tokens:\n            page_data.append(types)\n            return page_data\n\n        while tokens:\n            literal = tokens.pop(0)\n            if literal.type != 'literal':\n                return None\n\n            if literal.value == ':':\n                if not tokens:\n                    return None\n\n                if tokens[0].type == 'ident':\n                    ident = tokens.pop(0)\n                    pseudo_class = ident.lower_value\n\n                    if pseudo_class in ('left', 'right'):\n                        if types['side'] and types['side'] != pseudo_class:\n                            return None\n                        types['side'] = pseudo_class\n                        types['specificity'][2] += 1\n                        continue\n\n                    elif pseudo_class in ('blank', 'first'):\n                        types[pseudo_class] = True\n                        types['specificity'][1] += 1\n                        continue\n\n                elif tokens[0].type == 'function':\n                    function = tokens.pop(0)\n                    if function.name != 'nth':\n                        return None\n                    for i, argument in enumerate(function.arguments):\n                        if argument.type == 'ident' and argument.value == 'of':\n                            nth = function.arguments[:i - 1]\n                            group = function.arguments[i + 1:]\n                            break\n                    else:\n                        nth = function.arguments\n                        group = None\n\n                    nth_values = tinycss2.nth.parse_nth(nth)\n                    if nth_values is None:\n                        return None\n\n                    if group is not None:\n                        group = [\n                            token for token in group\n                            if token.type not in ('comment', 'whitespace')]\n                        if len(group) != 1:\n                            return None\n                        group, = group\n                        if group.type != 'ident':\n                            return None\n                        group = group.value\n\n                    types['index'] = (*nth_values, group)\n                    # TODO: specificity is not specified yet\n                    # https://github.com/w3c/csswg-drafts/issues/3524\n                    types['specificity'][1] += 1\n                    if group:\n                        types['specificity'][0] += 1\n                    continue\n\n                return None\n            elif literal.value == ',':\n                if tokens and any(types['specificity']):\n                    break\n                else:\n                    return None\n\n        page_data.append(types)\n\n    return page_data\n\n\ndef preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fetcher,\n                          matcher, page_rules, layers, font_config, counter_style,\n                          color_profiles, ignore_imports=False, layer=None):\n    \"\"\"Do what can be done early on stylesheet, before being in a document.\"\"\"\n    for rule in stylesheet_rules:\n        if getattr(rule, 'content', None) is None:\n            if rule.type == 'error':\n                LOGGER.warning(\n                    'Parse error at %d:%d: %s',\n                    rule.source_line, rule.source_column, rule.message)\n            if rule.type != 'at-rule':\n                continue\n            if rule.lower_at_keyword not in ('import', 'layer'):\n                LOGGER.warning(\n                    'Unknown empty rule %s at %d:%d',\n                    rule, rule.source_line, rule.source_column)\n                continue\n\n        if rule.type == 'qualified-rule':\n            try:\n                logger_level = WARNING\n                contents = tinycss2.parse_blocks_contents(rule.content)\n                selectors_declarations = list(\n                    preprocess_declarations(base_url, contents, rule.prelude))\n\n                if selectors_declarations:\n                    selectors_declarations = groupby(\n                        selectors_declarations, key=lambda x: x[0])\n                    for selectors, declarations in selectors_declarations:\n                        declarations = [\n                            declaration[1] for declaration in declarations]\n                        for selector in selectors:\n                            matcher.add_selector(selector, (declarations, layer))\n                            if selector.pseudo_element not in PSEUDO_ELEMENTS:\n                                prelude = tinycss2.serialize(rule.prelude)\n                                if selector.pseudo_element.startswith('-'):\n                                    logger_level = DEBUG\n                                    raise cssselect2.SelectorError(\n                                        f\"'{prelude}', \"\n                                        'ignored prefixed pseudo-element: '\n                                        f'{selector.pseudo_element}')\n                                else:\n                                    raise cssselect2.SelectorError(\n                                        f\"'{prelude}', \"\n                                        'unknown pseudo-element: '\n                                        f'{selector.pseudo_element}')\n                        ignore_imports = True\n                else:\n                    ignore_imports = True\n            except cssselect2.SelectorError as exc:\n                LOGGER.log(logger_level, 'Invalid or unsupported selector, %s', exc)\n                continue\n\n        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'import':\n            if ignore_imports:\n                LOGGER.warning(\n                    '@import rule %r not at the beginning of the '\n                    'the whole rule was ignored at %d:%d.',\n                    tinycss2.serialize(rule.prelude),\n                    rule.source_line, rule.source_column)\n                continue\n\n            tokens = remove_whitespace(rule.prelude)\n            url = None\n            if tokens:\n                if tokens[0].type == 'string':\n                    url = url_join(\n                        base_url, tokens[0].value, allow_relative=False,\n                        context='@import at %s:%s',\n                        context_args=(rule.source_line, rule.source_column))\n                else:\n                    url_tuple = get_url(tokens[0], base_url)\n                    if url_tuple and url_tuple[1][0] == 'external':\n                        url = url_tuple[1][1]\n            if url is None:\n                continue\n\n            new_layer = None\n            next_tokens = list(tokens[1:])\n            if next_tokens:\n                if next_tokens[0].type == 'function' and next_tokens[0].name == 'layer':\n                    function = next_tokens.pop(0)\n                    if not (new_layer := _parse_layer(function.arguments)):\n                        LOGGER.warning(\n                            'Invalid layer name %r '\n                            'the whole @import rule was ignored at %d:%d.',\n                            tinycss2.serialize(function),\n                            rule.source_line, rule.source_column)\n                        continue\n                elif next_tokens[0].type == 'ident' and next_tokens[0].value == 'layer':\n                    next_tokens.pop(0)\n                    new_layer = f'@anonymous{len(layers)}'\n                if new_layer:\n                    if layer is not None:\n                        new_layer = f'{layer}.{new_layer}'\n                    _add_layer(new_layer, layers)\n\n            media = media_queries.parse_media_query(next_tokens)\n            if media is None:\n                LOGGER.warning(\n                    'Invalid media type %r '\n                    'the whole @import rule was ignored at %d:%d.',\n                    tinycss2.serialize(rule.prelude),\n                    rule.source_line, rule.source_column)\n                continue\n            if not media_queries.evaluate_media_query(media, device_media_type):\n                continue\n            if url is not None:\n                try:\n                    CSS(\n                        url=url, url_fetcher=url_fetcher, media_type=device_media_type,\n                        font_config=font_config, counter_style=counter_style,\n                        color_profiles=color_profiles, matcher=matcher,\n                        page_rules=page_rules, layers=layers, layer=new_layer)\n                except URLFetchingError as exception:\n                    LOGGER.error('Failed to load stylesheet at %s : %s', url, exception)\n                    LOGGER.debug('Error while loading stylesheet:', exc_info=exception)\n\n        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'media':\n            media = media_queries.parse_media_query(rule.prelude)\n            if media is None:\n                LOGGER.warning(\n                    'Invalid media type %r '\n                    'the whole @media rule was ignored at %d:%d.',\n                    tinycss2.serialize(rule.prelude),\n                    rule.source_line, rule.source_column)\n                continue\n            if not media_queries.evaluate_media_query(media, device_media_type):\n                continue\n            content_rules = tinycss2.parse_rule_list(rule.content)\n            preprocess_stylesheet(\n                device_media_type, base_url, content_rules, url_fetcher, matcher,\n                page_rules, layers, font_config, counter_style, color_profiles,\n                ignore_imports=True)\n\n        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'page':\n            data = parse_page_selectors(rule)\n\n            if data is None:\n                LOGGER.warning(\n                    'Unsupported @page selector %r, '\n                    'the whole @page rule was ignored at %d:%d.',\n                    tinycss2.serialize(rule.prelude),\n                    rule.source_line, rule.source_column)\n                continue\n\n            ignore_imports = True\n            for page_data in data:\n                specificity = page_data.pop('specificity')\n                page_selector_type = PageSelectorType(**page_data)\n                content = tinycss2.parse_blocks_contents(rule.content)\n                declarations = list(preprocess_declarations(base_url, content))\n\n                if declarations:\n                    selector_list = [(specificity, None, page_selector_type)]\n                    page_rules.append((rule, selector_list, declarations))\n\n                for margin_rule in content:\n                    if margin_rule.type != 'at-rule' or margin_rule.content is None:\n                        continue\n                    declarations = list(preprocess_declarations(\n                        base_url,\n                        tinycss2.parse_blocks_contents(margin_rule.content)))\n                    if declarations:\n                        selector_list = [(\n                            specificity, f'@{margin_rule.lower_at_keyword}',\n                            page_selector_type)]\n                        page_rules.append((margin_rule, selector_list, declarations))\n\n        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'font-face':\n            ignore_imports = True\n            content = tinycss2.parse_blocks_contents(rule.content)\n            rule_descriptors = dict(\n                preprocess_descriptors('font-face', base_url, content))\n            for key in ('src', 'font_family'):\n                if key not in rule_descriptors:\n                    LOGGER.warning(\n                        \"Missing %s descriptor in '@font-face' rule at %d:%d\",\n                        key.replace('_', '-'), rule.source_line, rule.source_column)\n                    break\n            else:\n                if font_config is not None:\n                    font_config.add_font_face(rule_descriptors, url_fetcher)\n\n        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'color-profile':\n            ignore_imports = True\n\n            if (name := parse_color_profile_name(rule.prelude)) is None:\n                LOGGER.warning(\n                    'Invalid color profile name %r, the whole '\n                    '@color-profile rule was ignored at %d:%d.',\n                    tinycss2.serialize(rule.prelude), rule.source_line,\n                    rule.source_column)\n                continue\n\n            content = tinycss2.parse_blocks_contents(rule.content)\n            rule_descriptors = preprocess_descriptors(\n                'color-profile', base_url, content)\n\n            descriptors = {\n                'src': None,\n                'rendering-intent': 'relative-colorimetric',\n                'components': None,\n            }\n            for descriptor_name, descriptor_value in rule_descriptors:\n                if descriptor_name in descriptors:\n                    descriptors[descriptor_name] = descriptor_value\n                else:\n                    LOGGER.warning(\n                        'Unknown descriptor %r for profile named %r at %d:%d.',\n                        descriptor_name, tinycss2.serialize(rule.prelude),\n                        rule.source_line, rule.source_column)\n\n            if descriptors['src'] is None:\n                LOGGER.warning(\n                    'No source for profile named %r, the whole '\n                    '@color-profile rule was ignored at %d:%d.',\n                    tinycss2.serialize(rule.prelude), rule.source_line,\n                    rule.source_column)\n                continue\n\n            with fetch(url_fetcher, descriptors['src'][1]) as response:\n                try:\n                    color_profile = ColorProfile(response, descriptors)\n                except BaseException:\n                    LOGGER.warning(\n                        'Invalid profile file for profile named %r, the whole '\n                        '@color-profile rule was ignored at %d:%d.',\n                        tinycss2.serialize(rule.prelude), rule.source_line,\n                        rule.source_column)\n                    continue\n                else:\n                    color_profiles[name] = color_profile\n\n        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'counter-style':\n            name = counters.parse_counter_style_name(rule.prelude, counter_style)\n            if name is None:\n                LOGGER.warning(\n                    'Invalid counter style name %r, the whole '\n                    '@counter-style rule was ignored at %d:%d.',\n                    tinycss2.serialize(rule.prelude), rule.source_line,\n                    rule.source_column)\n                continue\n\n            ignore_imports = True\n            content = tinycss2.parse_blocks_contents(rule.content)\n            counter = {\n                'system': None,\n                'negative': None,\n                'prefix': None,\n                'suffix': None,\n                'range': None,\n                'pad': None,\n                'fallback': None,\n                'symbols': None,\n                'additive_symbols': None,\n            }\n            rule_descriptors = preprocess_descriptors(\n                'counter-style', base_url, content)\n\n            for descriptor_name, descriptor_value in rule_descriptors:\n                counter[descriptor_name] = descriptor_value\n\n            if counter['system'] is None:\n                system = (None, 'symbolic', None)\n            else:\n                system = counter['system']\n\n            if system[0] is None:\n                if system[1] in ('cyclic', 'fixed', 'symbolic'):\n                    if len(counter['symbols'] or []) < 1:\n                        LOGGER.warning(\n                            'In counter style %r at %d:%d, '\n                            'counter style %r needs at least one symbol',\n                            name, rule.source_line, rule.source_column, system[1])\n                        continue\n                elif system[1] in ('alphabetic', 'numeric'):\n                    if len(counter['symbols'] or []) < 2:\n                        LOGGER.warning(\n                            'In counter style %r at %d:%d, '\n                            'counter style %r needs at least two symbols',\n                            name, rule.source_line, rule.source_column, system[1])\n                        continue\n                elif system[1] == 'additive':\n                    if len(counter['additive_symbols'] or []) < 2:\n                        LOGGER.warning(\n                            'In counter style %r at %d:%d, '\n                            'counter style \"additive\" '\n                            'needs at least two additive symbols',\n                            name, rule.source_line, rule.source_column)\n                        continue\n\n            counter_style[name] = counter\n\n        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'layer':\n            new_layers = []\n            prelude = remove_whitespace(rule.prelude)\n            comma_separated_tokens = split_on_comma(prelude) if prelude else ()\n            for tokens in comma_separated_tokens:\n                if new_layer := _parse_layer(tokens):\n                    if layer is not None:\n                        new_layer = f'{layer}.{new_layer}'\n                    new_layers.append(new_layer)\n                else:\n                    new_layers = None\n                    break\n            if new_layers is None:\n                LOGGER.warning(\n                    'Unsupported @layer selector %r, '\n                    'the whole @layer rule was ignored at %d:%d.',\n                    tinycss2.serialize(rule.prelude),\n                    rule.source_line, rule.source_column)\n                continue\n            elif len(new_layers) > 1:\n                if rule.content:\n                    LOGGER.warning(\n                        '@layer rule with multiple layer names, '\n                        'the whole @layer rule was ignored at %d:%d.',\n                        rule.source_line, rule.source_column)\n                    continue\n                for new_layer in new_layers:\n                    _add_layer(new_layer, layers)\n                continue\n\n            if new_layers:\n                new_layer, = new_layers\n            else:\n                new_layer = f'@anonymous{len(layers)}'\n                if layer is not None:\n                    new_layer = f'{layer}.{new_layer}'\n            _add_layer(new_layer, layers)\n\n            if rule.content is None:\n                continue\n            content_rules = tinycss2.parse_rule_list(rule.content)\n            preprocess_stylesheet(\n                device_media_type, base_url, content_rules, url_fetcher, matcher,\n                page_rules, layers, font_config, counter_style, color_profiles,\n                ignore_imports=True, layer=new_layer)\n\n        else:\n            LOGGER.warning(\n                'Unknown rule %s at %d:%d',\n                rule, rule.source_line, rule.source_column)\n\n\ndef get_all_computed_styles(html, user_stylesheets=None, presentational_hints=False,\n                            font_config=None, counter_style=None, color_profiles=None,\n                            page_rules=None, layers=None, target_collector=None,\n                            forms=False):\n    \"\"\"Compute all the computed styles of all elements in ``html`` document.\n\n    Do everything from finding author stylesheets to parsing and applying them.\n\n    Return a ``style_for`` function that takes an element and an optional\n    pseudo-element type, and return a style dict object.\n\n    \"\"\"\n    # List stylesheets. Order here is not important ('origin' is).\n    sheets = []\n    if counter_style is None:\n        counter_style = counters.CounterStyle()\n    if font_config is None:\n        font_config = FontConfiguration()\n    for style in html._ua_counter_style():\n        for key, value in style.items():\n            counter_style[key] = value\n    for sheet in (html._ua_stylesheets(forms) or []):\n        sheets.append((sheet, 'user agent', None))\n    if presentational_hints:\n        for sheet in (html._ph_stylesheets() or []):\n            sheets.append((sheet, 'author', (0, 0, 0)))\n    for sheet in find_stylesheets(\n            html.wrapper_element, html.media_type, html.url_fetcher,\n            html.base_url, font_config, counter_style, color_profiles, page_rules,\n            layers):\n        sheets.append((sheet, 'author', None))\n    for sheet in (user_stylesheets or []):\n        sheets.append((sheet, 'user', None))\n\n    return StyleFor(html, sheets, presentational_hints, font_config, target_collector)\n"
  },
  {
    "path": "weasyprint/css/computed_values.py",
    "content": "\"\"\"Convert specified property values into computed values.\"\"\"\n\nfrom functools import partial\nfrom math import pi\n\nfrom tinycss2.color5 import parse_color\n\nfrom ..logger import LOGGER\nfrom ..text.line_break import strut\nfrom ..urls import get_link_attribute, get_url_tuple\nfrom .functions import check_math\nfrom .properties import INITIAL_VALUES, ZERO_PIXELS, Dimension\nfrom .units import ANGLE_TO_RADIANS, LENGTH_UNITS, to_pixels\nfrom .validation import validate_non_shorthand\n\n# Value in pixels of font-size for <absolute-size> keywords: 12pt (16px) for\n# medium, and scaling factors given in CSS3 for others:\n# https://www.w3.org/TR/css-fonts-3/#font-size-prop\nFONT_SIZE_KEYWORDS = {\n    # medium is 16px, others are a ratio of medium\n    name: INITIAL_VALUES['font_size'] * factor\n    for name, factor in (\n        ('xx-small', 3 / 5),\n        ('x-small', 3 / 4),\n        ('small', 8 / 9),\n        ('medium', 1),\n        ('large', 6 / 5),\n        ('x-large', 3 / 2),\n        ('xx-large', 2),\n    )\n}\n\n# These are unspecified, other than 'thin' <= 'medium' <= 'thick'.\n# Values are in pixels.\nBORDER_WIDTH_KEYWORDS = {\n    'thin': 1,\n    'medium': 3,\n    'thick': 5,\n}\nassert INITIAL_VALUES['border_top_width'] == BORDER_WIDTH_KEYWORDS['medium']\n\n# https://www.w3.org/TR/CSS21/fonts.html#propdef-font-weight\nFONT_WEIGHT_RELATIVE = {\n    'bolder': {\n        100: 400,\n        200: 400,\n        300: 400,\n        400: 700,\n        500: 700,\n        600: 900,\n        700: 900,\n        800: 900,\n        900: 900,\n    },\n    'lighter': {\n        100: 100,\n        200: 100,\n        300: 100,\n        400: 100,\n        500: 100,\n        600: 400,\n        700: 400,\n        800: 700,\n        900: 700,\n    },\n}\n\n# https://www.w3.org/TR/css-page-3/#size\nPAGE_SIZES = {\n    page_size: (Dimension(width, unit), Dimension(height, unit))\n    for page_size, width, height, unit in (\n        ('a10', 26, 37, 'mm'),\n        ('a9', 37, 52, 'mm'),\n        ('a8', 52, 74, 'mm'),\n        ('a7', 74, 105, 'mm'),\n        ('a6', 105, 148, 'mm'),\n        ('a5', 148, 210, 'mm'),\n        ('a4', 210, 297, 'mm'),\n        ('a3', 297, 420, 'mm'),\n        ('a2', 420, 594, 'mm'),\n        ('a1', 594, 841, 'mm'),\n        ('a0', 841, 1189, 'mm'),\n        ('b10', 31, 44, 'mm'),\n        ('b9', 44, 62, 'mm'),\n        ('b8', 62, 88, 'mm'),\n        ('b7', 88, 125, 'mm'),\n        ('b6', 125, 176, 'mm'),\n        ('b5', 176, 250, 'mm'),\n        ('b4', 250, 353, 'mm'),\n        ('b3', 353, 500, 'mm'),\n        ('b2', 500, 707, 'mm'),\n        ('b1', 707, 1000, 'mm'),\n        ('b0', 1000, 1414, 'mm'),\n        ('c10', 28, 40, 'mm'),\n        ('c9', 40, 57, 'mm'),\n        ('c8', 57, 81, 'mm'),\n        ('c7', 81, 114, 'mm'),\n        ('c6', 114, 162, 'mm'),\n        ('c5', 162, 229, 'mm'),\n        ('c4', 229, 324, 'mm'),\n        ('c3', 324, 458, 'mm'),\n        ('c2', 458, 648, 'mm'),\n        ('c1', 648, 917, 'mm'),\n        ('c0', 917, 1297, 'mm'),\n        ('jis-b10', 32, 45, 'mm'),\n        ('jis-b9', 45, 64, 'mm'),\n        ('jis-b8', 64, 91, 'mm'),\n        ('jis-b7', 91, 128, 'mm'),\n        ('jis-b6', 128, 182, 'mm'),\n        ('jis-b5', 182, 257, 'mm'),\n        ('jis-b4', 257, 364, 'mm'),\n        ('jis-b3', 364, 515, 'mm'),\n        ('jis-b2', 515, 728, 'mm'),\n        ('jis-b1', 728, 1030, 'mm'),\n        ('jis-b0', 1030, 1456, 'mm'),\n        ('letter', 8.5, 11, 'in'),\n        ('legal', 8.5, 14, 'in'),\n        ('ledger', 11, 17, 'in'),\n    )\n}\n# In \"portrait\" orientation.\nassert all(width.value < height.value for width, height in PAGE_SIZES.values())\n\nINITIAL_PAGE_SIZE = PAGE_SIZES['a4']\nINITIAL_VALUES['size'] = tuple(\n    to_pixels(size, None, 'size') for size in INITIAL_PAGE_SIZE)\n\n\n# Maps physical to functions getting block and inline directions.\nPHYSICAL_FUNCTIONS = {}\n\n\ndef register_logical(names, prefixes=('',), suffixes=('',)):\n    \"\"\"Decorator registering logical properties matching physical ``names``.\"\"\"\n\n    def decorator(function):\n        \"\"\"Register the properties ``names`` for ``function``.\"\"\"\n        for name in names:\n            name = name.replace('-', '_')\n            for prefix in prefixes:\n                for suffix in suffixes:\n                    property_name = name\n                    if prefix:\n                        property_name = f'{prefix}_{property_name}'\n                    if suffix:\n                        property_name = f'{property_name}_{suffix}'\n                    PHYSICAL_FUNCTIONS[property_name] = partial(\n                        function, name=name, prefix=prefix, suffix=suffix)\n        return function\n    return decorator\n\n\n@register_logical(('width', 'height'), prefixes=('', 'max', 'min'))\ndef physical_size(name, prefix, suffix, block, inline):\n    vertical_main_direction = block in ('ttb', 'btt')\n    vertical_property = 'height' in name\n    logical = 'block' if (vertical_property == vertical_main_direction) else 'inline'\n    return f'{prefix}_{logical}_size' if prefix else f'{logical}_size'\n\n\n@register_logical(\n    ('top', 'left', 'bottom', 'right'),\n    prefixes=('', 'padding', 'margin'))\n@register_logical(\n    ('top', 'left', 'bottom', 'right'),\n    prefixes=('border',), suffixes=('width', 'style', 'color'))\ndef physical_inset(name, prefix, suffix, block, inline):\n    if name == 'top':\n        logical = 'block' if block in ('ttb', 'btt') else 'inline'\n        side = 'start' if 'ttb' in (block, inline) else 'end'\n    elif name == 'bottom':\n        logical = 'block' if block in ('ttb', 'btt') else 'inline'\n        side = 'start' if 'btt' in (block, inline) else 'end'\n    elif name == 'left':\n        logical = 'block' if block in ('ltr', 'rtl') else 'inline'\n        side = 'start' if 'ltr' in (block, inline) else 'end'\n    elif name == 'right':\n        logical = 'block' if block in ('ltr', 'rtl') else 'inline'\n        side = 'start' if 'rtl' in (block, inline) else 'end'\n    prefix = f'{prefix or \"inset\"}_'\n    if suffix:\n        suffix = f'_{suffix}'\n    return f'{prefix}{logical}_{side}{suffix}'\n\n\n@register_logical(\n    ('top_left', 'top_right', 'bottom_left', 'bottom_right'),\n    prefixes=('border',), suffixes=('radius',))\ndef physical_radius(name, prefix, suffix, block, inline):\n    vertical, horizontal = name.split('_')\n    if block == 'ttb':\n        block = 'start' if vertical == 'top' else 'end'\n    elif block == 'btt':\n        block = 'start' if vertical == 'bottom' else 'end'\n    elif block == 'ltr':\n        block = 'start' if horizontal == 'left' else 'end'\n    elif block == 'rtl':\n        block = 'start' if horizontal == 'right' else 'end'\n    if inline == 'ttb':\n        inline = 'start' if vertical == 'top' else 'end'\n    elif inline == 'btt':\n        inline = 'start' if vertical == 'bottom' else 'end'\n    elif inline == 'ltr':\n        inline = 'start' if horizontal == 'left' else 'end'\n    elif inline == 'rtl':\n        inline = 'start' if horizontal == 'right' else 'end'\n    return f'{prefix}_{block}_{inline}_{suffix}'\n\n\n# Maps property names to functions returning the computed values\nCOMPUTER_FUNCTIONS = {}\n\n\ndef register_computer(name):\n    \"\"\"Decorator registering a property ``name`` for a function.\"\"\"\n    name = name.replace('-', '_')\n\n    def decorator(function):\n        \"\"\"Register the property ``name`` for ``function``.\"\"\"\n        COMPUTER_FUNCTIONS[name] = function\n        return function\n    return decorator\n\n\ndef compute_attr(style, values):\n    # TODO: use real token parsing instead of casting with Python types, and follow new\n    # syntax. See https://drafts.csswg.org/css-values-5/#attr-notation.\n    func_name, value = values\n    assert func_name == 'attr()'\n    attr_name, type_or_unit, fallback = value\n    try:\n        attr_value = style.element.get(attr_name, fallback)\n        if type_or_unit == 'string':\n            pass  # Keep the string\n        elif type_or_unit == 'url':\n            attr_value = get_url_tuple(attr_value, style.base_url)\n        elif type_or_unit == 'color':\n            attr_value = parse_color(attr_value.strip(), style['color_scheme'])\n        elif type_or_unit == 'integer':\n            attr_value = int(attr_value.strip())\n        elif type_or_unit == 'number':\n            attr_value = float(attr_value.strip())\n        elif type_or_unit == '%':\n            attr_value = Dimension(float(attr_value.strip()), '%')\n            type_or_unit = 'length'\n        elif type_or_unit in LENGTH_UNITS:\n            attr_value = Dimension(float(attr_value.strip()), type_or_unit)\n            type_or_unit = 'length'\n        elif type_or_unit in ANGLE_TO_RADIANS:\n            attr_value = Dimension(float(attr_value.strip()), type_or_unit)\n            type_or_unit = 'angle'\n        else:\n            return\n    except Exception:\n        return\n    return (type_or_unit, attr_value)\n\n\n@register_computer('background-image')\ndef background_image(style, name, values):\n    \"\"\"Compute lenghts in gradient background-image.\"\"\"\n    return tuple(image(style, name, value) for value in values)\n\n\n@register_computer('border-image-source')\ndef image(style, name, image):\n    \"\"\"Compute lenghts in gradient border-image-source.\"\"\"\n    type_, value = image\n    if type_ in ('linear-gradient', 'radial-gradient'):\n        value.stop_positions = tuple(\n            length(style, name, pos) if pos is not None else None\n            for pos in value.stop_positions)\n        value.color_hints = tuple(\n            length(style, name, hint) if hint is not None else None\n            for hint in value.color_hints)\n    if type_ == 'radial-gradient':\n        value.center, = compute_position(style, name, (value.center,))\n        if value.size_type == 'explicit':\n            value.size = length_or_percentage_tuple(style, name, value.size)\n    return image\n\n\n@register_computer('color')\n@register_computer('background-color')\n@register_computer('border-top-color')\n@register_computer('border-right-color')\n@register_computer('border-bottom-color')\n@register_computer('border-left-color')\n@register_computer('column-rule-color')\n@register_computer('outline-color')\n@register_computer('text-decoration-color')\ndef color(style, name, values):\n    return parse_color(values, style['color_scheme'])\n\n\n@register_computer('background-position')\n@register_computer('object-position')\ndef compute_position(style, name, values):\n    \"\"\"Compute lengths in background-position.\"\"\"\n    return tuple(\n        (origin_x, length(style, name, pos_x),\n         origin_y, length(style, name, pos_y))\n        for origin_x, pos_x, origin_y, pos_y in values)\n\n\n@register_computer('transform-origin')\ndef length_or_percentage_tuple(style, name, values):\n    \"\"\"Compute the lists of lengths that can be percentages.\"\"\"\n    return tuple(length(style, name, value) for value in values)\n\n\n@register_computer('border-spacing')\n@register_computer('size')\n@register_computer('clip')\ndef length_tuple(style, name, values):\n    \"\"\"Compute the properties with a list of lengths.\"\"\"\n    return tuple(length(style, name, value, pixels_only=True) for value in values)\n\n\n@register_computer('break-after')\n@register_computer('break-before')\ndef break_before_after(style, name, value):\n    \"\"\"Compute the ``break-before`` and ``break-after`` properties.\"\"\"\n    return 'page' if value == 'always' else value\n\n\n@register_computer('top')\n@register_computer('right')\n@register_computer('left')\n@register_computer('bottom')\n@register_computer('margin-top')\n@register_computer('margin-right')\n@register_computer('margin-bottom')\n@register_computer('margin-left')\n@register_computer('height')\n@register_computer('width')\n@register_computer('block-size')\n@register_computer('inline-size')\n@register_computer('min-width')\n@register_computer('min-height')\n@register_computer('min-block-size')\n@register_computer('min-inline-size')\n@register_computer('max-width')\n@register_computer('max-height')\n@register_computer('max-block-size')\n@register_computer('max-inline-size')\n@register_computer('padding-top')\n@register_computer('padding-right')\n@register_computer('padding-bottom')\n@register_computer('padding-left')\n@register_computer('text-indent')\n@register_computer('hyphenate-limit-zone')\n@register_computer('flex-basis')\n@register_computer('text-underline-offset')\n@register_computer('text-decoration-thickness')\ndef length(style, name, value, font_size=None, pixels_only=False):\n    \"\"\"Compute a length ``value``.\"\"\"\n    if value in ('auto', 'content', 'from-font') or check_math(value):\n        return value\n    elif value.value == 0:\n        return 0 if pixels_only else ZERO_PIXELS\n    elif value.unit not in LENGTH_UNITS:\n        # A percentage or 'auto': no conversion needed.\n        return value\n\n    pixels = to_pixels(value, style, name, font_size)\n    return pixels if pixels_only else Dimension(pixels, 'px')\n\n\n@register_computer('bleed-left')\n@register_computer('bleed-right')\n@register_computer('bleed-top')\n@register_computer('bleed-bottom')\ndef bleed(style, name, value):\n    if value == 'auto':\n        return Dimension(8 if 'crop' in style['marks'] else 0, 'px')\n    return length(style, name, value)\n\n\n@register_computer('letter-spacing')\ndef pixel_length(style, name, value):\n    if value == 'normal':\n        return value\n    return length(style, name, value, pixels_only=True)\n\n\n@register_computer('background-size')\ndef background_size(style, name, values):\n    \"\"\"Compute the ``background-size`` properties.\"\"\"\n    return tuple(\n        value if value in ('contain', 'cover') else\n        length_or_percentage_tuple(style, name, value)\n        for value in values)\n\n\n@register_computer('image-orientation')\ndef image_orientation(style, name, values):\n    \"\"\"Compute the ``image-orientation`` properties.\"\"\"\n    if values in ('none', 'from-image'):\n        return values\n    angle, flip = values\n    return (round(angle / pi * 2) % 4 * 90, flip)\n\n\n@register_computer('border-top-width')\n@register_computer('border-right-width')\n@register_computer('border-left-width')\n@register_computer('border-bottom-width')\n@register_computer('column-rule-width')\n@register_computer('outline-width')\ndef border_width(style, name, value):\n    \"\"\"Compute the ``border-*-width`` properties.\"\"\"\n    border_style = style[name.replace('width', 'style')]\n    if border_style in ('none', 'hidden'):\n        return 0\n\n    if value in BORDER_WIDTH_KEYWORDS:\n        return BORDER_WIDTH_KEYWORDS[value]\n\n    if isinstance(value, int):\n        # The initial value can get here, but length() would fail as\n        # it does not have a 'unit' attribute.\n        return value\n\n    return length(style, name, value, pixels_only=True)\n\n\n@register_computer('border-image-slice')\n@register_computer('mask-border-slice')\ndef border_image_slice(style, name, values):\n    \"\"\"Compute the ``border-image-slice`` property.\"\"\"\n    computed_values = []\n    fill = None\n    for value in values:\n        if value == 'fill':\n            fill = value\n        else:\n            number, unit = value\n            if unit is None:\n                computed_values.append(number)\n            else:\n                computed_values.append(Dimension(number, '%'))\n    if len(computed_values) == 1:\n        computed_values *= 4\n    elif len(computed_values) == 2:\n        computed_values *= 2\n    elif len(computed_values) == 3:\n        computed_values.append(computed_values[1])\n    return (*computed_values, fill)\n\n\n@register_computer('border-image-width')\n@register_computer('mask-border-width')\ndef border_image_width(style, name, values):\n    \"\"\"Compute the ``border-image-width`` property.\"\"\"\n    computed_values = []\n    for value in values:\n        if value == 'auto':\n            computed_values.append(value)\n        else:\n            number, unit = value\n            computed_values.append(number if unit is None else value)\n    if len(computed_values) == 1:\n        computed_values *= 4\n    elif len(computed_values) == 2:\n        computed_values *= 2\n    elif len(computed_values) == 3:\n        computed_values.append(computed_values[1])\n    return tuple(computed_values)\n\n\n@register_computer('border-image-outset')\n@register_computer('mask-border-outset')\ndef border_image_outset(style, name, values):\n    \"\"\"Compute the ``border-image-outset`` property.\"\"\"\n    computed_values = [\n        value if isinstance(value, (int, float)) else length(style, name, value)\n        for value in values]\n    if len(computed_values) == 1:\n        computed_values *= 4\n    elif len(computed_values) == 2:\n        computed_values *= 2\n    elif len(computed_values) == 3:\n        computed_values.append(computed_values[1])\n    return tuple(computed_values)\n\n\n@register_computer('border-image-repeat')\n@register_computer('mask-border-repeat')\ndef border_image_repeat(style, name, values):\n    \"\"\"Compute the ``border-image-repeat`` property.\"\"\"\n    return (values * 2) if len(values) == 1 else values\n\n\n@register_computer('column-width')\n@register_computer('outline-offset')\ndef length_pixels_only(style, name, value):\n    \"\"\"Compute a pixel length property.\"\"\"\n    return length(style, name, value, pixels_only=True)\n\n\n@register_computer('border-top-left-radius')\n@register_computer('border-top-right-radius')\n@register_computer('border-bottom-left-radius')\n@register_computer('border-bottom-right-radius')\ndef border_radius(style, name, values):\n    \"\"\"Compute the ``border-*-radius`` properties.\"\"\"\n    return tuple(length(style, name, value) for value in values)\n\n\n@register_computer('column-gap')\n@register_computer('row-gap')\ndef gap(style, name, value):\n    \"\"\"Compute the ``*-gap`` properties.\"\"\"\n    return value if value == 'normal' else length(style, name, value)\n\n\ndef _content_list(style, values):\n    computed_values = []\n    for value in values:\n        if value[0] in ('string', 'content', 'url', 'quote', 'leader()'):\n            computed_value = value\n        elif value[0] == 'attr()':\n            assert value[1][1] == 'string'\n            computed_value = compute_attr(style, value)\n        elif value[0] in (\n                'counter()', 'counters()', 'content()', 'element()',\n                'string()'):\n            # Other values need layout context, their computed value cannot be\n            # better than their specified value yet.\n            # See build.compute_content_list.\n            computed_value = value\n        elif value[0] in (\n                'target-counter()', 'target-counters()', 'target-text()'):\n            anchor_token = value[1][0]\n            if anchor_token[0] == 'attr()':\n                attr = compute_attr(style, anchor_token)\n                if attr is None:\n                    computed_value = None\n                else:\n                    computed_value = (value[0], (attr, *value[1][1:]))\n            else:\n                computed_value = value\n        if computed_value is None:\n            LOGGER.warning('Unable to compute %r value for content: %r' % (\n                style.element, ', '.join(str(item) for item in value)))\n        else:\n            computed_values.append(computed_value)\n\n    return tuple(computed_values)\n\n\n@register_computer('bookmark-label')\ndef bookmark_label(style, name, values):\n    \"\"\"Compute the ``bookmark-label`` property.\"\"\"\n    return _content_list(style, values)\n\n\n@register_computer('string-set')\ndef string_set(style, name, values):\n    \"\"\"Compute the ``string-set`` property.\"\"\"\n    # Spec asks for strings after custom keywords, but we allow content-lists\n    return tuple(\n        (string_set[0], _content_list(style, string_set[1]))\n        for string_set in values)\n\n\n@register_computer('content')\ndef content(style, name, values):\n    \"\"\"Compute the ``content`` property.\"\"\"\n    if len(values) == 1:\n        value, = values\n        if value == 'normal':\n            return 'inhibit' if style.pseudo_type else 'contents'\n        elif value == 'none':\n            return 'inhibit'\n    return _content_list(style, values)\n\n\n@register_computer('display')\ndef display(style, name, value):\n    \"\"\"Compute the ``display`` property.\"\"\"\n    # See https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo.\n    float_ = style.specified['float']\n    position = style.specified['position']\n    if position in ('absolute', 'fixed') or float_ != 'none' or style.is_root_element:\n        if value == ('inline-table',):\n            return ('block', 'table')\n        elif len(value) == 1 and value[0].startswith('table-'):\n            return ('block', 'flow')\n        elif value[0] == 'inline':\n            if 'list-item' in value:\n                return ('block', 'flow', 'list-item')\n            else:\n                return ('block', 'flow')\n    return value\n\n\n@register_computer('float')\ndef compute_float(style, name, value):\n    \"\"\"Compute the ``float`` property.\"\"\"\n    # See https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo.\n    position = style.specified['position']\n    if position in ('absolute', 'fixed') or position[0] == 'running()':\n        return 'none'\n    else:\n        return value\n\n\n@register_computer('font-size')\ndef font_size(style, name, value):\n    \"\"\"Compute the ``font-size`` property.\"\"\"\n    if value in FONT_SIZE_KEYWORDS:\n        return FONT_SIZE_KEYWORDS[value]\n\n    keyword_values = list(FONT_SIZE_KEYWORDS.values())\n    if style.parent_style is None:\n        parent_font_size = INITIAL_VALUES['font_size']\n    else:\n        parent_font_size = style.parent_style['font_size']\n\n    if value == 'larger':\n        for i, keyword_value in enumerate(keyword_values):\n            if keyword_value > parent_font_size:\n                return keyword_values[i]\n        else:\n            return parent_font_size * 1.2\n    elif value == 'smaller':\n        for i, keyword_value in enumerate(keyword_values[::-1]):\n            if keyword_value < parent_font_size:\n                return keyword_values[-i - 1]\n        else:\n            return parent_font_size * 0.8\n    elif isinstance(value, Dimension) and value.unit == '%':\n        return value.value * parent_font_size / 100\n    else:\n        return length(\n            style, name, value, pixels_only=True,\n            font_size=parent_font_size)\n\n\n@register_computer('font-weight')\ndef font_weight(style, name, value):\n    \"\"\"Compute the ``font-weight`` property.\"\"\"\n    if value == 'normal':\n        return 400\n    elif value == 'bold':\n        return 700\n    elif value in ('bolder', 'lighter'):\n        if style.parent_style is None:\n            parent_value = INITIAL_VALUES['font_weight']\n        else:\n            parent_value = style.parent_style['font_weight']\n        return FONT_WEIGHT_RELATIVE[value][parent_value]\n    else:\n        return value\n\n\ndef _compute_track_breadth(style, name, value):\n    \"\"\"Compute track breadth.\"\"\"\n    if value in ('auto', 'min-content', 'max-content'):\n        return value\n    elif isinstance(value, Dimension):\n        if value.unit and value.unit.lower() == 'fr':\n            return value\n        else:\n            return length(style, name, value)\n\n\ndef _track_size(style, name, values):\n    \"\"\"Compute track size.\"\"\"\n    return_values = []\n    for i, value in enumerate(values):\n        if i % 2 == 0:\n            # line name\n            return_values.append(value)\n        else:\n            # track section\n            track_breadth = _compute_track_breadth(style, name, value)\n            if track_breadth:\n                return_values.append(track_breadth)\n            elif value[0] == 'minmax()':\n                return_values.append((\n                    'minmax()',\n                    _compute_track_breadth(style, name, value[1]),\n                    _compute_track_breadth(style, name, value[2])))\n            elif value[0] == 'fit-content()':\n                return_values.append((\n                    'fit-content()', length(style, name, value[1])))\n            elif value[0] == 'repeat()':\n                return_values.append((\n                    'repeat()', value[1], _track_size(style, name, value[2])))\n    return tuple(return_values)\n\n\n@register_computer('grid-template-columns')\n@register_computer('grid-template-rows')\ndef grid_template(style, name, values):\n    \"\"\"Compute the ``grid-template-*`` properties.\"\"\"\n    if values == 'none' or values[0] == 'subgrid':\n        return values\n    else:\n        return _track_size(style, name, values)\n\n\n@register_computer('grid-auto-columns')\n@register_computer('grid-auto-rows')\ndef grid_auto(style, name, values):\n    \"\"\"Compute the ``grid-auto-*`` properties.\"\"\"\n    return_values = []\n    for value in values:\n        track_breadth = _compute_track_breadth(style, name, value)\n        if track_breadth:\n            return_values.append(track_breadth)\n        elif value[0] == 'minmax()':\n            return_values.append((\n                'minmax()', grid_auto(style, name, [value[1]])[0],\n                grid_auto(style, name, [value[2]])[0]))\n        elif value[0] == 'fit-content()':\n            return_values.append((\n                'fit-content()', grid_auto(style, name, [value[1]])[0]))\n    return tuple(return_values)\n\n\n@register_computer('line-height')\ndef line_height(style, name, value):\n    \"\"\"Compute the ``line-height`` property.\"\"\"\n    if value == 'normal':\n        return value\n    elif not value.unit:\n        return ('NUMBER', value.value)\n    elif value.unit == '%':\n        factor = value.value / 100\n        font_size_value = style['font_size']\n        pixels = factor * font_size_value\n    else:\n        pixels = length(style, name, value, pixels_only=True)\n    return ('PIXELS', pixels)\n\n\n@register_computer('anchor')\ndef anchor(style, name, values):\n    \"\"\"Compute the ``anchor`` property.\"\"\"\n    if values != 'none':\n        _, key = values\n        anchor_name = style.element.get(key) or None\n        return anchor_name\n\n\n@register_computer('link')\ndef link(style, name, values):\n    \"\"\"Compute the ``link`` property.\"\"\"\n    if values == 'none':\n        return\n    type_, value = values\n    if type_ == 'attr()':\n        return get_link_attribute(style.element, value, style.base_url)\n    return values\n\n\n@register_computer('lang')\ndef lang(style, name, values):\n    \"\"\"Compute the ``lang`` property.\"\"\"\n    if values == 'none':\n        return\n    name, key = values\n    if name == 'attr()':\n        return style.element.get(key) or None\n    elif name == 'string':\n        return key\n\n\n@register_computer('tab-size')\ndef tab_size(style, name, value):\n    \"\"\"Compute the ``tab-size`` property.\"\"\"\n    return value if isinstance(value, int) else length(style, name, value)\n\n\n@register_computer('transform')\ndef transform(style, name, value):\n    \"\"\"Compute the ``transform`` property.\"\"\"\n    result = []\n    for function, args in value:\n        if function == 'translate':\n            args = length_or_percentage_tuple(style, name, args)\n        result.append((function, args))\n    return tuple(result)\n\n\n@register_computer('vertical-align')\ndef vertical_align(style, name, value):\n    \"\"\"Compute the ``vertical-align`` property.\"\"\"\n    from ..css import resolve_math\n\n    # Use +/- half an em for super and sub, same as Pango.\n    # (See the SUPERSUB_RISE constant in pango-markup.c)\n    if check_math(value):\n        height, _ = strut(style)\n        result = resolve_math(value, style, 'vertical_align', height)\n        value = validate_non_shorthand((result,), 'vertical-align')[0][1]\n        if value is None:\n            value = 'baseline'\n    if value in ('baseline', 'middle', 'text-top', 'text-bottom', 'top', 'bottom'):\n        return value\n    elif value == 'super':\n        return style['font_size'] * 0.5\n    elif value == 'sub':\n        return style['font_size'] * -0.5\n    elif value.unit == '%':\n        height, _ = strut(style)\n        return height * value.value / 100\n    else:\n        return length(style, name, value, pixels_only=True)\n\n\n@register_computer('word-spacing')\ndef word_spacing(style, name, value):\n    \"\"\"Compute the ``word-spacing`` property.\"\"\"\n    return 0 if value == 'normal' else length(style, name, value, pixels_only=True)\n"
  },
  {
    "path": "weasyprint/css/counters.py",
    "content": "\"\"\"Implement counter styles.\n\nThese are defined in CSS Counter Styles Level 3:\nhttps://www.w3.org/TR/css-counter-styles-3/#counter-style-system\n\n\"\"\"\n\nfrom math import inf\n\nfrom .tokens import remove_whitespace\n\n\ndef symbol(string_or_url):\n    \"\"\"Create a string from a symbol.\"\"\"\n    # TODO: this function should handle images too, and return something else\n    # than strings.\n    type_, value = string_or_url\n    if type_ == 'string':\n        return value\n    return ''\n\n\ndef parse_counter_style_name(tokens, counter_style):\n    tokens = remove_whitespace(tokens)\n    if len(tokens) == 1:\n        token, = tokens\n        if token.type == 'ident':\n            if token.lower_value in ('decimal', 'disc'):\n                if token.lower_value not in counter_style:\n                    return token.value\n            elif token.lower_value != 'none':\n                return token.value\n\n\nclass CounterStyle(dict):\n    \"\"\"Counter styles dictionary.\n\n    Keep a list of counter styles defined by ``@counter-style`` rules, indexed\n    by their names.\n\n    See https://www.w3.org/TR/css-counter-styles-3/.\n\n    \"\"\"\n    def resolve_counter(self, counter_name, previous_types=None):\n        if counter_name[0] in ('symbols()', 'string'):\n            counter_type, arguments = counter_name\n            if counter_type == 'string':\n                system = (None, 'cyclic', None)\n                symbols = (('string', arguments),)\n                suffix = ('string', '')\n            elif counter_type == 'symbols()':\n                system = (\n                    None, arguments[0], 1 if arguments[0] == 'fixed' else None)\n                symbols = tuple(\n                    ('string', argument) for argument in arguments[1:])\n                suffix = ('string', ' ')\n            return {\n                'system': system,\n                'negative': (('string', '-'), ('string', '')),\n                'prefix': ('string', ''),\n                'suffix': suffix,\n                'range': 'auto',\n                'pad': (0, ''),\n                'fallback': 'decimal',\n                'symbols': symbols,\n                'additive_symbols': (),\n            }\n        elif counter_name in self:\n            # Avoid circular fallbacks\n            if previous_types is None:\n                previous_types = []\n            elif counter_name in previous_types:\n                return\n            previous_types.append(counter_name)\n\n            counter = self[counter_name].copy()\n            if counter['system']:\n                extends, system, _ = counter['system']\n            else:\n                extends, system = None, 'symbolic'\n\n            # Handle extends\n            while extends:\n                if system in self:\n                    extended_counter = self[system]\n                    counter['system'] = extended_counter['system']\n                    previous_types.append(system)\n                    if counter['system']:\n                        extends, system, _ = counter['system']\n                    else:\n                        extends, system = None, 'symbolic'\n                    if extends and system in previous_types:\n                        extends, system = 'extends', 'decimal'\n                        continue\n                    for name, value in extended_counter.items():\n                        if counter[name] is None and value is not None:\n                            counter[name] = value\n                else:\n                    return counter\n\n            return counter\n\n    def render_value(self, counter_value, counter_name=None, counter=None,\n                     previous_types=None):\n        \"\"\"Generate the counter representation.\n\n        See https://www.w3.org/TR/css-counter-styles-3/#generate-a-counter\n\n        \"\"\"\n        assert counter or counter_name\n        counter = counter or self.resolve_counter(counter_name, previous_types)\n        if counter is None:\n            if 'decimal' in self:\n                return self.render_value(counter_value, 'decimal')\n            else:\n                # Could happen if the UA stylesheet is not used\n                return ''\n\n        if counter['system']:\n            extends, system, fixed_number = counter['system']\n        else:\n            extends, system, fixed_number = None, 'symbolic', None\n\n        # Avoid circular fallbacks\n        if previous_types is None:\n            previous_types = []\n        elif system in previous_types:\n            return self.render_value(counter_value, 'decimal')\n        previous_types.append(counter_name)\n\n        # Handle extends\n        while extends:\n            if system in self:\n                extended_counter = self[system]\n                counter['system'] = extended_counter['system']\n                if counter['system']:\n                    extends, system, fixed_number = counter['system']\n                else:\n                    extends, system, fixed_number = None, 'symbolic', None\n                if system in previous_types:\n                    return self.render_value(counter_value, 'decimal')\n                previous_types.append(system)\n                for name, value in extended_counter.items():\n                    if counter[name] is None and value is not None:\n                        counter[name] = value\n            else:\n                return self.render_value(counter_value, 'decimal')\n\n        # Step 2\n        if counter['range'] in ('auto', None):\n            min_range, max_range = -inf, inf\n            if system in ('alphabetic', 'symbolic'):\n                min_range = 1\n            elif system == 'additive':\n                min_range = 0\n            counter_ranges = ((min_range, max_range),)\n        else:\n            counter_ranges = counter['range']\n        for min_range, max_range in counter_ranges:\n            if min_range <= counter_value <= max_range:\n                break\n        else:\n            return self.render_value(\n                counter_value, counter['fallback'] or 'decimal',\n                previous_types=previous_types)\n\n        # Step 3\n        initial = None\n        is_negative = counter_value < 0\n        if is_negative:\n            negative_prefix, negative_suffix = (\n                symbol(character) for character\n                in counter['negative'] or (('string', '-'), ('string', '')))\n            use_negative = (\n                system in\n                ('symbolic', 'alphabetic', 'numeric', 'additive'))\n            if use_negative:\n                counter_value = abs(counter_value)\n\n        # TODO: instead of using the decimal fallback when we have the wrong\n        # number of symbols, we should discard the whole counter. The problem\n        # only happens when extending from another style, it is easily refused\n        # during validation otherwise.\n\n        if system == 'cyclic':\n            length = len(counter['symbols'])\n            if length < 1:\n                return self.render_value(counter_value, 'decimal')\n            index = (counter_value - 1) % length\n            initial = symbol(counter['symbols'][index])\n\n        elif system == 'fixed':\n            length = len(counter['symbols'])\n            if length < 1:\n                return self.render_value(counter_value, 'decimal')\n            index = counter_value - fixed_number\n            if 0 <= index < length:\n                initial = symbol(counter['symbols'][index])\n            else:\n                return self.render_value(\n                    counter_value, counter['fallback'] or 'decimal',\n                    previous_types=previous_types)\n\n        elif system == 'symbolic':\n            length = len(counter['symbols'])\n            if length < 1:\n                return self.render_value(counter_value, 'decimal')\n            index = (counter_value - 1) % length\n            repeat = (counter_value - 1) // length + 1\n            initial = symbol(counter['symbols'][index]) * repeat\n\n        elif system == 'alphabetic':\n            length = len(counter['symbols'])\n            if length < 2:\n                return self.render_value(counter_value, 'decimal')\n            reversed_parts = []\n            while counter_value != 0:\n                counter_value -= 1\n                reversed_parts.append(symbol(\n                    counter['symbols'][counter_value % length]))\n                counter_value //= length\n            initial = ''.join(reversed(reversed_parts))\n\n        elif system == 'numeric':\n            if counter_value == 0:\n                initial = symbol(counter['symbols'][0])\n            else:\n                reversed_parts = []\n                length = len(counter['symbols'])\n                if length < 2:\n                    return self.render_value(counter_value, 'decimal')\n                counter_value = abs(counter_value)\n                while counter_value != 0:\n                    reversed_parts.append(symbol(\n                        counter['symbols'][counter_value % length]))\n                    counter_value //= length\n                initial = ''.join(reversed(reversed_parts))\n\n        elif system == 'additive':\n            if counter_value == 0:\n                for weight, symbol_string in counter['additive_symbols']:\n                    if weight == 0:\n                        initial = symbol(symbol_string)\n            else:\n                parts = []\n                if len(counter['additive_symbols']) < 1:\n                    return self.render_value(counter_value, 'decimal')\n                for weight, symbol_string in counter['additive_symbols']:\n                    repetitions = counter_value // weight\n                    parts.extend([symbol(symbol_string)] * repetitions)\n                    counter_value -= weight * repetitions\n                    if counter_value == 0:\n                        initial = ''.join(parts)\n                        break\n            if initial is None:\n                return self.render_value(\n                    counter_value, counter['fallback'] or 'decimal',\n                    previous_types=previous_types)\n\n        assert initial is not None\n\n        # Step 4\n        pad = counter['pad'] or (0, '')\n        pad_difference = pad[0] - len(initial)\n        if is_negative and use_negative:\n            pad_difference -= len(negative_prefix) + len(negative_suffix)\n        if pad_difference > 0:\n            initial = pad_difference * symbol(pad[1]) + initial\n\n        # Step 5\n        if is_negative and use_negative:\n            initial = negative_prefix + initial + negative_suffix\n\n        # Step 6\n        return initial\n\n    def render_marker(self, counter_name, counter_value):\n        \"\"\"Generate the content of a ::marker pseudo-element.\"\"\"\n        counter = self.resolve_counter(counter_name)\n        if counter is None:\n            if 'decimal' in self:\n                return self.render_marker('decimal', counter_value)\n            else:\n                # Could happen if the UA stylesheet is not used\n                return ''\n\n        prefix = symbol(counter['prefix'] or ('string', ''))\n        suffix = symbol(counter['suffix'] or ('string', '. '))\n\n        value = self.render_value(counter_value, counter_name=counter_name)\n        assert value is not None\n        return prefix + value + suffix\n\n    def copy(self):\n        # Values are dicts but they are never modified, no need to deepcopy\n        return CounterStyle(super().copy())\n"
  },
  {
    "path": "weasyprint/css/functions.py",
    "content": "\"\"\"CSS functions parsers.\"\"\"\n\n\nclass Function:\n    \"\"\"CSS function.\"\"\"\n    # See https://drafts.csswg.org/css-values-4/#functional-notation.\n\n    def __init__(self, token):\n        \"\"\"Create Function from function token.\"\"\"\n        if getattr(token, 'type', None) == 'function':\n            self.name = token.lower_name\n            self.arguments = token.arguments\n        else:\n            self.name = self.arguments = None\n\n    def split_space(self):\n        \"\"\"Split arguments on spaces.\"\"\"\n        if self.arguments is not None:\n            return [\n                argument for argument in self.arguments\n                if argument.type not in ('whitespace', 'comment')]\n\n    def split_comma(self, single_tokens=True, trailing=False):\n        \"\"\"Split arguments on commas.\n\n        Spaces in parentheses and after commas are removed.\n\n        If ``single_tokens`` is ``True``, check that only a single token is between\n        commas and flatten returned list.\n\n        If ``trailing`` is ``True``, allow a bare comma at the end.\n\n        \"\"\"\n        if self.arguments is None:\n            return\n\n        parts = [[]]\n        for token in self.arguments:\n            if token.type == 'literal' and token.value == ',':\n                parts.append([])\n                continue\n            if token.type not in ('comment', 'whitespace'):\n                parts[-1].append(token)\n\n        if trailing:\n            if single_tokens:\n                if all(len(part) == 1 for part in parts[:-1]):\n                    if len(parts[-1]) in (0, 1):\n                        return [part[0] if part else None for part in parts[:-1]]\n            elif all(parts[:-1]):\n                return parts\n        else:\n            if single_tokens:\n                if all(len(part) == 1 for part in parts):\n                    return [part[0] for part in parts]\n            elif all(parts):\n                return parts\n        return []\n\n\ndef check_attr(token, allowed_type=None):\n    function = Function(token)\n    if function.name != 'attr':\n        return\n\n    parts = function.split_comma(single_tokens=False, trailing=True)\n    if len(parts) == 1:\n        name_and_type, fallback = parts[0], ''\n    elif len(parts) == 2:\n        name_and_type, fallback = parts\n        # TODO: support fallbacks with multiple tokens and follow type.\n        if len(fallback) >= 1 and fallback[0].type == 'string':\n            fallback = fallback[0].value\n        else:\n            fallback = ''\n    else:\n        return\n\n    if any(token.type != 'ident' for token in name_and_type):\n        return\n    # TODO: follow new syntax, see https://drafts.csswg.org/css-values-5/#attr-notation.\n\n    name = name_and_type[0].value\n    type_or_unit = name_and_type[1].value if len(name_and_type) == 2 else 'string'\n    if allowed_type in (None, type_or_unit):\n        return ('attr()', (name, type_or_unit, fallback))\n\n\ndef check_counter(token, allowed_type=None):\n    from .validation.properties import list_style_type\n\n    function = Function(token)\n    arguments = function.split_comma()\n    if function.name == 'counter':\n        if len(arguments) not in (1, 2):\n            return\n    elif function.name == 'counters':\n        if len(arguments) not in (2, 3):\n            return\n    else:\n        return\n\n    result = []\n    ident = arguments.pop(0)\n    if ident.type != 'ident':\n        return\n    result.append(ident.value)\n\n    if function.name == 'counters':\n        string = arguments.pop(0)\n        if string.type != 'string':\n            return\n        result.append(string.value)\n\n    if arguments:\n        counter_style = list_style_type((arguments.pop(0),))\n        if counter_style is None:\n            return\n        result.append(counter_style)\n    else:\n        result.append('decimal')\n\n    return (f'{function.name}()', tuple(result))\n\n\ndef check_content(token):\n    function = Function(token)\n    if function.name == 'content':\n        arguments = function.split_comma()\n        if len(arguments) == 0:\n            return ('content()', 'text')\n        elif len(arguments) == 1:\n            ident = arguments.pop(0)\n            values = ('text', 'before', 'after', 'first-letter', 'marker')\n            if ident.type == 'ident' and ident.lower_value in values:\n                return ('content()', ident.lower_value)\n\n\ndef check_string_or_element(string_or_element, token):\n    function = Function(token)\n    arguments = function.split_comma()\n    if function.name == string_or_element and len(arguments) in (1, 2):\n        custom_ident = arguments.pop(0)\n        if custom_ident.type != 'ident':\n            return\n        custom_ident = custom_ident.value\n\n        if arguments:\n            ident = arguments.pop(0)\n            if ident.type != 'ident':\n                return\n            if ident.lower_value not in ('first', 'start', 'last', 'first-except'):\n                return\n            ident = ident.lower_value\n        else:\n            ident = 'first'\n\n        return (f'{string_or_element}()', (custom_ident, ident))\n\n\ndef check_var(token):\n    if token.type == '() block':\n        return any(check_var(item) for item in token.content)\n    function = Function(token)\n    if function.name is None:\n        return\n    arguments = function.split_space()\n    if function.name == 'var':\n        ident = arguments[0]\n        # TODO: we should check authorized tokens\n        # https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value\n        return ident.type == 'ident' and ident.value.startswith('--')\n    return any(check_var(argument) for argument in arguments)\n\n\ndef check_math(token):\n    # TODO: validate for real.\n    function = Function(token)\n    if (name := function.name) is None:\n        return\n    arguments = function.split_comma(single_tokens=False)\n    if name == 'calc':\n        return len(arguments) == 1\n    elif name in ('min', 'max'):\n        return len(arguments) >= 1\n    elif name == 'clamp':\n        return len(arguments) == 3\n    elif name == 'round':\n        return 1 <= len(arguments) <= 3\n    elif name in ('mod', 'rem'):\n        return len(arguments) == 2\n    elif name in ('sin', 'cos', 'tan'):\n        return len(arguments) == 1\n    elif name in ('asin', 'acos', 'atan'):\n        return len(arguments) == 1\n    elif name == 'atan2':\n        return len(arguments) == 2\n    elif name == 'pow':\n        return len(arguments) == 2\n    elif name == 'sqrt':\n        return len(arguments) == 1\n    elif name == 'hypot':\n        return len(arguments) >= 1\n    elif name == 'log':\n        return 1 <= len(arguments) <= 2\n    elif name == 'exp':\n        return len(arguments) == 1\n    elif name in ('abs', 'sign'):\n        return len(arguments) == 1\n    arguments = function.split_space()\n    return any(check_math(argument) for argument in arguments)\n"
  },
  {
    "path": "weasyprint/css/html5_ph.css",
    "content": "/*\n\nPresentational hints stylsheet for HTML.\n\nThis stylesheet contains all the presentational hints rules that can be\nexpressed as CSS.\n\nSee https://www.w3.org/TR/html5/rendering.html#rendering\n\nTODO: Attribute values are not case-insensitive, but they should be. We can add\na \"i\" flag when CSS Selectors Level 4 is supported.\n\n*/\n\npre[wrap] { white-space: pre-wrap }\n\nbr[clear=left i] { clear: left }\nbr[clear=right i] { clear: right }\nbr[clear=all i], br[clear=both i] { clear: both }\n\n:is(ol, li)[type=\"1\"] { list-style-type: decimal }\n:is(ol, li)[type=a] { list-style-type: lower-alpha }\n:is(ol, li)[type=A] { list-style-type: upper-alpha }\n:is(ol, li)[type=i] { list-style-type: lower-roman }\n:is(ol, li)[type=I] { list-style-type: upper-roman }\n:is(ul, li)[type=disc i] { list-style-type: disc }\n:is(ul, li)[type=circle i] { list-style-type: circle }\n:is(ul, li)[type=square i] { list-style-type: square }\n\ntable[align=left i] { float: left }\ntable[align=right i] { float: right }\ntable[align=center i] { margin-left: auto; margin-right: auto }\n:is(thead, tbody, tfoot, tr, td, th)[align=absmiddle i] { text-align: center }\n\ncaption[align=bottom i] { caption-side: bottom }\n:is(p, h1, h2, h3, h4, h5, h6)[align=left i] { text-align: left }\n:is(p, h1, h2, h3, h4, h5, h6)[align=right i] { text-align: right }\n:is(p, h1, h2, h3, h4, h5, h6)[align=center i] { text-align: center }\n:is(p, h1, h2, h3, h4, h5, h6)[align=justify i] { text-align: justify }\n:is(thead, tbody, tfoot, tr, td, th)[valign=top i] { vertical-align: top }\n:is(thead, tbody, tfoot, tr, td, th)[valign=middle i] { vertical-align: middle }\n:is(thead, tbody, tfoot, tr, td, th)[valign=bottom i] { vertical-align: bottom }\n:is(thead, tbody, tfoot, tr, td, th)[valign=baseline i] { vertical-align: baseline }\n\n:is(td, th)[nowrap] { white-space: nowrap }\n\ntable:is([rules=none i], [rules=groups i], [rules=rows i], [rules=cols i]) { border-style: hidden; border-collapse: collapse }\ntable[border]:not([border=\"0 i\"]) { border-style: outset }\ntable[frame=void i] { border-style: hidden }\ntable[frame=above i] { border-style: outset hidden hidden hidden }\ntable[frame=below i] { border-style: hidden hidden outset hidden }\ntable[frame=hsides i] { border-style: outset hidden outset hidden }\ntable[frame=lhs i] { border-style: hidden hidden hidden outset }\ntable[frame=rhs i] { border-style: hidden outset hidden hidden }\ntable[frame=vsides i] { border-style: hidden outset }\ntable[frame=box i], table[frame=border i] { border-style: outset }\n\ntable[border]:not([border=\"0\"]) > tr > :is(td, th), table[border]:not([border=\"0\"]) > :is(thead, tbody, tfoot) > tr > :is(td, th) { border-width: 1px; border-style: inset }\ntable:is([rules=none i], [rules=groups i], [rules=rows i]) > tr > :is(td, th), table:is([rules=none i], [rules=groups i], [rules=rows i]) > :is(thead, tbody, tfoot) > tr > :is(td, th) { border-width: 1px; border-style: none }\ntable[rules=cols i] > tr > :is(td, th), table[rules=cols i] > :is(thead, tbody, tfoot) > tr > :is(td, th) { border-width: 1px; border-style: none solid }\ntable[rules=all i] > tr > :is(td, th), table[rules=all i] > :is(thead, tbody, tfoot) > tr > :is(td, th) { border-width: 1px; border-style: solid }\ntable[rules=groups i] > colgroup { border-left-width: 1px; border-left-style: solid; border-right-width: 1px; border-right-style: solid }\ntable[rules=groups i] > :is(thead, tbody, tfoot) { border-top-width: 1px; border-top-style: solid; border-bottom-width: 1px; border-bottom-style: solid }\ntable[rules=rows i] > tr, table[rules=rows i] > :is(thead, tbody, tfoot) > tr { border-top-width: 1px; border-top-style: solid; border-bottom-width: 1px; border-bottom-style: solid }\n\nhr[align=left i] { margin-left: 0; margin-right: auto }\nhr[align=right i] { margin-left: auto; margin-right: 0 }\nhr[align=center i] { margin-left: auto; margin-right: auto }\nhr[color], hr[noshade] { border-style: solid }\n\niframe[frameborder=\"0\"], iframe[frameborder=no i] { border: none }\n\n:is(applet, embed, iframe, img, input, object)[align=left i] { float: left }\n:is(applet, embed, iframe, img, input, object)[align=right i] { float: right }\n:is(applet, embed, iframe, img, input, object)[align=top i] { vertical-align: top }\n:is(applet, embed, iframe, img, input, object)[align=baseline i] { vertical-align: baseline }\n:is(applet, embed, iframe, img, input, object)[align=texttop i] { vertical-align: text-top }\n:is(applet, embed, iframe, img, input, object):is([align=middle i], [align=absmiddle i], [align=absmiddle i]) { vertical-align: middle }\n:is(applet, embed, iframe, img, input, object)[align=bottom i] { vertical-align: bottom }\n"
  },
  {
    "path": "weasyprint/css/html5_ua.css",
    "content": "/*\n\nUser agent stylsheet for HTML.\n\nContributed by Peter Moulder.\nBased on suggested styles in the HTML5 specification, CSS 2.1, and\nwhat various web browsers use.\n\nhttps://dev.w3.org/html5/spec-LC/rendering.html#the-css-user-agent-style-sheet-and-presentational-hints\n\n*/\n\n/* WeasyPrint-only features */\n\n[id] { -weasy-anchor: attr(id) }\na[name] { -weasy-anchor: attr(name) }\n[lang] { -weasy-lang: attr(lang) }\na[href] { -weasy-link: attr(href) }\n\n/* Display and visibility */\n\n[hidden], area, base, basefont, command, datalist, head, input[type=hidden i], link, menu[type=context i], meta, noembed, noframes, param, rp, script, source, style, template, title, track { display: none }\naddress, article, aside, blockquote, body, center, dd, details, dir, div, dl, dt, frame, frameset, fieldset, figure, figcaption, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, html, legend, listing, main, menu, nav, ol, p, plaintext, pre, section, summary, ul, xmp { display: block }\nbutton, input, keygen, select, textarea { display: inline-block }\nli { display: list-item }\ntable { display: table }\ncaption { display: table-caption }\ncolgroup, colgroup[hidden] { display: table-column-group }\ncol, col[hidden] { display: table-column }\nthead, thead[hidden] { display: table-header-group }\ntbody, tbody[hidden] { display: table-row-group }\ntfoot, tfoot[hidden] { display: table-footer-group }\ntr, tr[hidden] { display: table-row }\ntd, th, td[hidden], th[hidden] { display: table-cell }\n:is(colgroup, col, thead, tbody, tfoot, tr, td, th)[hidden] { visibility: collapse }\n\n/* Margins and padding */\n\nblockquote, dir, dl, figure, listing, menu, ol, p, plaintext, pre, ul, xmp { margin-top: 1em; margin-bottom: 1em }\n:is(dir, dl, menu, ol, ul) :is(dir, dl, menu, ol, ul) { margin-top: 0; margin-bottom: 0 }\n\nbody { margin: 8px }\n\nh1 { margin-top: .67em; margin-bottom: .67em }\nh2 { margin-top: .83em; margin-bottom: .83em }\nh3 { margin-top: 1em; margin-bottom: 1em }\nh4 { margin-top: 1.33em; margin-bottom: 1.33em }\nh5 { margin-top: 1.67em; margin-bottom: 1.67em }\nh6 { margin-top: 2.33em; margin-bottom: 2.33em }\n\nblockquote, figure { margin-left: 40px; margin-right: 40px }\n\ndd { margin-left: 40px }\n[dir=ltr i] dd { margin-left: 0; margin-right: 40px }\n[dir=rtl i] dd { margin-left: 40px; margin-right: 0 }\n[dir] [dir=ltr i] dd { margin-left: 0; margin-right: 40px }\n[dir] [dir=rtl i] dd { margin-left: 40px; margin-right: 0 }\n[dir] [dir] [dir=ltr i] dd { margin-left: 0; margin-right: 40px }\n[dir] [dir] [dir=rtl i] dd { margin-left: 40px; margin-right: 0 }\ndd[dir=ltr i][dir][dir] { margin-left: 0; margin-right: 40px }\ndd[dir=rtl i][dir][dir] { margin-left: 40px; margin-right: 0 }\n\ndir, menu, ol, ul { padding-left: 40px }\n[dir=ltr i] :is(dir, menu, ol, ul) { padding-left: 40px; padding-right: 0 }\n[dir=rtl i] :is(dir, menu, ol, ul) { padding-left: 0; padding-right: 40px }\n[dir] [dir=ltr i] :is(dir, menu, ol, ul) { padding-left: 40px; padding-right: 0 }\n[dir] [dir=rtl i] :is(dir, menu, ol, ul) { padding-left: 0; padding-right: 40px }\n[dir] [dir] [dir=ltr i] :is(dir, menu, ol, ul) { padding-left: 40px; padding-right: 0 }\n[dir] [dir] [dir=rtl i] :is(dir, menu, ol, ul) { padding-left: 0; padding-right: 40px }\n:is(dir, menu, ol, ul)[dir=ltr i][dir][dir] { padding-left: 40px; padding-right: 0 }\n:is(dir, menu, ol, ul)[dir=rtl i][dir][dir] { padding-left: 0; padding-right: 40px }\n\ntable { border-spacing: 2px; border-collapse: separate }\ntd, th { padding: 1px }\n\n/* Alignment */\n\nthead, tbody, tfoot, table > tr { vertical-align: middle }\ntr, td, th { vertical-align: inherit }\nsub { vertical-align: sub }\nsup { vertical-align: super }\n\n/* Fonts and colors */\n\nhtml { color-scheme: light }\naddress, cite, dfn, em, i, var { font-style: italic }\nb, strong, th { font-weight: bold }\ncode, kbd, listing, plaintext, pre, samp, tt, xmp { font-family: monospace }\nh1 { font-size: 2em; font-weight: bold }\nh2 { font-size: 1.5em; font-weight: bold }\nh3 { font-size: 1.17em; font-weight: bold }\nh4 { font-size: 1em; font-weight: bold }\nh5 { font-size: .83em; font-weight: bold }\nh6 { font-size: .67em; font-weight: bold }\nbig { font-size: larger }\nsmall, sub, sup { font-size: smaller }\nsub, sup { line-height: normal }\n\n:link { color: blue }\nmark { background: yellow; color: black }\n\ntable, td, th { border-color: gray }\nthead, tbody, tfoot, tr { border-color: inherit }\ntable:is([rules], [frame]):is([frame=above i], [frame=below i], [frame=border i], [frame=box i], [frame=hsides i], [frame=lhs i], [frame=rhs i], [frame=void i], [frame=vsides i], [rules=all i], [rules=cols i], [rules=groups i], [rules=none i], [rules=rows i]), table[rules]:is([rules=all i], [rules=cols i], [rules=groups i], [rules=none i], [rules=rows i]) > tr > :is(td, th), table[rules]:is([rules=all i], [rules=cols i], [rules=groups i], [rules=none i], [rules=rows i]) > :is(thead, tbody, tfoot) > tr > :is(td, th) { border-color: black }\n\n/* Punctuation and decorations */\n\n:link, :visited, ins, u { text-decoration: underline }\nabbr[title], acronym[title] { text-decoration: dotted underline }\ndel, s, strike { text-decoration: line-through }\nq::before { content: open-quote }\nq::after { content: close-quote }\n\nbr::before { content: \"\\A\"; white-space: pre-line }\nwbr::before { content: \"\\200B\" }\nnobr { white-space: nowrap }\n\nhr { border-style: inset; border-width: 1px; color: gray; margin: .5em auto }\n\nlisting, plaintext, pre, xmp { white-space: pre }\ntextarea { white-space: pre-wrap }\nol { list-style-type: decimal }\ndir, menu, ul { list-style-type: disc }\n:is(dir, menu, ol, ul) ul { list-style-type: circle }\n:is(dir, menu, ol, ul) :is(dir, menu, ol, ul) ul { list-style-type: square }\n\n::marker { font-variant-numeric: tabular-nums; white-space: pre; text-transform: none }\n\n[dir=ltr i] { direction: ltr }\n[dir=rtl i] { direction: rtl }\n\n/* Text indent */\n\ntable, input, select, option, optgroup, button, textarea, keygen { text-indent: initial }\n\n/* Specific tags */\n\ncenter { text-align: center }\niframe:not([seamless]) { border: 2px inset }\nimg, svg { overflow: hidden }\nvideo { object-fit: contain }\ntable { box-sizing: border-box }\n\n/* Footnotes */\n\n::footnote-call { content: counter(footnote); vertical-align: super; font-size: smaller; line-height: inherit }\n::footnote-marker { content: counter(footnote) \". \" }\n\n/* Counters and bookmarks */\n\nol, ul { counter-reset: list-item }\nh1 { bookmark-level: 1 }\nh2 { bookmark-level: 2 }\nh3 { bookmark-level: 3 }\nh4 { bookmark-level: 4 }\nh5 { bookmark-level: 5 }\nh6 { bookmark-level: 6 }\nh1, h2, h3, h4, h5, h6 { bookmark-label: content(text) }\n\n/* Page breaks and hyphens */\n\nh1, h2, h3, h4, h5, h6 { hyphens: manual; break-after: avoid; break-inside: avoid }\nol, ul { break-before: avoid }\n\n/* Form fields */\n\nbutton, input, select, textarea { border: 1px solid black; font-size: .85em; height: 1.2em; padding: .2em; white-space: pre; width: 20em }\ninput:is([type=button i], [type=reset i], [type=submit i]), button { background: lightgrey; border-radius: .25em; text-align: center }\ninput:is([type=button i], [type=reset i], [type=submit i])[value], button[value] { max-width: 100%; width: auto }\ninput[type=submit i]:not([value])::before { content: \"Submit\" }\ninput[type=reset i]:not([value])::before { content: \"Reset\" }\ninput:is([type=checkbox i], [type=radio i]) { height: .7em; vertical-align: -.2em; width: .7em }\ninput:is([type=checkbox i], [type=radio i])[checked]::before { background: black; content: \"\"; display: block; height: 100% }\ninput[type=radio i], input[type=radio][checked]:before { border-radius: 50% }\ninput[value]:not([type=checkbox i], [type=radio i])::before { content: attr(value); display: block; overflow: hidden }\n:is(input, input[value=\"\"], input[type=checkbox i], input[type=radio i]) { content: \"\"; display: block }\nselect { background: lightgrey; border-radius: .25em .25em; position: relative; white-space: normal }\nselect[multiple] { height: 3.6em }\nselect:not([multiple])::before { content: \"˅\"; position: absolute; right: 0; text-align: center; width: 1.5em }\nselect option { padding-right: 1.5em; white-space: nowrap }\nselect:not([multiple]) option { display: none }\nselect[multiple] option, select:not(:has(option[selected])) option:first-of-type, select option[selected]:not(option[selected] ~ option[selected]) { display: block; overflow: hidden }\ntextarea { height: 3em; margin: .1em 0; overflow: hidden; overflow-wrap: break-word; padding: .2em; white-space: pre-wrap }\ntextarea:empty { height: 3em }\nfieldset { border: groove 2px; margin-left: 2px; margin-right: 2px; padding: .35em .625em .75em }\n\n/* Pages */\n\n@page {\n  margin: 75px;\n  @footnote { margin-top: 1em }\n  @top-left-corner     { text-align: right;  vertical-align:  middle }\n  @top-left            { text-align: left;   vertical-align:  middle }\n  @top-center          { text-align: center; vertical-align:  middle }\n  @top-right           { text-align: right;  vertical-align:  middle }\n  @top-right-corner    { text-align: left;   vertical-align:  middle }\n  @left-top            { text-align: center; vertical-align:  top    }\n  @left-middle         { text-align: center; vertical-align:  middle }\n  @left-bottom         { text-align: center; vertical-align:  bottom }\n  @right-top           { text-align: center; vertical-align:  top    }\n  @right-middle        { text-align: center; vertical-align:  middle }\n  @right-bottom        { text-align: center; vertical-align:  bottom }\n  @bottom-left-corner  { text-align: right;  vertical-align:  middle }\n  @bottom-left         { text-align: left;   vertical-align:  middle }\n  @bottom-center       { text-align: center; vertical-align:  middle }\n  @bottom-right        { text-align: right;  vertical-align:  middle }\n  @bottom-right-corner { text-align: left;   vertical-align:  middle }\n}\n\n/* Counters */\n\n@counter-style disc { system: cyclic; symbols: •; suffix: \" \" }\n@counter-style circle { system: cyclic; symbols: ◦; suffix: \" \" }\n@counter-style square { system: cyclic; symbols: ▪; suffix: \" \" }\n@counter-style disclosure-open { system: cyclic; symbols: ▾; suffix: \" \" }\n@counter-style disclosure-closed { system: cyclic; symbols: ▸; suffix: \" \" }\n@counter-style decimal { system: numeric; symbols: \"0\" \"1\" \"2\" \"3\" \"4\" \"5\" \"6\" \"7\" \"8\" \"9\" }\n@counter-style decimal-leading-zero { system: extends decimal; pad: 2 \"0\" }\n@counter-style arabic-indic { system: numeric; symbols: ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩ }\n@counter-style armenian { system: additive; range: 1 9999; additive-symbols: 9000 Ք, 8000 Փ, 7000 Ւ, 6000 Ց, 5000 Ր, 4000 Տ, 3000 Վ, 2000 Ս, 1000 Ռ, 900 Ջ, 800 Պ, 700 Չ, 600 Ո, 500 Շ, 400 Ն, 300 Յ, 200 Մ, 100 Ճ, 90 Ղ, 80 Ձ, 70 Հ, 60 Կ, 50 Ծ, 40 Խ, 30 Լ, 20 Ի, 10 Ժ, 9 Թ, 8 Ը, 7 Է, 6 Զ, 5 Ե, 4 Դ, 3 Գ, 2 Բ, 1 Ա }\n@counter-style upper-armenian {system: extends armenian }\n@counter-style lower-armenian { system: additive; range: 1 9999; additive-symbols: 9000 ք, 8000 փ, 7000 ւ, 6000 ց, 5000 ր, 4000 տ, 3000 վ, 2000 ս, 1000 ռ, 900 ջ, 800 պ, 700 չ, 600 ո, 500 շ, 400 ն, 300 յ, 200 մ, 100 ճ, 90 ղ, 80 ձ, 70 հ, 60 կ, 50 ծ, 40 խ, 30 լ, 20 ի, 10 ժ, 9 թ, 8 ը, 7 է, 6 զ, 5 ե, 4 դ, 3 գ, 2 բ, 1 ա }\n@counter-style bengali { system: numeric; symbols: ০ ১ ২ ৩ ৪ ৫ ৬ ৭ ৮ ৯ }\n@counter-style cambodian { system: numeric; symbols: ០ ១ ២ ៣ ៤ ៥ ៦ ៧ ៨ ៩ }\n@counter-style khmer { system: extends cambodian }\n@counter-style cjk-decimal { system: numeric; range: 0 infinite; symbols: 〇 一 二 三 四 五 六 七 八 九; suffix: \"、\" }\n@counter-style devanagari { system: numeric; symbols: ० १ २ ३ ४ ५ ६ ७ ८ ९ }\n@counter-style georgian { system: additive; range: 1 19999; additive-symbols: 10000 ჵ, 9000 ჰ, 8000 ჯ, 7000 ჴ, 6000 ხ, 5000 ჭ, 4000 წ, 3000 ძ, 2000 ც, 1000 ჩ, 900 შ, 800 ყ, 700 ღ, 600 ქ, 500 ფ, 400 ჳ, 300 ტ, 200 ს, 100 რ, 90 ჟ, 80 პ, 70 ო, 60 ჲ, 50 ნ, 40 მ, 30 ლ, 20 კ, 10 ი, 9 თ, 8 ჱ, 7 ზ, 6 ვ, 5 ე, 4 დ, 3 გ, 2 ბ, 1 ა }\n@counter-style gujarati { system: numeric; symbols: ૦ ૧ ૨ ૩ ૪ ૫ ૬ ૭ ૮ ૯ }\n@counter-style gurmukhi { system: numeric; symbols: ੦ ੧ ੨ ੩ ੪ ੫ ੬ ੭ ੮ ੯ }\n@counter-style hebrew { system: additive; range: 1 10999; additive-symbols: 10000 י׳, 9000 ט׳, 8000 ח׳, 7000 ז׳, 6000 ו׳, 5000 ה׳, 4000 ד׳, 3000 ג׳, 2000 ב׳, 1000 א׳, 400 ת, 300 ש, 200 ר, 100 ק, 90 צ, 80 פ, 70 ע, 60 ס, 50 נ, 40 מ, 30 ל, 20 כ, 19 יט, 18 יח, 17 יז, 16 טז, 15 טו, 10 י, 9 ט, 8 ח, 7 ז, 6 ו, 5 ה, 4 ד, 3 ג, 2 ב, 1 א }\n@counter-style kannada { system: numeric; symbols: ೦ ೧ ೨ ೩ ೪ ೫ ೬ ೭ ೮ ೯ }\n@counter-style lao { system: numeric; symbols: ໐ ໑ ໒ ໓ ໔ ໕ ໖ ໗ ໘ ໙ }\n@counter-style malayalam { system: numeric; symbols: ൦ ൧ ൨ ൩ ൪ ൫ ൬ ൭ ൮ ൯ }\n@counter-style mongolian { system: numeric; symbols: ᠐ ᠑ ᠒ ᠓ ᠔ ᠕ ᠖ ᠗ ᠘ ᠙ }\n@counter-style myanmar { system: numeric; symbols: ၀ ၁ ၂ ၃ ၄ ၅ ၆ ၇ ၈ ၉ }\n@counter-style oriya { system: numeric; symbols: ୦ ୧ ୨ ୩ ୪ ୫ ୬ ୭ ୮ ୯ }\n@counter-style persian { system: numeric; symbols: ۰ ۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ }\n@counter-style lower-roman { system: additive; range: 1 3999; additive-symbols: 1000 m, 900 cm, 500 d, 400 cd, 100 c, 90 xc, 50 l, 40 xl, 10 x, 9 ix, 5 v, 4 iv, 1 i }\n@counter-style upper-roman { system: additive; range: 1 3999; additive-symbols: 1000 M, 900 CM, 500 D, 400 CD, 100 C, 90 XC, 50 L, 40 XL, 10 X, 9 IX, 5 V, 4 IV, 1 I }\n@counter-style tamil { system: numeric; symbols: ௦ ௧ ௨ ௩ ௪ ௫ ௬ ௭ ௮ ௯ }\n@counter-style telugu { system: numeric; symbols: ౦ ౧ ౨ ౩ ౪ ౫ ౬ ౭ ౮ ౯ }\n@counter-style thai { system: numeric; symbols: ๐ ๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙ }\n@counter-style tibetan { system: numeric; symbols: ༠ ༡ ༢ ༣ ༤ ༥ ༦ ༧ ༨ ༩ } @counter-style lower-alpha { system: alphabetic; symbols: a b c d e f g h i j k l m n o p q r s t u v w x y z }\n@counter-style lower-latin { system: extends lower-alpha }\n@counter-style upper-alpha { system: alphabetic; symbols: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z }\n@counter-style upper-latin { system: extends upper-alpha }\n@counter-style cjk-earthly-branch { system: alphabetic; symbols: 子 丑 寅 卯 辰 巳 午 未 申 酉 戌 亥; suffix: \"、\" }\n@counter-style cjk-heavenly-stem { system: alphabetic; symbols: 甲 乙 丙 丁 戊 己 庚 辛 壬 癸; suffix: \"、\" }\n@counter-style lower-greek { system: alphabetic; symbols: α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω }\n@counter-style hiragana { system: alphabetic; symbols: あ い う え お か き く け こ さ し す せ そ た ち つ て と な に ぬ ね の は ひ ふ へ ほ ま み む め も や ゆ よ ら り る れ ろ わ ゐ ゑ を ん; suffix: \"、\" }\n@counter-style hiragana-iroha { system: alphabetic; symbols: い ろ は に ほ へ と ち り ぬ る を わ か よ た れ そ つ ね な ら む う ゐ の お く や ま け ふ こ え て あ さ き ゆ め み し ゑ ひ も せ す; suffix: \"、\" }\n@counter-style katakana { system: alphabetic; symbols: ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ テ ト ナ ニ ヌ ネ ノ ハ ヒ フ ヘ ホ マ ミ ム メ モ ヤ ユ ヨ ラ リ ル レ ロ ワ ヰ ヱ ヲ ン; suffix: \"、\" }\n@counter-style katakana-iroha { system: alphabetic; symbols: イ ロ ハ ニ ホ ヘ ト チ リ ヌ ル ヲ ワ カ ヨ タ レ ソ ツ ネ ナ ラ ム ウ ヰ ノ オ ク ヤ マ ケ フ コ エ テ ア サ キ ユ メ ミ シ ヱ ヒ モ セ ス; suffix: \"、\" }\n@counter-style japanese-informal { system: additive; range: -9999 9999; additive-symbols: 9000 九千, 8000 八千, 7000 七千, 6000 六千, 5000 五千, 4000 四千, 3000 三千, 2000 二千, 1000 千, 900 九百, 800 八百, 700 七百, 600 六百, 500 五百, 400 四百, 300 三百, 200 二百, 100 百, 90 九十, 80 八十, 70 七十, 60 六十, 50 五十, 40 四十, 30 三十, 20 二十, 10 十, 9 九, 8 八, 7 七, 6 六, 5 五, 4 四, 3 三, 2 二, 1 一, 0 〇; suffix: 、; negative: マイナス; fallback: cjk-decimal }\n@counter-style japanese-formal { system: additive; range: -9999 9999; additive-symbols: 9000 九阡, 8000 八阡, 7000 七阡, 6000 六阡, 5000 伍阡, 4000 四阡, 3000 参阡, 2000 弐阡, 1000 壱阡, 900 九百, 800 八百, 700 七百, 600 六百, 500 伍百, 400 四百, 300 参百, 200 弐百, 100 壱百, 90 九拾, 80 八拾, 70 七拾, 60 六拾, 50 伍拾, 40 四拾, 30 参拾, 20 弐拾, 10 壱拾, 9 九, 8 八, 7 七, 6 六, 5 伍, 4 四, 3 参, 2 弐, 1 壱, 0 零; suffix: 、; negative: マイナス; fallback: cjk-decimal }\n@counter-style korean-hangul-formal { system: additive; range: -9999 9999; additive-symbols: 9000 구천, 8000 팔천, 7000 칠천, 6000 육천, 5000 오천, 4000 사천, 3000 삼천, 2000 이천, 1000 일천, 900 구백, 800 팔백, 700 칠백, 600 육백, 500 오백, 400 사백, 300 삼백, 200 이백, 100 일백, 90 구십, 80 팔십, 70 칠십, 60 육십, 50 오십, 40 사십, 30 삼십, 20 이십, 10 일십, 9 구, 8 팔, 7 칠, 6 육, 5 오, 4 사, 3 삼, 2 이, 1 일, 0 영; suffix: \", \"; negative: \"마이너스  \" }\n@counter-style korean-hanja-informal { system: additive; range: -9999 9999; additive-symbols: 9000 九千, 8000 八千, 7000 七千, 6000 六千, 5000 五千, 4000 四千, 3000 三千, 2000 二千, 1000 千, 900 九百, 800 八百, 700 七百, 600 六百, 500 五百, 400 四百, 300 三百, 200 二百, 100 百, 90 九十, 80 八十, 70 七十, 60 六十, 50 五十, 40 四十, 30 三十, 20 二十, 10 十, 9 九, 8 八, 7 七, 6 六, 5 五, 4 四, 3 三, 2 二, 1 一, 0 零; suffix: \", \"; negative: \"마이너스  \" }\n@counter-style korean-hanja-formal { system: additive; range: -9999 9999; additive-symbols: 9000 九仟, 8000 八仟, 7000 七仟, 6000 六仟, 5000 五仟, 4000 四仟, 3000 參仟, 2000 貳仟, 1000 壹仟, 900 九百, 800 八百, 700 七百, 600 六百, 500 五百, 400 四百, 300 參百, 200 貳百, 100 壹百, 90 九拾, 80 八拾, 70 七拾, 60 六拾, 50 五拾, 40 四拾, 30 參拾, 20 貳拾, 10 壹拾, 9 九, 8 八, 7 七, 6 六, 5 五, 4 四, 3 參, 2 貳, 1 壹, 0 零; suffix: \", \"; negative: \"마이너스  \" }\n"
  },
  {
    "path": "weasyprint/css/html5_ua_form.css",
    "content": "/* Default stylesheet for PDF forms */\n\nbutton, input, select, textarea { appearance: auto }\nselect option, select:not([multiple])::before, input:not([type=\"submit\"])::before { visibility: hidden }\ntextarea { white-space: normal; text-indent: 10000% } /* Hide text but don’t change color used by PDF form */\n"
  },
  {
    "path": "weasyprint/css/media_queries.py",
    "content": "\"\"\"Handle media queries.\n\nhttps://www.w3.org/TR/mediaqueries-4/\n\n\"\"\"\n\nimport tinycss2\n\nfrom ..logger import LOGGER\nfrom .tokens import remove_whitespace, split_on_comma\n\n\ndef evaluate_media_query(query_list, device_media_type):\n    \"\"\"Return the boolean evaluation of `query_list` for the given\n    `device_media_type`.\n\n    :attr query_list: a cssutilts.stlysheets.MediaList\n    :attr device_media_type: a media type string (for now)\n\n    \"\"\"\n    # TODO: actual support for media queries, not just media types\n    return 'all' in query_list or device_media_type in query_list\n\n\ndef parse_media_query(tokens):\n    tokens = remove_whitespace(tokens)\n    if not tokens:\n        return ['all']\n    else:\n        media = []\n        if tokens[0].type == 'ident' and tokens[0].lower_value == 'only':\n            tokens = tokens[1:]\n        for part in split_on_comma(tokens):\n            types = [token.type for token in part]\n            if types == ['ident']:\n                media.append(part[0].lower_value)\n            else:\n                LOGGER.warning(\n                    'Expected a media type, got %r', tinycss2.serialize(part))\n                return\n        return media\n"
  },
  {
    "path": "weasyprint/css/properties.py",
    "content": "\"\"\"Various data about known CSS properties.\"\"\"\n\nimport collections\nfrom math import inf\n\nfrom tinycss2.color5 import parse_color\n\nDimension = collections.namedtuple('Dimension', ['value', 'unit'])\n\nZERO_PIXELS = Dimension(0, 'px')\n\nINITIAL_VALUES = {\n    # CSS 2.1: https://www.w3.org/TR/CSS21/propidx.html\n    'bottom': 'auto',\n    'caption_side': 'top',\n    'clear': 'none',\n    'clip': (),  # computed value for 'auto'\n    'color': parse_color('black'),  # chosen by the user agent\n    'direction': 'ltr',\n    'display': ('inline', 'flow'),\n    'empty_cells': 'show',\n    'float': 'none',\n    'left': 'auto',\n    'line_height': 'normal',\n    'margin_top': ZERO_PIXELS,\n    'margin_right': ZERO_PIXELS,\n    'margin_bottom': ZERO_PIXELS,\n    'margin_left': ZERO_PIXELS,\n    'padding_top': ZERO_PIXELS,\n    'padding_right': ZERO_PIXELS,\n    'padding_bottom': ZERO_PIXELS,\n    'padding_left': ZERO_PIXELS,\n    'position': 'static',\n    'right': 'auto',\n    'table_layout': 'auto',\n    'top': 'auto',\n    'unicode_bidi': 'normal',\n    'vertical_align': 'baseline',\n    'visibility': 'visible',\n    'z_index': 'auto',\n\n    # Backgrounds and Borders 3 (CR): https://www.w3.org/TR/css-backgrounds-3/\n    'background_attachment': ('scroll',),\n    'background_clip': ('border-box',),\n    'background_color': 'transparent',\n    'background_image': (('none', None),),\n    'background_origin': ('padding-box',),\n    'background_position': (('left', Dimension(0, '%'),\n                             'top', Dimension(0, '%')),),\n    'background_repeat': (('repeat', 'repeat'),),\n    'background_size': (('auto', 'auto'),),\n    'border_bottom_color': 'currentcolor',\n    'border_bottom_left_radius': (ZERO_PIXELS, ZERO_PIXELS),\n    'border_bottom_right_radius': (ZERO_PIXELS, ZERO_PIXELS),\n    'border_bottom_style': 'none',\n    'border_bottom_width': 3,\n    'border_collapse': 'separate',\n    'border_left_color': 'currentcolor',\n    'border_left_style': 'none',\n    'border_left_width': 3,\n    'border_right_color': 'currentcolor',\n    'border_right_style': 'none',\n    'border_right_width': 3,\n    'border_spacing': (0, 0),\n    'border_top_color': 'currentcolor',\n    'border_top_left_radius': (ZERO_PIXELS, ZERO_PIXELS),\n    'border_top_right_radius': (ZERO_PIXELS, ZERO_PIXELS),\n    'border_top_style': 'none',\n    'border_top_width': 3,  # computed value for 'medium'\n    'border_image_source': ('none', None),\n    'border_image_slice': (\n        Dimension(100, '%'), Dimension(100, '%'),\n        Dimension(100, '%'), Dimension(100, '%'),\n        None),\n    'border_image_width': (1, 1, 1, 1),\n    'border_image_outset': (\n        Dimension(0, None), Dimension(0, None),\n        Dimension(0, None), Dimension(0, None)),\n    'border_image_repeat': ('stretch', 'stretch'),\n    'mask_border_source': ('none', None),\n    'mask_border_slice': (\n        Dimension(100, '%'), Dimension(100, '%'),\n        Dimension(100, '%'), Dimension(100, '%'),\n        None),\n    'mask_border_width': ('auto', 'auto', 'auto', 'auto'),\n    'mask_border_outset': (\n        Dimension(0, None), Dimension(0, None),\n        Dimension(0, None), Dimension(0, None)),\n    'mask_border_repeat': ('stretch', 'stretch'),\n    'mask_border_mode': 'alpha',\n\n    # Color Adjustment 1 (CRD): https://www.w3.org/TR/css-color-adjust-1\n    'color_scheme': 'normal',\n\n    # Color 3 (REC): https://www.w3.org/TR/css-color-3/\n    'opacity': 1,\n\n    # Multi-column Layout (WD): https://www.w3.org/TR/css-multicol-1/\n    'column_width': 'auto',\n    'column_count': 'auto',\n    'column_rule_color': 'currentcolor',\n    'column_rule_style': 'none',\n    'column_rule_width': 'medium',\n    'column_fill': 'balance',\n    'column_span': 'none',\n\n    # Fonts 3 (REC): https://www.w3.org/TR/css-fonts-3/\n    'font_family': ('serif',),  # depends on user agent\n    'font_feature_settings': 'normal',\n    'font_kerning': 'auto',\n    'font_language_override': 'normal',\n    'font_size': 16,  # actually medium, but we define medium from this\n    'font_stretch': 'normal',\n    'font_style': 'normal',\n    'font_variant': 'normal',\n    'font_variant_alternates': 'normal',\n    'font_variant_caps': 'normal',\n    'font_variant_east_asian': 'normal',\n    'font_variant_ligatures': 'normal',\n    'font_variant_numeric': 'normal',\n    'font_variant_position': 'normal',\n    'font_weight': 400,\n\n    # Fonts 4 (WD): https://www.w3.org/TR/css-fonts-4/\n    'font_variation_settings': 'normal',\n\n    # Fragmentation 3/4 (CR/WD): https://www.w3.org/TR/css-break-4/\n    'box_decoration_break': 'slice',\n    'break_after': 'auto',\n    'break_before': 'auto',\n    'break_inside': 'auto',\n    'margin_break': 'auto',\n    'orphans': 2,\n    'widows': 2,\n\n    # Generated Content 3 (WD): https://www.w3.org/TR/css-content-3/\n    'bookmark_label': (('content', 'text'),),\n    'bookmark_level': 'none',\n    'bookmark_state': 'open',\n    'content': 'normal',\n    'footnote_display': 'block',\n    'footnote_policy': 'auto',\n    'quotes': 'auto',\n    'string_set': 'none',\n\n    # Images 3/4 (CR/WD): https://www.w3.org/TR/css-images-4/\n    'image_resolution': 1,  # dppx\n    'image_rendering': 'auto',\n    'image_orientation': 'from-image',\n    'object_fit': 'fill',\n    'object_position': (('left', Dimension(50, '%'),\n                         'top', Dimension(50, '%')),),\n\n    # Paged Media 3 (WD): https://www.w3.org/TR/css-page-3/\n    'size': None,  # set to A4 in computed_values\n    'page': 'auto',\n    'bleed_left': 'auto',\n    'bleed_right': 'auto',\n    'bleed_top': 'auto',\n    'bleed_bottom': 'auto',\n    'marks': (),  # computed value for 'none'\n\n    # Text 3/4 (WD/WD): https://www.w3.org/TR/css-text-4/\n    'hyphenate_character': '‐',  # computed value chosen by the user agent\n    'hyphenate_limit_chars': (5, 2, 2),\n    'hyphenate_limit_zone': ZERO_PIXELS,\n    'hyphens': 'manual',\n    'letter_spacing': 'normal',\n    'tab_size': 8,\n    'text_align_all': 'start',\n    'text_align_last': 'auto',\n    'text_indent': ZERO_PIXELS,\n    'text_transform': 'none',\n    'white_space': 'normal',\n    'word_break': 'normal',\n    'word_spacing': 0,  # computed value for 'normal'\n\n    # Transforms 1 (CR): https://www.w3.org/TR/css-transforms-1/\n    'transform_origin': (Dimension(50, '%'), Dimension(50, '%')),\n    'transform': (),  # computed value for 'none'\n\n    # User Interface 3/4 (REC/WD): https://www.w3.org/TR/css-ui-4/\n    'appearance': 'none',\n    'outline_color': 'currentcolor',  # invert is not supported\n    'outline_style': 'none',\n    'outline_width': 3,  # computed value for 'medium'\n    'outline_offset': 0,\n\n    # Sizing 3 (WD): https://www.w3.org/TR/css-sizing-3/\n    'box_sizing': 'content-box',\n    'height': 'auto',\n    'max_height': Dimension(inf, 'px'),  # parsed value for 'none'\n    'max_width': Dimension(inf, 'px'),\n    'min_height': 'auto',\n    'min_width': 'auto',\n    'width': 'auto',\n\n    # Logical Properties and Values 1 (WD): https://www.w3.org/TR/css-logical-1/\n    'block_size': 'auto',\n    'inline_size': 'auto',\n    'max_block_size': Dimension(inf, 'px'),  # parsed value for 'none',\n    'max_inline_size': Dimension(inf, 'px'),\n    'min_block_size': 'auto',\n    'min_inline_size': 'auto',\n    'margin_block_start': 0,\n    'margin_inline_start': 0,\n    'margin_block_end': 0,\n    'margin_inline_end': 0,\n    'padding_block_start': 0,\n    'padding_inline_start': 0,\n    'padding_block_end': 0,\n    'padding_inline_end': 0,\n    'inset_block_start': 'auto',\n    'inset_inline_start': 'auto',\n    'inset_block_end': 'auto',\n    'inset_inline_end': 'auto',\n    'border_block_start_width': 3,\n    'border_inline_start_width': 3,\n    'border_block_end_width': 3,\n    'border_inline_end_width': 3,\n    'border_block_start_style': 'none',\n    'border_inline_start_style': 'none',\n    'border_block_end_style': 'none',\n    'border_inline_end_style': 'none',\n    'border_block_start_color': 'currentcolor',\n    'border_inline_start_color': 'currentcolor',\n    'border_block_end_color': 'currentcolor',\n    'border_inline_end_color': 'currentcolor',\n    'border_start_start_radius': 0,\n    'border_start_end_radius': 0,\n    'border_end_start_radius': 0,\n    'border_end_end_radius': 0,\n\n    # Flexible Box Layout Module 1 (CR): https://www.w3.org/TR/css-flexbox-1/\n    'flex_basis': 'auto',\n    'flex_direction': 'row',\n    'flex_grow': 0,\n    'flex_shrink': 1,\n    'flex_wrap': 'nowrap',\n\n    # Grid Layout Module Level 2 (CR): https://www.w3.org/TR/css-grid-2/\n    'grid_auto_columns': ('auto',),\n    'grid_auto_flow': ('row',),\n    'grid_auto_rows': ('auto',),\n    'grid_template_areas': 'none',\n    'grid_template_columns': 'none',\n    'grid_template_rows': 'none',\n    'grid_row_start': 'auto',\n    'grid_column_start': 'auto',\n    'grid_row_end': 'auto',\n    'grid_column_end': 'auto',\n\n    # CSS Box Alignment Module Level 3 (WD): https://www.w3.org/TR/css-align-3/\n    'align_content': ('normal',),\n    'align_items': ('normal',),\n    'align_self': ('auto',),\n    'justify_content': ('normal',),\n    'justify_items': ('normal',),\n    'justify_self': ('auto',),\n    'order': 0,\n    'column_gap': 'normal',\n    'row_gap': 'normal',\n\n    # Text Decoration Module 3/4 (CR/WD): https://www.w3.org/TR/css-text-decor-4/\n    'text_decoration_line': 'none',\n    'text_decoration_color': 'currentcolor',\n    'text_decoration_style': 'solid',\n    'text_decoration_thickness': 'auto',\n    'text_underline_offset': 'auto',\n\n    # Overflow Module 3/4 (WD): https://www.w3.org/TR/css-overflow-4/\n    'block_ellipsis': 'none',\n    'continue': 'auto',\n    'max_lines': 'none',\n    'overflow': 'visible',\n    'overflow_wrap': 'normal',\n    'text_overflow': 'clip',\n\n    # Lists Module 3 (WD): https://drafts.csswg.org/css-lists-3/\n    # Means 'none', but allow `display: list-item` to increment the\n    # list-item counter. If we ever have a way for authors to query\n    # computed values (JavaScript?), this value should serialize to 'none'.\n    'counter_increment': 'auto',\n    'counter_reset': (),  # parsed value for 'none'\n    'counter_set': (),  # parsed value for 'none'\n    'list_style_image': ('none', None),\n    'list_style_position': 'outside',\n    'list_style_type': 'disc',\n\n    # Proprietary\n    'anchor': None,  # computed value of 'none'\n    'link': None,  # computed value of 'none'\n    'lang': None,  # computed value of 'none'\n}\n\n\nKNOWN_PROPERTIES = set(name.replace('_', '-') for name in INITIAL_VALUES)\n\n# Do not list shorthand properties here as we handle them before inheritance.\n#\n# Values inherited but not applicable to print are not included.\n#\n# text_decoration is not a really inherited, see\n# https://www.w3.org/TR/CSS2/text.html#propdef-text-decoration\n#\n# link: click events normally bubble up to link ancestors\n#   See https://lists.w3.org/Archives/Public/www-style/2012Jun/0315.html\nINHERITED = {\n    'block_ellipsis',\n    'border_collapse',\n    'border_spacing',\n    'caption_side',\n    'color',\n    'color_scheme',\n    'direction',\n    'empty_cells',\n    'font_family',\n    'font_feature_settings',\n    'font_kerning',\n    'font_language_override',\n    'font_size',\n    'font_style',\n    'font_stretch',\n    'font_variant',\n    'font_variant_alternates',\n    'font_variant_caps',\n    'font_variant_east_asian',\n    'font_variant_ligatures',\n    'font_variant_numeric',\n    'font_variant_position',\n    'font_variation_settings',\n    'font_weight',\n    'hyphens',\n    'hyphenate_character',\n    'hyphenate_limit_chars',\n    'hyphenate_limit_zone',\n    'image_rendering',\n    'image_resolution',\n    'lang',\n    'letter_spacing',\n    'line_height',\n    'link',\n    'list_style_image',\n    'list_style_position',\n    'list_style_type',\n    'orphans',\n    'overflow_wrap',\n    'quotes',\n    'tab_size',\n    'text_align_all',\n    'text_align_last',\n    'text_indent',\n    'text_transform',\n    'text_underline_offset',\n    'visibility',\n    'white_space',\n    'widows',\n    'word_break',\n    'word_spacing',\n}\n\n\n# https://www.w3.org/TR/CSS21/tables.html#model\n# See also https://lists.w3.org/Archives/Public/www-style/2012Jun/0066.html\n# Only non-inherited properties need to be included here.\nTABLE_WRAPPER_BOX_PROPERTIES = {\n    'bottom',\n    'break_after',\n    'break_before',\n    'clear',\n    'counter_increment',\n    'counter_reset',\n    'counter_set',\n    'float',\n    'left',\n    'margin_top',\n    'margin_bottom',\n    'margin_left',\n    'margin_right',\n    'margin_block_start',\n    'margin_block_end',\n    'margin_inline_start',\n    'margin_inline_end',\n    'opacity',\n    'overflow',\n    'position',\n    'right',\n    'top',\n    'transform',\n    'transform_origin',\n    'vertical_align',\n    'z_index',\n}\n\n\n# Properties that have an initial value that is not always the same when\n# computed.\nINITIAL_NOT_COMPUTED = {\n    'display',\n    'column_gap',\n    'bleed_top',\n    'bleed_left',\n    'bleed_bottom',\n    'bleed_right',\n    'outline_width',\n    'outline_color',\n    'column_rule_width',\n    'column_rule_color',\n    'border_top_width',\n    'border_left_width',\n    'border_bottom_width',\n    'border_right_width',\n    'border_top_color',\n    'border_left_color',\n    'border_bottom_color',\n    'border_right_color',\n    'border_block_start_width',\n    'border_inline_start_width',\n    'border_block_end_width',\n    'border_inline_end_width',\n    'border_block_start_color',\n    'border_inline_start_color',\n    'border_block_end_color',\n    'border_inline_end_color',\n    'background_color',\n}\n"
  },
  {
    "path": "weasyprint/css/targets.py",
    "content": "\"\"\"Handle target-counter, target-counters and target-text.\n\nThe TargetCollector is a structure providing required targets' counter_values\nand stuff needed to build pending targets later, when the layout of all\ntargeted anchors has been done.\n\n\"\"\"\n\nimport copy\n\nfrom ..logger import LOGGER\n\n\nclass TargetLookupItem:\n    \"\"\"Item controlling pending targets and page based target counters.\n\n    Collected in the TargetCollector's ``target_lookup_items``.\n\n    \"\"\"\n    def __init__(self, state='pending'):\n        self.state = state\n\n        # Required by target-counter and target-counters to access the\n        # target's .cached_counter_values.\n        # Needed for target-text via extract_text().\n        self.target_box = None\n\n        # Functions that have to been called to check pending targets.\n        # Keys are (source_box, css_token).\n        self.parse_again_functions = {}\n\n        # Anchor position during pagination (page_number - 1)\n        self.page_maker_index = None\n\n        # target_box's page_counters during pagination\n        self.cached_page_counter_values = {}\n\n\nclass CounterLookupItem:\n    \"\"\"Item controlling page based counters.\n\n    Collected in the TargetCollector's ``counter_lookup_items``.\n\n    \"\"\"\n    def __init__(self, parse_again, missing_counters, missing_target_counters):\n        # Function that have to been called to check pending counter.\n        self.parse_again = parse_again\n\n        # Missing counters and target counters\n        self.missing_counters = missing_counters\n        self.missing_target_counters = missing_target_counters\n\n        # Box position during pagination (page_number - 1)\n        self.page_maker_index = None\n\n        # Marker for remake_page\n        self.pending = False\n\n        # Targeting box's page_counters during pagination\n        self.cached_page_counter_values = {}\n\n\ndef anchor_name_from_token(anchor_token):\n    \"\"\"Get anchor name from string or uri token.\"\"\"\n    if anchor_token[0] == 'string' and anchor_token[1].startswith('#'):\n        return anchor_token[1][1:]\n    elif anchor_token[0] == 'url' and anchor_token[1][0] == 'internal':\n        return anchor_token[1][1]\n\n\nclass TargetCollector:\n    \"\"\"Collector of HTML targets used by CSS content with ``target-*``.\"\"\"\n\n    def __init__(self):\n        # Lookup items for targets and page counters\n        self.target_lookup_items = {}\n        self.counter_lookup_items = {}\n\n        # When collecting is True, compute_content_list() collects missing\n        # page counters in CounterLookupItems. Otherwise, it mixes in the\n        # TargetLookupItem's cached_page_counter_values.\n        # Is switched to False in check_pending_targets().\n        self.collecting = True\n\n        # had_pending_targets is set to True when a target is needed but has\n        # not been seen yet. check_pending_targets then uses this information\n        # to call the needed parse_again functions.\n        self.had_pending_targets = False\n\n    def collect_anchor(self, anchor_name):\n        \"\"\"Create a TargetLookupItem for the given `anchor_name``.\"\"\"\n        if isinstance(anchor_name, str):\n            if self.target_lookup_items.get(anchor_name) is not None:\n                LOGGER.warning('Anchor defined twice: %r', anchor_name)\n            else:\n                self.target_lookup_items.setdefault(\n                    anchor_name, TargetLookupItem())\n\n    def lookup_target(self, anchor_token, source_box, css_token, parse_again):\n        \"\"\"Get a TargetLookupItem corresponding to ``anchor_token``.\n\n        If it is already filled by a previous anchor-element, the status is\n        'up-to-date'. Otherwise, it is 'pending', we must parse the whole\n        tree again.\n\n        \"\"\"\n        anchor_name = anchor_name_from_token(anchor_token)\n        item = self.target_lookup_items.get(\n            anchor_name, TargetLookupItem('undefined'))\n\n        if item.state == 'pending':\n            self.had_pending_targets = True\n            item.parse_again_functions.setdefault(\n                (source_box, css_token), parse_again)\n\n        if item.state == 'undefined':\n            LOGGER.error(\n                'Content discarded: target points to undefined anchor %r',\n                anchor_token)\n\n        return item\n\n    def store_target(self, anchor_name, target_counter_values, target_box):\n        \"\"\"Store a target called ``anchor_name``.\n\n        If there is a pending TargetLookupItem, it is updated. Only previously\n        collected anchors are stored.\n\n        \"\"\"\n        item = self.target_lookup_items.get(anchor_name)\n        if item and item.state == 'pending':\n            item.state = 'up-to-date'\n            item.target_box = target_box\n            # Store the counter_values in the target_box like\n            # compute_content_list does.\n            if target_box.cached_counter_values is None:\n                target_box.cached_counter_values = {\n                    key: value.copy() for key, value\n                    in target_counter_values.items()}\n\n    def collect_missing_counters(self, parent_box, css_token,\n                                 parse_again_function, missing_counters,\n                                 missing_target_counters):\n        \"\"\"Collect missing (probably page-based) counters during formatting.\n\n        The ``missing_counters`` are re-used during pagination.\n\n        The ``missing_link`` attribute added to the parent_box is required to\n        connect the paginated boxes to their originating ``parent_box``.\n\n        \"\"\"\n        # No counter collection during pagination\n        if not self.collecting:\n            return\n\n        # No need to add empty miss-lists\n        if missing_counters or missing_target_counters:\n            if parent_box.missing_link is None:\n                parent_box.missing_link = parent_box\n            counter_lookup_item = CounterLookupItem(\n                parse_again_function, missing_counters,\n                missing_target_counters)\n            self.counter_lookup_items.setdefault(\n                (parent_box, css_token), counter_lookup_item)\n\n    def check_pending_targets(self):\n        \"\"\"Check pending targets if needed.\"\"\"\n        if self.had_pending_targets:\n            for item in self.target_lookup_items.values():\n                for function in item.parse_again_functions.values():\n                    function()\n            self.had_pending_targets = False\n        # Ready for pagination\n        self.collecting = False\n\n    def cache_target_page_counters(self, anchor_name, page_counter_values,\n                                   page_maker_index, page_maker):\n        \"\"\"Store target's current ``page_maker_index`` and page counter values.\n\n        Eventually update associated targeting boxes.\n\n        \"\"\"\n        # Only store page counters when paginating\n        if self.collecting:\n            return\n\n        item = self.target_lookup_items.get(anchor_name)\n        if item and item.state == 'up-to-date':\n            item.page_maker_index = page_maker_index\n            if item.cached_page_counter_values != page_counter_values:\n                item.cached_page_counter_values = copy.deepcopy(\n                    page_counter_values)\n\n                # Spread the news: update boxes affected by a change in the\n                # anchor's page counter values.\n                for (_, css_token), item in self.counter_lookup_items.items():\n                    # Only update items that need counters in their content\n                    if css_token != 'content':\n                        continue\n\n                    # Don't update if item has no missing target counter\n                    missing_counters = item.missing_target_counters.get(\n                        anchor_name)\n                    if missing_counters is None:\n                        continue\n\n                    # Pending marker for remake_page\n                    if (item.page_maker_index is None or\n                            item.page_maker_index >= len(page_maker)):\n                        item.pending = True\n                        continue\n\n                    # TODO: Is the item at all interested in the new\n                    # page_counter_values? It probably is and this check is a\n                    # brake.\n                    for counter_name in missing_counters:\n                        counter_value = page_counter_values.get(counter_name)\n                        if counter_value is not None:\n                            remake_state = (\n                                page_maker[item.page_maker_index][-1])\n                            remake_state['content_changed'] = True\n                            item.parse_again(item.cached_page_counter_values)\n                            break\n                    # Hint: the box's own cached page counters trigger a\n                    # separate 'content_changed'.\n"
  },
  {
    "path": "weasyprint/css/tokens.py",
    "content": "\"\"\"CSS tokens parsers.\"\"\"\n\nimport functools\nfrom abc import ABC, abstractmethod\nfrom math import e, inf, nan, pi\n\nfrom tinycss2.ast import DimensionToken, IdentToken, NumberToken, PercentageToken\nfrom tinycss2.color5 import parse_color\n\nfrom ..logger import LOGGER\nfrom ..urls import get_url_tuple\nfrom . import functions\nfrom .functions import check_math\nfrom .properties import Dimension\nfrom .units import ANGLE_TO_RADIANS, LENGTH_UNITS, RESOLUTION_TO_DPPX\n\nZERO_PERCENT = Dimension(0, '%')\nFIFTY_PERCENT = Dimension(50, '%')\nHUNDRED_PERCENT = Dimension(100, '%')\nBACKGROUND_POSITION_PERCENTAGES = {\n    'top': ZERO_PERCENT,\n    'left': ZERO_PERCENT,\n    'center': FIFTY_PERCENT,\n    'bottom': HUNDRED_PERCENT,\n    'right': HUNDRED_PERCENT,\n}\n\nDIRECTION_KEYWORDS = {\n    # ('angle', radians), 0 upwards, then clockwise.\n    ('to', 'top'): ('angle', 0),\n    ('to', 'right'): ('angle', pi / 2),\n    ('to', 'bottom'): ('angle', pi),\n    ('to', 'left'): ('angle', pi * 3 / 2),\n    # ('corner', keyword).\n    ('to', 'top', 'left'): ('corner', 'top_left'),\n    ('to', 'left', 'top'): ('corner', 'top_left'),\n    ('to', 'top', 'right'): ('corner', 'top_right'),\n    ('to', 'right', 'top'): ('corner', 'top_right'),\n    ('to', 'bottom', 'left'): ('corner', 'bottom_left'),\n    ('to', 'left', 'bottom'): ('corner', 'bottom_left'),\n    ('to', 'bottom', 'right'): ('corner', 'bottom_right'),\n    ('to', 'right', 'bottom'): ('corner', 'bottom_right'),\n}\n\nE = NumberToken(0, 0, e, None, 'e')\nPI = NumberToken(0, 0, pi, None, 'π')\nPLUS_INFINITY = NumberToken(0, 0, inf, None, '∞')\nMINUS_INFINITY = NumberToken(0, 0, -inf, None, '-∞')\nNAN = NumberToken(0, 0, nan, None, 'NaN')\n\n\nclass InvalidValues(ValueError):  # noqa: N818\n    \"\"\"Invalid or unsupported values for a known CSS property.\"\"\"\n\n\nclass PercentageInMath(ValueError):  # noqa: N818\n    \"\"\"Percentage in math function without reference length.\"\"\"\n\n\nclass RelativeLengthInMath(ValueError):  # noqa: N818\n    \"\"\"Relative length unit in math function without reference style.\"\"\"\n\n\nclass Pending(ABC):\n    \"\"\"Abstract class representing property value with pending validation.\"\"\"\n    # See https://drafts.csswg.org/css-variables-2/#variables-in-shorthands.\n    def __init__(self, tokens, name):\n        self.tokens = tokens\n        self.name = name\n        self._reported_error = False\n\n    @abstractmethod\n    def validate(self, tokens, wanted_key):\n        \"\"\"Get validated value for wanted key.\"\"\"\n        raise NotImplementedError\n\n    def solve(self, tokens, wanted_key):\n        \"\"\"Get validated value or raise error.\"\"\"\n        try:\n            if not tokens:\n                # Having no tokens is allowed by grammar but refused by all\n                # properties and expanders.\n                raise InvalidValues('no value')\n            return self.validate(tokens, wanted_key)\n        except InvalidValues as exception:\n            if self._reported_error:\n                raise exception\n            source_line = self.tokens[0].source_line\n            source_column = self.tokens[0].source_column\n            value = ' '.join(token.serialize() for token in tokens)\n            message = exception.args[0] if exception.args else 'invalid value'\n            LOGGER.warning(\n                'Ignored `%s: %s` at %d:%d, %s.',\n                self.name, value, source_line, source_column, message)\n            self._reported_error = True\n            raise exception\n\n\ndef parse_color_hint(tokens):\n    if len(tokens) == 1:\n        return get_length(tokens[0], percentage=True)\n\n\ndef parse_color_stop(tokens):\n    if len(tokens) == 1:\n        color = parse_color(tokens[0])\n        if color == 'currentcolor':\n            # TODO: return the current color instead\n            return parse_color('black'), None\n        if color is not None:\n            return color, None\n    elif len(tokens) == 2:\n        color = parse_color(tokens[0])\n        position = get_length(tokens[1], negative=True, percentage=True)\n        if color is not None and position is not None:\n            return color, position\n    raise InvalidValues\n\n\ndef parse_color_stops_and_hints(color_stops_hints):\n    if not color_stops_hints:\n        raise InvalidValues\n\n    color_stops = [parse_color_stop(color_stops_hints[0])]\n    color_hints = []\n    previous_was_color_stop = True\n\n    for tokens in color_stops_hints[1:]:\n        if hint := parse_color_hint(tokens):\n            color_hints.append(hint)\n            previous_was_color_stop = False\n        elif previous_was_color_stop:\n            color_hints.append(FIFTY_PERCENT)\n            color_stops.append(parse_color_stop(tokens))\n            previous_was_color_stop = True\n        else:\n            color_stops.append(parse_color_stop(tokens))\n            previous_was_color_stop = True\n\n    if not previous_was_color_stop:\n        raise InvalidValues\n\n    return color_stops, color_hints\n\n\ndef parse_linear_gradient_parameters(arguments):\n    first_arg = arguments[0]\n    if len(first_arg) == 1:\n        angle = get_angle(first_arg[0])\n        if angle is not None:\n            return ('angle', angle), arguments[1:]\n    else:\n        result = DIRECTION_KEYWORDS.get(tuple(map(get_keyword, first_arg)))\n        if result is not None:\n            return result, arguments[1:]\n    return ('angle', pi), arguments  # Default direction is 'to bottom'\n\n\ndef parse_2d_position(tokens):\n    \"\"\"Common syntax of background-position and transform-origin.\"\"\"\n    if len(tokens) == 1:\n        tokens = [tokens[0], IdentToken(0, 0, 'center')]\n    elif len(tokens) != 2:\n        return None\n\n    token_1, token_2 = tokens\n    length_1 = get_length(token_1, percentage=True)\n    length_2 = get_length(token_2, percentage=True)\n    if length_1 and length_2:\n        return length_1, length_2\n    keyword_1, keyword_2 = map(get_keyword, tokens)\n    if length_1 and keyword_2 in ('top', 'center', 'bottom'):\n        return length_1, BACKGROUND_POSITION_PERCENTAGES[keyword_2]\n    elif length_2 and keyword_1 in ('left', 'center', 'right'):\n        return BACKGROUND_POSITION_PERCENTAGES[keyword_1], length_2\n    elif (keyword_1 in ('left', 'center', 'right') and\n          keyword_2 in ('top', 'center', 'bottom')):\n        return (BACKGROUND_POSITION_PERCENTAGES[keyword_1],\n                BACKGROUND_POSITION_PERCENTAGES[keyword_2])\n    elif (keyword_1 in ('top', 'center', 'bottom') and\n          keyword_2 in ('left', 'center', 'right')):\n        # Swap tokens. They need to be in (horizontal, vertical) order.\n        return (BACKGROUND_POSITION_PERCENTAGES[keyword_2],\n                BACKGROUND_POSITION_PERCENTAGES[keyword_1])\n\n\ndef parse_position(tokens):\n    \"\"\"Parse background-position and object-position.\n\n    See https://drafts.csswg.org/css-backgrounds-3/#the-background-position\n    https://drafts.csswg.org/css-images-3/#propdef-object-position\n\n    \"\"\"\n    result = parse_2d_position(tokens)\n    if result is not None:\n        pos_x, pos_y = result\n        return 'left', pos_x, 'top', pos_y\n\n    if len(tokens) == 4:\n        keyword_1 = get_keyword(tokens[0])\n        keyword_2 = get_keyword(tokens[2])\n        length_1 = get_length(tokens[1], percentage=True)\n        length_2 = get_length(tokens[3], percentage=True)\n        if length_1 and length_2:\n            if (keyword_1 in ('left', 'right') and\n                    keyword_2 in ('top', 'bottom')):\n                return keyword_1, length_1, keyword_2, length_2\n            if (keyword_2 in ('left', 'right') and\n                    keyword_1 in ('top', 'bottom')):\n                return keyword_2, length_2, keyword_1, length_1\n\n    if len(tokens) == 3:\n        length = get_length(tokens[2], percentage=True)\n        if length is not None:\n            keyword = get_keyword(tokens[1])\n            other_keyword = get_keyword(tokens[0])\n        else:\n            length = get_length(tokens[1], percentage=True)\n            other_keyword = get_keyword(tokens[2])\n            keyword = get_keyword(tokens[0])\n\n        if length is not None:\n            if other_keyword == 'center':\n                if keyword in ('top', 'bottom'):\n                    return 'left', FIFTY_PERCENT, keyword, length\n                if keyword in ('left', 'right'):\n                    return keyword, length, 'top', FIFTY_PERCENT\n            elif (keyword in ('left', 'right') and\n                    other_keyword in ('top', 'bottom')):\n                return keyword, length, other_keyword, ZERO_PERCENT\n            elif (keyword in ('top', 'bottom') and\n                    other_keyword in ('left', 'right')):\n                return other_keyword, ZERO_PERCENT, keyword, length\n\n\ndef parse_radial_gradient_parameters(arguments):\n    shape = None\n    position = None\n    size = None\n    size_shape = None\n    stack = arguments[0][::-1]\n    while stack:\n        token = stack.pop()\n        keyword = get_keyword(token)\n        if keyword == 'at':\n            position = parse_position(stack[::-1])\n            if position is None:\n                return\n            break\n        elif keyword in ('circle', 'ellipse') and shape is None:\n            shape = keyword\n        elif keyword in ('closest-corner', 'farthest-corner',\n                         'closest-side', 'farthest-side') and size is None:\n            size = 'keyword', keyword\n        else:\n            if stack and size is None:\n                length_1 = get_length(token, percentage=True)\n                length_2 = get_length(stack[-1], percentage=True)\n                if None not in (length_1, length_2):\n                    size = 'explicit', (length_1, length_2)\n                    size_shape = 'ellipse'\n                    stack.pop()\n            if size is None:\n                length_1 = get_length(token)\n                if length_1 is not None:\n                    size = 'explicit', (length_1, length_1)\n                    size_shape = 'circle'\n            if size is None:\n                return\n    if (shape, size_shape) in (('circle', 'ellipse'), ('circle', 'ellipse')):\n        return\n    return (\n        shape or size_shape or 'ellipse',\n        size or ('keyword', 'farthest-corner'),\n        position or ('left', FIFTY_PERCENT, 'top', FIFTY_PERCENT),\n        arguments[1:])\n\n\ndef split_on_comma(tokens):\n    \"\"\"Split a list of tokens on commas, ie ``LiteralToken(',')``.\n\n    Only \"top-level\" comma tokens are splitting points, not commas inside a\n    function or blocks.\n\n    \"\"\"\n    parts = []\n    this_part = []\n    for token in tokens:\n        if token.type == 'literal' and token.value == ',':\n            parts.append(this_part)\n            this_part = []\n        else:\n            this_part.append(token)\n    parts.append(this_part)\n    return tuple(parts)\n\n\ndef remove_whitespace(tokens):\n    \"\"\"Remove any top-level whitespace and comments in a token list.\"\"\"\n    return tuple(\n        token for token in tokens\n        if token.type not in ('whitespace', 'comment'))\n\n\ndef get_keyword(token):\n    \"\"\"If ``token`` is a keyword, return its lowercase name.\n\n    Otherwise return ``None``.\n\n    \"\"\"\n    if token.type == 'ident':\n        return token.lower_value\n\n\ndef get_custom_ident(token):\n    \"\"\"If ``token`` is a keyword, return its name.\n\n    Otherwise return ``None``.\n\n    \"\"\"\n    if token.type == 'ident':\n        return token.value\n\n\ndef get_single_keyword(tokens):\n    \"\"\"If ``values`` is a 1-element list of keywords, return its name.\n\n    Otherwise return ``None``.\n\n    \"\"\"\n    if len(tokens) == 1:\n        token = tokens[0]\n        if token.type == 'ident':\n            return token.lower_value\n\n\ndef get_number(token, negative=True, integer=False):\n    \"\"\"Parse a <number> token.\"\"\"\n    from . import resolve_math\n\n    if check_math(token):\n        try:\n            resolved = resolve_math(token)\n        except (PercentageInMath, RelativeLengthInMath):\n            return\n        else:\n            if resolved is None:\n                return\n            if resolved.type != 'number':\n                return\n            value = resolved.value\n            if not negative and value < 0:\n                value = 0\n            if integer:\n                # TODO: always round x.5 to +inf, see\n                # https://drafts.csswg.org/css-values-4/#combine-integers.\n                value = round(value)\n            return Dimension(value, None)\n    elif token.type == 'number':\n        if integer:\n            if token.int_value is not None:\n                if negative or token.int_value >= 0:\n                    return Dimension(token.int_value, None)\n        elif negative or token.value >= 0:\n            return Dimension(token.value, None)\n\n\ndef get_string(token):\n    \"\"\"Parse a <string> token.\"\"\"\n    if token.type == 'string':\n        return ('string', token.value)\n    if token.type == 'function':\n        if token.name == 'attr':\n            return functions.check_attr(token, 'string')\n        elif token.name in ('counter', 'counters'):\n            return functions.check_counter(token)\n        elif token.name == 'content':\n            return functions.check_content(token)\n        elif token.name == 'string':\n            return functions.check_string_or_element('string', token)\n\n\ndef get_percentage(token, negative=True):\n    \"\"\"Parse a <percentage> token.\"\"\"\n    from . import resolve_math\n\n    if check_math(token):\n        try:\n            token = resolve_math(token) or token\n        except (PercentageInMath, RelativeLengthInMath):\n            return\n        else:\n            # Range clamp.\n            if not negative:\n                token.value = max(0, token.value)\n    if token.type == 'percentage' and (negative or token.value >= 0):\n        return Dimension(token.value, '%')\n\n\ndef get_length(token, negative=True, percentage=False):\n    \"\"\"Parse a <length> token.\"\"\"\n    from . import resolve_math\n\n    if check_math(token):\n        try:\n            token = resolve_math(token) or token\n        except PercentageInMath:\n            # PercentageInMath is raised in priority to help discarding percentages for\n            # properties that don’t allow them.\n            return token if percentage else None\n        except RelativeLengthInMath:\n            return token\n        else:\n            # Range clamp.\n            if not negative and token.type not in ('function', 'number'):\n                token.value = max(0, token.value)\n    if percentage and token.type == 'percentage':\n        if negative or token.value >= 0:\n            return Dimension(token.value, '%')\n    if token.type == 'dimension' and token.unit.lower() in LENGTH_UNITS:\n        if negative or token.value >= 0:\n            return Dimension(token.value, token.unit.lower())\n    if token.type == 'number' and token.value == 0:\n        return Dimension(0, None)\n\n\ndef get_angle(token):\n    \"\"\"Parse an <angle> token in radians.\"\"\"\n    from . import resolve_math\n\n    try:\n        token = resolve_math(token) or token\n    except (PercentageInMath, RelativeLengthInMath):\n        return\n    if token.type == 'number' and token.value == 0:\n        # Legacy syntax: https://drafts.csswg.org/css-values-4/#angles.\n        return 0\n    elif token.type == 'dimension':\n        factor = ANGLE_TO_RADIANS.get(token.unit.lower())\n        if factor is not None:\n            return token.value * factor\n\n\ndef get_resolution(token):\n    \"\"\"Parse a <resolution> token in dppx.\"\"\"\n    from . import resolve_math\n\n    try:\n        token = resolve_math(token) or token\n    except (PercentageInMath, RelativeLengthInMath):\n        return\n    if token.type == 'dimension':\n        factor = RESOLUTION_TO_DPPX.get(token.unit.lower())\n        if factor is not None:\n            return token.value * factor\n\n\ndef get_image(token, base_url):\n    \"\"\"Parse an <image> token.\"\"\"\n    from ..images import LinearGradient, RadialGradient\n\n    if parsed_url := get_url(token, base_url):\n        assert parsed_url[0] == 'url'\n        if parsed_url[1][0] == 'external':\n            return 'url', parsed_url[1][1]\n    function = functions.Function(token)\n    arguments = function.split_comma(single_tokens=False)\n    if not arguments:\n        return\n    repeating = function.name.startswith('repeating-')\n    if function.name in ('linear-gradient', 'repeating-linear-gradient'):\n        direction, color_stops = parse_linear_gradient_parameters(arguments)\n        color_stops, color_hints = parse_color_stops_and_hints(color_stops)\n        return 'linear-gradient', LinearGradient(\n            color_stops, direction, repeating, color_hints)\n    elif function.name in ('radial-gradient', 'repeating-radial-gradient'):\n        result = parse_radial_gradient_parameters(arguments)\n        if result is not None:\n            shape, size, position, color_stops = result\n        else:\n            shape = 'ellipse'\n            size = 'keyword', 'farthest-corner'\n            position = 'left', FIFTY_PERCENT, 'top', FIFTY_PERCENT\n            color_stops = arguments\n        color_stops, color_hints = parse_color_stops_and_hints(color_stops)\n        return 'radial-gradient', RadialGradient(\n            color_stops, shape, size, position, repeating, color_hints)\n\n\ndef get_url(token, base_url):\n    \"\"\"Parse an <url> token.\"\"\"\n    if token.type == 'url':\n        url = get_url_tuple(token.value, base_url)\n    elif token.type == 'function':\n        if token.name == 'attr':\n            return functions.check_attr(token, 'url')\n        elif token.name == 'url' and len(token.arguments) in (1, 2):\n            # Ignore url modifiers\n            # See https://drafts.csswg.org/css-values-3/#urls\n            url = get_url_tuple(token.arguments[0].value, base_url)\n        else:\n            return\n    else:\n        return\n\n    if url is None:\n        raise InvalidValues(f'Relative URI reference without a base URI: {url!r}')\n\n    return ('url', url)\n\n\ndef get_quote(token):\n    \"\"\"Parse a <quote> token.\"\"\"\n    keyword = get_keyword(token)\n    if keyword in (\n            'open-quote', 'close-quote',\n            'no-open-quote', 'no-close-quote'):\n        return keyword\n\n\ndef get_target(token, base_url):\n    \"\"\"Parse a <target> token.\"\"\"\n    function = functions.Function(token)\n    arguments = function.split_comma()\n    if function.name == 'target-counter':\n        if len(arguments) not in (2, 3):\n            return\n    elif function.name == 'target-counters':\n        if len(arguments) not in (3, 4):\n            return\n    elif function.name == 'target-text':\n        if len(arguments) not in (1, 2):\n            return\n    else:\n        return\n\n    values = []\n\n    link = arguments.pop(0)\n    string_link = get_string(link)\n    if string_link is None:\n        url = get_url(link, base_url)\n        if url is None:\n            return\n        values.append(url)\n    else:\n        values.append(string_link)\n\n    if function.name.startswith('target-counter'):\n        ident = arguments.pop(0)\n        if ident.type != 'ident':\n            return\n        values.append(ident.value)\n\n        if function.name == 'target-counters':\n            string = get_string(arguments.pop(0))\n            if string is None:\n                return\n            values.append(string)\n\n        if arguments:\n            counter_style = get_keyword(arguments.pop(0))\n        else:\n            counter_style = 'decimal'\n        values.append(counter_style)\n    else:\n        if arguments:\n            content = get_keyword(arguments.pop(0))\n            if content not in ('content', 'before', 'after', 'first-letter'):\n                return\n        else:\n            content = 'content'\n        values.append(content)\n\n    return (f'{function.name}()', tuple(values))\n\n\ndef get_content_list(tokens, base_url):\n    \"\"\"Parse <content-list> tokens.\"\"\"\n    # See https://www.w3.org/TR/css-content-3/#typedef-content-list\n    parsed_tokens = [get_content_list_token(token, base_url) for token in tokens]\n    if None not in parsed_tokens:\n        return parsed_tokens\n\n\ndef get_content_list_token(token, base_url):\n    \"\"\"Parse one of the <content-list> tokens.\"\"\"\n    # See https://drafts.csswg.org/css-content-3/#content-values.\n\n    # <string>\n    if (string := get_string(token)) is not None:\n        return string\n\n    # contents\n    if get_keyword(token) == 'contents':\n        return ('content()', 'text')\n\n    # <uri>\n    if (url := get_url(token, base_url)) is not None:\n        return url\n\n    # <quote>\n    if (quote := get_quote(token)) is not None:\n        return ('quote', quote)\n\n    # <target>\n    if (target := get_target(token, base_url)) is not None:\n        return target\n\n    function = functions.Function(token)\n    arguments = function.split_comma()\n\n    # <leader()>\n    if function.name == 'leader':\n        if len(arguments) != 1:\n            return\n        arg, = arguments\n        if arg.type == 'ident':\n            if arg.value == 'dotted':\n                string = '.'\n            elif arg.value == 'solid':\n                string = '_'\n            elif arg.value == 'space':\n                string = ' '\n            else:\n                return\n        elif arg.type == 'string':\n            string = arg.value\n        return ('leader()', ('string', string))\n\n    # <element()>\n    elif function.name == 'element':\n        return functions.check_string_or_element('element', token)\n\n\ndef single_keyword(function):\n    \"\"\"Decorator for validators that only accept a single keyword.\"\"\"\n    @functools.wraps(function)\n    def keyword_validator(tokens):\n        \"\"\"Wrap a validator to call get_single_keyword on tokens.\"\"\"\n        keyword = get_single_keyword(tokens)\n        if function(keyword):\n            return keyword\n    return keyword_validator\n\n\ndef single_token(function):\n    \"\"\"Decorator for validators that only accept a single token.\"\"\"\n    @functools.wraps(function)\n    def single_token_validator(tokens, *args):\n        \"\"\"Validate a property whose token is single.\"\"\"\n        if len(tokens) == 1:\n            return function(tokens[0], *args)\n    single_token_validator.__func__ = function\n    return single_token_validator\n\n\ndef comma_separated_list(function):\n    \"\"\"Decorator for validators that accept a comma separated list.\"\"\"\n    @functools.wraps(function)\n    def wrapper(tokens, *args):\n        results = []\n        for part in split_on_comma(tokens):\n            result = function(remove_whitespace(part), *args)\n            if result is None:\n                return None\n            results.append(result)\n        return tuple(results)\n    wrapper.single_value = function\n    return wrapper\n\n\ndef tokenize(item, function=None, unit=None):\n    \"\"\"Transform a computed value result into a token.\"\"\"\n    if isinstance(item, (DimensionToken, Dimension)):\n        value = function(item.value) if function else item.value\n        return DimensionToken(0, 0, value, None, str(value), item.unit.lower())\n    elif isinstance(item, PercentageToken):\n        value = function(item.value) if function else item.value\n        return PercentageToken(0, 0, value, None, str(value))\n    elif isinstance(item, (NumberToken, int, float)):\n        if isinstance(item, NumberToken):\n            value = item.value\n        else:\n            value = item\n        value = function(value) if function else value\n        int_value = round(value) if float(value).is_integer() else None\n        representation = str(int_value if float(value).is_integer() else value)\n        if unit is None:\n            return NumberToken(0, 0, value, int_value, representation)\n        elif unit == '%':\n            return PercentageToken(0, 0, value, int_value, representation)\n        else:\n            return DimensionToken(0, 0, value, int_value, representation, unit)\n"
  },
  {
    "path": "weasyprint/css/units.py",
    "content": "\"\"\"Constants and helpers for units.\"\"\"\n\nimport math\n\nfrom ..logger import LOGGER\nfrom ..text.line_break import character_ratio, strut\n\n# How many radians is one <unit>?\n# https://drafts.csswg.org/css-values-4/#angles\nANGLE_TO_RADIANS = {\n    'rad': 1,\n    'turn': 2 * math.pi,\n    'deg': math.pi / 180,\n    'grad': math.pi / 200,\n}\n\n# How many CSS pixels is one <unit>?\n# https://www.w3.org/TR/CSS21/syndata.html#length-units\nLENGTHS_TO_PIXELS = {\n    'px': 1,\n    'pt': 1 / 0.75,\n    'pc': 16,\n    'in': 96,\n    'cm': 96 / 2.54,\n    'mm': 96 / 25.4,\n    'q': 96 / 25.4 / 4,\n}\n\n# How many dppx is one <unit>?\n# https://drafts.csswg.org/css-values/#resolution\nRESOLUTION_TO_DPPX = {\n    'dppx': 1,\n    'x': 1,\n    'dpi': 1 / LENGTHS_TO_PIXELS['in'],\n    'dpcm': 1 / LENGTHS_TO_PIXELS['cm'],\n}\n\n# Sets of units.\n# https://drafts.csswg.org/css-values-4/#lengths\nABSOLUTE_UNITS = set(LENGTHS_TO_PIXELS)\nFONT_UNITS = {\n     'em',  'ex',  'cap',  'ch',  'ic',  'lh',\n    'rem', 'rex', 'rcap', 'rch', 'ric', 'rlh',\n}\nVIEWPORT_UNITS = {\n     'vw',  'vh',  'vi',  'vb',  'vmin',  'vmax',\n    'lvw', 'lvh', 'lvi', 'lvb', 'lvmin', 'lvmax',\n    'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax',\n    'dvw', 'dvh', 'dvi', 'dvb', 'dvmin', 'dvmax',\n    'pvw', 'pvh', 'pvi', 'pvb', 'pvmin', 'pvmax',\n}\nRELATIVE_UNITS = FONT_UNITS | VIEWPORT_UNITS\nLENGTH_UNITS = ABSOLUTE_UNITS | RELATIVE_UNITS\n# https://drafts.csswg.org/css-values-4/#angles\nANGLE_UNITS = set(ANGLE_TO_RADIANS)\n\n\ndef to_pixels(value, style, property_name, font_size=None):\n    \"\"\"Get number of pixels corresponding to a length.\"\"\"\n    if value.value == 0:\n        return 0\n    elif (unit := value.unit.lower()) == 'px':\n        return value.value\n    elif unit in LENGTHS_TO_PIXELS:\n        # Convert absolute lengths to pixels.\n        return value.value * LENGTHS_TO_PIXELS[unit]\n    elif unit in FONT_UNITS:\n        assert (style, font_size) != (None, None)\n        if font_size is None:\n            font_size = style['font_size']\n        if unit == 'lh':\n            if property_name in ('font_size', 'line_height'):\n                if style.parent_style is None:\n                    parent_style = style.root_style\n                else:\n                    parent_style = style.parent_style\n                line_height, _ = strut(parent_style)\n            else:\n                line_height, _ = strut(style)\n            return value.value * line_height\n        elif unit == 'rlh':\n            parent_style = style.root_style\n            line_height, _ = strut(parent_style)\n            return value.value * line_height\n        elif unit == 'em':\n            return value.value * font_size\n        elif unit == 'rem':\n            return value.value * style.root_style['font_size']\n        elif unit.startswith('r'):\n            ratio = character_ratio(style.root_style, unit[1:])\n            return value.value * style.root_style['font_size'] * ratio\n        else:\n            ratio = character_ratio(style, unit)\n            return value.value * font_size * ratio\n    elif unit in VIEWPORT_UNITS:\n        page_size = style.initial_page_sizes['box' if unit[0] == 'p' else 'area']\n        if page_size is None:\n            LOGGER.warn(f'{unit} unit resolved before first page layout')\n            from .computed_values import INITIAL_PAGE_SIZE\n            page_width = to_pixels(INITIAL_PAGE_SIZE[0], None, None)\n            page_height = to_pixels(INITIAL_PAGE_SIZE[1], None, None)\n        else:\n            page_width, page_height = page_size\n        # TODO: use writing-mode for vi and vb.\n        if unit.endswith(('vw', 'vi')):\n            return value.value / 100 * page_width\n        elif unit.endswith(('vh', 'vb')):\n            return value.value / 100 * page_height\n        elif unit.endswith('vmin'):\n            return value.value / 100 * min(page_width, page_height)\n        elif unit.endswith('vmax'):\n            return value.value / 100 * max(page_width, page_height)\n\n\ndef to_radians(value):\n    \"\"\"Get number of radians corresponding to an angle.\"\"\"\n    if (unit := value.unit.lower()) == 'rad':\n        return value.value\n    elif unit in ANGLE_TO_RADIANS:\n        return value.value * ANGLE_TO_RADIANS[unit]\n"
  },
  {
    "path": "weasyprint/css/validation/__init__.py",
    "content": "\"\"\"Validate properties, expanders and descriptors.\"\"\"\n\nfrom cssselect2 import SelectorError, compile_selector_list\nfrom tinycss2 import parse_blocks_contents, serialize\nfrom tinycss2.ast import FunctionBlock, IdentToken, LiteralToken, WhitespaceToken\n\nfrom ... import LOGGER\nfrom ..tokens import InvalidValues, remove_whitespace\nfrom .expanders import EXPANDERS\nfrom .properties import PREFIX, PROPRIETARY, UNSTABLE, validate_non_shorthand\n\n# Not applicable to the print media\nNOT_PRINT_MEDIA = {\n    # Aural media\n    'azimuth',\n    'cue',\n    'cue-after',\n    'cue-before',\n    'elevation',\n    'pause',\n    'pause-after',\n    'pause-before',\n    'pitch-range',\n    'pitch',\n    'play-during',\n    'richness',\n    'speak-header',\n    'speak-numeral',\n    'speak-punctuation',\n    'speak',\n    'speech-rate',\n    'stress',\n    'voice-family',\n    'volume',\n    # Animations, transitions, timelines\n    'animation',\n    'animation-composition',\n    'animation-delay',\n    'animation-direction',\n    'animation-duration',\n    'animation-fill-mode',\n    'animation-iteration-count',\n    'animation-name',\n    'animation-play-state',\n    'animation-range',\n    'animation-range-end',\n    'animation-range-start',\n    'animation-timeline',\n    'animation-timing-function',\n    'timeline-scope',\n    'transition',\n    'transition-delay',\n    'transition-duration',\n    'transition-property',\n    'transition-timing-function',\n    'view-timeline',\n    'view-timeline-axis',\n    'view-timeline-inset',\n    'view-timeline-name',\n    'view-transition-name',\n    'will-change',\n    # Dynamic and interactive\n    'caret',\n    'caret-color',\n    'caret-shape',\n    'cursor',\n    'field-sizing',\n    'pointer-events',\n    'resize',\n    'touch-action',\n    # Browser viewport scrolling\n    'overscroll-behavior',\n    'overscroll-behavior-block',\n    'overscroll-behavior-inline',\n    'overscroll-behavior-x',\n    'overscroll-behavior-y',\n    'scroll-behavior',\n    'scroll-margin',\n    'scroll-margin-block',\n    'scroll-margin-block-end',\n    'scroll-margin-block-start',\n    'scroll-margin-bottom',\n    'scroll-margin-inline',\n    'scroll-margin-inline-end',\n    'scroll-margin-inline-start',\n    'scroll-margin-left',\n    'scroll-margin-right',\n    'scroll-margin-top',\n    'scroll-padding',\n    'scroll-padding-block',\n    'scroll-padding-block-end',\n    'scroll-padding-block-start',\n    'scroll-padding-bottom',\n    'scroll-padding-inline',\n    'scroll-padding-inline-end',\n    'scroll-padding-inline-start',\n    'scroll-padding-left',\n    'scroll-padding-right',\n    'scroll-padding-top',\n    'scroll-snap-align',\n    'scroll-snap-stop',\n    'scroll-snap-type',\n    'scroll-timeline',\n    'scroll-timeline-axis',\n    'scroll-timeline-name',\n    'scrollbar-color',\n    'scrollbar-gutter',\n    'scrollbar-width',\n}\nNESTING_SELECTOR = LiteralToken(1, 1, '&')\nROOT_TOKEN = LiteralToken(1, 1, ':'), IdentToken(1, 1, 'root')\n\n\ndef preprocess_declarations(base_url, declarations, prelude=None):\n    \"\"\"Expand shorthand properties, filter unsupported properties and values.\n\n    Log a warning for every ignored declaration.\n\n    Return a iterable of ``(name, value, important)`` tuples.\n\n    \"\"\"\n    # Compile list of selectors.\n    if prelude is not None:\n        try:\n            if NESTING_SELECTOR in prelude:\n                # Handle & selector in non-nested rule. MDN explains that & is\n                # then equivalent to :scope, and :scope is equivalent to :root\n                # as we don’t support :scope yet.\n                original_prelude, prelude = prelude, []\n                for token in original_prelude:\n                    if token == NESTING_SELECTOR:\n                        prelude.extend(ROOT_TOKEN)\n                    else:\n                        prelude.append(token)\n            selectors = compile_selector_list(prelude)\n        except SelectorError:\n            raise SelectorError(f\"'{serialize(prelude)}'\")\n\n    # Yield declarations.\n    is_token = LiteralToken(1, 1, ':'), FunctionBlock(1, 1, 'is', prelude)\n    for declaration in declarations:\n        if declaration.type == 'error':\n            LOGGER.warning(\n                'Error: %s at %d:%d.',\n                declaration.message,\n                declaration.source_line, declaration.source_column)\n\n        if declaration.type == 'qualified-rule':\n            # Nested rule.\n            if prelude is None:\n                continue\n            declaration_prelude = []\n            token_groups = [[]]\n            for token in declaration.prelude:\n                if token == ',':\n                    token_groups.append([])\n                else:\n                    token_groups[-1].append(token)\n            for token_group in token_groups:\n                if NESTING_SELECTOR in token_group:\n                    # Replace & selector by parent.\n                    for token in declaration.prelude:\n                        if token == NESTING_SELECTOR:\n                            declaration_prelude.extend(is_token)\n                        else:\n                            declaration_prelude.append(token)\n                else:\n                    # No & selector, prepend parent.\n                    is_token = (\n                        LiteralToken(1, 1, ':'),\n                        FunctionBlock(1, 1, 'is', prelude))\n                    declaration_prelude.extend([\n                        *is_token, WhitespaceToken(1, 1, ' '),\n                        *token_group])\n                declaration_prelude.append(LiteralToken(1, 1, ','))\n            yield from preprocess_declarations(\n                base_url, parse_blocks_contents(declaration.content),\n                declaration_prelude[:-1])\n\n        if declaration.type != 'declaration':\n            continue\n\n        name = declaration.name\n        if not name.startswith('--'):\n            name = declaration.lower_name\n\n        def validation_error(level, reason):\n            getattr(LOGGER, level)(\n                'Ignored `%s:%s` at %d:%d, %s.',\n                declaration.name, serialize(declaration.value),\n                declaration.source_line, declaration.source_column, reason)\n\n        if name in NOT_PRINT_MEDIA:\n            validation_error(\n                'debug', 'the property does not apply for the print media')\n            continue\n\n        if name.startswith(PREFIX):\n            unprefixed_name = name[len(PREFIX):]\n            if unprefixed_name in PROPRIETARY:\n                name = unprefixed_name\n            elif unprefixed_name in UNSTABLE:\n                LOGGER.warning(\n                    'Deprecated `%s:%s` at %d:%d, '\n                    'prefixes on unstable attributes are deprecated, '\n                    'use %r instead.',\n                    declaration.name, serialize(declaration.value),\n                    declaration.source_line, declaration.source_column,\n                    unprefixed_name)\n                name = unprefixed_name\n            else:\n                LOGGER.warning(\n                    'Ignored `%s:%s` at %d:%d, '\n                    'prefix on this attribute is not supported, '\n                    'use %r instead.',\n                    declaration.name, serialize(declaration.value),\n                    declaration.source_line, declaration.source_column,\n                    unprefixed_name)\n                continue\n\n        if name.startswith('-') and not name.startswith('--'):\n            validation_error('debug', 'prefixed selectors are ignored')\n            continue\n\n        validator = EXPANDERS.get(name, validate_non_shorthand)\n        tokens = remove_whitespace(declaration.value)\n        try:\n            # Having no tokens is allowed by grammar but refused by all\n            # properties and expanders.\n            if not tokens:\n                raise InvalidValues('no value')\n            # Use list() to consume generators now and catch any error.\n            result = list(validator(tokens, name, base_url))\n        except InvalidValues as exc:\n            validation_error(\n                'warning',\n                exc.args[0] if exc.args and exc.args[0] else 'invalid value')\n            continue\n\n        important = declaration.important\n        for long_name, value in result:\n            if prelude is not None:\n                declaration = (long_name.replace('-', '_'), value, important)\n                yield selectors, declaration\n            else:\n                yield long_name.replace('-', '_'), value, important\n"
  },
  {
    "path": "weasyprint/css/validation/descriptors.py",
    "content": "\"\"\"Validate descriptors used for some at-rules.\"\"\"\n\nfrom math import inf\n\nimport tinycss2\n\nfrom ...logger import LOGGER\nfrom . import properties\n\nfrom ..tokens import (  # isort:skip\n    InvalidValues, comma_separated_list, get_custom_ident, get_keyword, get_number,\n    get_single_keyword, get_url, remove_whitespace, single_keyword, single_token,\n    split_on_comma)\n\nDESCRIPTORS = {\n    'font-face': {},\n    'counter-style': {},\n    'color-profile': {},\n}\nNOT_PRINT_MEDIA = (\n    'font-display',\n)\n\n\nclass NoneFakeToken:\n    type = 'ident'\n    lower_value = 'none'\n\n\nclass NormalFakeToken:\n    type = 'ident'\n    lower_value = 'normal'\n\n\ndef preprocess_descriptors(rule, base_url, descriptors):\n    \"\"\"Filter unsupported names and values for descriptors.\n\n    Log a warning for every ignored descriptor.\n\n    Return a iterable of ``(name, value)`` tuples.\n\n    \"\"\"\n    for descriptor in descriptors:\n        if descriptor.type != 'declaration' or descriptor.important:\n            continue\n        tokens = remove_whitespace(descriptor.value)\n        try:\n            if descriptor.name in NOT_PRINT_MEDIA:\n                continue\n            elif descriptor.name not in DESCRIPTORS[rule]:\n                raise InvalidValues('descriptor not supported')\n\n            function = DESCRIPTORS[rule][descriptor.name]\n            if function.wants_base_url:\n                value = function(tokens, base_url)\n            else:\n                value = function(tokens)\n            if value is None:\n                raise InvalidValues\n            result = ((descriptor.name, value),)\n        except InvalidValues as exc:\n            LOGGER.warning(\n                'Ignored `%s:%s` at %d:%d, %s.',\n                descriptor.name, tinycss2.serialize(descriptor.value),\n                descriptor.source_line, descriptor.source_column,\n                exc.args[0] if exc.args and exc.args[0] else 'invalid value')\n            continue\n\n        for long_name, value in result:\n            yield long_name.replace('-', '_'), value\n\n\ndef descriptor(rule, descriptor_name=None, wants_base_url=False):\n    \"\"\"Decorator adding a function to the ``DESCRIPTORS``.\n\n    The name of the descriptor covered by the decorated function is set to\n    ``descriptor_name`` if given, or is inferred from the function name\n    (replacing underscores by hyphens).\n\n    :param wants_base_url:\n        The function takes the stylesheet’s base URL as an additional\n        parameter.\n\n    \"\"\"\n    def decorator(function):\n        \"\"\"Add ``function`` to the ``DESCRIPTORS``.\"\"\"\n        if descriptor_name is None:\n            name = function.__name__.replace('_', '-')\n        else:\n            name = descriptor_name\n        assert name not in DESCRIPTORS[rule], name\n\n        function.wants_base_url = wants_base_url\n        DESCRIPTORS[rule][name] = function\n        return function\n    return decorator\n\n\ndef expand_font_variant(tokens):\n    keyword = get_single_keyword(tokens)\n    if keyword in ('normal', 'none'):\n        for suffix in (\n                '-alternates', '-caps', '-east-asian', '-numeric',\n                '-position'):\n            yield suffix, [NormalFakeToken]\n        token = NormalFakeToken if keyword == 'normal' else NoneFakeToken\n        yield '-ligatures', [token]\n    else:\n        features = {\n            'alternates': [],\n            'caps': [],\n            'east-asian': [],\n            'ligatures': [],\n            'numeric': [],\n            'position': []}\n        for token in tokens:\n            keyword = get_keyword(token)\n            if keyword == 'normal':\n                # We don't allow 'normal', only the specific values\n                raise InvalidValues\n            for feature in features:\n                function_name = f'font_variant_{feature.replace(\"-\", \"_\")}'\n                if getattr(properties, function_name)([token]):\n                    features[feature].append(token)\n                    break\n            else:\n                raise InvalidValues\n        for feature, tokens in features.items():\n            if tokens:\n                yield (f'-{feature}', tokens)\n\n\n@descriptor('font-face')\ndef font_family(tokens, allow_spaces=False):\n    \"\"\"``font-family`` descriptor validation.\"\"\"\n    allowed_types = ['ident']\n    if allow_spaces:\n        allowed_types.append('whitespace')\n    if len(tokens) == 1 and tokens[0].type == 'string':\n        return tokens[0].value\n    if tokens and all(token.type in allowed_types for token in tokens):\n        return ' '.join(\n            token.value for token in tokens if token.type == 'ident')\n\n\n@descriptor('font-face', wants_base_url=True)\n@comma_separated_list\ndef src(tokens, base_url):\n    \"\"\"``src`` descriptor validation.\"\"\"\n    if len(tokens) in (1, 2):\n        tokens, token = tokens[:-1], tokens[-1]\n        if token.type == 'function' and token.lower_name == 'format':\n            tokens, token = tokens[:-1], tokens[-1]\n        if token.type == 'function' and token.lower_name == 'local':\n            return 'local', font_family(token.arguments, allow_spaces=True)\n        url = get_url(token, base_url)\n        if url is not None and url[0] == 'url':\n            return url[1]\n\n\n@descriptor('font-face')\n@single_keyword\ndef font_style(keyword):\n    \"\"\"``font-style`` descriptor validation.\"\"\"\n    return keyword in ('normal', 'italic', 'oblique')\n\n\n@descriptor('font-face')\n@single_token\ndef font_weight(token):\n    \"\"\"``font-weight`` descriptor validation.\"\"\"\n    keyword = get_keyword(token)\n    if keyword in ('normal', 'bold'):\n        return keyword\n    if number := get_number(token, integer=True):\n        if number.value in (100, 200, 300, 400, 500, 600, 700, 800, 900):\n            return number.value\n\n\n@descriptor('font-face')\n@single_keyword\ndef font_stretch(keyword):\n    \"\"\"``font-stretch`` descriptor validation.\"\"\"\n    return keyword in (\n        'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed',\n        'normal',\n        'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded')\n\n\n@descriptor('font-face')\ndef font_feature_settings(tokens):\n    \"\"\"``font-feature-settings`` descriptor validation.\"\"\"\n    return properties.font_feature_settings(tokens)\n\n\n@descriptor('font-face')\ndef font_variant(tokens):\n    \"\"\"``font-variant`` descriptor validation.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword in ('normal', 'none', 'inherit'):\n            return []\n    values = []\n    for name, sub_tokens in expand_font_variant(tokens):\n        try:\n            values.append(properties.validate_non_shorthand(\n                sub_tokens, f'font-variant{name}', required=True))\n        except InvalidValues:\n            return None\n    return values\n\n\n@descriptor('font-face')\n@comma_separated_list\n@single_token\ndef unicode_range(token):\n    \"\"\"``unicode_range`` descriptor validation.\"\"\"\n    if token.type == 'unicode-range':\n        return token\n\n\n@descriptor('counter-style')\ndef system(tokens):\n    \"\"\"``system`` descriptor validation.\"\"\"\n    if len(tokens) > 2:\n        return\n\n    keyword = get_keyword(tokens[0])\n    if keyword == 'extends':\n        if len(tokens) == 2:\n            if second_keyword := get_keyword(tokens[1]):\n                return (keyword, second_keyword, None)\n    elif keyword == 'fixed':\n        if len(tokens) == 1:\n            return (None, 'fixed', 1)\n        elif number := get_number(tokens[1], integer=True):\n            return (None, 'fixed', number.value)\n    elif len(tokens) == 1 and keyword in (\n            'cyclic', 'numeric', 'alphabetic', 'symbolic', 'additive'):\n        return (None, keyword, None)\n\n\n@descriptor('counter-style', wants_base_url=True)\ndef negative(tokens, base_url):\n    \"\"\"``negative`` descriptor validation.\"\"\"\n    if len(tokens) > 2:\n        return\n\n    values = []\n    tokens = list(tokens)\n    while tokens:\n        token = tokens.pop(0)\n        if token.type in ('string', 'ident'):\n            values.append(('string', token.value))\n            continue\n        url = get_url(token, base_url)\n        if url is not None and url[0] == 'url':\n            values.append(('url', url[1]))\n\n    if len(values) == 1:\n        values.append(('string', ''))\n\n    if len(values) == 2:\n        return values\n\n\n@descriptor('counter-style', 'prefix', wants_base_url=True)\n@descriptor('counter-style', 'suffix', wants_base_url=True)\ndef prefix_suffix(tokens, base_url):\n    \"\"\"``prefix`` and ``suffix`` descriptors validation.\"\"\"\n    if len(tokens) != 1:\n        return\n\n    token, = tokens\n    if token.type in ('string', 'ident'):\n        return ('string', token.value)\n    url = get_url(token, base_url)\n    if url is not None and url[0] == 'url':\n        return ('url', url[1])\n\n\n@descriptor('counter-style')\n@comma_separated_list\ndef range(tokens):\n    \"\"\"``range`` descriptor validation.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_single_keyword(tokens)\n        if keyword == 'auto':\n            return 'auto'\n    elif len(tokens) == 2:\n        values = []\n        for i, token in enumerate(tokens):\n            if token.type == 'ident' and token.value == 'infinite':\n                values.append(inf if i else -inf)\n            elif number := get_number(token, integer=True):\n                values.append(number.value)\n        if len(values) == 2 and values[0] <= values[1]:\n            return tuple(values)\n\n\n@descriptor('counter-style', wants_base_url=True)\ndef pad(tokens, base_url):\n    \"\"\"``pad`` descriptor validation.\"\"\"\n    if len(tokens) == 2:\n        values = [None, None]\n        for token in tokens:\n            if number := get_number(token, integer=True, negative=False):\n                if values[0] is None:\n                    values[0] = number.value\n            elif token.type in ('string', 'ident'):\n                values[1] = ('string', token.value)\n            url = get_url(token, base_url)\n            if url is not None and url[0] == 'url':\n                values[1] = ('url', url[1])\n\n        if None not in values:\n            return tuple(values)\n\n\n@descriptor('counter-style')\n@single_token\ndef fallback(token):\n    \"\"\"``fallback`` descriptor validation.\"\"\"\n    ident = get_custom_ident(token)\n    if ident != 'none':\n        return ident\n\n\n@descriptor('counter-style', wants_base_url=True)\ndef symbols(tokens, base_url):\n    \"\"\"``symbols`` descriptor validation.\"\"\"\n    values = []\n    for token in tokens:\n        if token.type in ('string', 'ident'):\n            values.append(('string', token.value))\n            continue\n        url = get_url(token, base_url)\n        if url is not None and url[0] == 'url':\n            values.append(('url', url[1]))\n            continue\n        return\n    return tuple(values)\n\n\n@descriptor('counter-style', wants_base_url=True)\ndef additive_symbols(tokens, base_url):\n    \"\"\"``additive-symbols`` descriptor validation.\"\"\"\n    results = []\n    for part in split_on_comma(tokens):\n        if not (result := pad(remove_whitespace(part), base_url)):\n            return\n        if results and results[-1][0] <= result[0]:\n            return\n        results.append(result)\n    return tuple(results)\n\n\n@descriptor('color-profile', descriptor_name='src', wants_base_url=True)\n@single_token\ndef color_profile_src(token, base_url):\n    url = get_url(token, base_url)\n    if url is not None and url[0] == 'url':\n        return url[1]\n\n\n@descriptor('color-profile')\n@single_keyword\ndef rendering_intent(keyword):\n    possible_values = (\n        'relative-colorimetric', 'absolute-colorimetric', 'perceptual', 'saturation')\n    if keyword in possible_values:\n        return keyword\n\n\n@descriptor('color-profile')\n@comma_separated_list\ndef components(tokens):\n    components = []\n    for token in tokens:\n        if token.type == 'ident':\n            components.append(token.value)\n        else:\n            return\n    return components\n"
  },
  {
    "path": "weasyprint/css/validation/expanders.py",
    "content": "\"\"\"Validate properties expanders.\"\"\"\n\nimport functools\n\nfrom tinycss2.ast import DimensionToken, IdentToken, NumberToken\nfrom tinycss2.color5 import parse_color\n\nfrom ..functions import check_var\nfrom ..properties import INITIAL_VALUES\nfrom .descriptors import expand_font_variant\n\nfrom ..tokens import (  # isort:skip\n    InvalidValues, Pending, get_keyword, get_single_keyword, split_on_comma)\nfrom .properties import (  # isort:skip\n    background_attachment, background_image, background_position, background_repeat,\n    background_size, block_ellipsis, border_image_source, border_image_slice,\n    border_image_width, border_image_outset, border_image_repeat, border_style,\n    border_width, box, column_count, column_width, flex_basis, flex_direction,\n    flex_grow_shrink, flex_wrap, font_family, font_size, font_stretch, font_style,\n    font_variant_caps, font_weight, gap, grid_line, grid_template, line_height,\n    list_style_image, list_style_position, list_style_type, mask_border_mode,\n    other_colors, overflow_wrap, text_decoration_thickness, validate_non_shorthand)\n\nEXPANDERS = {}\n\n\nclass PendingExpander(Pending):\n    \"\"\"Expander with validation done when defining calculated values.\"\"\"\n    def __init__(self, tokens, validator):\n        super().__init__(tokens, validator.keywords['name'])\n        self.validator = validator\n\n    def validate(self, tokens, wanted_key):\n        for key, value in self.validator(tokens):\n            if key.startswith('-'):\n                key = f'{self.validator.keywords[\"name\"]}{key}'\n            if key == wanted_key:\n                return value\n        raise KeyError\n\n\ndef _find_var(tokens, expander, expanded_names):\n    \"\"\"Return pending expanders when var is found in tokens.\"\"\"\n    for token in tokens:\n        if check_var(token):\n            # Found CSS variable, keep pending-substitution values.\n            pending = PendingExpander(tokens, expander)\n            return {name: pending for name in expanded_names}\n\n\ndef expander(property_name):\n    \"\"\"Decorator adding a function to the ``EXPANDERS``.\"\"\"\n    def expander_decorator(function):\n        \"\"\"Add ``function`` to the ``EXPANDERS``.\"\"\"\n        assert property_name not in EXPANDERS, property_name\n        EXPANDERS[property_name] = function\n        return function\n    return expander_decorator\n\n\ndef generic_expander(*expanded_names, **kwargs):\n    \"\"\"Decorator helping expanders to handle ``inherit`` and ``initial``.\n\n    Wrap an expander so that it does not have to handle the 'inherit' and\n    'initial' cases, and can just yield name suffixes. Missing suffixes\n    get the initial value.\n\n    \"\"\"\n    wants_base_url = kwargs.pop('wants_base_url', False)\n    assert not kwargs\n\n    def generic_expander_decorator(wrapped):\n        \"\"\"Decorate the ``wrapped`` expander.\"\"\"\n        @functools.wraps(wrapped)\n        def generic_expander_wrapper(tokens, name, base_url):\n            \"\"\"Wrap the expander.\"\"\"\n            expander = functools.partial(\n                generic_expander_wrapper, name=name, base_url=base_url)\n\n            skip_validation = False\n            keyword = get_single_keyword(tokens)\n            if keyword in ('inherit', 'initial'):\n                results = {name: keyword for name in expanded_names}\n                skip_validation = True\n            else:\n                results = _find_var(tokens, expander, expanded_names)\n                if results:\n                    skip_validation = True\n\n            if not skip_validation:\n                results = {}\n                if wants_base_url:\n                    result = wrapped(tokens, name, base_url)\n                else:\n                    result = wrapped(tokens, name)\n                for new_name, new_token in result:\n                    assert new_name in expanded_names, new_name\n                    if new_name in results:\n                        raise InvalidValues(\n                            f'got multiple {new_name.strip(\"-\")} values '\n                            f'in a {name} shorthand')\n                    results[new_name] = new_token\n\n            for new_name in expanded_names:\n                if new_name.startswith('-'):\n                    # new_name is a suffix\n                    actual_new_name = f'{name}{new_name}'\n                else:\n                    actual_new_name = new_name\n\n                if new_name in results:\n                    value = results[new_name]\n                    if not skip_validation:\n                        # validate_non_shorthand returns ((name, value),)\n                        (actual_new_name, value), = validate_non_shorthand(\n                            value, actual_new_name, base_url, required=True)\n                else:\n                    value = 'initial'\n\n                yield actual_new_name, value\n        return generic_expander_wrapper\n    return generic_expander_decorator\n\n\n@expander('margin-block')\n@expander('margin-inline')\n@expander('padding-block')\n@expander('padding-inline')\n@expander('border-block-color')\n@expander('border-block-style')\n@expander('border-block-width')\n@expander('border-inline-color')\n@expander('border-inline-style')\n@expander('border-inline-width')\n@expander('inset-block')\n@expander('inset-inline')\ndef expand_two_logical_sides(tokens, name, base_url):\n    \"\"\"Expand properties setting a token for two logical sides of a box.\"\"\"\n    yield from _expand_sides(tokens, name, base_url, ('start', 'end'))\n\n\n@expander('border-color')\n@expander('border-style')\n@expander('border-width')\n@expander('margin')\n@expander('padding')\n@expander('bleed')\n@expander('inset')\ndef expand_four_sides(tokens, name, base_url):\n    \"\"\"Expand properties setting a token for four sides of a box, possibly logical.\"\"\"\n    sides = ('top', 'right', 'bottom', 'left')\n    if tokens and get_keyword(tokens[0]) == 'logical':\n        sides = ('block-start', 'inline-start', 'block-end', 'inline-end')\n        tokens = tokens[1:]\n    yield from _expand_sides(tokens, name, base_url, sides)\n\n\ndef _expand_sides(tokens, name, base_url, sides):\n    \"\"\"Expand properties setting a token for two or four sides of a box.\"\"\"\n    # Define expanded names.\n    expanded_names = []\n    for side in sides:\n        if name.endswith(('-color', '-style', '-width')):\n            # For example, border-color becomes border-*-color, not border-color-*.\n            expanded_names.append(f'{name[:-6]}-{side}-{name[-5:]}')\n        else:\n            if name == 'inset' and '-' not in side:\n                # Physical \"inset\" does not yield \"inset-top\", just \"top\".\n                expanded_names.append(side)\n            else:\n                expanded_names.append(f'{name}-{side}')\n\n    # Return pending expanders if var is found.\n    expander = functools.partial(expand_four_sides, name=name, base_url=base_url)\n    if result := _find_var(tokens, expander, expanded_names):\n        yield from result.items()\n        return\n\n    # Make sure we have the right number of tokens.\n    if len(tokens) == 1:\n        tokens *= len(sides)\n    elif len(sides) == 4 and len(tokens) == 2:\n        tokens *= 2  # (bottom, left) defaults to (top, right)\n    elif len(sides) == 4 and len(tokens) == 3:\n        tokens += (tokens[1],)  # left defaults to right\n    elif len(tokens) != len(sides):\n        raise InvalidValues(f'Expected 1 to {len(sides)} tokens, got {len(tokens)}')\n    for expanded_name, token in zip(expanded_names, tokens):\n        # validate_non_shorthand returns ((name, value),), we yield (name, value).\n        yield validate_non_shorthand([token], expanded_name, base_url, required=True)[0]\n\n\n@expander('border-radius')\n@generic_expander(\n    'border-top-left-radius', 'border-top-right-radius',\n    'border-bottom-right-radius', 'border-bottom-left-radius',\n    wants_base_url=True)\ndef border_radius(tokens, name, base_url):\n    \"\"\"Validator for the ``border-radius`` property.\"\"\"\n    current = horizontal = []\n    vertical = []\n    for token in tokens:\n        if token.type == 'literal' and token.value == '/':\n            if current is horizontal:\n                if token == tokens[-1]:\n                    raise InvalidValues('Expected value after \"/\" separator')\n                else:\n                    current = vertical\n            else:\n                raise InvalidValues('Expected only one \"/\" separator')\n        else:\n            current.append(token)\n\n    if not vertical:\n        vertical = horizontal[:]\n\n    for values in horizontal, vertical:\n        # Make sure we have 4 tokens\n        if len(values) == 1:\n            values *= 4\n        elif len(values) == 2:\n            values *= 2  # (br, bl) defaults to (tl, tr)\n        elif len(values) == 3:\n            values.append(values[1])  # bl defaults to tr\n        elif len(values) != 4:\n            raise InvalidValues(\n                f'Expected 1 to 4 token components got {len(values)}')\n    corners = ('top-left', 'top-right', 'bottom-right', 'bottom-left')\n    for corner, tokens in zip(corners, zip(horizontal, vertical)):\n        name = f'border-{corner}-radius'\n        validate_non_shorthand(tokens, name, base_url, required=True)\n        yield name, tokens\n\n\n@expander('list-style')\n@generic_expander('-type', '-position', '-image', wants_base_url=True)\ndef expand_list_style(tokens, name, base_url):\n    \"\"\"Expand the ``list-style`` shorthand property.\n\n    See https://www.w3.org/TR/CSS21/generate.html#propdef-list-style\n\n    \"\"\"\n    type_specified = image_specified = False\n    none_count = 0\n    for token in tokens:\n        if get_keyword(token) == 'none':\n            # Can be either -style or -image, see at the end which is not\n            # otherwise specified.\n            none_count += 1\n            none_token = token\n            continue\n\n        if list_style_image([token], base_url) is not None:\n            suffix = '-image'\n            image_specified = True\n        elif list_style_position([token]) is not None:\n            suffix = '-position'\n        elif list_style_type([token]) is not None:\n            suffix = '-type'\n            type_specified = True\n        else:\n            raise InvalidValues\n        yield suffix, [token]\n\n    if not type_specified and none_count:\n        yield '-type', [none_token]\n        none_count -= 1\n\n    if not image_specified and none_count:\n        yield '-image', [none_token]\n        none_count -= 1\n\n    if none_count:\n        # Too many none tokens.\n        raise InvalidValues\n\n\n@expander('border')\ndef expand_border(tokens, name, base_url):\n    \"\"\"Expand the ``border`` shorthand property.\n\n    See https://www.w3.org/TR/CSS21/box.html#propdef-border\n\n    \"\"\"\n    for suffix in ('top', 'right', 'bottom', 'left'):\n        yield from expand_border_side(tokens, f'{name}-{suffix}', base_url)\n\n\n@expander('border-block')\n@expander('border-inline')\ndef expand_logical_border(tokens, name, base_url):\n    \"\"\"Expand the logical ``border-*`` shorthands property.\"\"\"\n    for suffix in ('start', 'end'):\n        yield from expand_border_side(tokens, f'{name}-{suffix}', base_url)\n\n\n@expander('border-top')\n@expander('border-right')\n@expander('border-bottom')\n@expander('border-left')\n@expander('border-block-start')\n@expander('border-block-end')\n@expander('border-inline-start')\n@expander('border-inline-end')\n@expander('column-rule')\n@expander('outline')\n@generic_expander('-width', '-color', '-style')\ndef expand_border_side(tokens, name):\n    \"\"\"Expand the ``border-*`` shorthand properties.\n\n    See https://www.w3.org/TR/CSS21/box.html#propdef-border-top\n\n    \"\"\"\n    for token in tokens:\n        if parse_color(token) is not None:\n            suffix = '-color'\n        elif border_width([token]) is not None:\n            suffix = '-width'\n        elif border_style([token]) is not None:\n            suffix = '-style'\n        else:\n            raise InvalidValues\n        yield suffix, [token]\n\n\n@expander('border-image')\n@generic_expander('-outset', '-repeat', '-slice', '-source', '-width',\n                  wants_base_url=True)\ndef expand_border_image(tokens, name, base_url):\n    \"\"\"Expand the ``border-image-*`` shorthand properties.\n\n    See https://drafts.csswg.org/css-backgrounds/#the-border-image\n\n    \"\"\"\n    tokens = list(tokens)\n    while tokens:\n        if border_image_source(tokens[:1], base_url):\n            yield '-source', [tokens.pop(0)]\n        elif border_image_repeat(tokens[:1]):\n            repeats = [tokens.pop(0)]\n            while tokens and border_image_repeat(tokens[:1]):\n                repeats.append(tokens.pop(0))\n            yield '-repeat', repeats\n        elif border_image_slice(tokens[:1]) or get_keyword(tokens[0]) == 'fill':\n            slices = [tokens.pop(0)]\n            while tokens and border_image_slice(slices + tokens[:1]):\n                slices.append(tokens.pop(0))\n            yield '-slice', slices\n            if tokens and tokens[0].type == 'literal' and tokens[0].value == '/':\n                # slices / *\n                tokens.pop(0)\n            else:\n                # slices other\n                continue\n            if not tokens:\n                # slices /\n                raise InvalidValues\n            if border_image_width(tokens[:1]):\n                widths = [tokens.pop(0)]\n                while tokens and border_image_width(widths + tokens[:1]):\n                    widths.append(tokens.pop(0))\n                yield '-width', widths\n                if tokens and tokens[0].type == 'literal' and tokens[0].value == '/':\n                    # slices / widths / slash *\n                    tokens.pop(0)\n                else:\n                    # slices / widths other\n                    continue\n            elif tokens and tokens[0].type == 'literal' and tokens[0].value == '/':\n                # slices / / *\n                tokens.pop(0)\n            else:\n                # slices / other\n                raise InvalidValues\n            if not tokens:\n                # slices / * /\n                raise InvalidValues\n            if border_image_outset(tokens[:1]):\n                outsets = [tokens.pop(0)]\n                while tokens and border_image_outset(outsets + tokens[:1]):\n                    outsets.append(tokens.pop(0))\n                yield '-outset', outsets\n            else:\n                # slash / * / other\n                raise InvalidValues\n        else:\n            raise InvalidValues\n\n\n@expander('mask-border')\n@generic_expander('-outset', '-repeat', '-slice', '-source', '-width', '-mode',\n                  wants_base_url=True)\ndef expand_mask_border(tokens, name, base_url):\n    \"\"\"Expand the ``mask-border-*`` shorthand properties.\n\n    See https://drafts.fxtf.org/css-masking/#the-mask-border\n\n    \"\"\"\n    tokens = list(tokens)\n    while tokens:\n        if border_image_source(tokens[:1], base_url):\n            yield '-source', [tokens.pop(0)]\n        elif mask_border_mode(tokens[:1]):\n            yield '-mode', [tokens.pop(0)]\n        elif border_image_repeat(tokens[:1]):\n            repeats = [tokens.pop(0)]\n            while tokens and border_image_repeat(tokens[:1]):\n                repeats.append(tokens.pop(0))\n            yield '-repeat', repeats\n        elif border_image_slice(tokens[:1]) or get_keyword(tokens[0]) == 'fill':\n            slices = [tokens.pop(0)]\n            while tokens and border_image_slice(slices + tokens[:1]):\n                slices.append(tokens.pop(0))\n            yield '-slice', slices\n            if tokens and tokens[0].type == 'literal' and tokens[0].value == '/':\n                # slices / *\n                tokens.pop(0)\n            else:\n                # slices other\n                continue\n            if not tokens:\n                # slices /\n                raise InvalidValues\n            if border_image_width(tokens[:1]):\n                widths = [tokens.pop(0)]\n                while tokens and border_image_width(widths + tokens[:1]):\n                    widths.append(tokens.pop(0))\n                yield '-width', widths\n                if tokens and tokens[0].type == 'literal' and tokens[0].value == '/':\n                    # slices / widths / slash *\n                    tokens.pop(0)\n                else:\n                    # slices / widths other\n                    continue\n            elif tokens and tokens[0].type == 'literal' and tokens[0].value == '/':\n                # slices / / *\n                tokens.pop(0)\n            else:\n                # slices / other\n                raise InvalidValues\n            if not tokens:\n                # slices / * /\n                raise InvalidValues\n            if border_image_outset(tokens[:1]):\n                outsets = [tokens.pop(0)]\n                while tokens and border_image_outset(outsets + tokens[:1]):\n                    outsets.append(tokens.pop(0))\n                yield '-outset', outsets\n            else:\n                # slash / * / other\n                raise InvalidValues\n        else:\n            raise InvalidValues\n\n\n@expander('background')\ndef expand_background(tokens, name, base_url):\n    \"\"\"Expand the ``background`` shorthand property.\n\n    See https://drafts.csswg.org/css-backgrounds-3/#the-background\n\n    \"\"\"\n    expanded_names = (\n        'background-color', 'background-image', 'background-repeat',\n        'background-attachment', 'background-position', 'background-size',\n        'background-clip', 'background-origin')\n    keyword = get_single_keyword(tokens)\n    if keyword in ('initial', 'inherit'):\n        for name in expanded_names:\n            yield name, keyword\n        return\n\n    expander = functools.partial(\n        expand_background, name=name, base_url=base_url)\n    if result := _find_var(tokens, expander, expanded_names):\n        yield from result.items()\n        return\n\n    def parse_layer(tokens, final_layer=False):\n        results = {}\n\n        def add(name, value):\n            if value is None:\n                return False\n            name = f'background-{name}'\n            if name in results:\n                raise InvalidValues\n            results[name] = value\n            return True\n\n        # Make `tokens` a stack\n        tokens = tokens[::-1]\n        while tokens:\n            if add('repeat',\n                   background_repeat.single_value(tokens[-2:][::-1])):\n                del tokens[-2:]\n                continue\n            token = tokens[-1:]\n            if final_layer and add('color', other_colors(token)):\n                tokens.pop()\n                continue\n            if add('image', background_image.single_value(token, base_url)):\n                tokens.pop()\n                continue\n            if add('repeat', background_repeat.single_value(token)):\n                tokens.pop()\n                continue\n            if add('attachment', background_attachment.single_value(token)):\n                tokens.pop()\n                continue\n            for n in (4, 3, 2, 1)[-len(tokens):]:\n                n_tokens = tokens[-n:][::-1]\n                position = background_position.single_value(n_tokens)\n                if position is not None:\n                    assert add('position', position)\n                    del tokens[-n:]\n                    if (tokens and tokens[-1].type == 'literal' and\n                            tokens[-1].value == '/'):\n                        for n in (3, 2)[-len(tokens):]:\n                            # n includes the '/' delimiter.\n                            n_tokens = tokens[-n:-1][::-1]\n                            size = background_size.single_value(n_tokens)\n                            if size is not None:\n                                assert add('size', size)\n                                del tokens[-n:]\n                    break\n            if position is not None:\n                continue\n            if add('origin', box.single_value(token)):\n                tokens.pop()\n                next_token = tokens[-1:]\n                if add('clip', box.single_value(next_token)):\n                    tokens.pop()\n                else:\n                    # The same keyword sets both\n                    add('clip', box.single_value(token))\n                continue\n            raise InvalidValues\n\n        color = results.pop(\n            'background-color', INITIAL_VALUES['background_color'])\n        for name in expanded_names:\n            if name not in results and name != 'background-color':\n                results[name] = INITIAL_VALUES[name.replace('-', '_')][0]\n        return color, results\n\n    layers = reversed(split_on_comma(tokens))\n    color, last_layer = parse_layer(next(layers), final_layer=True)\n    results = {key: [value] for key, value in last_layer.items()}\n    for tokens in layers:\n        _, layer = parse_layer(tokens)\n        for name, value in layer.items():\n            results[name].append(value)\n    for name, values in results.items():\n        yield name, values[::-1]  # \"Un-reverse\"\n    yield 'background-color', color\n\n\n@expander('text-decoration')\n@generic_expander('-line', '-color', '-style', '-thickness')\ndef expand_text_decoration(tokens, name):\n    \"\"\"Expand the ``text-decoration`` shorthand property.\"\"\"\n    line = []\n    color = []\n    style = []\n    thickness = []\n    none_in_line = False\n\n    for token in tokens:\n        keyword = get_keyword(token)\n        if keyword in ('none', 'underline', 'overline', 'line-through', 'blink'):\n            line.append(token)\n            if none_in_line:\n                raise InvalidValues\n            elif keyword == 'none':\n                none_in_line = True\n        elif keyword in ('solid', 'double', 'dotted', 'dashed', 'wavy'):\n            if style:\n                raise InvalidValues\n            style.append(token)\n        elif parse_color(token):\n            if color:\n                raise InvalidValues\n            color.append(token)\n        elif text_decoration_thickness([token]):\n            if thickness:\n                raise InvalidValues\n            thickness.append(token)\n        else:\n            raise InvalidValues\n\n    if line:\n        yield '-line', line\n    if color:\n        yield '-color', color\n    if style:\n        yield '-style', style\n    if thickness:\n        yield '-thickness', thickness\n\n\ndef expand_page_break_before_after(tokens, name):\n    \"\"\"Expand legacy ``page-break-before`` and ``page-break-after`` properties.\n\n    See https://www.w3.org/TR/css-break-3/#page-break-properties\n\n    \"\"\"\n    keyword = get_single_keyword(tokens)\n    new_name = name.split('-', 1)[1]\n    if keyword in ('auto', 'left', 'right', 'avoid'):\n        yield new_name, tokens\n    elif keyword == 'always':\n        token = IdentToken(\n            tokens[0].source_line, tokens[0].source_column, 'page')\n        yield new_name, [token]\n    else:\n        raise InvalidValues\n\n\n@expander('page-break-after')\n@generic_expander('break-after')\ndef expand_page_break_after(tokens, name):\n    \"\"\"Expand legacy ``page-break-after`` property.\n\n    See https://www.w3.org/TR/css-break-3/#page-break-properties\n\n    \"\"\"\n    return expand_page_break_before_after(tokens, name)\n\n\n@expander('page-break-before')\n@generic_expander('break-before')\ndef expand_page_break_before(tokens, name):\n    \"\"\"Expand legacy ``page-break-before`` property.\n\n    See https://www.w3.org/TR/css-break-3/#page-break-properties\n\n    \"\"\"\n    return expand_page_break_before_after(tokens, name)\n\n\n@expander('page-break-inside')\n@generic_expander('break-inside')\ndef expand_page_break_inside(tokens, name):\n    \"\"\"Expand the legacy ``page-break-inside`` property.\n\n    See https://www.w3.org/TR/css-break-3/#page-break-properties\n\n    \"\"\"\n    keyword = get_single_keyword(tokens)\n    if keyword in ('auto', 'avoid'):\n        yield 'break-inside', tokens\n    else:\n        raise InvalidValues\n\n\n@expander('columns')\n@generic_expander('column-width', 'column-count')\ndef expand_columns(tokens, name):\n    \"\"\"Expand the ``columns`` shorthand property.\"\"\"\n    name = None\n    if len(tokens) == 2 and get_keyword(tokens[0]) == 'auto':\n        tokens = tokens[::-1]\n    for token in tokens:\n        if column_width([token]) is not None and name != 'column-width':\n            name = 'column-width'\n        elif column_count([token]) is not None:\n            name = 'column-count'\n        else:\n            raise InvalidValues\n        yield name, [token]\n    if len(tokens) == 1:\n        name = 'column-width' if name == 'column-count' else 'column-count'\n        token = IdentToken(\n            tokens[0].source_line, tokens[0].source_column, 'auto')\n        yield name, [token]\n\n\n@expander('font-variant')\n@generic_expander('-alternates', '-caps', '-east-asian', '-ligatures',\n                  '-numeric', '-position')\ndef font_variant(tokens, name):\n    \"\"\"Expand the ``font-variant`` shorthand property.\n\n    https://www.w3.org/TR/css-fonts-3/#font-variant-prop\n\n    \"\"\"\n    return expand_font_variant(tokens)\n\n\n@expander('font')\n@generic_expander('-style', '-variant-caps', '-weight', '-stretch', '-size',\n                  'line-height', '-family')  # line-height is not a suffix\ndef expand_font(tokens, name):\n    \"\"\"Expand the ``font`` shorthand property.\n\n    https://www.w3.org/TR/css-fonts-3/#font-prop\n\n    \"\"\"\n    expand_font_keyword = get_single_keyword(tokens)\n    if expand_font_keyword in ('caption', 'icon', 'menu', 'message-box',\n                               'small-caption', 'status-bar'):\n        raise InvalidValues('System fonts are not supported')\n\n    # Make `tokens` a stack\n    tokens = list(reversed(tokens))\n    # Values for font-style, font-variant-caps, font-weight and font-stretch\n    # can come in any order and are all optional.\n    for _ in range(4):\n        token = tokens.pop()\n        if get_keyword(token) == 'normal':\n            # Just ignore 'normal' keywords. Unspecified properties will get\n            # their initial token, which is 'normal' for all four here.\n            continue\n\n        if font_style([token]) is not None:\n            suffix = '-style'\n        elif font_variant_caps([token]) is not None:\n            suffix = '-variant-caps'\n        elif font_weight([token]) is not None:\n            suffix = '-weight'\n        elif font_stretch([token]) is not None:\n            suffix = '-stretch'\n        else:\n            # We’re done with these four, continue with font-size\n            break\n        yield suffix, [token]\n\n        if not tokens:\n            raise InvalidValues\n    else:\n        if not tokens:\n            raise InvalidValues\n        token = tokens.pop()\n\n    # Then font-size is mandatory\n    # Latest `token` from the loop.\n    if font_size([token]) is None:\n        raise InvalidValues\n    yield '-size', [token]\n\n    # Then line-height is optional, but font-family is not so the list\n    # must not be empty yet\n    if not tokens:\n        raise InvalidValues\n\n    token = tokens.pop()\n    if token.type == 'literal' and token.value == '/':\n        token = tokens.pop()\n        if line_height([token]) is None:\n            raise InvalidValues\n        yield 'line-height', [token]\n    else:\n        # We pop()ed a font-family, add it back\n        tokens.append(token)\n\n    # Reverse the stack to get normal list\n    tokens.reverse()\n    if font_family(tokens) is None:\n        raise InvalidValues\n    yield '-family', tokens\n\n\n@expander('word-wrap')\n@generic_expander('overflow-wrap')\ndef expand_word_wrap(tokens, name):\n    \"\"\"Expand the ``word-wrap`` legacy property.\n\n    See https://www.w3.org/TR/css-text-3/#overflow-wrap\n\n    \"\"\"\n    keyword = overflow_wrap(tokens)\n    if keyword is None:\n        raise InvalidValues\n    yield 'overflow-wrap', tokens\n\n\n@expander('flex')\n@generic_expander('-grow', '-shrink', '-basis')\ndef expand_flex(tokens, name):\n    \"\"\"Expand the ``flex`` property.\"\"\"\n    keyword = get_single_keyword(tokens)\n    if keyword == 'none':\n        line, column = tokens[0].source_line, tokens[0].source_column\n        zero_token = NumberToken(line, column, 0, 0, '0')\n        auto_token = IdentToken(line, column, 'auto')\n        yield '-grow', [zero_token]\n        yield '-shrink', [zero_token]\n        yield '-basis', [auto_token]\n    else:\n        grow, shrink, basis = 1, 1, None\n        grow_found, shrink_found, basis_found = False, False, False\n        for token in tokens:\n            # \"A unitless zero that is not already preceded by two flex factors\n            # must be interpreted as a flex factor.\"\n            forced_flex_factor = (\n                token.type == 'number' and token.int_value == 0 and\n                not all((grow_found, shrink_found)))\n            if not basis_found and not forced_flex_factor:\n                new_basis = flex_basis([token])\n                if new_basis is not None:\n                    basis = token\n                    basis_found = True\n                    continue\n            if not grow_found:\n                new_grow = flex_grow_shrink([token])\n                if new_grow is None:\n                    raise InvalidValues\n                else:\n                    grow = new_grow\n                    grow_found = True\n                    continue\n            elif not shrink_found:\n                new_shrink = flex_grow_shrink([token])\n                if new_shrink is None:\n                    raise InvalidValues\n                else:\n                    shrink = new_shrink\n                    shrink_found = True\n                    continue\n            else:\n                raise InvalidValues\n        line, column = tokens[0].source_line, tokens[0].source_column\n        int_grow = int(grow) if float(grow).is_integer() else None\n        int_shrink = int(shrink) if float(shrink).is_integer() else None\n        grow_token = NumberToken(line, column, grow, int_grow, str(grow))\n        shrink_token = NumberToken(\n            line, column, shrink, int_shrink, str(shrink))\n        if not basis_found:\n            basis = DimensionToken(line, column, 0, 0, '0', 'px')\n        yield '-grow', [grow_token]\n        yield '-shrink', [shrink_token]\n        yield '-basis', [basis]\n\n\n@expander('flex-flow')\n@generic_expander('flex-direction', 'flex-wrap')\ndef expand_flex_flow(tokens, name):\n    \"\"\"Expand the ``flex-flow`` property.\"\"\"\n    if len(tokens) == 2:\n        for sorted_tokens in tokens, tokens[::-1]:\n            direction = flex_direction([sorted_tokens[0]])\n            wrap = flex_wrap([sorted_tokens[1]])\n            if direction and wrap:\n                yield 'flex-direction', [sorted_tokens[0]]\n                yield 'flex-wrap', [sorted_tokens[1]]\n                break\n        else:\n            raise InvalidValues\n    elif len(tokens) == 1:\n        direction = flex_direction([tokens[0]])\n        if direction:\n            yield 'flex-direction', [tokens[0]]\n        else:\n            wrap = flex_wrap([tokens[0]])\n            if wrap:\n                yield 'flex-wrap', [tokens[0]]\n            else:\n                raise InvalidValues\n    else:\n        raise InvalidValues\n\n\ndef _expand_grid_template(tokens, name):\n    line, column = tokens[0].source_line, tokens[0].source_column\n    none = IdentToken(line, column, 'none')\n    if len(tokens) == 1 and get_keyword(tokens[0]) == 'none':\n        yield '-columns', [none]\n        yield '-rows', [none]\n        yield '-areas', [none]\n        return\n    slash_separated = [[]]\n    for token in tokens:\n        if token.type == 'literal' and token.value == '/':\n            slash_separated.append([])\n        else:\n            slash_separated[-1].append(token)\n    if len(slash_separated) == 2:\n        rows = grid_template(slash_separated[0])\n        columns = grid_template(slash_separated[1])\n        if columns:\n            if rows:\n                yield '-columns', slash_separated[1]\n                yield '-rows', slash_separated[0]\n                yield '-areas', [none]\n                return\n            columns = slash_separated[1]\n        else:\n            raise InvalidValues\n    elif len(slash_separated) == 1:\n        columns = [none]\n    else:\n        raise InvalidValues\n    # TODO: Handle last syntax.\n    raise InvalidValues\n\n\n@expander('grid-template')\n@generic_expander('-columns', '-rows', '-areas')\ndef expand_grid_template(tokens, name):\n    \"\"\"Expand the ``grid-template`` property.\"\"\"\n    yield from _expand_grid_template(tokens, name)\n\n\n@expander('grid')\n@generic_expander('-template-columns', '-template-rows', '-template-areas',\n                  '-auto-columns', '-auto-rows', '-auto-flow')\ndef expand_grid(tokens, name):\n    \"\"\"Expand the ``grid`` property.\"\"\"\n    line, column = tokens[0].source_line, tokens[0].source_column\n    auto = IdentToken(line, column, 'auto')\n    none = IdentToken(line, column, 'none')\n    row = IdentToken(line, column, 'row')\n    column = IdentToken(line, column, 'column')\n    try:\n        template = tuple(_expand_grid_template(tokens, 'grid-template'))\n    except InvalidValues:\n        pass\n    else:\n        for key, value in template:\n            yield f'-template-{key.split(\"-\")[-1]}', value\n        yield '-auto-columns', [auto]\n        yield '-auto-rows', [auto]\n        yield '-auto-flow', [row]\n        return\n    split_tokens = [[]]\n    for token in tokens:\n        if token.type == 'literal' and token.value == '/':\n            split_tokens.append([])\n            continue\n        split_tokens[-1].append(token)\n    if len(split_tokens) != 2:\n        raise InvalidValues\n    auto_track = None\n    dense = None\n    templates = {'row': [], 'column': []}\n    iterable = zip(split_tokens, templates.items())\n    for tokens, (track, track_templates) in iterable:\n        auto_flow_token = False\n        for token in tokens:\n            if get_keyword(token) == 'dense':\n                if dense or (auto_track and auto_track != track):\n                    raise InvalidValues\n                dense = token\n                auto_track = track\n            elif get_keyword(token) == 'auto-flow':\n                if auto_flow_token or (auto_track and auto_track != track):\n                    raise InvalidValues\n                auto_flow_token = True\n                auto_track = track\n            elif token == tokens[-1]:\n                track_templates.append(token)\n            else:\n                raise InvalidValues\n    if not auto_track:\n        raise InvalidValues\n    non_auto_track = 'row' if auto_track == 'column' else 'column'\n    auto_track_token = column if auto_track == 'column' else row\n    yield '-auto-flow', (\n        (auto_track_token, dense) if dense else (auto_track_token,))\n    yield f'-auto-{auto_track}s', tuple(templates[auto_track])\n    yield f'-auto-{non_auto_track}s', [auto]\n    yield f'-template-{auto_track}s', [none]\n    yield f'-template-{non_auto_track}s', tuple(templates[non_auto_track])\n    yield '-template-areas', [none]\n\n\ndef _expand_grid_column_row_area(tokens, max_number):\n    grid_lines = [[]]\n    for token in tokens:\n        if token.type == 'literal' and token.value == '/':\n            grid_lines.append([])\n            continue\n        grid_lines[-1].append(token)\n    if not 1 <= len(grid_lines) <= max_number:\n        raise InvalidValues\n    validations = []\n    for tokens in grid_lines:\n        if not (validation := grid_line(tokens)):\n            raise InvalidValues\n        validations.append(validation)\n        yield tuple(tokens)\n    auto = IdentToken(token.source_line, token.source_column, 'auto')\n    if (lines := len(grid_lines)) <= 1:\n        custom_ident = set(validations[0][:2]) == {None}\n        value = tuple(grid_lines[0]) if custom_ident else (auto,)\n        grid_lines.append(tokens)\n        validations.append(validations[0])\n        yield value\n    if lines <= 2 < max_number:\n        custom_ident = set(validations[0][:2]) == {None}\n        yield tuple(grid_lines[0]) if custom_ident else (auto,)\n    if lines <= 3 < max_number:\n        custom_ident = set(validations[1][:2]) == {None}\n        yield tuple(grid_lines[1]) if custom_ident else (auto,)\n\n\n@expander('grid-column')\n@expander('grid-row')\n@generic_expander('-start', '-end')\ndef expand_grid_column_row(tokens, name):\n    \"\"\"Expand the ``grid-[column|row]`` properties.\"\"\"\n    tokens_list = _expand_grid_column_row_area(tokens, 2)\n    for tokens, side in zip(tokens_list, ('start', 'end')):\n        yield f'-{side}', tokens\n\n\n@expander('grid-area')\n@generic_expander('grid-row-start', 'grid-row-end',\n                  'grid-column-start', 'grid-column-end')\ndef expand_grid_area(tokens, name):\n    \"\"\"Expand the ``grid-area`` property.\"\"\"\n    tokens_list = _expand_grid_column_row_area(tokens, 4)\n    sides = ('row-start', 'column-start', 'row-end', 'column-end')\n    for tokens, side in zip(tokens_list, sides):\n        yield f'grid-{side}', tokens\n\n\n@expander('grid-gap')\n@expander('gap')\n@generic_expander('column-gap', 'row-gap')\ndef expand_gap(tokens, name):\n    \"\"\"Expand the ``gap`` property.\"\"\"\n    if len(tokens) == 1:\n        if gap(tokens) is None:\n            raise InvalidValues\n        yield 'row-gap', tokens\n        yield 'column-gap', tokens\n    elif len(tokens) == 2:\n        column_gap, row_gap = gap(tokens[0:1]), gap(tokens[1:2])\n        if None in (column_gap, row_gap):\n            raise InvalidValues\n        yield 'row-gap', tokens[0:1]\n        yield 'column-gap', tokens[1:2]\n    else:\n        raise InvalidValues\n\n\n@expander('grid-column-gap')\n@generic_expander('column-gap')\ndef expand_legacy_column_gap(tokens, name):\n    \"\"\"Expand legacy ``grid-column-gap`` property.\"\"\"\n    keyword = gap(tokens)\n    if keyword is None:\n        raise InvalidValues\n    yield 'column-gap', tokens\n\n\n@expander('grid-row-gap')\n@generic_expander('row-gap')\ndef expand_legacy_row_gap(tokens, name):\n    \"\"\"Expand legacy ``grid-row-gap`` property.\"\"\"\n    keyword = gap(tokens)\n    if keyword is None:\n        raise InvalidValues\n    yield 'row-gap', tokens\n\n\n@expander('place-content')\n@generic_expander('align-content', 'justify-content')\ndef expand_place_content(tokens, name):\n    \"\"\"Expand the ``place-content`` property.\"\"\"\n    # TODO\n    raise InvalidValues\n\n\n@expander('place-items')\n@generic_expander('align-items', 'justify-items')\ndef expand_place_items(tokens, name):\n    \"\"\"Expand the ``place-items`` property.\"\"\"\n    # TODO\n    raise InvalidValues\n\n\n@expander('place-self')\n@generic_expander('align-self', 'justify-self')\ndef expand_place_self(tokens, name):\n    \"\"\"Expand the ``place-self`` property.\"\"\"\n    # TODO\n    raise InvalidValues\n\n\n@expander('line-clamp')\n@generic_expander('max-lines', 'continue', 'block-ellipsis')\ndef expand_line_clamp(tokens, name):\n    \"\"\"Expand the ``line-clamp`` property.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_single_keyword(tokens)\n        if keyword == 'none':\n            line, column = tokens[0].source_line, tokens[0].source_column\n            none_token = IdentToken(line, column, 'none')\n            auto_token = IdentToken(line, column, 'auto')\n            yield 'max-lines', [none_token]\n            yield 'continue', [auto_token]\n            yield 'block-ellipsis', [none_token]\n        elif tokens[0].type == 'number' and tokens[0].int_value is not None:\n            line, column = tokens[0].source_line, tokens[0].source_column\n            auto_token = IdentToken(line, column, 'auto')\n            discard_token = IdentToken(line, column, 'discard')\n            yield 'max-lines', [tokens[0]]\n            yield 'continue', [discard_token]\n            yield 'block-ellipsis', [auto_token]\n        else:\n            raise InvalidValues\n    elif len(tokens) == 2:\n        if tokens[0].type == 'number':\n            max_lines = tokens[0].int_value\n            ellipsis = block_ellipsis([tokens[1]])\n            if max_lines and ellipsis is not None:\n                line, column = tokens[0].source_line, tokens[0].source_column\n                discard_token = IdentToken(line, column, 'discard')\n                yield 'max-lines', [tokens[0]]\n                yield 'continue', [discard_token]\n                yield 'block-ellipsis', [tokens[1]]\n            else:\n                raise InvalidValues\n        else:\n            raise InvalidValues\n    else:\n        raise InvalidValues\n\n\n@expander('text-align')\n@generic_expander('-all', '-last')\ndef expand_text_align(tokens, name):\n    \"\"\"Expand the ``text-align`` property.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_single_keyword(tokens)\n        if keyword is None:\n            raise InvalidValues\n        if keyword == 'justify-all':\n            line, column = tokens[0].source_line, tokens[0].source_column\n            align_all = IdentToken(line, column, 'justify')\n        else:\n            align_all = tokens[0]\n        yield '-all', [align_all]\n        if keyword == 'justify':\n            line, column = tokens[0].source_line, tokens[0].source_column\n            align_last = IdentToken(line, column, 'start')\n        else:\n            align_last = align_all\n        yield '-last', [align_last]\n    else:\n        raise InvalidValues\n"
  },
  {
    "path": "weasyprint/css/validation/properties.py",
    "content": "\"\"\"Validate properties.\n\nSee https://www.w3.org/TR/CSS21/propidx.html and various CSS3 modules.\n\n\"\"\"\n\nfrom math import inf\n\nfrom tinycss2 import parse_component_value_list\nfrom tinycss2.color5 import parse_color\n\nfrom .. import computed_values\nfrom ..functions import Function, check_var\nfrom ..properties import KNOWN_PROPERTIES, ZERO_PIXELS, Dimension\n\nfrom ..tokens import (  # isort:skip\n    InvalidValues, Pending, comma_separated_list, get_angle, get_content_list,\n    get_content_list_token, get_custom_ident, get_image, get_keyword, get_length,\n    get_number, get_percentage, get_resolution, get_single_keyword, get_url,\n    parse_2d_position, parse_position, remove_whitespace, single_keyword, single_token)\n\nPREFIX = '-weasy-'\nPROPRIETARY = set()\nUNSTABLE = set()\n\n# Yes/no validators for non-shorthand properties\n# Maps property names to functions taking a property name and a value list,\n# returning a value or None for invalid.\n# For properties that take a single value, that value is returned by itself\n# instead of a list.\nPROPERTIES = {}\n\n\nclass PendingProperty(Pending):\n    \"\"\"Property with validation done when defining calculated values.\"\"\"\n    def validate(self, tokens, wanted_key):\n        return validate_non_shorthand(tokens, self.name)[0][1]\n\n\n# Validators\n\ndef property(property_name=None, proprietary=False, unstable=False,\n             wants_base_url=False):\n    \"\"\"Decorator adding a function to the ``PROPERTIES``.\n\n    The name of the property covered by the decorated function is set to\n    ``property_name`` if given, or is inferred from the function name\n    (replacing underscores by hyphens).\n\n    :param proprietary:\n        Proprietary (vendor-specific, non-standard) are prefixed: anchors can\n        for example be set using ``-weasy-anchor: attr(id)``.\n        See https://www.w3.org/TR/CSS/#proprietary\n    :param unstable:\n        Mark properties that are defined in specifications that didn't reach\n        the Candidate Recommandation stage. They can be used both\n        vendor-prefixed or unprefixed.\n        See https://www.w3.org/TR/CSS/#unstable-syntax\n    :param wants_base_url:\n        The function takes the stylesheet’s base URL as an additional\n        parameter.\n\n    \"\"\"\n    def decorator(function):\n        \"\"\"Add ``function`` to the ``PROPERTIES``.\"\"\"\n        if property_name is None:\n            name = function.__name__.replace('_', '-')\n        else:\n            name = property_name\n        assert name in KNOWN_PROPERTIES, name\n        assert name not in PROPERTIES, name\n\n        function.wants_base_url = wants_base_url\n        PROPERTIES[name] = function\n        if proprietary:\n            PROPRIETARY.add(name)\n        if unstable:\n            UNSTABLE.add(name)\n        return function\n    return decorator\n\n\ndef validate_non_shorthand(tokens, name, base_url=None, required=False):\n    \"\"\"Validator for non-shorthand properties.\"\"\"\n    if name.startswith('--'):\n        # TODO: validate content\n        return ((name, tokens),)\n\n    if not required and name not in KNOWN_PROPERTIES:\n        raise InvalidValues('unknown property')\n\n    if not required and name not in PROPERTIES:\n        raise InvalidValues('property not supported yet')\n\n    function = PROPERTIES[name]\n    for token in tokens:\n        if check_var(token):\n            # Found CSS variable, return pending-substitution values.\n            return ((name, PendingProperty(tokens, name)),)\n\n    keyword = get_single_keyword(tokens)\n    if keyword in ('initial', 'inherit'):\n        value = keyword\n    else:\n        if function.wants_base_url:\n            value = function(tokens, base_url)\n        else:\n            value = function(tokens)\n        if value is None:\n            raise InvalidValues\n    return ((name, value),)\n\n\n@property()\n@comma_separated_list\n@single_keyword\ndef background_attachment(keyword):\n    \"\"\"``background-attachment`` property validation.\"\"\"\n    return keyword in ('scroll', 'fixed', 'local')\n\n\n@property('background-color')\n@property('border-top-color')\n@property('border-right-color')\n@property('border-bottom-color')\n@property('border-left-color')\n@property('border-block-start-color')\n@property('border-block-end-color')\n@property('border-inline-start-color')\n@property('border-inline-end-color')\n@property('column-rule-color', unstable=True)\n@property('text-decoration-color')\n@single_token\ndef other_colors(token):\n    if parse_color(token):\n        return token\n\n\n@property()\n@single_token\ndef outline_color(token):\n    if get_keyword(token) == 'invert':\n        return 'currentcolor'\n    elif parse_color(token):\n        return token\n\n\n@property()\n@single_keyword\ndef border_collapse(keyword):\n    return keyword in ('separate', 'collapse')\n\n\n@property()\n@single_keyword\ndef empty_cells(keyword):\n    \"\"\"``empty-cells`` property validation.\"\"\"\n    return keyword in ('show', 'hide')\n\n\n@property('color')\n@single_token\ndef color(token):\n    \"\"\"``*-color`` and ``color`` properties validation.\"\"\"\n    result = parse_color(token)\n    if result == 'currentcolor':\n        return 'inherit'\n    elif result:\n        return token\n\n\n@property('background-image', wants_base_url=True)\n@comma_separated_list\n@single_token\ndef background_image(token, base_url):\n    if get_keyword(token) == 'none':\n        return 'none', None\n    return get_image(token, base_url)\n\n\n@property('list-style-image', wants_base_url=True)\n@single_token\ndef list_style_image(token, base_url):\n    \"\"\"``list-style-image`` property validation.\"\"\"\n    if get_keyword(token) == 'none':\n        return 'none', None\n    parsed_url = get_url(token, base_url)\n    if parsed_url:\n        if parsed_url[0] == 'url' and parsed_url[1][0] == 'external':\n            return 'url', parsed_url[1][1]\n\n\n@property()\ndef transform_origin(tokens):\n    \"\"\"``transform-origin`` property validation.\"\"\"\n    if len(tokens) == 3:\n        # Ignore third parameter as 3D transforms are ignored.\n        tokens = tokens[:2]\n    return parse_2d_position(tokens)\n\n\n@property()\n@comma_separated_list\ndef background_position(tokens):\n    \"\"\"``background-position`` property validation.\"\"\"\n    return parse_position(tokens)\n\n\n@property()\n@comma_separated_list\ndef object_position(tokens):\n    \"\"\"``object-position`` property validation.\"\"\"\n    return parse_position(tokens)\n\n\n@property()\n@comma_separated_list\ndef background_repeat(tokens):\n    \"\"\"``background-repeat`` property validation.\"\"\"\n    keywords = tuple(map(get_keyword, tokens))\n    if keywords == ('repeat-x',):\n        return ('repeat', 'no-repeat')\n    if keywords == ('repeat-y',):\n        return ('no-repeat', 'repeat')\n    if keywords in (('no-repeat',), ('repeat',), ('space',), ('round',)):\n        return keywords * 2\n    if len(keywords) == 2 and all(\n            k in ('no-repeat', 'repeat', 'space', 'round')\n            for k in keywords):\n        return keywords\n\n\n@property()\n@comma_separated_list\ndef background_size(tokens):\n    \"\"\"Validation for ``background-size``.\"\"\"\n    if len(tokens) == 1:\n        token = tokens[0]\n        keyword = get_keyword(token)\n        if keyword in ('contain', 'cover'):\n            return keyword\n        if keyword == 'auto':\n            return ('auto', 'auto')\n        length = get_length(token, negative=False, percentage=True)\n        if length:\n            return (length, 'auto')\n    elif len(tokens) == 2:\n        values = []\n        for token in tokens:\n            length = get_length(token, negative=False, percentage=True)\n            if length:\n                values.append(length)\n            elif get_keyword(token) == 'auto':\n                values.append('auto')\n        if len(values) == 2:\n            return tuple(values)\n\n\n@property('background-clip')\n@property('background-origin')\n@comma_separated_list\n@single_keyword\ndef box(keyword):\n    \"\"\"Validation for the ``<box>`` type used in ``background-clip``\n    and ``background-origin``.\"\"\"\n    return keyword in ('border-box', 'padding-box', 'content-box')\n\n\n@property()\ndef border_spacing(tokens):\n    \"\"\"Validator for the `border-spacing` property.\"\"\"\n    lengths = [get_length(token, negative=False) for token in tokens]\n    if all(lengths):\n        if len(lengths) == 1:\n            return (lengths[0], lengths[0])\n        elif len(lengths) == 2:\n            return tuple(lengths)\n\n\n@property('border-top-right-radius')\n@property('border-bottom-right-radius')\n@property('border-bottom-left-radius')\n@property('border-top-left-radius')\n@property('border-start-start-radius')\n@property('border-start-end-radius')\n@property('border-end-start-radius')\n@property('border-end-end-radius')\ndef border_corner_radius(tokens):\n    \"\"\"Validator for the `border-*-radius` properties.\"\"\"\n    lengths = [get_length(token, negative=False, percentage=True) for token in tokens]\n    if all(lengths):\n        if len(lengths) == 1:\n            return (lengths[0], lengths[0])\n        elif len(lengths) == 2:\n            return tuple(lengths)\n\n\n@property('border-top-style')\n@property('border-right-style')\n@property('border-left-style')\n@property('border-bottom-style')\n@property('border-block-start-style')\n@property('border-block-end-style')\n@property('border-inline-start-style')\n@property('border-inline-end-style')\n@property('column-rule-style', unstable=True)\n@single_keyword\ndef border_style(keyword):\n    \"\"\"``border-*-style`` properties validation.\"\"\"\n    return keyword in ('none', 'hidden', 'dotted', 'dashed', 'double',\n                       'inset', 'outset', 'groove', 'ridge', 'solid')\n\n\n@property('break-before')\n@property('break-after')\n@single_keyword\ndef break_before_after(keyword):\n    \"\"\"``break-before`` and ``break-after`` properties validation.\"\"\"\n    return keyword in ('auto', 'avoid', 'avoid-page', 'page', 'left', 'right',\n                       'recto', 'verso', 'avoid-column', 'column', 'always')\n\n\n@property()\n@single_keyword\ndef break_inside(keyword):\n    \"\"\"``break-inside`` property validation.\"\"\"\n    return keyword in ('auto', 'avoid', 'avoid-page', 'avoid-column')\n\n\n@property()\n@single_keyword\ndef box_decoration_break(keyword):\n    \"\"\"``box-decoration-break`` property validation.\"\"\"\n    return keyword in ('slice', 'clone')\n\n\n@property()\n@single_token\ndef block_ellipsis(token):\n    \"\"\"``box-ellipsis`` property validation.\"\"\"\n    if token.type == 'string':\n        return ('string', token.value)\n    else:\n        keyword = get_keyword(token)\n        if keyword in ('none', 'auto'):\n            return keyword\n\n\n@property('continue', unstable=True)\n@single_keyword\ndef continue_(keyword):\n    \"\"\"``continue`` property validation.\"\"\"\n    return keyword in ('auto', 'discard')\n\n\n@property(unstable=True)\n@single_token\ndef max_lines(token):\n    if number := get_number(token, negative=False, integer=True):\n        return number.value\n    elif get_keyword(token) == 'none':\n        return 'none'\n\n\n@property(unstable=True)\n@single_keyword\ndef margin_break(keyword):\n    \"\"\"``margin-break`` property validation.\"\"\"\n    return keyword in ('auto', 'keep', 'discard')\n\n\n@property(unstable=True)\n@single_token\ndef page(token):\n    \"\"\"``page`` property validation.\"\"\"\n    if token.type == 'ident':\n        return 'auto' if token.lower_value == 'auto' else token.value\n\n\n@property('bleed-left', unstable=True)\n@property('bleed-right', unstable=True)\n@property('bleed-top', unstable=True)\n@property('bleed-bottom', unstable=True)\n@single_token\ndef bleed(token):\n    \"\"\"``bleed`` property validation.\"\"\"\n    if get_keyword(token) == 'auto':\n        return 'auto'\n    else:\n        return get_length(token)\n\n\n@property(unstable=True)\ndef marks(tokens):\n    \"\"\"``marks`` property validation.\"\"\"\n    if len(tokens) == 2:\n        keywords = tuple(get_keyword(token) for token in tokens)\n        if 'crop' in keywords and 'cross' in keywords:\n            return keywords\n    elif len(tokens) == 1:\n        if (keyword := get_keyword(tokens[0])) in ('crop', 'cross'):\n            return (keyword,)\n        elif keyword == 'none':\n            return ()\n\n\n@property('outline-style')\n@single_keyword\ndef outline_style(keyword):\n    \"\"\"``outline-style`` properties validation.\"\"\"\n    return keyword in ('none', 'dotted', 'dashed', 'double', 'inset',\n                       'outset', 'groove', 'ridge', 'solid')\n\n\n@property('border-top-width')\n@property('border-right-width')\n@property('border-left-width')\n@property('border-bottom-width')\n@property('border-block-start-width')\n@property('border-block-end-width')\n@property('border-inline-start-width')\n@property('border-inline-end-width')\n@property('column-rule-width', unstable=True)\n@property('outline-width')\n@single_token\ndef border_width(token):\n    \"\"\"Border, column rule and outline widths properties validation.\"\"\"\n    if length := get_length(token, negative=False):\n        return length\n    if (keyword := get_keyword(token)) in ('thin', 'medium', 'thick'):\n        return keyword\n\n\n@property('border-image-source', wants_base_url=True)\n@property('mask-border-source', wants_base_url=True)\n@single_token\ndef border_image_source(token, base_url):\n    if get_keyword(token) == 'none':\n        return 'none', None\n    return get_image(token, base_url)\n\n\n@property('border-image-slice')\n@property('mask-border-slice')\ndef border_image_slice(tokens):\n    values = []\n    fill = False\n    for i, token in enumerate(tokens):\n        # Don't use get_length() because a dimension with a unit is disallowed.\n        if percentage := get_percentage(token, negative=False):\n            values.append(percentage)\n        elif get_keyword(token) == 'fill' and not fill and i in (0, len(tokens) - 1):\n            fill = True\n            values.append('fill')\n        elif number := get_number(token, negative=False):\n            values.append(number)\n        else:\n            return\n\n    if 1 <= len(values) - int(fill) <= 4:\n        return tuple(values)\n\n\n@property('border-image-width')\n@property('mask-border-width')\ndef border_image_width(tokens):\n    values = []\n    for token in tokens:\n        if get_keyword(token) == 'auto':\n            values.append('auto')\n        elif number := get_number(token, negative=False):\n            values.append(number)\n        elif length := get_length(token, negative=False, percentage=True):\n            values.append(length)\n        else:\n            return\n\n    if 1 <= len(values) <= 4:\n        return tuple(values)\n\n\n@property('border-image-outset')\n@property('mask-border-outset')\ndef border_image_outset(tokens):\n    values = []\n    for token in tokens:\n        if number := get_number(token, negative=False):\n            values.append(number)\n        elif length := get_length(token, negative=False):\n            values.append(length)\n        else:\n            return\n\n    if 1 <= len(values) <= 4:\n        return tuple(values)\n\n\n@property('border-image-repeat')\n@property('mask-border-repeat')\ndef border_image_repeat(tokens):\n    if 1 <= len(tokens) <= 2:\n        keywords = tuple(get_keyword(token) for token in tokens)\n        if set(keywords) <= {'stretch', 'repeat', 'round', 'space'}:\n            return keywords\n\n\n@property()\n@single_keyword\ndef mask_border_mode(keyword):\n    return keyword in ('luminance', 'alpha')\n\n\n@property(unstable=True)\n@single_token\ndef column_width(token):\n    \"\"\"``column-width`` property validation.\"\"\"\n    if length := get_length(token, negative=False):\n        return length\n    keyword = get_keyword(token)\n    if keyword == 'auto':\n        return keyword\n\n\n@property(unstable=True)\n@single_keyword\ndef column_span(keyword):\n    \"\"\"``column-span`` property validation.\"\"\"\n    return keyword in ('all', 'none')\n\n\n@property()\n@single_keyword\ndef box_sizing(keyword):\n    \"\"\"Validation for the ``box-sizing`` property from css3-ui\"\"\"\n    return keyword in ('padding-box', 'border-box', 'content-box')\n\n\n@property()\n@single_keyword\ndef caption_side(keyword):\n    \"\"\"``caption-side`` properties validation.\"\"\"\n    return keyword in ('top', 'bottom')\n\n\n@property()\n@single_keyword\ndef clear(keyword):\n    \"\"\"``clear`` property validation.\"\"\"\n    return keyword in ('left', 'right', 'inline-start', 'inline-end', 'both', 'none')\n\n\n@property()\n@single_token\ndef clip(token):\n    \"\"\"Validation for the ``clip`` property.\"\"\"\n    function = Function(token)\n    arguments = function.split_comma()\n    if function.name == 'rect' and len(arguments) == 4:\n        values = []\n        for argument in arguments:\n            if get_keyword(argument) == 'auto':\n                values.append('auto')\n            elif length := get_length(argument):\n                values.append(length)\n            else:\n                return\n        return tuple(values)\n    elif get_keyword(token) == 'auto':\n        return ()\n\n\n@property(wants_base_url=True)\ndef content(tokens, base_url):\n    \"\"\"``content`` property validation.\"\"\"\n    # See https://www.w3.org/TR/css-content-3/#content-property\n    tokens = list(tokens)\n    parsed_tokens = []\n    while tokens:\n        if len(tokens) >= 2 and tokens[1].type == 'literal' and tokens[1].value == ',':\n            token, tokens = tokens[0], tokens[2:]\n            if parsed_token := get_image(token, base_url) or get_url(token, base_url):\n                parsed_tokens.append(parsed_token)\n            else:\n                return\n        else:\n            break\n    if len(tokens) == 0:\n        return\n    if len(tokens) >= 3 and tokens[-1].type == 'string' and (\n            tokens[-2].type == 'literal' and tokens[-2].value == '/'):\n        # Ignore text for speech\n        tokens = tokens[:-2]\n    keyword = get_single_keyword(tokens)\n    if keyword in ('normal', 'none'):\n        return (keyword,)\n    return get_content_list(tokens, base_url)\n\n\n@property()\ndef counter_increment(tokens):\n    \"\"\"``counter-increment`` property validation.\"\"\"\n    return counter(tokens, default_integer=1)\n\n\n@property()\ndef counter_reset(tokens):\n    \"\"\"``counter-reset`` property validation.\"\"\"\n    return counter(tokens, default_integer=0)\n\n\n@property()\ndef counter_set(tokens):\n    \"\"\"``counter-set`` property validation.\"\"\"\n    return counter(tokens, default_integer=0)\n\n\ndef counter(tokens, default_integer):\n    \"\"\"``counter-increment`` and ``counter-reset`` properties validation.\"\"\"\n    if get_single_keyword(tokens) == 'none':\n        return ()\n    tokens = iter(tokens)\n    token = next(tokens, None)\n    assert token, 'got an empty token list'\n    results = []\n    while token is not None:\n        if token.type != 'ident':\n            return  # expected a keyword here\n        counter_name = token.value\n        if counter_name in ('none', 'initial', 'inherit'):\n            raise InvalidValues(f'Invalid counter name: {counter_name}')\n        token = next(tokens, None)\n        if token and (number := get_number(token, integer=True)):\n            # Found an integer. Use it and get the next token.\n            integer = number.value\n            token = next(tokens, None)\n        else:\n            # Not an integer. Might be the next counter name. Keep `token` for the next\n            # loop iteration.\n            integer = default_integer\n        results.append((counter_name, integer))\n    return tuple(results)\n\n\n@property('top')\n@property('right')\n@property('left')\n@property('bottom')\n@property('inset-block-start')\n@property('inset-block-end')\n@property('inset-inline-start')\n@property('inset-inline-end')\n@property('margin-top')\n@property('margin-right')\n@property('margin-bottom')\n@property('margin-left')\n@property('margin-block-start')\n@property('margin-block-end')\n@property('margin-inline-start')\n@property('margin-inline-end')\n@property('text-underline-offset')\n@single_token\ndef lenght_precentage_or_auto(token):\n    \"\"\"``margin-*`` and various other properties validation.\"\"\"\n    if length := get_length(token, percentage=True):\n        return length\n    if get_keyword(token) == 'auto':\n        return 'auto'\n\n\n@property('height')\n@property('width')\n@property('block-size')\n@property('inline-size')\n@single_token\ndef width_height(token):\n    \"\"\"Validation for the ``width`` and ``height`` properties.\"\"\"\n    if length := get_length(token, negative=False, percentage=True):\n        return length\n    if get_keyword(token) == 'auto':\n        return 'auto'\n\n\n@property('column-gap', unstable=True)\n@property('row-gap', unstable=True)\n@single_token\ndef gap(token):\n    \"\"\"Validation for the ``column-gap`` and ``row-gap`` properties.\"\"\"\n    if length := get_length(token, percentage=True, negative=False):\n        return length\n    keyword = get_keyword(token)\n    if keyword == 'normal':\n        return keyword\n\n\n@property(unstable=True)\n@single_keyword\ndef column_fill(keyword):\n    \"\"\"``column-fill`` property validation.\"\"\"\n    return keyword in ('auto', 'balance')\n\n\n@property()\n@single_keyword\ndef direction(keyword):\n    \"\"\"``direction`` property validation.\"\"\"\n    return keyword in ('ltr', 'rtl')\n\n\n@property()\ndef display(tokens):\n    \"\"\"``display`` property validation.\"\"\"\n    for token in tokens:\n        if token.type != 'ident':\n            return\n\n    if len(tokens) == 1:\n        value = tokens[0].value\n        if value in (\n                'none', 'table-caption', 'table-row-group', 'table-cell',\n                'table-header-group', 'table-footer-group', 'table-row',\n                'table-column-group', 'table-column'):\n            return (value,)\n        elif value in ('inline-table', 'inline-flex', 'inline-grid'):\n            return tuple(value.split('-'))\n        elif value == 'inline-block':\n            return ('inline', 'flow-root')\n\n    outside = inside = list_item = None\n    for token in tokens:\n        value = token.value\n        if value in ('block', 'inline'):\n            if outside:\n                return\n            outside = value\n        elif value in ('flow', 'flow-root', 'table', 'flex', 'grid'):\n            if inside:\n                return\n            inside = value\n        elif value == 'list-item':\n            if list_item:\n                return\n            list_item = value\n        else:\n            return\n\n    outside = outside or 'block'\n    inside = inside or 'flow'\n    if list_item:\n        if inside in ('flow', 'flow-root'):\n            return (outside, inside, list_item)\n    else:\n        return (outside, inside)\n\n\n@property('float')\n@single_keyword\ndef float_(keyword):  # XXX do not hide the \"float\" builtin\n    \"\"\"``float`` property validation.\"\"\"\n    return keyword in (\n        'left', 'right', 'inline-start', 'inline-end', 'footnote', 'none')\n\n\n@property()\n@comma_separated_list\ndef font_family(tokens):\n    \"\"\"``font-family`` property validation.\"\"\"\n    if len(tokens) == 1 and tokens[0].type == 'string':\n        return tokens[0].value\n    elif tokens and all(token.type == 'ident' for token in tokens):\n        return ' '.join(token.value for token in tokens)\n\n\n@property()\n@single_keyword\ndef font_kerning(keyword):\n    return keyword in ('auto', 'normal', 'none')\n\n\n@property()\n@single_token\ndef font_language_override(token):\n    keyword = get_keyword(token)\n    if keyword == 'normal':\n        return keyword\n    elif token.type == 'string':\n        return token.value\n\n\n@property()\ndef font_variant_ligatures(tokens):\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword in ('normal', 'none'):\n            return keyword\n    values = []\n    couples = (\n        ('common-ligatures', 'no-common-ligatures'),\n        ('historical-ligatures', 'no-historical-ligatures'),\n        ('discretionary-ligatures', 'no-discretionary-ligatures'),\n        ('contextual', 'no-contextual'))\n    all_values = []\n    for couple in couples:\n        all_values.extend(couple)\n    for token in tokens:\n        if token.type != 'ident':\n            return None\n        if token.value in all_values:\n            concurrent_values = next(\n                couple for couple in couples if token.value in couple)\n            if any(value in values for value in concurrent_values):\n                return None\n            else:\n                values.append(token.value)\n        else:\n            return None\n    if values:\n        return tuple(values)\n\n\n@property()\n@single_keyword\ndef font_variant_position(keyword):\n    return keyword in ('normal', 'sub', 'super')\n\n\n@property()\n@single_keyword\ndef font_variant_caps(keyword):\n    return keyword in (\n        'normal', 'small-caps', 'all-small-caps', 'petite-caps',\n        'all-petite-caps', 'unicase', 'titling-caps')\n\n\n@property()\ndef font_variant_numeric(tokens):\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword == 'normal':\n            return keyword\n    values = []\n    couples = (\n        ('lining-nums', 'oldstyle-nums'),\n        ('proportional-nums', 'tabular-nums'),\n        ('diagonal-fractions', 'stacked-fractions'),\n        ('ordinal',), ('slashed-zero',))\n    all_values = []\n    for couple in couples:\n        all_values.extend(couple)\n    for token in tokens:\n        if token.type != 'ident':\n            return None\n        if token.value in all_values:\n            concurrent_values = next(\n                couple for couple in couples if token.value in couple)\n            if any(value in values for value in concurrent_values):\n                return None\n            else:\n                values.append(token.value)\n        else:\n            return None\n    if values:\n        return tuple(values)\n\n\n@property()\ndef font_feature_settings(tokens):\n    \"\"\"``font-feature-settings`` property validation.\"\"\"\n    if len(tokens) == 1 and get_keyword(tokens[0]) == 'normal':\n        return 'normal'\n\n    @comma_separated_list\n    def font_feature_settings_list(tokens):\n        feature, value = None, None\n\n        if len(tokens) == 2:\n            tokens, token = tokens[:-1], tokens[-1]\n            if token.type == 'ident':\n                value = {'on': 1, 'off': 0}.get(token.value)\n            elif number := get_number(token, negative=False, integer=True):\n                value = number.value\n        elif len(tokens) == 1:\n            value = 1\n\n        if len(tokens) == 1:\n            token, = tokens\n            if token.type == 'string' and len(token.value) == 4:\n                if all(0x20 <= ord(letter) <= 0x7f for letter in token.value):\n                    feature = token.value\n\n        if feature is not None and value is not None:\n            return feature, value\n\n    return font_feature_settings_list(tokens)\n\n\n@property()\n@single_keyword\ndef font_variant_alternates(keyword):\n    # TODO: support other values\n    # See https://drafts.csswg.org/css-fonts/#font-variant-alternates-prop\n    return keyword in ('normal', 'historical-forms')\n\n\n@property()\ndef font_variant_east_asian(tokens):\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword == 'normal':\n            return keyword\n    values = []\n    couples = (\n        ('jis78', 'jis83', 'jis90', 'jis04', 'simplified', 'traditional'),\n        ('full-width', 'proportional-width'),\n        ('ruby',))\n    all_values = []\n    for couple in couples:\n        all_values.extend(couple)\n    for token in tokens:\n        if token.type != 'ident':\n            return None\n        if token.value in all_values:\n            concurrent_values = next(\n                couple for couple in couples if token.value in couple)\n            if any(value in values for value in concurrent_values):\n                return None\n            else:\n                values.append(token.value)\n        else:\n            return None\n    if values:\n        return tuple(values)\n\n\n@property()\ndef font_variation_settings(tokens):\n    \"\"\"``font-variation-settings`` property validation.\"\"\"\n    if len(tokens) == 1 and get_keyword(tokens[0]) == 'normal':\n        return 'normal'\n\n    @comma_separated_list\n    def font_variation_settings_list(tokens):\n        if len(tokens) == 2:\n            key, value = tokens\n            if key.type == 'string' and value.type == 'number':\n                return key.value, value.value\n\n    return font_variation_settings_list(tokens)\n\n\n@property()\n@single_token\ndef font_size(token):\n    \"\"\"``font-size`` property validation.\"\"\"\n    if length := get_length(token, negative=False, percentage=True):\n        return length\n    font_size_keyword = get_keyword(token)\n    if font_size_keyword in ('smaller', 'larger'):\n        return font_size_keyword\n    if font_size_keyword in computed_values.FONT_SIZE_KEYWORDS:\n        return font_size_keyword\n\n\n@property()\n@single_keyword\ndef font_style(keyword):\n    \"\"\"``font-style`` property validation.\"\"\"\n    return keyword in ('normal', 'italic', 'oblique')\n\n\n@property()\n@single_keyword\ndef font_stretch(keyword):\n    \"\"\"Validation for the ``font-stretch`` property.\"\"\"\n    return keyword in (\n        'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed',\n        'normal',\n        'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded')\n\n\n@property()\n@single_token\ndef font_weight(token):\n    \"\"\"``font-weight`` property validation.\"\"\"\n    keyword = get_keyword(token)\n    if keyword in ('normal', 'bold', 'bolder', 'lighter'):\n        return keyword\n    if token.type == 'number' and token.int_value is not None:\n        if token.int_value in (100, 200, 300, 400, 500, 600, 700, 800, 900):\n            return token.int_value\n\n\n@property()\n@single_keyword\ndef object_fit(keyword):\n    # TODO: Figure out what the spec means by \"'scale-down' flag\".\n    #   As of this writing, neither Firefox nor chrome support\n    #   anything other than a single keyword as is done here.\n    return keyword in ('fill', 'contain', 'cover', 'none', 'scale-down')\n\n\n@property(unstable=True)\n@single_token\ndef image_resolution(token):\n    # TODO: support 'snap' and 'from-image'\n    return get_resolution(token)\n\n\n@property('letter-spacing')\n@property('word-spacing')\n@single_token\ndef spacing(token):\n    \"\"\"Validation for ``letter-spacing`` and ``word-spacing``.\"\"\"\n    if get_keyword(token) == 'normal':\n        return 'normal'\n    if length := get_length(token):\n        return length\n\n\n@property()\n@single_token\ndef outline_offset(token):\n    \"\"\"Validation for ``outline-offset``.\"\"\"\n    if length := get_length(token):\n        return length\n\n\n@property()\n@single_token\ndef line_height(token):\n    \"\"\"``line-height`` property validation.\"\"\"\n    if get_keyword(token) == 'normal':\n        return 'normal'\n    elif number := get_number(token, negative=False):\n        return number\n    elif length := get_length(token, negative=False, percentage=True):\n        return length\n\n\n@property()\n@single_keyword\ndef list_style_position(keyword):\n    \"\"\"``list-style-position`` property validation.\"\"\"\n    return keyword in ('inside', 'outside')\n\n\n@property()\n@single_token\ndef list_style_type(token):\n    \"\"\"``list-style-type`` property validation.\"\"\"\n    if token.type == 'ident':\n        return token.value\n    elif token.type == 'string':\n        return ('string', token.value)\n    elif token.type == 'function' and token.name == 'symbols':\n        allowed_types = ('cyclic', 'numeric', 'alphabetic', 'symbolic', 'fixed')\n        if not (function_arguments := remove_whitespace(token.arguments)):\n            return\n        arguments = []\n        if function_arguments[0].type == 'ident':\n            if function_arguments[0].value in allowed_types:\n                index = 1\n                arguments.append(function_arguments[0].value)\n            else:\n                return\n        else:\n            arguments.append('symbolic')\n            index = 0\n        if len(function_arguments) < index + 1:\n            return\n        for i in range(index, len(function_arguments)):\n            if function_arguments[i].type != 'string':\n                return\n            arguments.append(function_arguments[i].value)\n        if arguments[0] in ('alphabetic', 'numeric'):\n            if len(arguments) < 3:\n                return\n        return ('symbols()', tuple(arguments))\n\n\n@property('min-width')\n@property('min-height')\n@property('min-block-size')\n@property('min-inline-size')\n@single_token\ndef min_width_height(token):\n    \"\"\"``min-width`` and ``min-height`` properties validation.\"\"\"\n    # See https://www.w3.org/TR/css-flexbox-1/#min-size-auto\n    if get_keyword(token) == 'auto':\n        return 'auto'\n    else:\n        return length_or_precentage([token])\n\n\n@property('padding-top')\n@property('padding-right')\n@property('padding-bottom')\n@property('padding-left')\n@property('padding-block-start')\n@property('padding-block-end')\n@property('padding-inline-start')\n@property('padding-inline-end')\n@single_token\ndef length_or_precentage(token):\n    \"\"\"``padding-*`` properties validation.\"\"\"\n    if length := get_length(token, negative=False, percentage=True):\n        return length\n\n\n@property('max-width')\n@property('max-height')\n@property('max-block-size')\n@property('max-inline-size')\n@single_token\ndef max_width_height(token):\n    \"\"\"Validation for max-width and max-height\"\"\"\n    if length := get_length(token, negative=False, percentage=True):\n        return length\n    if get_keyword(token) == 'none':\n        return Dimension(inf, 'px')\n\n\n@property()\n@single_token\ndef opacity(token):\n    \"\"\"Validation for the ``opacity`` property.\"\"\"\n    if number := get_number(token):\n        return min(1, max(0, number.value))\n    elif percentage := get_percentage(token):\n        return min(1, max(0, percentage.value / 100))\n\n\n@property()\n@single_token\ndef z_index(token):\n    \"\"\"Validation for the ``z-index`` property.\"\"\"\n    if get_keyword(token) == 'auto':\n        return 'auto'\n    elif number := get_number(token, integer=True):\n        return number.value\n\n\n@property('orphans')\n@property('widows')\n@single_token\ndef orphans_widows(token):\n    \"\"\"Validation for the ``orphans`` and ``widows`` properties.\"\"\"\n    if number := get_number(token, negative=False, integer=True):\n        if number.value >= 1:\n            return number.value\n\n\n@property(unstable=True)\n@single_token\ndef column_count(token):\n    \"\"\"Validation for the ``column-count`` property.\"\"\"\n    if number := get_number(token, negative=False, integer=True):\n        if number.value >= 1:\n            return number.value\n    elif get_keyword(token) == 'auto':\n        return 'auto'\n\n\n@property()\n@single_keyword\ndef overflow(keyword):\n    \"\"\"Validation for the ``overflow`` property.\"\"\"\n    return keyword in ('auto', 'visible', 'hidden', 'scroll')\n\n\n@property()\n@single_keyword\ndef text_overflow(keyword):\n    \"\"\"Validation for the ``text-overflow`` property.\"\"\"\n    return keyword in ('clip', 'ellipsis')\n\n\n@property()\n@single_token\ndef position(token):\n    \"\"\"``position`` property validation.\"\"\"\n    if token.type == 'function' and token.name == 'running':\n        if len(token.arguments) == 1 and token.arguments[0].type == 'ident':\n            return ('running()', token.arguments[0].value)\n    keyword = get_single_keyword([token])\n    if keyword in ('static', 'relative', 'absolute', 'fixed'):\n        return keyword\n\n\n@property()\ndef quotes(tokens):\n    \"\"\"``quotes`` property validation.\"\"\"\n    if len(tokens) == 1:\n        if (keyword := get_keyword(tokens[0])) in ('auto', 'none'):\n            return keyword\n    if (tokens and len(tokens) % 2 == 0 and\n            all(token.type == 'string' for token in tokens)):\n        strings = tuple(token.value for token in tokens)\n        # Separate open and close quotes.\n        # eg.  ('«', '»', '“', '”')  -> (('«', '“'), ('»', '”'))\n        return strings[::2], strings[1::2]\n\n\n@property()\n@single_keyword\ndef table_layout(keyword):\n    \"\"\"Validation for the ``table-layout`` property\"\"\"\n    if keyword in ('fixed', 'auto'):\n        return keyword\n\n\n@property()\n@single_keyword\ndef text_align_all(keyword):\n    \"\"\"``text-align-all`` property validation.\"\"\"\n    return keyword in ('left', 'right', 'center', 'justify', 'start', 'end')\n\n\n@property()\n@single_keyword\ndef text_align_last(keyword):\n    \"\"\"``text-align-last`` property validation.\"\"\"\n    return keyword in ('auto', 'left', 'right', 'center', 'justify', 'start', 'end')\n\n\n@property()\ndef text_decoration_line(tokens):\n    \"\"\"``text-decoration-line`` property validation.\"\"\"\n    if (keywords := {get_keyword(token) for token in tokens}) == {'none'}:\n        return 'none'\n    allowed_values = {'underline', 'overline', 'line-through', 'blink'}\n    if len(tokens) == len(keywords) and keywords.issubset(allowed_values):\n        return keywords\n\n\n@property()\n@single_keyword\ndef text_decoration_style(keyword):\n    \"\"\"``text-decoration-style`` property validation.\"\"\"\n    if keyword in ('solid', 'double', 'dotted', 'dashed', 'wavy'):\n        return keyword\n\n\n@property()\n@single_token\ndef text_decoration_thickness(token):\n    \"\"\"``text-decoration-thickness`` property validation.\"\"\"\n    if length := get_length(token, percentage=True):\n        return length\n    elif (keyword := get_keyword(token)) in ('auto', 'from-font'):\n        return keyword\n\n\n@property()\n@single_token\ndef text_indent(token):\n    \"\"\"``text-indent`` property validation.\"\"\"\n    if length := get_length(token, percentage=True):\n        return length\n\n\n@property()\n@single_keyword\ndef text_transform(keyword):\n    \"\"\"``text-align`` property validation.\"\"\"\n    return keyword in ('none', 'uppercase', 'lowercase', 'capitalize', 'full-width')\n\n\n@property()\n@single_token\ndef vertical_align(token):\n    \"\"\"Validation for the ``vertical-align`` property\"\"\"\n    if length := get_length(token, percentage=True):\n        return length\n    keyword = get_keyword(token)\n    if keyword in ('baseline', 'middle', 'sub', 'super',\n                   'text-top', 'text-bottom', 'top', 'bottom'):\n        return keyword\n\n\n@property()\n@single_keyword\ndef visibility(keyword):\n    \"\"\"``white-space`` property validation.\"\"\"\n    return keyword in ('visible', 'hidden', 'collapse')\n\n\n@property()\n@single_keyword\ndef white_space(keyword):\n    \"\"\"``white-space`` property validation.\"\"\"\n    return keyword in ('normal', 'pre', 'nowrap', 'pre-wrap', 'pre-line')\n\n\n@property()\n@single_keyword\ndef overflow_wrap(keyword):\n    \"\"\"``overflow-wrap`` property validation.\"\"\"\n    return keyword in ('anywhere', 'normal', 'break-word')\n\n\n@property()\n@single_keyword\ndef word_break(keyword):\n    \"\"\"``word-break`` property validation.\"\"\"\n    return keyword in ('normal', 'break-all')\n\n\n@property()\n@single_token\ndef flex_basis(token):\n    \"\"\"``flex-basis`` property validation.\"\"\"\n    if (basis := width_height([token])) is not None:\n        return basis\n    elif get_keyword(token) == 'content':\n        return 'content'\n\n\n@property()\n@single_keyword\ndef flex_direction(keyword):\n    \"\"\"``flex-direction`` property validation.\"\"\"\n    return keyword in ('row', 'row-reverse', 'column', 'column-reverse')\n\n\n@property('flex-grow')\n@property('flex-shrink')\n@single_token\ndef flex_grow_shrink(token):\n    if number := get_number(token):\n        return number.value\n\n\ndef _inflexible_breadth(token):\n    \"\"\"Parse ``inflexible-breadth``.\"\"\"\n    if (keyword := get_keyword(token)) in ('auto', 'min-content', 'max-content'):\n        return keyword\n    elif keyword:\n        return\n    elif length := get_length(token, negative=False, percentage=True):\n        return length\n\n\ndef _track_breadth(token):\n    \"\"\"Parse ``track-breadth``.\"\"\"\n    if token.type == 'dimension' and token.value >= 0 and token.unit.lower() == 'fr':\n        return Dimension(token.value, token.unit.lower())\n    return _inflexible_breadth(token)\n\n\ndef _track_size(token):\n    \"\"\"Parse ``track-size``.\"\"\"\n    if track_breadth := _track_breadth(token):\n        return track_breadth\n    function = Function(token)\n    arguments = function.split_comma()\n    if function.name == 'minmax':\n        if len(arguments) == 2:\n            inflexible_breadth = _inflexible_breadth(arguments[0])\n            track_breadth = _track_breadth(arguments[1])\n            if inflexible_breadth and track_breadth:\n                return ('minmax()', inflexible_breadth, track_breadth)\n    elif function.name == 'fit-content':\n        if len(arguments) == 1:\n            if length := get_length(arguments[0], negative=False, percentage=True):\n                return ('fit-content()', length)\n\n\ndef _fixed_size(token):\n    \"\"\"Parse ``fixed-size``.\"\"\"\n    if length := get_length(token, negative=False, percentage=True):\n        return length\n    function = Function(token)\n    arguments = function.split_comma()\n    if function.name == 'minmax' and len(arguments) == 2:\n        if length := get_length(arguments[0], negative=False, percentage=True):\n            track_breadth = _track_breadth(arguments[1])\n            if track_breadth:\n                return ('minmax()', length, track_breadth)\n        keyword = get_keyword(arguments[0])\n        if keyword in ('min-content', 'max-content', 'auto') or length:\n            fixed_breadth = get_length(arguments[1], negative=False, percentage=True)\n            if fixed_breadth:\n                return ('minmax()', length or keyword, fixed_breadth)\n\n\ndef _line_names(token):\n    \"\"\"Parse ``line-names``.\"\"\"\n    return_line_names = []\n    if token.type == '[] block':\n        for token in token.content:\n            if token.type == 'ident':\n                return_line_names.append(token.value)\n            elif token.type != 'whitespace':\n                return\n        return tuple(return_line_names)\n\n\n@property('grid-auto-columns')\n@property('grid-auto-rows')\ndef grid_auto(tokens):\n    \"\"\"``grid-auto-columns`` and ``grid-auto-rows`` properties validation.\"\"\"\n    return_tokens = []\n    for token in tokens:\n        if track_size := _track_size(token):\n            return_tokens.append(track_size)\n            continue\n        return\n    return tuple(return_tokens)\n\n\n@property()\ndef grid_auto_flow(tokens):\n    \"\"\"``grid-auto-flow`` property validation.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword in ('row', 'column'):\n            return (keyword,)\n        elif keyword == 'dense':\n            return (keyword, 'row')\n    elif len(tokens) == 2:\n        keywords = [get_keyword(token) for token in tokens]\n        if 'dense' in keywords and ('row' in keywords or 'column' in keywords):\n            return tuple(keywords)\n\n\n@property('grid-template-columns')\n@property('grid-template-rows')\ndef grid_template(tokens):\n    \"\"\"``grid-template-columns`` and ``grid-template-rows`` validation.\"\"\"\n    return_tokens = []\n    if len(tokens) == 1 and get_keyword(tokens[0]) == 'none':\n        return 'none'\n    if get_keyword(tokens[0]) == 'subgrid':\n        return_tokens.append('subgrid')\n        subgrid_tokens = []\n        for token in tokens[1:]:\n            line_names = _line_names(token)\n            if line_names is not None:\n                subgrid_tokens.append(line_names)\n                continue\n            function = Function(token)\n            arguments = function.split_comma(single_tokens=False)\n            if arguments is None or len(arguments) != 2:\n                return\n            repeat, tracks = arguments\n            if len(repeat) != 1 or not tracks:\n                return\n            repeat, = repeat\n            if function.name == 'repeat' and len(arguments) >= 2:\n                if (repeat.type == 'number' and float(repeat.value).is_integer() and\n                        repeat.value >= 1):\n                    number = repeat.int_value\n                elif get_keyword(repeat) == 'auto-fill':\n                    number = 'auto-fill'\n                else:\n                    return\n                line_names_list = []\n                for argument in tracks:\n                    line_names = _line_names(argument)\n                    if line_names is not None:\n                        line_names_list.append(line_names)\n                subgrid_tokens.append(('repeat()', number, tuple(line_names_list)))\n                continue\n            return\n        return_tokens.append(tuple(subgrid_tokens))\n    else:\n        includes_auto_repeat = False\n        includes_track = False\n        last_is_line_name = False\n        for token in tokens:\n            line_names = _line_names(token)\n            if line_names is not None:\n                if last_is_line_name:\n                    return\n                last_is_line_name = True\n                return_tokens.append(line_names)\n                continue\n            fixed_size = _fixed_size(token)\n            if fixed_size:\n                if not last_is_line_name:\n                    return_tokens.append(())\n                last_is_line_name = False\n                return_tokens.append(fixed_size)\n                continue\n            track_size = _track_size(token)\n            if track_size:\n                if not last_is_line_name:\n                    return_tokens.append(())\n                last_is_line_name = False\n                return_tokens.append(track_size)\n                includes_track = True\n                continue\n            function = Function(token)\n            arguments = function.split_comma(single_tokens=False)\n            if arguments is None or len(arguments) != 2:\n                return\n            repeat, tracks = arguments\n            if len(repeat) != 1 or not tracks:\n                return\n            repeat, = repeat\n            if function.name == 'repeat' and len(arguments) >= 2:\n                if number := get_number(repeat, negative=False, integer=True):\n                    if number.value >= 1:\n                        number = number.value\n                elif get_keyword(repeat) in ('auto-fill', 'auto-fit'):\n                    # auto-repeat\n                    if includes_auto_repeat:\n                        return\n                    number = repeat.value\n                    includes_auto_repeat = True\n                else:\n                    return\n                names_and_sizes = []\n                repeat_last_is_line_name = False\n                for arg in tracks:\n                    line_names = _line_names(arg)\n                    if line_names is not None:\n                        if repeat_last_is_line_name:\n                            return\n                        names_and_sizes.append(line_names)\n                        repeat_last_is_line_name = True\n                        continue\n                    # fixed-repeat\n                    fixed_size = _fixed_size(arg)\n                    if fixed_size:\n                        if not repeat_last_is_line_name:\n                            names_and_sizes.append(())\n                        repeat_last_is_line_name = False\n                        names_and_sizes.append(fixed_size)\n                        continue\n                    # track-repeat\n                    track_size = _track_size(arg)\n                    if track_size:\n                        includes_track = True\n                        if not repeat_last_is_line_name:\n                            names_and_sizes.append(())\n                        repeat_last_is_line_name = False\n                        names_and_sizes.append(track_size)\n                        continue\n                    return\n                if not last_is_line_name:\n                    return_tokens.append(())\n                last_is_line_name = False\n                if not repeat_last_is_line_name:\n                    names_and_sizes.append(())\n                return_tokens.append(('repeat()', number, tuple(names_and_sizes)))\n                continue\n            return\n        if includes_auto_repeat and includes_track:\n            return\n        if not last_is_line_name:\n            return_tokens.append(())\n    return tuple(return_tokens)\n\n\n@property()\ndef grid_template_areas(tokens):\n    \"\"\"``grid-template-areas`` property validation.\"\"\"\n    if len(tokens) == 1 and get_keyword(tokens[0]) == 'none':\n        return 'none'\n    grid_areas = []\n    for token in tokens:\n        if token.type != 'string':\n            return\n        component_values = parse_component_value_list(token.value)\n        row = []\n        last_is_dot = False\n        for value in component_values:\n            if value.type == 'ident':\n                row.append(value.value)\n                last_is_dot = False\n            elif value.type == 'literal' and value.value == '.':\n                if last_is_dot:\n                    continue\n                row.append(None)\n                last_is_dot = True\n            elif value.type == 'whitespace':\n                last_is_dot = False\n            else:\n                return\n        if not row:\n            return\n        grid_areas.append(tuple(row))\n    # check row / column have the same sizes\n    if len(set(len(row) for row in grid_areas)) == 1:\n        # check areas are continuous rectangles\n        coordinates = set()\n        areas = set()\n        for y, row in enumerate(grid_areas):\n            for x, area in enumerate(row):\n                if (x, y) in coordinates or area is None:\n                    continue\n                if area in areas:\n                    return\n                areas.add(area)\n                coordinates.add((x, y))\n                nx = x\n                for nx, narea in enumerate(row[x+1:], start=x+1):\n                    if narea != area:\n                        break\n                    coordinates.add((nx, y))\n                else:\n                    nx += 1\n                for ny, nrow in enumerate(grid_areas[y+1:], start=y+1):\n                    if set(nrow[x:nx]) == {area}:\n                        for nnx in range(x, nx):\n                            coordinates.add((nnx, ny))\n                    else:\n                        break\n        return tuple(grid_areas)\n\n\n@property('grid-row-start')\n@property('grid-row-end')\n@property('grid-column-start')\n@property('grid-column-end')\ndef grid_line(tokens):\n    \"\"\"``grid-[row|column]-[start—end]`` properties validation.\"\"\"\n    if len(tokens) == 1:\n        token = tokens[0]\n        if keyword := get_keyword(token):\n            if keyword == 'auto':\n                return keyword\n            elif keyword != 'span':\n                return (None, None, token.value)\n        elif number := get_number(token, integer=True):\n            if number.value != 0:\n                return (None, number.value, None)\n        return\n    number = ident = span = None\n    for token in tokens:\n        if keyword := get_keyword(token):\n            if keyword == 'auto':\n                return\n            if keyword == 'span':\n                if span is None:\n                    span = 'span'\n                    continue\n            elif keyword and ident is None:\n                ident = token.value\n                continue\n        elif item := get_number(token, integer=True):\n            if item.value != 0 and number is None:\n                number = item.value\n                continue\n        return\n    if span:\n        if isinstance(number, int) and number < 0:\n            return\n        elif ident or number:\n            return (span, number, ident)\n    elif number:\n        return (span, number, ident)\n\n\n@property()\n@single_keyword\ndef flex_wrap(keyword):\n    \"\"\"``flex-wrap`` property validation.\"\"\"\n    return keyword in ('nowrap', 'wrap', 'wrap-reverse')\n\n\n@property()\ndef justify_content(tokens):\n    \"\"\"``justify-content`` property validation.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword in (\n                'center', 'space-between', 'space-around', 'space-evenly',\n                'stretch', 'normal', 'flex-start', 'flex-end',\n                'start', 'end', 'left', 'right'):\n            return (keyword,)\n    elif len(tokens) == 2:\n        keywords = tuple(get_keyword(token) for token in tokens)\n        if keywords[0] in ('safe', 'unsafe'):\n            if keywords[1] in (\n                    'center', 'start', 'end', 'flex-start', 'flex-end', 'left',\n                    'right'):\n                return keywords\n\n\n@property()\ndef justify_items(tokens):\n    \"\"\"``justify-items`` property validation.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword in (\n                'normal', 'stretch', 'center', 'start', 'end', 'self-start',\n                'self-end', 'flex-start', 'flex-end', 'left', 'right',\n                'legacy'):\n            return (keyword,)\n        elif keyword == 'baseline':\n            return ('first', keyword)\n    elif len(tokens) == 2:\n        keywords = tuple(get_keyword(token) for token in tokens)\n        if keywords[0] in ('safe', 'unsafe'):\n            if keywords[1] in (\n                    'center', 'start', 'end', 'self-start', 'self-end',\n                    'flex-start', 'flex-end', 'left', 'right'):\n                return keywords\n        elif 'baseline' in keywords:\n            if 'first' in keywords or 'last' in keywords:\n                return keywords\n        elif 'legacy' in keywords:\n            if set(keywords) & {'left', 'right', 'center'}:\n                return keywords\n\n\n@property()\ndef justify_self(tokens):\n    \"\"\"``justify-self`` property validation.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword in (\n                'auto', 'normal', 'stretch', 'center', 'start', 'end',\n                'self-start', 'self-end', 'flex-start', 'flex-end', 'left',\n                'right'):\n            return (keyword,)\n        elif keyword == 'baseline':\n            return ('first', keyword)\n    elif len(tokens) == 2:\n        keywords = tuple(get_keyword(token) for token in tokens)\n        if keywords[0] in ('safe', 'unsafe'):\n            if keywords[1] in (\n                    'center', 'start', 'end', 'self-start', 'self-end',\n                    'flex-start', 'flex-end', 'left', 'right'):\n                return keywords\n        elif 'baseline' in keywords:\n            if 'first' in keywords or 'last' in keywords:\n                return keywords\n\n\n@property()\ndef align_items(tokens):\n    \"\"\"``align-items`` property validation.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword in (\n                'normal', 'stretch', 'center', 'start', 'end', 'self-start',\n                'self-end', 'flex-start', 'flex-end'):\n            return (keyword,)\n        elif keyword == 'baseline':\n            return ('first', keyword)\n    elif len(tokens) == 2:\n        keywords = tuple(get_keyword(token) for token in tokens)\n        if keywords[0] in ('safe', 'unsafe'):\n            if keywords[1] in (\n                    'center', 'start', 'end', 'self-start', 'self-end',\n                    'flex-start', 'flex-end'):\n                return keywords\n        elif 'baseline' in keywords:\n            if 'first' in keywords or 'last' in keywords:\n                return keywords\n\n\n@property()\ndef align_self(tokens):\n    \"\"\"``align-self`` property validation.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword in (\n                'auto', 'normal', 'stretch', 'center', 'start', 'end',\n                'self-start', 'self-end', 'flex-start', 'flex-end'):\n            return (keyword,)\n        elif keyword == 'baseline':\n            return ('first', keyword)\n    elif len(tokens) == 2:\n        keywords = tuple(get_keyword(token) for token in tokens)\n        if keywords[0] in ('safe', 'unsafe'):\n            if keywords[1] in (\n                    'center', 'start', 'end', 'self-start', 'self-end',\n                    'flex-start', 'flex-end'):\n                return keywords\n        elif 'baseline' in keywords:\n            if 'first' in keywords or 'last' in keywords:\n                return keywords\n\n\n@property()\ndef align_content(tokens):\n    \"\"\"``align-content`` property validation.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_keyword(tokens[0])\n        if keyword in (\n                'center', 'space-between', 'space-around', 'space-evenly',\n                'stretch', 'normal', 'flex-start', 'flex-end',\n                'start', 'end'):\n            return (keyword,)\n        elif keyword == 'baseline':\n            return ('first', keyword)\n    elif len(tokens) == 2:\n        keywords = tuple(get_keyword(token) for token in tokens)\n        if keywords[0] in ('safe', 'unsafe'):\n            if keywords[1] in (\n                    'center', 'start', 'end', 'flex-start', 'flex-end'):\n                return keywords\n        elif 'baseline' in keywords:\n            if 'first' in keywords or 'last' in keywords:\n                return keywords\n\n\n@property()\n@single_token\ndef order(token):\n    if number := get_number(token, integer=True):\n        return number.value\n\n\n@property(unstable=True)\n@single_keyword\ndef image_rendering(keyword):\n    \"\"\"Validation for ``image-rendering``.\"\"\"\n    return keyword in ('auto', 'crisp-edges', 'pixelated')\n\n\n@property(unstable=True)\ndef image_orientation(tokens):\n    \"\"\"Validation for ``image-orientation``.\"\"\"\n    keyword = get_single_keyword(tokens)\n    if keyword in ('none', 'from-image'):\n        return keyword\n    angle, flip = None, None\n    for token in tokens:\n        keyword = get_keyword(token)\n        if keyword == 'flip':\n            if flip is not None:\n                return\n            flip = True\n            continue\n        if angle is None:\n            angle = get_angle(token)\n            if angle is not None:\n                continue\n        return\n    angle = 0 if angle is None else angle\n    flip = False if flip is None else flip\n    return (angle, flip)\n\n\n@property(unstable=True)\ndef size(tokens):\n    \"\"\"``size`` property validation.\n\n    See https://www.w3.org/TR/css-page-3/#page-size-prop\n\n    \"\"\"\n    lengths = [get_length(token, negative=False) for token in tokens]\n    if all(lengths):\n        if len(lengths) == 1:\n            return (lengths[0], lengths[0])\n        elif len(lengths) == 2:\n            return tuple(lengths)\n\n    keywords = [get_keyword(token) for token in tokens]\n    if len(keywords) == 1:\n        keyword = keywords[0]\n        if keyword in computed_values.PAGE_SIZES:\n            return computed_values.PAGE_SIZES[keyword]\n        elif keyword in ('auto', 'portrait'):\n            return computed_values.INITIAL_PAGE_SIZE\n        elif keyword == 'landscape':\n            return computed_values.INITIAL_PAGE_SIZE[::-1]\n\n    if len(keywords) == 2:\n        if keywords[0] in ('portrait', 'landscape'):\n            orientation, page_size = keywords\n        elif keywords[1] in ('portrait', 'landscape'):\n            page_size, orientation = keywords\n        else:\n            page_size = None\n        if page_size in computed_values.PAGE_SIZES:\n            width_height = computed_values.PAGE_SIZES[page_size]\n            if orientation == 'portrait':\n                return width_height\n            else:\n                height, width = width_height\n                return width, height\n\n\n@property(proprietary=True)\n@single_token\ndef anchor(token):\n    \"\"\"Validation for ``anchor``.\"\"\"\n    if get_keyword(token) == 'none':\n        return 'none'\n    function = Function(token)\n    if arguments := function.split_space():\n        prototype = (function.name, [argument.type for argument in arguments])\n        if prototype == ('attr', ['ident']):\n            return ('attr()', arguments[0].value)\n\n\n@property(proprietary=True, wants_base_url=True)\n@single_token\ndef link(token, base_url):\n    \"\"\"Validation for ``link``.\"\"\"\n    if get_keyword(token) == 'none':\n        return 'none'\n    parsed_url = get_url(token, base_url)\n    if parsed_url:\n        return parsed_url\n    function = Function(token)\n    if arguments := function.split_space():\n        prototype = (function.name, [argument.type for argument in arguments])\n        if prototype == ('attr', ['ident']):\n            return ('attr()', arguments[0].value)\n\n\n@property()\n@single_token\ndef tab_size(token):\n    \"\"\"Validation for ``tab-size``.\n\n    See https://www.w3.org/TR/css-text-3/#tab-size\n\n    \"\"\"\n    if token.type == 'number' and token.int_value is not None:\n        if value := token.int_value:\n            return value\n    return get_length(token, negative=False)\n\n\n@property(unstable=True)\n@single_token\ndef hyphens(token):\n    \"\"\"Validation for ``hyphens``.\"\"\"\n    if (keyword := get_keyword(token)) in ('none', 'manual', 'auto'):\n        return keyword\n\n\n@property(unstable=True)\n@single_token\ndef hyphenate_character(token):\n    \"\"\"Validation for ``hyphenate-character``.\"\"\"\n    if get_keyword(token) == 'auto':\n        return '‐'\n    elif token.type == 'string':\n        return token.value\n\n\n@property(unstable=True)\n@single_token\ndef hyphenate_limit_zone(token):\n    \"\"\"Validation for ``hyphenate-limit-zone``.\"\"\"\n    return get_length(token, negative=False, percentage=True)\n\n\n@property(unstable=True)\ndef hyphenate_limit_chars(tokens):\n    \"\"\"Validation for ``hyphenate-limit-chars``.\"\"\"\n    if len(tokens) == 1:\n        token, = tokens\n        keyword = get_keyword(token)\n        if keyword == 'auto':\n            return (5, 2, 2)\n        elif number := get_number(token, integer=True):\n            return (number.value, 2, 2)\n    elif len(tokens) == 2:\n        total, left = tokens\n        total_keyword = get_keyword(total)\n        left_keyword = get_keyword(left)\n        if total_number := get_number(total, integer=True):\n            if left_number := get_number(left, integer=True):\n                return (total_number.value, left_number.value, left_number.value)\n            elif left_keyword == 'auto':\n                return (total_number.value, 2, 2)\n        elif total_keyword == 'auto':\n            if left_number := get_number(left, integer=True):\n                return (5, left_number.value, left_number.value)\n            elif left_keyword == 'auto':\n                return (5, 2, 2)\n    elif len(tokens) == 3:\n        total, left, right = tokens\n        result = []\n        for token in total, left, right:\n            if get_keyword(token) == 'auto':\n                result.append(5 if token is total else 2)\n            elif number := get_number(token, integer=True):\n                result.append(number.value)\n            else:\n                return\n        return tuple(result)\n\n\n@property(proprietary=True)\n@single_token\ndef lang(token):\n    \"\"\"Validation for ``lang``.\"\"\"\n    if get_keyword(token) == 'none':\n        return 'none'\n    function = Function(token)\n    if arguments := function.split_space():\n        prototype = (function.name, [argument.type for argument in arguments])\n        if prototype == ('attr', ['ident']):\n            return ('attr()', arguments[0].value)\n    elif token.type == 'string':\n        return ('string', token.value)\n\n\n@property(unstable=True, wants_base_url=True)\ndef bookmark_label(tokens, base_url):\n    \"\"\"Validation for ``bookmark-label``.\"\"\"\n    parsed_tokens = tuple(get_content_list_token(token, base_url) for token in tokens)\n    if None not in parsed_tokens:\n        return parsed_tokens\n\n\n@property(unstable=True)\n@single_token\ndef bookmark_level(token):\n    \"\"\"Validation for ``bookmark-level``.\"\"\"\n    if number := get_number(token, negative=False, integer=True):\n        if number.value >= 1:\n            return number.value\n    elif get_keyword(token) == 'none':\n        return 'none'\n\n\n@property(unstable=True)\n@single_keyword\ndef bookmark_state(keyword):\n    \"\"\"Validation for ``bookmark-state``.\"\"\"\n    return keyword in ('open', 'closed')\n\n\n@property(unstable=True)\n@single_keyword\ndef footnote_display(keyword):\n    \"\"\"Validation for ``footnote-display``.\"\"\"\n    return keyword in ('block', 'inline', 'compact')\n\n\n@property(unstable=True)\n@single_keyword\ndef footnote_policy(keyword):\n    \"\"\"Validation for ``footnote-policy``.\"\"\"\n    return keyword in ('auto', 'line', 'block')\n\n\n@property(unstable=True, wants_base_url=True)\n@comma_separated_list\ndef string_set(tokens, base_url):\n    \"\"\"Validation for ``string-set``.\"\"\"\n    # Spec asks for strings after custom keywords, but we allow content-lists\n    if len(tokens) >= 2:\n        var_name = get_custom_ident(tokens[0])\n        if var_name is None:\n            return\n        parsed_tokens = tuple(\n            get_content_list_token(token, base_url) for token in tokens[1:])\n        if None not in parsed_tokens:\n            return (var_name, parsed_tokens)\n    elif tokens and get_keyword(tokens[0]) == 'none':\n        return 'none', ()\n\n\n@property()\ndef transform(tokens):\n    \"\"\"Validation for ``transform``.\"\"\"\n    if get_single_keyword(tokens) == 'none':\n        return ()\n    else:\n        transforms = []\n        for token in tokens:\n            function = Function(token)\n            arguments = function.split_comma()\n            if arguments is None:\n                return\n\n            all_numbers = {argument.type for argument in arguments} == {'number'}\n            if len(arguments) == 1:\n                angle = get_angle(arguments[0])\n                length = get_length(arguments[0], percentage=True)\n                if function.name == 'rotate' and angle is not None:\n                    transforms.append((function.name, angle))\n                elif function.name in ('skewx', 'skew') and angle is not None:\n                    transforms.append(('skew', (angle, 0)))\n                elif function.name == 'skewy' and angle is not None:\n                    transforms.append(('skew', (0, angle)))\n                elif function.name in ('translatex', 'translate') and length:\n                    transforms.append(('translate', (length, ZERO_PIXELS)))\n                elif function.name == 'translatey' and length:\n                    transforms.append(('translate', (ZERO_PIXELS, length)))\n                elif function.name == 'scalex' and all_numbers:\n                    transforms.append(('scale', (arguments[0].value, 1)))\n                elif function.name == 'scaley' and all_numbers:\n                    transforms.append(('scale', (1, arguments[0].value)))\n                elif function.name == 'scale' and all_numbers:\n                    transforms.append(('scale', (arguments[0].value,) * 2))\n                else:\n                    return\n            elif len(arguments) == 2:\n                if function.name == 'scale' and all_numbers:\n                    values = tuple(argument.value for argument in arguments)\n                    transforms.append((function.name, values))\n                elif function.name == 'translate':\n                    lengths = tuple(\n                        get_length(token, percentage=True) for token in arguments)\n                    if all(lengths):\n                        transforms.append((function.name, lengths))\n                    else:\n                        return\n                elif function.name == 'skew':\n                    angles = tuple(get_angle(token) for token in arguments)\n                    if all(angle is not None for angle in angles):\n                        transforms.append((function.name, angles))\n                    else:\n                        return\n                else:\n                    return\n            elif len(arguments) == 6 and function.name == 'matrix' and all_numbers:\n                transforms.append(\n                    (function.name, tuple(argument.value for argument in arguments)))\n            else:\n                return\n        return tuple(transforms)\n\n\n@property()\n@single_token\ndef appearance(token):\n    \"\"\"``appearance`` property validation.\"\"\"\n    if (keyword := get_keyword(token)) in ('none', 'auto'):\n        return keyword\n\n\n@property()\ndef color_scheme(tokens):\n    \"\"\"``color-scheme`` property validation.\"\"\"\n    if len(tokens) == 1:\n        keyword = get_single_keyword(tokens)\n        if keyword == 'normal':\n            return keyword\n        elif keyword != 'only':\n            return (keyword,)\n        else:\n            return\n    else:\n        keywords = []\n        only = False\n        for i, token in enumerate(tokens):\n            keyword = get_keyword(token)\n            if keyword == 'only':\n                if only or i not in (0, len(tokens) - 1):\n                    return\n                else:\n                    only = True\n            elif keyword == 'normal':\n                return\n            elif keyword:\n                keywords.append(keyword)\n            else:\n                return\n        if only:\n            keywords.append('only')\n        return tuple(keywords)\n"
  },
  {
    "path": "weasyprint/document.py",
    "content": "\"\"\"Document generation management.\"\"\"\n\nimport functools\nimport io\nfrom hashlib import md5\nfrom pathlib import Path\n\nfrom . import CSS, DEFAULT_OPTIONS\nfrom .anchors import gather_anchors, make_page_bookmark_tree\nfrom .css import get_all_computed_styles\nfrom .css.counters import CounterStyle\nfrom .css.targets import TargetCollector\nfrom .draw import draw_page\nfrom .formatting_structure.build import build_formatting_structure\nfrom .html import get_html_metadata\nfrom .images import get_image_from_uri as original_get_image_from_uri\nfrom .layout import LayoutContext, layout_document\nfrom .logger import PROGRESS_LOGGER\nfrom .matrix import Matrix\nfrom .pdf import VARIANTS, generate_pdf\nfrom .pdf.metadata import DocumentMetadata\nfrom .text.fonts import FontConfiguration\n\n\nclass Page:\n    \"\"\"Represents a single rendered page.\n\n    Should be obtained from :attr:`Document.pages` but not\n    instantiated directly.\n\n    \"\"\"\n\n    def __init__(self, page_box):\n        #: The page width, including margins, in CSS pixels.\n        self.width = page_box.margin_width()\n\n        #: The page height, including margins, in CSS pixels.\n        self.height = page_box.margin_height()\n\n        #: The page bleed widths as a :obj:`dict` with ``'top'``, ``'right'``,\n        #: ``'bottom'`` and ``'left'`` as keys, and values in CSS pixels.\n        self.bleed = {\n            side: page_box.style[f'bleed_{side}'].value\n            for side in ('top', 'right', 'bottom', 'left')}\n\n        #: The :obj:`list` of ``(level, label, target, state)``\n        #: :obj:`tuples <tuple>`. ``level`` and ``label`` are respectively an\n        #: :obj:`int` and a :obj:`string <str>`, based on the CSS properties\n        #: of the same names. ``target`` is an ``(x, y)`` point in CSS pixels\n        #: from the top-left of the page.\n        self.bookmarks = []\n\n        #: The :obj:`list` of ``(link_type, target, rectangle, box)``\n        #: :obj:`tuples <tuple>`. A ``rectangle`` is ``(x, y, width, height)``,\n        #: in CSS pixels from the top-left of the page. ``link_type`` is one of\n        #: three strings:\n        #:\n        #: * ``'external'``: ``target`` is an absolute URL\n        #: * ``'internal'``: ``target`` is an anchor name (see\n        #:   :attr:`Page.anchors`).\n        #:   The anchor might be defined in another page,\n        #:   in multiple pages (in which case the first occurence is used),\n        #:   or not at all.\n        #: * ``'attachment'``: ``target`` is an absolute URL and points\n        #:   to a resource to attach to the document.\n        self.links = []\n\n        #: The :obj:`dict` mapping each anchor name to its target, an\n        #: ``(x, y)`` point in CSS pixels from the top-left of the page.\n        self.anchors = {}\n\n        #: The :obj:`dict` mapping form elements to a list\n        #: of ``(element, attributes, rectangle)`` :obj:`tuples <tuple>`.\n        #: A ``rectangle`` is ``(x, y, width, height)``, in CSS\n        #: pixels from the top-left of the page. ``atributes`` is a\n        #: :obj:`dict` of HTML tag attributes and values.\n        #: The key ``None`` will contain inputs that are not part of a form.\n        self.forms = {None: []}\n\n        gather_anchors(page_box, self.anchors, self.links, self.bookmarks, self.forms)\n        self._page_box = page_box\n\n    def paint(self, stream, scale=1):\n        \"\"\"Paint the page into the PDF file.\"\"\"\n        with stream.stacked():\n            stream.transform(a=scale, d=scale)\n            draw_page(self._page_box, stream)\n\n\nclass DiskCache:\n    \"\"\"Dict-like storing images content on disk.\n\n    Bytestring values are stored on disk. Other lightweight Python objects\n    (i.e. RasterImage instances) are still stored in memory.\n\n    \"\"\"\n\n    def __init__(self, folder):\n        self._path = Path(folder)\n        self._path.mkdir(parents=True, exist_ok=True)\n        self._memory_cache = {}\n        self._disk_paths = set()\n\n    def _path_from_key(self, key):\n        digest = md5(key.encode(), usedforsecurity=False).hexdigest()\n        return self._path / digest\n\n    def __getitem__(self, key):\n        if key in self._memory_cache:\n            return self._memory_cache[key]\n        else:\n            return self._path_from_key(key).read_bytes()\n\n    def __setitem__(self, key, value):\n        if isinstance(value, bytes):\n            path = self._path_from_key(key)\n            self._disk_paths.add(path)\n            path.write_bytes(value)\n        else:\n            self._memory_cache[key] = value\n\n    def __contains__(self, key):\n        return (\n            key in self._memory_cache or\n            self._path_from_key(key).exists())\n\n    def __del__(self):\n        try:\n            for path in self._disk_paths:\n                path.unlink(missing_ok=True)\n            self._path.rmdir()\n        except Exception:\n            # Silently ignore errors while clearing cache\n            pass\n\n\nclass Document:\n    \"\"\"A rendered document ready to be painted in a pydyf stream.\n\n    Typically obtained from :meth:`HTML.render() <weasyprint.HTML.render>`, but\n    can also be instantiated directly with a list of :class:`pages <Page>`, a\n    set of :class:`metadata <DocumentMetadata>`, a :class:`url_fetcher\n    <weasyprint.urls.URLFetcher>`, and a :class:`font_config\n    <weasyprint.text.fonts.FontConfiguration>`.\n\n    \"\"\"\n\n    @classmethod\n    def _build_layout_context(cls, html, font_config, counter_style, color_profiles,\n                              options):\n        target_collector = TargetCollector()\n        page_rules = []\n        layers = []\n        user_stylesheets = []\n        cache = options['cache']\n        if cache is None:\n            cache = {}\n        elif not isinstance(cache, (dict, DiskCache)):\n            cache = DiskCache(cache)\n        for css in options['stylesheets'] or []:\n            if not hasattr(css, 'matcher'):\n                css = CSS(\n                    guess=css, media_type=html.media_type,\n                    font_config=font_config, counter_style=counter_style,\n                    color_profiles=color_profiles)\n            user_stylesheets.append(css)\n        style_for = get_all_computed_styles(\n            html, user_stylesheets, options['presentational_hints'], font_config,\n            counter_style, color_profiles, page_rules, layers, target_collector,\n            options['pdf_forms'])\n        get_image_from_uri = functools.partial(\n            original_get_image_from_uri, cache=cache,\n            url_fetcher=html.url_fetcher, options=options)\n        PROGRESS_LOGGER.info('Step 4 - Creating formatting structure')\n        context = LayoutContext(\n            style_for, get_image_from_uri, font_config, counter_style,\n            target_collector)\n        return context\n\n    @classmethod\n    def _render(cls, html, font_config, counter_style, color_profiles, options):\n        if font_config is None:\n            font_config = FontConfiguration()\n\n        if counter_style is None:\n            counter_style = CounterStyle()\n\n        if color_profiles is None:\n            color_profiles = {}\n\n        context = cls._build_layout_context(\n            html, font_config, counter_style, color_profiles, options)\n\n        root_box = build_formatting_structure(\n            html.etree_element, context.style_for, context.get_image_from_uri,\n            html.base_url, context.target_collector, counter_style,\n            context.footnotes)\n\n        page_boxes = layout_document(html, root_box, context)\n        rendering = cls(\n            [Page(page_box) for page_box in page_boxes],\n            DocumentMetadata(**get_html_metadata(html)),\n            html.url_fetcher, font_config, color_profiles)\n        rendering._html = html\n        return rendering\n\n    def __init__(self, pages, metadata, url_fetcher, font_config, color_profiles):\n        #: A list of :class:`Page` objects.\n        self.pages = pages\n        #: A :class:`DocumentMetadata` object.\n        #: Contains information that does not belong to a specific page\n        #: but to the whole document.\n        self.metadata = metadata\n        #: A :class:`weasyprint.urls.URLFetcher` object (see :ref:`URL Fetchers`.)\n        self.url_fetcher = url_fetcher\n        #: A :obj:`dict` of fonts used by the document. Keys are hashes used to\n        #: identify fonts, values are ``Font`` objects.\n        self.fonts = {}\n\n        # Keep a reference to font_config to avoid its garbage collection until\n        # rendering is destroyed. This is needed as font_config.__del__ removes\n        # fonts that may be used when rendering\n        self.font_config = font_config\n\n        self.color_profiles = color_profiles\n\n    def copy(self, pages='all'):\n        \"\"\"Take a subset of the pages.\n\n        :type pages: :term:`iterable`\n        :param pages:\n            An iterable of :class:`Page` objects from :attr:`pages`.\n        :return:\n            A new :class:`Document` object.\n\n        Examples:\n\n        Write two PDF files for odd-numbered and even-numbered pages::\n\n            # Python lists count from 0 but pages are numbered from 1.\n            # [::2] is a slice of even list indexes but odd-numbered pages.\n            document.copy(document.pages[::2]).write_pdf('odd_pages.pdf')\n            document.copy(document.pages[1::2]).write_pdf('even_pages.pdf')\n\n        Combine multiple documents into one PDF file,\n        using metadata from the first::\n\n            all_pages = [p for doc in documents for p in doc.pages]\n            documents[0].copy(all_pages).write_pdf('combined.pdf')\n\n        \"\"\"\n        if pages == 'all':\n            pages = self.pages\n        elif not isinstance(pages, list):\n            pages = list(pages)\n        return type(self)(\n            pages, self.metadata, self.url_fetcher, self.font_config,\n            self.color_profiles)\n\n    def make_bookmark_tree(self, scale=1, transform_pages=False):\n        \"\"\"Make a tree of all bookmarks in the document.\n\n        :param float scale:\n            Zoom scale.\n        :param bool transform_pages:\n            A boolean defining whether the default PDF page transformation\n            matrix has to be applied to bookmark coordinates, setting the\n            bottom-left corner as the origin.\n        :return: A list of bookmark subtrees.\n            A subtree is ``(label, target, children, state)``. ``label`` is\n            a string, ``target`` is ``(page_number, x, y)``  and ``children``\n            is a list of child subtrees.\n\n        \"\"\"\n        root = []\n        # At one point in the document, for each \"output\" depth, how much\n        # to add to get the source level (CSS values of bookmark-level).\n        # E.g. with <h1> then <h3>, level_shifts == [0, 1]\n        # 1 means that <h3> has depth 3 - 1 = 2 in the output.\n        skipped_levels = []\n        last_by_depth = [root]\n        previous_level = 0\n        for page_number, page in enumerate(self.pages):\n            if transform_pages:\n                matrix = Matrix(a=scale, d=-scale, f=page.height * scale)\n            else:\n                matrix = Matrix(a=scale, d=scale)\n            previous_level = make_page_bookmark_tree(\n                page, skipped_levels, last_by_depth, previous_level,\n                page_number, matrix)\n        return root\n\n    def write_pdf(self, target=None, zoom=1, finisher=None, **options):\n        \"\"\"Paint the pages in a PDF file, with metadata.\n\n        :type target:\n            :class:`str`, :class:`pathlib.Path` or :term:`file object`\n        :param target:\n            A filename where the PDF file is generated, a file object, or\n            :obj:`None`.\n        :param float zoom:\n            The zoom factor in PDF units per CSS units.  **Warning**:\n            All CSS units are affected, including physical units like\n            ``cm`` and named sizes like ``A4``.  For values other than\n            1, the physical CSS units will thus be \"wrong\".\n        :type finisher: :term:`callable`\n        :param finisher:\n            A finisher function or callable that accepts the document and a\n            :class:`pydyf.PDF` object as parameters. Can be passed to perform\n            post-processing on the PDF right before the trailer is written.\n        :param options:\n            The ``options`` parameter includes by default the\n            :data:`weasyprint.DEFAULT_OPTIONS` values.\n        :returns:\n            The PDF as :obj:`bytes` if ``target`` is not provided or\n            :obj:`None`, otherwise :obj:`None` (the PDF is written to\n            ``target``).\n\n        \"\"\"\n        new_options = DEFAULT_OPTIONS.copy()\n        new_options.update(options)\n        options = new_options\n\n        # Set default PDF version for PDF variants.\n        if variant := options['pdf_variant']:\n            _, properties = VARIANTS[variant]\n            if 'version' in properties and not options['pdf_version']:\n                options['pdf_version'] = properties['version']\n            if 'identifier' in properties and not options['pdf_identifier']:\n                options['pdf_identifier'] = properties['identifier']\n\n        pdf = generate_pdf(self, target, zoom, **options)\n\n        if finisher:\n            finisher(self, pdf)\n\n        identifier = options['pdf_identifier']\n        compress = not options['uncompressed_pdf']\n        version = options['pdf_version']\n\n        if target is None:\n            output = io.BytesIO()\n            pdf.write(output, version, identifier, compress)\n            return output.getvalue()\n\n        if hasattr(target, 'write'):\n            pdf.write(target, version, identifier, compress)\n        else:\n            with open(target, 'wb') as fd:\n                pdf.write(fd, version, identifier, compress)\n"
  },
  {
    "path": "weasyprint/draw/__init__.py",
    "content": "\"\"\"Take an \"after layout\" box tree and draw it onto a pydyf stream.\"\"\"\n\nimport operator\nfrom math import floor\nfrom xml.etree import ElementTree\n\nfrom ..formatting_structure import boxes\nfrom ..images import SVGImage\nfrom ..layout import replaced\nfrom ..layout.background import BackgroundLayer\nfrom ..matrix import Matrix\nfrom ..stacking import StackingContext\nfrom .border import draw_border, draw_line, draw_outline, rounded_box, set_mask_border\nfrom .color import styled_color\nfrom .text import draw_text\n\n\ndef draw_page(page, stream):\n    \"\"\"Draw the given PageBox.\"\"\"\n    marks = page.style['marks']\n    stacking_context = StackingContext.from_page(page)\n    draw_background(\n        stream, stacking_context.box.background, clip_box=False, bleed=page.bleed,\n        marks=marks)\n    set_mask_border(stream, page)\n    draw_background(stream, page.canvas_background, clip_box=False)\n    draw_border(stream, page)\n    draw_stacking_context(stream, stacking_context)\n\n\ndef draw_stacking_context(stream, stacking_context):\n    \"\"\"Draw a ``stacking_context`` on ``stream``.\"\"\"\n    # See https://www.w3.org/TR/CSS2/zindex.html.\n    with stream.stacked():\n        box = stacking_context.box\n\n        # Apply the viewport_overflow to the html box, see #35.\n        if box.is_for_root_element and (\n                stacking_context.page.style['overflow'] != 'visible'):\n            rounded_box(stream, stacking_context.page.rounded_padding_box())\n            stream.clip()\n            stream.end()\n\n        if box.is_absolutely_positioned() and box.style['clip']:\n            top, right, bottom, left = box.style['clip']\n            if top == 'auto':\n                top = 0\n            if right == 'auto':\n                right = 0\n            if bottom == 'auto':\n                bottom = box.border_height()\n            if left == 'auto':\n                left = box.border_width()\n            stream.rectangle(\n                box.border_box_x() + right, box.border_box_y() + top,\n                left - right, bottom - top)\n            stream.clip()\n            stream.end()\n\n        if box.style['opacity'] < 1:\n            original_stream = stream\n            stream = stream.add_group(*stream.page_rectangle)\n\n        if box.transformation_matrix:\n            if box.transformation_matrix.determinant:\n                stream.transform(*box.transformation_matrix.values)\n            else:\n                return\n\n        # Point 1 is done in draw_page.\n\n        # Point 2.\n        if isinstance(box, (boxes.BlockBox, boxes.MarginBox, boxes.InlineBlockBox,\n                            boxes.TableCellBox, boxes.FlexContainerBox,\n                            boxes.GridContainerBox, boxes.ReplacedBox)):\n            set_mask_border(stream, box)\n            # The canvas background was removed by layout_backgrounds.\n            draw_background(stream, box.background)\n            draw_border(stream, box)\n\n        with stream.stacked():\n            # Dont clip the page box, see #35.\n            clip = (\n                box.style['overflow'] != 'visible' and\n                not isinstance(box, boxes.PageBox))\n            if clip:\n                # Only clip the content and the children:\n                # - the background is already clipped,\n                # - the border must *not* be clipped.\n                rounded_box(stream, box.rounded_padding_box())\n                stream.clip()\n                stream.end()\n\n            # Point 3.\n            for child_context in stacking_context.negative_z_contexts:\n                draw_stacking_context(stream, child_context)\n\n            # Point 4.\n            for block in stacking_context.block_level_boxes:\n                set_mask_border(stream, block)\n\n                if isinstance(block, boxes.TableBox):\n                    draw_table(stream, block)\n                else:\n                    draw_background(stream, block.background)\n                    draw_border(stream, block)\n\n            # Point 5.\n            for child_context in stacking_context.float_contexts:\n                draw_stacking_context(stream, child_context)\n\n            # Point 6.\n            if isinstance(box, boxes.InlineBox):\n                draw_inline_level(stream, stacking_context.page, box)\n\n            # Point 7.\n            draw_block_level(\n                stacking_context.page, stream, {box: stacking_context.blocks_and_cells})\n\n            # Point 8.\n            for child_context in stacking_context.zero_z_contexts:\n                draw_stacking_context(stream, child_context)\n\n            # Point 9.\n            for child_context in stacking_context.positive_z_contexts:\n                draw_stacking_context(stream, child_context)\n\n        # Point 10.\n        draw_outline(stream, box)\n\n        if box.style['opacity'] < 1:\n            group_id = stream.id\n            stream = original_stream\n            with stream.stacked():\n                stream.set_alpha(box.style['opacity'], stroke=True, fill=True)\n                stream.draw_x_object(group_id)\n\n\ndef draw_background(stream, bg, clip_box=True, bleed=None, marks=()):\n    \"\"\"Draw the background color and image to a ``pdf.stream.Stream``.\n\n    If ``clip_box`` is set to ``False``, the background is not clipped to the\n    border box of the background, but only to the painting area.\n\n    \"\"\"\n    if bg is None:\n        return\n\n    with stream.stacked():\n        if clip_box:\n            for box in bg.layers[-1].clipped_boxes:\n                rounded_box(stream, box)\n            stream.clip()\n            stream.end()\n\n        # Draw background color.\n        if bg.color.alpha > 0:\n            with stream.artifact(), stream.stacked():\n                stream.set_color(bg.color)\n                painting_area = bg.layers[-1].painting_area\n                stream.rectangle(*painting_area)\n                stream.clip()\n                stream.end()\n                stream.rectangle(*painting_area)\n                stream.fill()\n\n        # Draw crop marks and crosses.\n        if bleed and marks:\n            x, y, width, height = bg.layers[-1].painting_area\n            half_bleed = {key: value * 0.5 for key, value in bleed.items()}\n            svg = f'''\n              <svg height=\"{height}\" width=\"{width}\"\n                   fill=\"transparent\" stroke=\"black\" stroke-width=\"1\"\n                   xmlns=\"http://www.w3.org/2000/svg\">\n            '''\n            if 'crop' in marks:\n                svg += f'''\n                  <path d=\"M0,{bleed['top']} h{half_bleed['left']}\" />\n                  <path d=\"M0,{bleed['top']} h{half_bleed['right']}\"\n                        transform=\"translate({width},0) scale(-1,1)\" />\n                  <path d=\"M0,{bleed['bottom']} h{half_bleed['right']}\"\n                        transform=\"translate({width},{height}) scale(-1,-1)\" />\n                  <path d=\"M0,{bleed['bottom']} h{half_bleed['left']}\"\n                        transform=\"translate(0,{height}) scale(1,-1)\" />\n                  <path d=\"M{bleed['left']},0 v{half_bleed['top']}\" />\n                  <path d=\"M{bleed['right']},0 v{half_bleed['bottom']}\"\n                        transform=\"translate({width},{height}) scale(-1,-1)\" />\n                  <path d=\"M{bleed['left']},0 v{half_bleed['bottom']}\"\n                        transform=\"translate(0,{height}) scale(1,-1)\" />\n                  <path d=\"M{bleed['right']},0 v{half_bleed['top']}\"\n                        transform=\"translate({width},0) scale(-1,1)\" />\n                '''\n            if 'cross' in marks:\n                svg += f'''\n                  <circle r=\"{half_bleed['top']}\" transform=\"scale(0.5)\n                     translate({width},{half_bleed['top']}) scale(0.5)\" />\n                  <path transform=\"scale(0.5) translate({width},0)\" d=\"\n                    M-{half_bleed['top']},{half_bleed['top']} h{bleed['top']}\n                    M0,0 v{bleed['top']}\" />\n                  <circle r=\"{half_bleed['bottom']}\" transform=\"\n                    translate(0,{height}) scale(0.5)\n                    translate({width},-{half_bleed['bottom']}) scale(0.5)\" />\n                  <path d=\"M-{half_bleed['bottom']},-{half_bleed['bottom']}\n                    h{bleed['bottom']} M0,0 v-{bleed['bottom']}\" transform=\"\n                    translate(0,{height}) scale(0.5) translate({width},0)\" />\n                  <circle r=\"{half_bleed['left']}\" transform=\"scale(0.5)\n                    translate({half_bleed['left']},{height}) scale(0.5)\" />\n                  <path d=\"M{half_bleed['left']},-{half_bleed['left']}\n                    v{bleed['left']} M0,0 h{bleed['left']}\"\n                    transform=\"scale(0.5) translate(0,{height})\" />\n                  <circle r=\"{half_bleed['right']}\" transform=\"\n                    translate({width},0) scale(0.5)\n                    translate(-{half_bleed['right']},{height}) scale(0.5)\" />\n                  <path d=\"M-{half_bleed['right']},-{half_bleed['right']}\n                    v{bleed['right']} M0,0 h-{bleed['right']}\" transform=\"\n                    translate({width},0) scale(0.5) translate(0,{height})\" />\n                '''\n            svg += '</svg>'\n            tree = ElementTree.fromstring(svg)\n            image = SVGImage(tree, None, None, None)\n            # Painting area is the PDF media box\n            size = (width, height)\n            position = (x, y)\n            repeat = ('no-repeat', 'no-repeat')\n            unbounded = True\n            painting_area = position + size\n            positioning_area = (0, 0, width, height)\n            clipped_boxes = []\n            layer = BackgroundLayer(\n                image, size, position, repeat, unbounded, painting_area,\n                positioning_area, clipped_boxes)\n            bg.layers.insert(0, layer)\n        # Paint in reversed order: first layer is \"closest\" to the viewer.\n        for layer in reversed(bg.layers):\n            draw_background_image(stream, layer, bg.style)\n\n\ndef draw_background_image(stream, layer, style):\n    if layer.image is None or 0 in layer.size:\n        return\n\n    painting_x, painting_y, painting_width, painting_height = layer.painting_area\n    positioning_x, positioning_y, positioning_width, positioning_height = (\n        layer.positioning_area)\n    position_x, position_y = layer.position\n    repeat_x, repeat_y = layer.repeat\n    image_width, image_height = layer.size\n\n    if repeat_x == 'no-repeat' and repeat_y == 'no-repeat':\n        with stream.artifact():\n            # We don't use a pattern when we don't need to because some viewers\n            # (e.g., Preview on Mac) introduce unnecessary pixelation when vector\n            # images are used in patterns.\n            if not layer.unbounded:\n                stream.rectangle(\n                    painting_x, painting_y, painting_width, painting_height)\n                stream.clip()\n                stream.end()\n            # Put the image in a group so that masking outside the image and\n            # masking within the image don't conflict.\n            group = stream.add_group(*stream.page_rectangle)\n            group.transform(e=position_x + positioning_x, f=position_y + positioning_y)\n            layer.image.draw(group, image_width, image_height, style)\n            stream.draw_x_object(group.id)\n        return\n\n    if repeat_x == 'no-repeat':\n        # We want at least the whole image_width drawn on sub_surface, but we\n        # want to be sure it will not be repeated on the painting_width. We\n        # double the painting width to ensure viewers don't incorrectly bleed\n        # the edge of the pattern into the painting area. (See #1539.)\n        repeat_width = max(image_width, 2 * painting_width)\n    elif repeat_x in ('repeat', 'round'):\n        # We repeat the image each image_width.\n        repeat_width = image_width\n    else:\n        assert repeat_x == 'space'\n        n_repeats = floor(positioning_width / image_width)\n        if n_repeats >= 2:\n            # The repeat width is the whole positioning width with one image\n            # removed, divided by (the number of repeated images - 1). This\n            # way, we get the width of one image + one space. We ignore\n            # background-position for this dimension.\n            repeat_width = (positioning_width - image_width) / (n_repeats - 1)\n            position_x = 0\n        else:\n            # We don't repeat the image.\n            repeat_width = positioning_width\n\n    # Comments above apply here too.\n    if repeat_y == 'no-repeat':\n        repeat_height = max(image_height, 2 * painting_height)\n    elif repeat_y in ('repeat', 'round'):\n        repeat_height = image_height\n    else:\n        assert repeat_y == 'space'\n        n_repeats = floor(positioning_height / image_height)\n        if n_repeats >= 2:\n            repeat_height = (positioning_height - image_height) / (n_repeats - 1)\n            position_y = 0\n        else:\n            repeat_height = positioning_height\n\n    matrix = Matrix(e=position_x + positioning_x, f=position_y + positioning_y)\n    matrix @= stream.ctm\n    pattern = stream.add_pattern(\n        0, 0, image_width, image_height, repeat_width, repeat_height, matrix)\n    group = pattern.add_group(0, 0, repeat_width, repeat_height)\n\n    with stream.artifact(), stream.stacked():\n        layer.image.draw(group, image_width, image_height, style)\n        with pattern.artifact():\n            pattern.draw_x_object(group.id)\n        stream.set_color_space('Pattern')\n        stream.set_color_special(pattern.id)\n        if layer.unbounded:\n            x1, y1, x2, y2 = stream.page_rectangle\n            stream.rectangle(x1, y1, x2 - x1, y2 - y1)\n        else:\n            stream.rectangle(painting_x, painting_y, painting_width, painting_height)\n        stream.fill()\n\n\ndef draw_table(stream, table):\n    # Draw backgrounds.\n    draw_background(stream, table.background)\n    for column_group in table.column_groups:\n        draw_background(stream, column_group.background)\n        for column in column_group.children:\n            draw_background(stream, column.background)\n    for row_group in table.children:\n        draw_background(stream, row_group.background)\n        for row in row_group.children:\n            draw_background(stream, row.background)\n            for cell in row.children:\n                draw_cell_background = (\n                    table.style['border_collapse'] == 'collapse' or\n                    cell.style['empty_cells'] == 'show' or\n                    not cell.empty)\n                if draw_cell_background:\n                    draw_background(stream, cell.background)\n\n    # Draw borders.\n    if table.style['border_collapse'] == 'collapse':\n        return draw_collapsed_borders(stream, table)\n    draw_border(stream, table)\n    for row_group in table.children:\n        for row in row_group.children:\n            for cell in row.children:\n                if cell.style['empty_cells'] == 'show' or not cell.empty:\n                    draw_border(stream, cell)\n\n\ndef draw_collapsed_borders(stream, table):\n    \"\"\"Draw borders of table cells when they collapse.\"\"\"\n    row_heights = [\n        row.height for row_group in table.children\n        for row in row_group.children]\n    column_widths = table.column_widths\n    if not (row_heights and column_widths):\n        # One of the list is empty: don’t bother with empty tables.\n        return\n    row_positions = [\n        row.position_y for row_group in table.children\n        for row in row_group.children]\n    column_positions = list(table.column_positions)\n    grid_height = len(row_heights)\n    grid_width = len(column_widths)\n    assert grid_width == len(column_positions)\n    vertical_borders, horizontal_borders = table.collapsed_border_grid\n    # Add the end of the last column.\n    column_positions.append(column_positions[-1] + column_widths[-1])\n    # Add the end of the last row.\n    row_positions.append(row_positions[-1] + row_heights[-1])\n    if table.children[0].is_header:\n        header_rows = len(table.children[0].children)\n    else:\n        header_rows = 0\n    if table.children[-1].is_footer:\n        footer_rows = len(table.children[-1].children)\n    else:\n        footer_rows = 0\n    skipped_rows = table.skipped_rows\n    if skipped_rows:\n        body_rows_offset = skipped_rows - header_rows\n    else:\n        body_rows_offset = 0\n    original_grid_height = len(vertical_borders)\n    footer_rows_offset = original_grid_height - grid_height\n\n    def row_number(y, horizontal):\n        # Examples in comments for 2 headers rows, 5 body rows, 3 footer rows.\n        if header_rows and y < header_rows + int(horizontal):\n            # Row in header: y < 2 for vertical, y < 3 for horizontal.\n            return y\n        elif footer_rows and y >= grid_height - footer_rows - int(horizontal):\n            # Row in footer: y >= 7 for vertical, y >= 6 for horizontal.\n            return y + footer_rows_offset\n        else:\n            # Row in body: 2 >= y > 7 for vertical, 3 >= y > 6 for horizontal.\n            return y + body_rows_offset\n\n    segments = []\n\n    def half_max_width(border_list, yx_pairs, vertical=True):\n        result = 0\n        for y, x in yx_pairs:\n            if vertical:\n                inside = 0 <= y < grid_height and 0 <= x <= grid_width\n            else:\n                inside = 0 <= y <= grid_height and 0 <= x < grid_width\n            if inside:\n                yy = row_number(y, horizontal=not vertical)\n                _, (_, width, _) = border_list[yy][x]\n                result = max(result, width)\n        return result / 2\n\n    def add_vertical(x, y):\n        yy = row_number(y, horizontal=False)\n        score, (style, width, color) = vertical_borders[yy][x]\n        if width == 0 or color.alpha == 0:\n            return\n        pos_x = column_positions[x]\n        pos_y1 = row_positions[y]\n        if y != 0 or not table.skip_cell_border_top:\n            pos_y1 -= half_max_width(\n                horizontal_borders, [(y, x - 1), (y, x)], vertical=False)\n        pos_y2 = row_positions[y + 1]\n        if y != grid_height - 1 or not table.skip_cell_border_bottom:\n            pos_y2 += half_max_width(\n                horizontal_borders, [(y + 1, x - 1), (y + 1, x)], vertical=False)\n        segments.append((\n            score, style, width, color, 'left', (pos_x, pos_y1, 0, pos_y2 - pos_y1)))\n\n    def add_horizontal(x, y):\n        if y == 0 and table.skip_cell_border_top:\n            return\n        if y == grid_height and table.skip_cell_border_bottom:\n            return\n        yy = row_number(y, horizontal=True)\n        score, (style, width, color) = horizontal_borders[yy][x]\n        if width == 0 or color.alpha == 0:\n            return\n        pos_y = row_positions[y]\n        shift_before = half_max_width(vertical_borders, [(y - 1, x), (y, x)])\n        shift_after = half_max_width(vertical_borders, [(y - 1, x + 1), (y, x + 1)])\n        pos_x1 = column_positions[x] - shift_before\n        pos_x2 = column_positions[x + 1] + shift_after\n        segments.append((\n            score, style, width, color, 'top', (pos_x1, pos_y, pos_x2 - pos_x1, 0)))\n\n    for x in range(grid_width):\n        add_horizontal(x, 0)\n    for y in range(grid_height):\n        add_vertical(0, y)\n        for x in range(grid_width):\n            add_vertical(x + 1, y)\n            add_horizontal(x, y + 1)\n\n    # Sort bigger scores last (painted later, on top).\n    segments.sort(key=operator.itemgetter(0))\n\n    for segment in segments:\n        _, style, width, color, side, border_box = segment\n        bx, by, bw, bh = border_box\n        color = styled_color(style, color, side)\n        with stream.artifact(), stream.stacked():\n            draw_line(stream, bx, by, bx + bw, by + bh, width, style, color)\n\n\ndef draw_replacedbox(stream, box):\n    \"\"\"Draw the given :class:`boxes.ReplacedBox` to a ``pdf.stream.Stream``.\"\"\"\n    if box.style['visibility'] != 'visible' or not box.width or not box.height:\n        return\n\n    draw_width, draw_height, draw_x, draw_y = replaced.replacedbox_layout(box)\n    if draw_width <= 0 or draw_height <= 0:\n        return\n\n    with stream.stacked():\n        stream.set_alpha(1)\n        stream.transform(e=draw_x, f=draw_y)\n        with stream.stacked():\n            # TODO: Use the real intrinsic size here, not affected by\n            # 'image-resolution'?\n            box.replacement.draw(stream, draw_width, draw_height, box.style)\n\n\ndef draw_inline_level(stream, page, box, offset_x=0, text_overflow='clip',\n                      block_ellipsis='none'):\n    if isinstance(box, StackingContext):\n        stacking_context = box\n        allowed_boxes = (boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)\n        assert isinstance(stacking_context.box, allowed_boxes)\n        draw_stacking_context(stream, stacking_context)\n    else:\n        set_mask_border(stream, box)\n        draw_background(stream, box.background)\n        draw_border(stream, box)\n        if isinstance(box, (boxes.InlineBox, boxes.LineBox)):\n            if isinstance(box, boxes.LineBox):\n                text_overflow = box.text_overflow\n                block_ellipsis = box.block_ellipsis\n            ellipsis = 'none'\n            for i, child in enumerate(box.children):\n                if i == len(box.children) - 1:\n                    # Last child\n                    ellipsis = block_ellipsis\n                if isinstance(child, StackingContext):\n                    child_offset_x = offset_x\n                else:\n                    child_offset_x = offset_x + child.position_x - box.position_x\n                if isinstance(child, boxes.TextBox):\n                    with stream.marked(child, 'Span'):\n                        draw_text(\n                            stream, child, child_offset_x, text_overflow, ellipsis)\n                else:\n                    draw_inline_level(\n                        stream, page, child, child_offset_x, text_overflow, ellipsis)\n        elif isinstance(box, boxes.InlineReplacedBox):\n            with stream.marked(box, 'Figure'):\n                draw_replacedbox(stream, box)\n        else:\n            assert isinstance(box, boxes.TextBox)\n            # Should only happen for list markers.\n            draw_text(stream, box, offset_x, text_overflow)\n\n\ndef draw_block_level(page, stream, blocks_and_cells):\n    for block, blocks_and_cells in blocks_and_cells.items():\n        if isinstance(block, boxes.ReplacedBox):\n            with stream.marked(block, 'Figure'):\n                draw_replacedbox(stream, block)\n        elif block.children:\n            if isinstance(block.children[-1], boxes.LineBox):\n                for child in block.children:\n                    draw_inline_level(stream, page, child)\n        draw_block_level(page, stream, blocks_and_cells)\n"
  },
  {
    "path": "weasyprint/draw/border.py",
    "content": "\"\"\"Draw borders.\"\"\"\n\nfrom math import ceil, cos, floor, pi, sin, sqrt, tan\n\nfrom ..formatting_structure import boxes\nfrom ..layout import replaced\nfrom ..layout.percent import percentage\nfrom ..matrix import Matrix\nfrom .color import get_color, styled_color\n\nSIDES = ('top', 'right', 'bottom', 'left')\n\n\ndef set_mask_border(stream, box):\n    \"\"\"Set ``box`` mask border as alpha state on ``stream``.\"\"\"\n    if box.style['mask_border_source'][0] == 'none' or box.mask_border_image is None:\n        return\n    x, y, w, h, tl, tr, br, bl = box.rounded_border_box()\n    matrix = Matrix(e=x, f=y)\n    matrix @= stream.ctm\n    mask_stream = stream.set_alpha_state(x, y, w, h, box.style['mask_border_mode'])\n    draw_border_image(\n        box, mask_stream, box.mask_border_image, box.style['mask_border_slice'],\n        box.style['mask_border_repeat'], box.style['mask_border_outset'],\n        box.style['mask_border_width'])\n\n\ndef draw_column_rules(stream, box):\n    \"\"\"Draw the column rules to a ``pdf.stream.Stream``.\"\"\"\n    border_widths = (0, 0, 0, box.style['column_rule_width'])\n    skip_next = True\n    for child in box.children:\n        if child.style['column_span'] == 'all':\n            skip_next = True\n            continue\n        elif skip_next:\n            skip_next = False\n            continue\n        with stream.stacked():\n            rule_width = box.style['column_rule_width']\n            rule_style = box.style['column_rule_style']\n            if box.style['column_gap'] == 'normal':\n                gap = box.style['font_size']  # normal equals 1em\n            else:\n                gap = percentage(box.style['column_gap'], box.style, box.width)\n            position_x = (\n                child.position_x - (box.style['column_rule_width'] + gap) / 2)\n            border_box = position_x, child.position_y, rule_width, child.height\n            clip_border_segment(\n                stream, rule_style, rule_width, 'left', border_box, border_widths)\n            color = styled_color(\n                rule_style, get_color(box.style, 'column_rule_color'), 'left')\n            draw_rect_border(stream, border_box, border_widths, rule_style, color)\n\n\ndef draw_border(stream, box):\n    \"\"\"Draw the box borders and column rules to a ``pdf.stream.Stream``.\"\"\"\n\n    # The box is hidden, easy.\n    if box.style['visibility'] != 'visible':\n        return\n\n    # Draw column rules.\n    columns = (\n        isinstance(box, boxes.BlockContainerBox) and (\n            box.style['column_width'] != 'auto' or\n            box.style['column_count'] != 'auto'))\n    if columns and box.style['column_rule_width']:\n        with stream.artifact():\n            draw_column_rules(stream, box)\n\n    # If there's a border image, that takes precedence.\n    if box.style['border_image_source'][0] != 'none' and box.border_image is not None:\n        with stream.artifact():\n            draw_border_image(\n                box, stream, box.border_image, box.style['border_image_slice'],\n                box.style['border_image_repeat'], box.style['border_image_outset'],\n                box.style['border_image_width'])\n        return\n\n    widths = [getattr(box, f'border_{side}_width') for side in SIDES]\n\n    if set(widths) == {0}:\n        # No border, return early.\n        return\n\n    colors = [get_color(box.style, f'border_{side}_color') for side in SIDES]\n    styles = [\n        colors[i].alpha and box.style[f'border_{side}_style']\n        for (i, side) in enumerate(SIDES)]\n\n    simple_style = set(styles) in ({'solid'}, {'double'})  # one style, simple lines\n    single_color = len(set(colors)) == 1  # one color\n    four_sides = 0 not in widths  # no 0-width border, to avoid PDF artifacts\n    if simple_style and single_color and four_sides:\n        # Simple case, we only draw rounded rectangles.\n        with stream.artifact():\n            draw_rounded_border(stream, box, styles[0], colors[0])\n        return\n\n    # We're not smart enough to find a good way to draw the borders, we must\n    # draw them side by side. Order is not specified, but this one seems to be\n    # close to what other browsers do.\n    values = tuple(zip(SIDES, widths, colors, styles))\n    for index in (2, 3, 1, 0):\n        side, width, color, style = values[index]\n        if width == 0 or not color:\n            continue\n        with stream.artifact(), stream.stacked():\n            clip_border_segment(\n                stream, style, width, side, box.rounded_border_box()[:4],\n                widths, box.rounded_border_box()[4:])\n            draw_rounded_border(stream, box, style, styled_color(style, color, side))\n\n\ndef draw_border_image(box, stream, image, border_slice, border_repeat, border_outset,\n                      border_width):\n    \"\"\"Draw ``image`` as a border image for ``box`` on ``stream`` as specified.\"\"\"\n    # Shared by border-image-* and mask-border-*.\n    width, height, ratio = image.get_intrinsic_size(\n        box.style['image_resolution'], box.style['font_size'])\n    intrinsic_width, intrinsic_height = replaced.default_image_sizing(\n        width, height, ratio, specified_width=None, specified_height=None,\n        default_width=box.border_width(), default_height=box.border_height())\n\n    image_slice = border_slice[:4]\n    should_fill = border_slice[4]\n\n    def compute_slice_dimension(dimension, intrinsic):\n        if isinstance(dimension, (int, float)):\n            return min(dimension, intrinsic)\n        else:\n            assert dimension.unit == '%'\n            return min(100, dimension.value) / 100 * intrinsic\n\n    slice_top = compute_slice_dimension(image_slice[0], intrinsic_height)\n    slice_right = compute_slice_dimension(image_slice[1], intrinsic_width)\n    slice_bottom = compute_slice_dimension(image_slice[2], intrinsic_height)\n    slice_left = compute_slice_dimension(image_slice[3], intrinsic_width)\n\n    repeat_x, repeat_y = border_repeat\n\n    x, y, w, h, tl, tr, br, bl = box.rounded_border_box()\n    px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box()\n    border_left = px - x\n    border_top = py - y\n    border_right = w - pw - border_left\n    border_bottom = h - ph - border_top\n\n    def compute_outset_dimension(dimension, from_border):\n        if dimension.unit is None:\n            return dimension.value * from_border\n        else:\n            assert dimension.unit == 'px'\n            return dimension.value\n\n    outset_top = compute_outset_dimension(border_outset[0], border_top)\n    outset_right = compute_outset_dimension(border_outset[1], border_right)\n    outset_bottom = compute_outset_dimension(border_outset[2], border_bottom)\n    outset_left = compute_outset_dimension(border_outset[3], border_left)\n\n    x -= outset_left\n    y -= outset_top\n    w += outset_left + outset_right\n    h += outset_top + outset_bottom\n\n    def compute_width_adjustment(dimension, original, intrinsic,\n                                 area_dimension):\n        if dimension == 'auto':\n            return intrinsic\n        elif isinstance(dimension, (int, float)):\n            return dimension * original\n        elif dimension.unit == '%':\n            return dimension.value / 100 * area_dimension\n        else:\n            assert dimension.unit == 'px'\n            return dimension.value\n\n    # We make adjustments to the border_* variables after handling outsets\n    # because numerical outsets are relative to border-width, not\n    # border-image-width. Also, the border image area that is used\n    # for percentage-based border-image-width values includes any expanded\n    # area due to border-image-outset.\n    border_top = compute_width_adjustment(\n        border_width[0], border_top, slice_top, h)\n    border_right = compute_width_adjustment(\n        border_width[1], border_right, slice_right, w)\n    border_bottom = compute_width_adjustment(\n        border_width[2], border_bottom, slice_bottom, h)\n    border_left = compute_width_adjustment(\n        border_width[3], border_left, slice_left, w)\n\n    def draw_border_image_region(x, y, width, height, slice_x, slice_y, slice_width,\n                                 slice_height, repeat_x='stretch', repeat_y='stretch',\n                                 scale_x=None, scale_y=None):\n        if 0 in (intrinsic_width, width, slice_width):\n            scale_x = 0\n        else:\n            extra_dx = 0\n            if not scale_x:\n                scale_x = (height / slice_height) if height and slice_height else 1\n            if repeat_x == 'repeat':\n                n_repeats_x = ceil(width / slice_width / scale_x)\n            elif repeat_x == 'space':\n                n_repeats_x = floor(width / slice_width / scale_x)\n                # Space is before the first repeat and after the last,\n                # so there's one more space than repeat.\n                extra_dx = (\n                    (width / scale_x - n_repeats_x * slice_width) / (n_repeats_x + 1))\n            elif repeat_x == 'round':\n                n_repeats_x = max(1, round(width / slice_width / scale_x))\n                scale_x = width / (n_repeats_x * slice_width)\n            else:\n                n_repeats_x = 1\n                scale_x = width / slice_width\n\n        if 0 in (intrinsic_height, height, slice_height):\n            scale_y = 0\n        else:\n            extra_dy = 0\n            if not scale_y:\n                scale_y = (width / slice_width) if width and slice_width else 1\n            if repeat_y == 'repeat':\n                n_repeats_y = ceil(height / slice_height / scale_y)\n            elif repeat_y == 'space':\n                n_repeats_y = floor(height / slice_height / scale_y)\n                # Space is before the first repeat and after the last,\n                # so there's one more space than repeat.\n                extra_dy = (\n                    (height / scale_y - n_repeats_y * slice_height) / (n_repeats_y + 1))\n            elif repeat_y == 'round':\n                n_repeats_y = max(1, round(height / slice_height / scale_y))\n                scale_y = height / (n_repeats_y * slice_height)\n            else:\n                n_repeats_y = 1\n                scale_y = height / slice_height\n\n        if 0 in (scale_x, scale_y):\n            return scale_x, scale_y\n\n        rendered_width = intrinsic_width * scale_x\n        rendered_height = intrinsic_height * scale_y\n        offset_x = rendered_width * slice_x / intrinsic_width\n        offset_y = rendered_height * slice_y / intrinsic_height\n\n        with stream.stacked():\n            stream.rectangle(x, y, width, height)\n            stream.clip()\n            stream.end()\n            stream.transform(e=x - offset_x + extra_dx, f=y - offset_y + extra_dy)\n            stream.transform(a=scale_x, d=scale_y)\n            for i in range(n_repeats_x):\n                for j in range(n_repeats_y):\n                    with stream.stacked():\n                        translate_x = i * (slice_width + extra_dx)\n                        translate_y = j * (slice_height + extra_dy)\n                        stream.transform(e=translate_x, f=translate_y)\n                        stream.rectangle(\n                            offset_x / scale_x, offset_y / scale_y,\n                            slice_width, slice_height)\n                        stream.clip()\n                        stream.end()\n                        image.draw(stream, intrinsic_width, intrinsic_height, box.style)\n\n        return scale_x, scale_y\n\n    # Top left.\n    scale_left, scale_top = draw_border_image_region(\n        x, y, border_left, border_top, 0, 0, slice_left, slice_top)\n    # Top right.\n    draw_border_image_region(\n        x + w - border_right, y, border_right, border_top,\n        intrinsic_width - slice_right, 0, slice_right, slice_top)\n    # Bottom right.\n    scale_right, scale_bottom = draw_border_image_region(\n        x + w - border_right, y + h - border_bottom, border_right, border_bottom,\n        intrinsic_width - slice_right, intrinsic_height - slice_bottom,\n        slice_right, slice_bottom)\n    # Bottom left.\n    draw_border_image_region(\n        x, y + h - border_bottom, border_left, border_bottom,\n        0, intrinsic_height - slice_bottom, slice_left, slice_bottom)\n    if x_middle := slice_left + slice_right < intrinsic_width:\n        # Top middle.\n        draw_border_image_region(\n            x + border_left, y, w - border_left - border_right, border_top,\n            slice_left, 0, intrinsic_width - slice_left - slice_right,\n            slice_top, repeat_x=repeat_x)\n        # Bottom middle.\n        draw_border_image_region(\n            x + border_left, y + h - border_bottom,\n            w - border_left - border_right, border_bottom,\n            slice_left, intrinsic_height - slice_bottom,\n            intrinsic_width - slice_left - slice_right, slice_bottom,\n            repeat_x=repeat_x)\n    if y_middle := slice_top + slice_bottom < intrinsic_height:\n        # Right middle.\n        draw_border_image_region(\n            x + w - border_right, y + border_top,\n            border_right, h - border_top - border_bottom,\n            intrinsic_width - slice_right, slice_top,\n            slice_right, intrinsic_height - slice_top - slice_bottom,\n            repeat_y=repeat_y)\n        # Left middle.\n        draw_border_image_region(\n            x, y + border_top, border_left, h - border_top - border_bottom,\n            0, slice_top, slice_left,\n            intrinsic_height - slice_top - slice_bottom,\n            repeat_y=repeat_y)\n    if should_fill and x_middle and y_middle:\n        # Fill middle.\n        draw_border_image_region(\n            x + border_left, y + border_top, w - border_left - border_right,\n            h - border_top - border_bottom, slice_left, slice_top,\n            intrinsic_width - slice_left - slice_right,\n            intrinsic_height - slice_top - slice_bottom,\n            repeat_x=repeat_x, repeat_y=repeat_y,\n            scale_x=scale_left or scale_right, scale_y=scale_top or scale_bottom)\n\n\ndef clip_border_segment(stream, style, width, side, border_box,\n                        border_widths=None, radii=None):\n    \"\"\"Clip one segment of box border.\n\n    The strategy is to remove the zones not needed because of the style or the\n    side before painting.\n\n    \"\"\"\n    bbx, bby, bbw, bbh = border_box\n    (tlh, tlv), (trh, trv), (brh, brv), (blh, blv) = radii or 4 * ((0, 0),)\n    bt, br, bb, bl = border_widths or 4 * (width,)\n\n    def transition_point(x1, y1, x2, y2):\n        \"\"\"Get the point use for border transition.\n\n        The extra boolean returned is ``True`` if the point is in the padding\n        box (ie. the padding box is rounded).\n\n        This point is not specified. We must be sure to be inside the rounded\n        padding box, and in the zone defined in the \"transition zone\" allowed\n        by the specification. We chose the corner of the transition zone. It's\n        easy to get and gives quite good results, but it seems to be different\n        from what other browsers do.\n\n        \"\"\"\n        return (\n            ((x1, y1), True) if abs(x1) > abs(x2) and abs(y1) > abs(y2)\n            else ((x2, y2), False))\n\n    def corner_half_length(a, b):\n        \"\"\"Return the length of the half of one ellipsis corner.\n\n        Inspired by [Ramanujan, S., \"Modular Equations and Approximations to\n        pi\" Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372],\n        wonderfully explained by Dr Rob.\n\n        https://mathforum.org/dr.math/faq/formulas/\n\n        \"\"\"\n        x = (a - b) / (a + b)\n        return pi / 8 * (a + b) * (\n            1 + 3 * x ** 2 / (10 + sqrt(4 - 3 * x ** 2)))\n\n    def draw_dash(cx, cy, width=0, height=0, r=0):\n        \"\"\"Draw a single dash or dot centered on cx, cy.\"\"\"\n        if style == 'dotted':\n            ratio = r / sqrt(pi)\n            stream.move_to(cx + r, cy)\n            stream.curve_to(cx + r, cy + ratio, cx + ratio, cy + r, cx, cy + r)\n            stream.curve_to(cx - ratio, cy + r, cx - r, cy + ratio, cx - r, cy)\n            stream.curve_to(cx - r, cy - ratio, cx - ratio, cy - r, cx, cy - r)\n            stream.curve_to(cx + ratio, cy - r, cx + r, cy - ratio, cx + r, cy)\n            stream.close()\n        elif style == 'dashed':\n            stream.rectangle(cx - width / 2, cy - height / 2, width, height)\n\n    if side == 'top':\n        (px1, py1), rounded1 = transition_point(tlh, tlv, bl, bt)\n        (px2, py2), rounded2 = transition_point(-trh, trv, -br, bt)\n        width = bt\n        way = 1\n        angle = 1\n        main_offset = bby\n    elif side == 'right':\n        (px1, py1), rounded1 = transition_point(-trh, trv, -br, bt)\n        (px2, py2), rounded2 = transition_point(-brh, -brv, -br, -bb)\n        width = br\n        way = 1\n        angle = 2\n        main_offset = bbx + bbw\n    elif side == 'bottom':\n        (px1, py1), rounded1 = transition_point(blh, -blv, bl, -bb)\n        (px2, py2), rounded2 = transition_point(-brh, -brv, -br, -bb)\n        width = bb\n        way = -1\n        angle = 3\n        main_offset = bby + bbh\n    elif side == 'left':\n        (px1, py1), rounded1 = transition_point(tlh, tlv, bl, bt)\n        (px2, py2), rounded2 = transition_point(blh, -blv, bl, -bb)\n        width = bl\n        way = -1\n        angle = 4\n        main_offset = bbx\n\n    if side in ('top', 'bottom'):\n        a1, b1 = px1 - bl / 2, way * py1 - width / 2\n        a2, b2 = -px2 - br / 2, way * py2 - width / 2\n        line_length = bbw - px1 + px2\n        length = bbw\n        stream.move_to(bbx + bbw, main_offset)\n        stream.line_to(bbx, main_offset)\n        stream.line_to(bbx + px1, main_offset + py1)\n        stream.line_to(bbx + bbw + px2, main_offset + py2)\n    elif side in ('left', 'right'):\n        a1, b1 = -way * px1 - width / 2, py1 - bt / 2\n        a2, b2 = -way * px2 - width / 2, -py2 - bb / 2\n        line_length = bbh - py1 + py2\n        length = bbh\n        stream.move_to(main_offset, bby + bbh)\n        stream.line_to(main_offset, bby)\n        stream.line_to(main_offset + px1, bby + py1)\n        stream.line_to(main_offset + px2, bby + bbh + py2)\n\n    if style in ('dotted', 'dashed'):\n        dash = width if style == 'dotted' else 3 * width\n        stream.clip(even_odd=True)\n        stream.end()\n        if rounded1 or rounded2:\n            # At least one of the two corners is rounded.\n            chl1 = corner_half_length(a1, b1)\n            chl2 = corner_half_length(a2, b2)\n            length = line_length + chl1 + chl2\n            dash_length = round(length / dash)\n            if rounded1 and rounded2:\n                # 2x dashes.\n                dash = length / (dash_length + dash_length % 2)\n            else:\n                # 2x - 1/2 dashes.\n                dash = length / (dash_length + dash_length % 2 - 0.5)\n            dashes1 = ceil((chl1 - dash / 2) / dash)\n            dashes2 = ceil((chl2 - dash / 2) / dash)\n            line = floor(line_length / dash)\n\n            def draw_dashes(dashes, line, way, x, y, px, py, chl):\n                if style == 'dotted':\n                    if dashes == 0:\n                        return line + 1, -1\n                    elif dashes == 1:\n                        return line + 1, -0.5\n\n                    for i in range(1, dashes, 2):\n                        a = ((2 * angle - way) + i * way * dash / chl) / 4 * pi\n                        cx = x if side in ('top', 'bottom') else main_offset\n                        cy = y if side in ('left', 'right') else main_offset\n                        draw_dash(\n                            cx + px - (abs(px) - dash / 2) * cos(a),\n                            cy + py - (abs(py) - dash / 2) * sin(a),\n                            r=(dash / 2))\n                    next_a = ((2 * angle - way) + (i + 2) * way * dash / chl) / 4 * pi\n                    offset = next_a / pi * 2 - angle\n                    if dashes % 2:\n                        line += 1\n                    return line, offset\n\n                if dashes == 0:\n                    return line + 1, -1/3\n\n                for i in range(0, dashes, 2):\n                    i += 0.5  # half dash\n                    angle1 = (\n                        ((2 * angle - way) + i * way * dash / chl) /\n                        4 * pi)\n                    angle2 = (min if way > 0 else max)(\n                        ((2 * angle - way) + (i + 1) * way * dash / chl) /\n                        4 * pi,\n                        angle * pi / 2)\n                    if side in ('top', 'bottom'):\n                        stream.move_to(x + px, main_offset + py)\n                        stream.line_to(\n                            x + px - way * px * 1 / tan(angle2), main_offset)\n                        stream.line_to(\n                            x + px - way * px * 1 / tan(angle1), main_offset)\n                    elif side in ('left', 'right'):\n                        stream.move_to(main_offset + px, y + py)\n                        stream.line_to(\n                            main_offset, y + py + way * py * tan(angle2))\n                        stream.line_to(\n                            main_offset, y + py + way * py * tan(angle1))\n                    if angle2 == angle * pi / 2:\n                        offset = (angle1 - angle2) / ((\n                            ((2 * angle - way) + (i + 1) * way * dash / chl) /\n                            4 * pi) - angle1)\n                        line += 1\n                        break\n                else:\n                    offset = 1 - (\n                        (angle * pi / 2 - angle2) / (angle2 - angle1))\n                return line, offset\n\n            line, offset = draw_dashes(dashes1, line, way, bbx, bby, px1, py1, chl1)\n            line = draw_dashes(\n                dashes2, line, -way, bbx + bbw, bby + bbh, px2, py2, chl2)[0]\n\n            if line_length > 1e-6:\n                for i in range(0, line, 2):\n                    i += offset\n                    if side in ('top', 'bottom'):\n                        x1 = bbx + px1 + i * dash\n                        x2 = bbx + px1 + (i + 1) * dash\n                        y1 = main_offset - (width if way < 0 else 0)\n                        y2 = y1 + width\n                    elif side in ('left', 'right'):\n                        y1 = bby + py1 + i * dash\n                        y2 = bby + py1 + (i + 1) * dash\n                        x1 = main_offset - (width if way > 0 else 0)\n                        x2 = x1 + width\n                    draw_dash(\n                        x1 + (x2 - x1) / 2, y1 + (y2 - y1) / 2,\n                        x2 - x1, y2 - y1, width / 2)\n        else:\n            # No rounded corner, dashes on corners and evenly spaced between.\n            number_of_spaces = floor(length / dash / 2)\n            number_of_dashes = number_of_spaces + 1\n            if style == 'dotted':\n                dash = width\n                if number_of_spaces:\n                    space = (length - number_of_dashes * dash) / number_of_spaces\n                else:\n                    space = 0  # no space, unused\n            elif style == 'dashed':\n                space = dash = length / (number_of_spaces + number_of_dashes) or 1\n            for i in range(number_of_dashes + 1):\n                advance = i * (space + dash)\n                if side == 'top':\n                    cx, cy = bbx + advance + dash / 2, bby + width / 2\n                    dash_width, dash_height = dash, width\n                elif side == 'right':\n                    cx, cy = bbx + bbw - width / 2, bby + advance + dash / 2\n                    dash_width, dash_height = width, dash\n                elif side == 'bottom':\n                    cx, cy = bbx + advance + dash / 2, bby + bbh - width / 2\n                    dash_width, dash_height = dash, width\n                elif side == 'left':\n                    cx, cy = bbx + width / 2, bby + advance + dash / 2\n                    dash_width, dash_height = width, dash\n                draw_dash(cx, cy, dash_width, dash_height, dash / 2)\n    stream.clip(even_odd=True)\n    stream.end()\n\n\ndef draw_rounded_border(stream, box, style, color):\n    if style in ('ridge', 'groove'):\n        stream.set_color(color[0])\n        rounded_box(stream, box.rounded_padding_box())\n        rounded_box(stream, box.rounded_box_ratio(1 / 2))\n        stream.fill(even_odd=True)\n        stream.set_color(color[1])\n        rounded_box(stream, box.rounded_box_ratio(1 / 2))\n        rounded_box(stream, box.rounded_border_box())\n        stream.fill(even_odd=True)\n        return\n    stream.set_color(color)\n    rounded_box(stream, box.rounded_padding_box())\n    if style == 'double':\n        rounded_box(stream, box.rounded_box_ratio(1 / 3))\n        rounded_box(stream, box.rounded_box_ratio(2 / 3))\n    rounded_box(stream, box.rounded_border_box())\n    stream.fill(even_odd=True)\n\n\ndef draw_rect_border(stream, box, widths, style, color):\n    bbx, bby, bbw, bbh = box\n    bt, br, bb, bl = widths\n    if style in ('ridge', 'groove'):\n        stream.set_color(color[0])\n        stream.rectangle(*box)\n        stream.rectangle(\n            bbx + bl / 2, bby + bt / 2,\n            bbw - (bl + br) / 2, bbh - (bt + bb) / 2)\n        stream.fill(even_odd=True)\n        stream.rectangle(\n            bbx + bl / 2, bby + bt / 2,\n            bbw - (bl + br) / 2, bbh - (bt + bb) / 2)\n        stream.rectangle(bbx + bl, bby + bt, bbw - bl - br, bbh - bt - bb)\n        stream.set_color(color[1])\n        stream.fill(even_odd=True)\n        return\n    stream.set_color(color)\n    stream.rectangle(*box)\n    if style == 'double':\n        stream.rectangle(\n            bbx + bl / 3, bby + bt / 3,\n            bbw - (bl + br) / 3, bbh - (bt + bb) / 3)\n        stream.rectangle(\n            bbx + bl * 2 / 3, bby + bt * 2 / 3,\n            bbw - (bl + br) * 2 / 3, bbh - (bt + bb) * 2 / 3)\n    stream.rectangle(bbx + bl, bby + bt, bbw - bl - br, bbh - bt - bb)\n    stream.fill(even_odd=True)\n\n\ndef draw_line(stream, x1, y1, x2, y2, thickness, style, color, offset=0):\n    assert x1 == x2 or y1 == y2  # Only works for vertical or horizontal lines\n\n    with stream.stacked():\n        if style not in ('ridge', 'groove'):\n            stream.set_color(color, stroke=True)\n\n        if style == 'dashed':\n            stream.set_dash([5 * thickness], offset)\n        elif style == 'dotted':\n            stream.set_line_cap(1)\n            stream.set_dash([0, 2 * thickness], offset)\n\n        if style == 'double':\n            stream.set_line_width(thickness / 3)\n            if x1 == x2:\n                stream.move_to(x1 - thickness / 3, y1)\n                stream.line_to(x2 - thickness / 3, y2)\n                stream.move_to(x1 + thickness / 3, y1)\n                stream.line_to(x2 + thickness / 3, y2)\n            elif y1 == y2:\n                stream.move_to(x1, y1 - thickness / 3)\n                stream.line_to(x2, y2 - thickness / 3)\n                stream.move_to(x1, y1 + thickness / 3)\n                stream.line_to(x2, y2 + thickness / 3)\n        elif style in ('ridge', 'groove'):\n            stream.set_line_width(thickness / 2)\n            stream.set_color(color[0], stroke=True)\n            if x1 == x2:\n                stream.move_to(x1 + thickness / 4, y1)\n                stream.line_to(x2 + thickness / 4, y2)\n            elif y1 == y2:\n                stream.move_to(x1, y1 + thickness / 4)\n                stream.line_to(x2, y2 + thickness / 4)\n            stream.stroke()\n            stream.set_color(color[1], stroke=True)\n            if x1 == x2:\n                stream.move_to(x1 - thickness / 4, y1)\n                stream.line_to(x2 - thickness / 4, y2)\n            elif y1 == y2:\n                stream.move_to(x1, y1 - thickness / 4)\n                stream.line_to(x2, y2 - thickness / 4)\n        elif style == 'wavy':\n            assert y1 == y2  # Only allowed for text decoration\n            up = 1\n            radius = 0.75 * thickness\n\n            stream.rectangle(x1, y1 - 2 * radius, x2 - x1, 4 * radius)\n            stream.clip()\n            stream.end()\n\n            x = x1 - offset\n            stream.move_to(x, y1)\n            while x < x2:\n                stream.set_line_width(thickness)\n                stream.curve_to(\n                    x + radius / 2, y1 + up * radius,\n                    x + 3 * radius / 2, y1 + up * radius,\n                    x + 2 * radius, y1)\n                x += 2 * radius\n                up *= -1\n        else:\n            stream.set_line_width(thickness)\n            stream.move_to(x1, y1)\n            stream.line_to(x2, y2)\n        stream.stroke()\n\n\ndef draw_outline(stream, box):\n    width = box.style['outline_width']\n    offset = box.style['outline_offset']\n    color = get_color(box.style, 'outline_color')\n    style = box.style['outline_style']\n    if box.style['visibility'] == 'visible' and width and color.alpha:\n        outline_box = (\n            box.border_box_x() - width - offset,\n            box.border_box_y() - width - offset,\n            box.border_width() + 2 * width + 2 * offset,\n            box.border_height() + 2 * width + 2 * offset)\n        for side in SIDES:\n            with stream.artifact(), stream.stacked():\n                clip_border_segment(stream, style, width, side, outline_box)\n                draw_rect_border(\n                    stream, outline_box, 4 * (width,), style,\n                    styled_color(style, color, side))\n\n    for child in box.children:\n        if isinstance(child, boxes.Box):\n            draw_outline(stream, child)\n\n\ndef rounded_box(stream, radii):\n    \"\"\"Draw the path of the border radius box.\n\n    ``widths`` is a tuple of the inner widths (top, right, bottom, left) from\n    the border box. Radii are adjusted from these values. Default is (0, 0, 0,\n    0).\n\n    \"\"\"\n    x, y, w, h, tl, tr, br, bl = radii\n\n    if all(0 in corner for corner in (tl, tr, br, bl)):\n        # No radius, draw a rectangle\n        stream.rectangle(x, y, w, h)\n        return\n\n    r = 0.45\n\n    stream.move_to(x + tl[0], y)\n    stream.line_to(x + w - tr[0], y)\n    stream.curve_to(\n        x + w - tr[0] * r, y, x + w, y + tr[1] * r, x + w, y + tr[1])\n    stream.line_to(x + w, y + h - br[1])\n    stream.curve_to(\n        x + w, y + h - br[1] * r, x + w - br[0] * r, y + h, x + w - br[0],\n        y + h)\n    stream.line_to(x + bl[0], y + h)\n    stream.curve_to(\n        x + bl[0] * r, y + h, x, y + h - bl[1] * r, x, y + h - bl[1])\n    stream.line_to(x, y + tl[1])\n    stream.curve_to(\n        x, y + tl[1] * r, x + tl[0] * r, y, x + tl[0], y)\n"
  },
  {
    "path": "weasyprint/draw/color.py",
    "content": "\"\"\"Draw colors.\"\"\"\n\nfrom colorsys import hsv_to_rgb, rgb_to_hsv\n\nfrom tinycss2.color5 import parse_color\n\n\ndef get_color(style, key):\n    \"\"\"Return color, taking care of possible currentColor value.\"\"\"\n    value = style[key]\n    return value if value != 'currentcolor' else style['color']\n\n\ndef darken(color):\n    \"\"\"Return a darker color.\"\"\"\n    # TODO: handle color spaces.\n    hue, saturation, value = rgb_to_hsv(*color.to('srgb')[:3])\n    value /= 1.5\n    saturation /= 1.25\n    return parse_color(\n        'rgb(%f%% %f%% %f%%/%f)' % (*hsv_to_rgb(hue, saturation, value), color.alpha))\n\n\ndef lighten(color):\n    \"\"\"Return a lighter color.\"\"\"\n    # TODO: handle color spaces.\n    hue, saturation, value = rgb_to_hsv(*color.to('srgb')[:3])\n    value = 1 - (1 - value) / 1.5\n    if saturation:\n        saturation = 1 - (1 - saturation) / 1.25\n    return parse_color(\n        'rgb(%f%% %f%% %f%%/%f)' % (*hsv_to_rgb(hue, saturation, value), color.alpha))\n\n\ndef styled_color(style, color, side):\n    \"\"\"Return inset, outset, ridge and groove border colors.\"\"\"\n    if style in ('inset', 'outset'):\n        do_lighten = (side in ('top', 'left')) ^ (style == 'inset')\n        return (lighten if do_lighten else darken)(color)\n    elif style in ('ridge', 'groove'):\n        if (side in ('top', 'left')) ^ (style == 'ridge'):\n            return lighten(color), darken(color)\n        else:\n            return darken(color), lighten(color)\n    return color\n"
  },
  {
    "path": "weasyprint/draw/text.py",
    "content": "\"\"\"Draw text.\"\"\"\n\nfrom io import BytesIO\nfrom xml.etree import ElementTree\n\nfrom PIL import Image\n\nfrom ..images import RasterImage, SVGImage\nfrom ..logger import LOGGER\nfrom ..matrix import Matrix\nfrom ..text.ffi import FROM_UNITS, TO_UNITS, ffi, pango\nfrom ..text.fonts import get_hb_object_data\nfrom ..text.line_break import get_last_word_end\nfrom .border import draw_line\nfrom .color import get_color\n\n\ndef draw_text(stream, textbox, offset_x, text_overflow, block_ellipsis):\n    \"\"\"Draw a textbox to a pydyf stream.\"\"\"\n    from ..layout.percent import percentage\n\n    # Pango crashes with font-size: 0.\n    assert textbox.style['font_size']\n\n    # Don’t draw invisible textboxes.\n    if textbox.style['visibility'] != 'visible':\n        return\n\n    # Draw underline and overline.\n    text_decoration_values = textbox.style['text_decoration_line']\n    text_decoration_color = get_color(textbox.style, 'text_decoration_color')\n    if 'underline' in text_decoration_values or 'overline' in text_decoration_values:\n        if textbox.style['text_decoration_thickness'] in ('auto', 'from-font'):\n            thickness = textbox.pango_layout.underline_thickness\n        else:\n            thickness = percentage(\n                textbox.style['text_decoration_thickness'], textbox.style,\n                textbox.style['font_size'])\n    if 'overline' in text_decoration_values:\n        offset_y = (\n            textbox.baseline - textbox.pango_layout.ascent + thickness / 2)\n        draw_text_decoration(\n            stream, textbox, offset_x, offset_y, thickness,\n            text_decoration_color)\n    if 'underline' in text_decoration_values:\n        if textbox.style['text_underline_offset'] == 'auto':\n            underline_offset = - textbox.pango_layout.underline_position\n        else:\n            underline_offset = percentage(\n                textbox.style['text_underline_offset'], textbox.style,\n                textbox.style['font_size'])\n        offset_y = textbox.baseline + underline_offset + thickness / 2\n        draw_text_decoration(\n            stream, textbox, offset_x, offset_y, thickness,\n            text_decoration_color)\n\n    # Draw text.\n    x, y = textbox.position_x, textbox.position_y + textbox.baseline\n    stream.set_color(textbox.style['color'])\n    textbox.pango_layout.reactivate(textbox.style)\n    stream.begin_text()\n    emojis = draw_first_line(\n        stream, textbox, text_overflow, block_ellipsis, Matrix(d=-1, e=x, f=y))\n    stream.end_text()\n\n    # Draw emojis.\n    draw_emojis(stream, textbox.style, x, y, emojis)\n\n    # Draw line through.\n    if 'line-through' in text_decoration_values:\n        thickness = textbox.pango_layout.strikethrough_thickness\n        offset_y = textbox.baseline - textbox.pango_layout.strikethrough_position\n        draw_text_decoration(\n            stream, textbox, offset_x, offset_y, thickness, text_decoration_color)\n    textbox.pango_layout.deactivate()\n\n\ndef draw_emojis(stream, style, x, y, emojis):\n    \"\"\"Draw list of emojis.\"\"\"\n    font_size = style['font_size']\n    for image, font, a, d, e, f in emojis:\n        with stream.stacked():\n            stream.transform(a=a, d=d, e=x + e * font_size, f=y + f)\n            image.draw(stream, font_size, font_size, style)\n\n\ndef draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):\n    \"\"\"Draw the given ``textbox`` line to the document ``stream``.\"\"\"\n    # Don’t draw lines with only invisible characters.\n    if not textbox.text.strip():\n        return []\n\n    if textbox.style['font_size'] < 1e-6:  # default float precision used by pydyf\n        return []\n\n    pango.pango_layout_set_single_paragraph_mode(textbox.pango_layout.layout, True)\n\n    if text_overflow == 'ellipsis' or block_ellipsis != 'none':\n        assert textbox.pango_layout.max_width is not None\n        max_width = textbox.pango_layout.max_width\n        pango.pango_layout_set_width(\n            textbox.pango_layout.layout, int(max_width * TO_UNITS))\n        if text_overflow == 'ellipsis':\n            pango.pango_layout_set_ellipsize(\n                textbox.pango_layout.layout, pango.PANGO_ELLIPSIZE_END)\n        else:\n            if block_ellipsis == 'auto':\n                ellipsis = '…'\n            else:\n                assert block_ellipsis[0] == 'string'\n                ellipsis = block_ellipsis[1]\n\n            # Remove last word if hyphenated.\n            new_text = textbox.pango_layout.text\n            if new_text.endswith(textbox.style['hyphenate_character']):\n                last_word_end = get_last_word_end(\n                    new_text[:-len(textbox.style['hyphenate_character'])],\n                    textbox.style['lang'])\n                if last_word_end:\n                    new_text = new_text[:last_word_end]\n\n            textbox.pango_layout.set_text(new_text + ellipsis)\n\n    first_line, index = textbox.pango_layout.get_first_line()\n\n    if block_ellipsis != 'none':\n        while index:\n            last_word_end = get_last_word_end(\n                textbox.pango_layout.text[:-len(ellipsis)],\n                textbox.style['lang'])\n            if last_word_end is None:\n                break\n            new_text = textbox.pango_layout.text[:last_word_end]\n            textbox.pango_layout.set_text(new_text + ellipsis)\n            first_line, index = textbox.pango_layout.get_first_line()\n\n    utf8_text = textbox.pango_layout.text.encode()\n    stream.set_text_matrix(*matrix.values)\n    previous_pango_font = None\n    string = ''\n    x_advance = 0\n    emojis = []\n    run = first_line.runs[0]\n    while run != ffi.NULL:\n        # Get Pango objects.\n        glyph_item = run.data\n        run = run.next\n        glyph_string = glyph_item.glyphs\n        glyphs_info = glyph_string.glyphs\n        number_of_glyphs = glyph_string.num_glyphs\n        offset = glyph_item.item.offset\n        clusters = glyph_string.log_clusters\n\n        # Get positions of the glyphs in the UTF-8 string.\n        utf8_positions = [offset + clusters[i] for i in range(number_of_glyphs)]\n        if glyph_item.item.analysis.level % 2:\n            utf8_positions.insert(0, offset + glyph_item.item.length)  # rtl\n        else:\n            utf8_positions.append(offset + glyph_item.item.length)  # ltr\n\n        pango_font = glyph_item.item.analysis.font\n        if pango_font != previous_pango_font:\n            # Add font file content and get font size.\n            previous_pango_font = pango_font\n            font, font_size = stream.add_font(pango_font)\n\n            # Workaround for https://gitlab.gnome.org/GNOME/pango/-/issues/530.\n            if pango.pango_version() < 14802:\n                font_size = textbox.style['font_size']\n\n            # Go through the run glyphs.\n            if string:\n                stream.show_text(string)\n            string = ''\n            stream.set_font_size(font.hash, 1 if font.bitmap else font_size)\n        string += '<'\n        for i in range(number_of_glyphs):\n            glyph_info = glyphs_info[i]\n            glyph_id = glyph_info.glyph\n            width = glyph_info.geometry.width\n\n            # Display zero-width empty glyph.\n            if glyph_id == pango.PANGO_GLYPH_EMPTY:\n                string += f'>{-width / font_size}<'\n                continue\n\n            # Display .notdef and log warning for missing glyphs.\n            if glyph_id & pango.PANGO_GLYPH_UNKNOWN_FLAG:\n                codepoint = glyph_id - pango.PANGO_GLYPH_UNKNOWN_FLAG\n                LOGGER.warning(\n                    '.notdef glyph rendered for Unicode string unsupported by fonts: '\n                    f'\"{chr(codepoint)}\" (U+{codepoint:04X})')\n                glyph_id = font.get_unused_glyph_id(codepoint)\n                font.widths[glyph_id] = round(width * 1000 * FROM_UNITS / font_size)\n                if 0 not in font.widths:\n                    # \"width\" is actually Pango’s get_approximate_char_width. Force\n                    # .notdef’s to use this width, even if it’s not the right, as we\n                    # want to keep Pango’s layout for next glyphs.\n                    font.widths[0] = font.widths[glyph_id]\n\n            # Create mapping between glyphs and Unicode codepoints.\n            if glyph_id not in font.to_unicode:\n                utf8_slice = slice(*sorted(utf8_positions[i:i+2]))\n                font.to_unicode[glyph_id] = utf8_text[utf8_slice].decode()\n\n            # Set horizontal and vertical offsets.\n            offset = glyph_info.geometry.x_offset / font_size\n            rise = glyph_info.geometry.y_offset / 1000\n            if rise:\n                if string[-1] == '<':\n                    string = string[:-1]\n                else:\n                    string += '>'\n                stream.show_text(string)\n                stream.set_text_rise(-rise)\n                string = ''\n                if offset:\n                    string = f'{-offset}'\n                string += f'<{glyph_id:02x}>' if font.bitmap else f'<{glyph_id:04x}>'\n                stream.show_text(string)\n                stream.set_text_rise(0)\n                string = '<'\n            else:\n                if offset:\n                    string += f'>{-offset}<'\n                string += f'{glyph_id:02x}' if font.bitmap else f'{glyph_id:04x}'\n\n            # Get glyph logical widths.\n            if glyph_id in font.widths:\n                logical_width = font.widths[glyph_id]\n            else:\n                pango.pango_font_get_glyph_extents(\n                    pango_font, glyph_id, stream.ink_rect, stream.logical_rect)\n                logical_width = font.widths[glyph_id] = round(\n                    stream.logical_rect.width * 1000 * FROM_UNITS / font_size)\n\n            # Set kerning, word spacing, letter spacing.\n            kerning = logical_width + offset - width * 1000 * FROM_UNITS / font_size\n            if kerning:\n                string += f'>{int(kerning)}<'\n\n            # Create list of emojis.\n            if font.svg:\n                svg_data = get_hb_object_data(font.hb_face, 'svg', glyph_id)\n                if svg_data:\n                    # Do as explained in specification\n                    # https://learn.microsoft.com/typography/opentype/spec/svg\n                    tree = ElementTree.fromstring(svg_data)\n                    if tree.get('id') != f'glyph{glyph_id}':\n                        defs = ElementTree.Element('defs')\n                        for child in list(tree):\n                            defs.append(child)\n                            tree.remove(child)\n                        tree.append(defs)\n                        ElementTree.SubElement(\n                            tree, 'use', attrib={'href': f'#glyph{glyph_id}'})\n                    if 'viewBox' not in tree.attrib:\n                        tree.attrib['viewBox'] = f'0 0 {font.upem} {font.upem}'\n                    image = SVGImage(tree, None, None, None)\n                    a = d = 1\n                    emojis.append([image, font, a, d, x_advance, 0])\n            elif font.png:\n                png_data = get_hb_object_data(font.hb_font, 'png', glyph_id)\n                if png_data:\n                    pillow_image = Image.open(BytesIO(png_data))\n                    image_id = f'{font.hash}{glyph_id}'\n                    image = RasterImage(pillow_image, image_id, png_data)\n                    d = logical_width / 1000\n                    a = pillow_image.width / pillow_image.height * d\n                    pango.pango_font_get_glyph_extents(\n                        pango_font, glyph_id, stream.ink_rect,\n                        stream.logical_rect)\n                    f = -stream.logical_rect.y\n                    f = f * FROM_UNITS / font_size - font_size\n                    emojis.append([image, font, a, d, x_advance, f])\n\n            x_advance += (logical_width + offset - kerning) / 1000\n\n        # Close the last glyphs list, remove if empty.\n        if string[-1] == '<':\n            string = string[:-1]\n        else:\n            string += '>'\n\n    # Draw text.\n    stream.show_text(string)\n\n    return emojis\n\n\ndef draw_text_decoration(stream, textbox, offset_x, offset_y, thickness, color):\n    \"\"\"Draw text-decoration of ``textbox`` to a ``pdf.stream.Stream``.\"\"\"\n    draw_line(\n        stream, textbox.position_x, textbox.position_y + offset_y,\n        textbox.position_x + textbox.width, textbox.position_y + offset_y,\n        thickness, textbox.style['text_decoration_style'], color, offset_x)\n"
  },
  {
    "path": "weasyprint/formatting_structure/boxes.py",
    "content": "\"\"\"Classes for all types of boxes in the CSS formatting structure / box model.\n\nSee https://www.w3.org/TR/CSS21/visuren.html\n\nNames are the same as in CSS 2.1 with the exception of ``TextBox``. In\nWeasyPrint, any text is in a ``TextBox``. What CSS calls anonymous inline boxes\nare text boxes but not all text boxes are anonymous inline boxes.\n\nSee https://www.w3.org/TR/CSS21/visuren.html#anonymous\n\nAbstract classes, should not be instantiated:\n\n* Box\n* BlockLevelBox\n* InlineLevelBox\n* BlockContainerBox\n* ReplacedBox\n* ParentBox\n* AtomicInlineLevelBox\n\nConcrete classes:\n\n* PageBox\n* BlockBox\n* InlineBox\n* InlineBlockBox\n* BlockReplacedBox\n* InlineReplacedBox\n* TextBox\n* LineBox\n* Various table-related Box subclasses\n\nAll concrete box classes whose name contains \"Inline\" or \"Block\" have one of\nthe following \"outside\" behavior:\n\n* Block-level (inherits from :class:`BlockLevelBox`)\n* Inline-level (inherits from :class:`InlineLevelBox`)\n\nand one of the following \"inside\" behavior:\n\n* Block container (inherits from :class:`BlockContainerBox`)\n* Inline content (InlineBox and :class:`TextBox`)\n* Replaced content (inherits from :class:`ReplacedBox`)\n\n… with various combinasions of both.\n\nSee respective docstrings for details.\n\n\"\"\"\n\nimport itertools\nimport sys\n\nfrom ..css import AnonymousStyle\n\n\nclass Box:\n    \"\"\"Abstract base class for all boxes.\"\"\"\n    # Definitions for the rules generating anonymous table boxes\n    # https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes\n    proper_table_child = False\n    internal_table_or_caption = False\n    tabular_container = False\n\n    # Keep track of removed collapsing spaces for wrap opportunities.\n    leading_collapsible_space = False\n    trailing_collapsible_space = False\n\n    # Default, may be overriden on instances.\n    is_table_wrapper = False\n    is_flex_item = False\n    is_grid_item = False\n    is_for_root_element = False\n    is_column = False\n    is_leader = False\n    is_outside_marker = False\n\n    # Other properties\n    transformation_matrix = None\n    bookmark_label = None\n    string_set = None\n    footnote = None\n    cached_counter_values = None\n    missing_link = None\n    link_annotation = None\n    force_fragmentation = False\n\n    # Default, overriden on some subclasses\n    def all_children(self):\n        return self.children\n\n    def descendants(self, placeholders=False):\n        \"\"\"A flat generator for a box, its children and descendants.\"\"\"\n        yield self\n        for child in self.children:\n            if placeholders or isinstance(child, Box):\n                yield from child.descendants(placeholders)\n            else:\n                yield child\n\n    def __init__(self, element_tag, style, element):\n        self.element_tag = element_tag\n        self.element = element\n        self.style = style\n        self.remove_decoration_sides = set()\n        self.children = []\n        self.first_letter_style = None\n        self.first_line_style = None\n\n    def __repr__(self):\n        return f'<{type(self).__name__} {self.element_tag}>'\n\n    @classmethod\n    def anonymous_from(cls, parent, *args, **kwargs):\n        \"\"\"Return an anonymous box that inherits from ``parent``.\"\"\"\n        style = AnonymousStyle(parent.style)\n        return cls(parent.element_tag, style, parent.element, *args, **kwargs)\n\n    def copy(self):\n        \"\"\"Return shallow copy of the box.\"\"\"\n        cls = type(self)\n        # Create a new instance without calling __init__: parameters are\n        # different depending on the class.\n        new_box = cls.__new__(cls)\n        # Copy attributes\n        new_box.__dict__.update(self.__dict__)\n        return new_box\n\n    def deepcopy(self):\n        \"\"\"Return a copy of the box with recursive copies of its children.\"\"\"\n        return self.copy()\n\n    def translate(self, dx=0, dy=0, ignore_floats=False):\n        \"\"\"Change the box’s position.\n\n        Also update the children’s positions accordingly.\n\n        \"\"\"\n        # Overridden in ParentBox to also translate children, if any.\n        if dx == dy == 0:\n            return\n        self.position_x += dx\n        self.position_y += dy\n        for child in self.all_children():\n            if not (ignore_floats and child.is_floated()):\n                child.translate(dx, dy, ignore_floats)\n\n    # Heights and widths\n\n    def padding_width(self):\n        \"\"\"Width of the padding box.\"\"\"\n        return self.width + self.padding_left + self.padding_right\n\n    def padding_height(self):\n        \"\"\"Height of the padding box.\"\"\"\n        return self.height + self.padding_top + self.padding_bottom\n\n    def border_width(self):\n        \"\"\"Width of the border box.\"\"\"\n        return self.padding_width() + self.border_left_width + \\\n            self.border_right_width\n\n    def border_height(self):\n        \"\"\"Height of the border box.\"\"\"\n        return self.padding_height() + self.border_top_width + \\\n            self.border_bottom_width\n\n    def margin_width(self):\n        \"\"\"Width of the margin box (aka. outer box).\"\"\"\n        return self.border_width() + self.margin_left + self.margin_right\n\n    def margin_height(self):\n        \"\"\"Height of the margin box (aka. outer box).\"\"\"\n        return self.border_height() + self.margin_top + self.margin_bottom\n\n    # Corners positions\n\n    def content_box_x(self):\n        \"\"\"Absolute horizontal position of the content box.\"\"\"\n        return self.position_x + self.margin_left + self.padding_left + \\\n            self.border_left_width\n\n    def content_box_y(self):\n        \"\"\"Absolute vertical position of the content box.\"\"\"\n        return self.position_y + self.margin_top + self.padding_top + \\\n            self.border_top_width\n\n    def padding_box_x(self):\n        \"\"\"Absolute horizontal position of the padding box.\"\"\"\n        return self.position_x + self.margin_left + self.border_left_width\n\n    def padding_box_y(self):\n        \"\"\"Absolute vertical position of the padding box.\"\"\"\n        return self.position_y + self.margin_top + self.border_top_width\n\n    def border_box_x(self):\n        \"\"\"Absolute horizontal position of the border box.\"\"\"\n        return self.position_x + self.margin_left\n\n    def border_box_y(self):\n        \"\"\"Absolute vertical position of the border box.\"\"\"\n        return self.position_y + self.margin_top\n\n    def hit_area(self):\n        \"\"\"Return the (x, y, w, h) rectangle where the box is clickable.\"\"\"\n        # \"Border area. That's the area that hit-testing is done on.\"\n        # https://lists.w3.org/Archives/Public/www-style/2012Jun/0318.html\n        # TODO: manage the border radii, use outer_border_radii instead\n        return (self.border_box_x(), self.border_box_y(),\n                self.border_width(), self.border_height())\n\n    def rounded_box(self, bt, br, bb, bl):\n        \"\"\"Position, size and radii of a box inside the outer border box.\n\n        bt, br, bb, and bl are distances from the outer border box,\n        defining a rectangle to be rounded.\n\n        \"\"\"\n        tlrx, tlry = self.border_top_left_radius\n        trrx, trry = self.border_top_right_radius\n        brrx, brry = self.border_bottom_right_radius\n        blrx, blry = self.border_bottom_left_radius\n\n        # TODO: clamp all computed values, see #2705.\n        tlrx = min(max(0, tlrx - bl), sys.maxsize)\n        tlry = min(max(0, tlry - bt), sys.maxsize)\n        trrx = min(max(0, trrx - br), sys.maxsize)\n        trry = min(max(0, trry - bt), sys.maxsize)\n        brrx = min(max(0, brrx - br), sys.maxsize)\n        brry = min(max(0, brry - bb), sys.maxsize)\n        blrx = min(max(0, blrx - bl), sys.maxsize)\n        blry = min(max(0, blry - bb), sys.maxsize)\n\n        x = self.border_box_x() + bl\n        y = self.border_box_y() + bt\n        width = self.border_width() - bl - br\n        height = self.border_height() - bt - bb\n\n        # Fix overlapping curves\n        # See https://www.w3.org/TR/css-backgrounds-3/#corner-overlap\n        ratio = min([1] + [\n            extent / sum_radii\n            for extent, sum_radii in (\n                (width, tlrx + trrx),\n                (width, blrx + brrx),\n                (height, tlry + blry),\n                (height, trry + brry),\n            )\n            if sum_radii > 0\n        ])\n        return (\n            x, y, width, height,\n            (tlrx * ratio, tlry * ratio),\n            (trrx * ratio, trry * ratio),\n            (brrx * ratio, brry * ratio),\n            (blrx * ratio, blry * ratio))\n\n    def rounded_box_ratio(self, ratio):\n        return self.rounded_box(\n            self.border_top_width * ratio,\n            self.border_right_width * ratio,\n            self.border_bottom_width * ratio,\n            self.border_left_width * ratio)\n\n    def rounded_padding_box(self):\n        \"\"\"Return the position, size and radii of the rounded padding box.\"\"\"\n        return self.rounded_box(\n            self.border_top_width,\n            self.border_right_width,\n            self.border_bottom_width,\n            self.border_left_width)\n\n    def rounded_border_box(self):\n        \"\"\"Return the position, size and radii of the rounded border box.\"\"\"\n        return self.rounded_box(0, 0, 0, 0)\n\n    def rounded_content_box(self):\n        \"\"\"Return the position, size and radii of the rounded content box.\"\"\"\n        return self.rounded_box(\n            self.border_top_width + self.padding_top,\n            self.border_right_width + self.padding_right,\n            self.border_bottom_width + self.padding_bottom,\n            self.border_left_width + self.padding_left)\n\n    # Positioning schemes\n\n    def is_floated(self):\n        \"\"\"Return whether this box is floated.\"\"\"\n        return self.style['float'] in ('left', 'right', 'inline-start', 'inline-end')\n\n    def is_footnote(self):\n        \"\"\"Return whether this box is a footnote.\"\"\"\n        return self.style['float'] == 'footnote'\n\n    def is_absolutely_positioned(self):\n        \"\"\"Return whether this box is in the absolute positioning scheme.\"\"\"\n        return self.style['position'] in ('absolute', 'fixed')\n\n    def is_running(self):\n        \"\"\"Return whether this box is a running element.\"\"\"\n        return self.style['position'][0] == 'running()'\n\n    def is_in_normal_flow(self):\n        \"\"\"Return whether this box is in normal flow.\"\"\"\n        return not (\n            self.is_floated() or self.is_absolutely_positioned() or\n            self.is_running() or self.is_footnote())\n\n    def is_monolithic(self):\n        \"\"\"Return whether this box is monolithic.\"\"\"\n        # https://www.w3.org/TR/css-break-3/#monolithic\n        return (\n            isinstance(self, AtomicInlineLevelBox) or\n            isinstance(self, ReplacedBox) or\n            self.style['overflow'] in ('auto', 'scroll') or\n            (self.style['overflow'] == 'hidden' and\n             self.style['height'] != 'auto'))\n\n    def establishes_formatting_context(self):\n        \"\"\"Return whether this box establishes a block formatting context.\"\"\"\n        # See https://www.w3.org/TR/CSS2/visuren.html#block-formatting\n        return (\n            self.is_floated() or\n            self.is_absolutely_positioned() or\n            self.is_column or\n            (isinstance(self, BlockContainerBox) and not isinstance(self, BlockBox)) or\n            (isinstance(self, BlockBox) and self.style['overflow'] != 'visible') or\n            'flow-root' in self.style['display'])\n\n    # Start and end page values for named pages\n\n    def page_values(self):\n        \"\"\"Return start and end page values.\"\"\"\n        return (self.style['page'], self.style['page'])\n\n    # PDF attachments\n\n    def is_attachment(self):\n        \"\"\"Return whether this link should be stored as a PDF attachment.\"\"\"\n        from ..html import element_has_link_type\n\n        if self.element is not None and self.element.tag == 'a':\n            return element_has_link_type(self.element, 'attachment')\n        return False\n\n    # Forms\n\n    def is_input(self):\n        \"\"\"Return whether this box is a form input.\"\"\"\n        # https://html.spec.whatwg.org/multipage/forms.html#category-submit\n        if self.style['appearance'] == 'auto' and self.element is not None:\n            if self.element.tag in ('button', 'input', 'select', 'textarea'):\n                return not isinstance(self, (LineBox, TextBox))\n        return False\n\n    def is_form(self):\n        \"\"\"Return whether this box is a form element.\"\"\"\n        if self.element is None:\n            return False\n        return self.element.tag == 'form'\n\n\nclass ParentBox(Box):\n    \"\"\"A box that has children.\"\"\"\n    def __init__(self, element_tag, style, element, children):\n        super().__init__(element_tag, style, element)\n        self.children = tuple(children)\n\n    def _reset_spacing(self, side):\n        \"\"\"Set to 0 the margin, padding and border of ``side``.\"\"\"\n        self.remove_decoration_sides.add(side)\n        setattr(self, f'margin_{side}', 0)\n        setattr(self, f'padding_{side}', 0)\n        setattr(self, f'border_{side}_width', 0)\n\n    def remove_decoration(self, start, end):\n        if self.style['box_decoration_break'] == 'clone':\n            return\n        if start:\n            self._reset_spacing('top')\n        if end:\n            self._reset_spacing('bottom')\n\n    def copy_with_children(self, new_children):\n        \"\"\"Create a new equivalent box with given ``new_children``.\"\"\"\n        new_box = self.copy()\n        new_box.children = new_children\n\n        # Clear and reset removed decorations as we don't want to keep the\n        # previous data, for example when a box is split between two pages.\n        self.remove_decoration_sides = set()\n\n        return new_box\n\n    def deepcopy(self):\n        result = self.copy()\n        result.children = list(child.deepcopy() for child in self.children)\n        return result\n\n    def get_wrapped_table(self):\n        \"\"\"Get the table wrapped by the box.\"\"\"\n        assert self.is_table_wrapper\n        for child in self.children:\n            if isinstance(child, TableBox):\n                return child\n        else:  # pragma: no cover\n            raise ValueError('Table wrapper without a table')\n\n    def page_values(self):\n        start_value, end_value = super().page_values()\n        # TODO: We should find Class A possible page breaks according to\n        # https://drafts.csswg.org/css-page-3/#propdef-page\n        # Keep only children in normal flow for now.\n        children = [\n            child for child in self.children if child.is_in_normal_flow()]\n        if children:\n            if len(children) == 1:\n                page_values = children[0].page_values()\n                start_value = page_values[0] or start_value\n                end_value = page_values[1] or end_value\n            else:\n                start_box, end_box = children[0], children[-1]\n                start_value = start_box.page_values()[0] or start_value\n                end_value = end_box.page_values()[1] or end_value\n        return start_value, end_value\n\n    def top_margin_collapses(self):\n        return not (\n            self.border_top_width or self.padding_top or\n            self.is_flex_item or self.is_grid_item or\n            self.establishes_formatting_context() or\n            self.is_table_wrapper or\n            self.is_for_root_element)\n\n    def bottom_margin_collapses(self):\n        return not (\n            self.border_bottom_width or self.padding_bottom or\n            self.is_flex_item or self.is_grid_item or\n            self.establishes_formatting_context() or\n            self.is_table_wrapper or\n            self.is_for_root_element)\n\n\nclass BlockLevelBox(Box):\n    \"\"\"A box that participates in an block formatting context.\n\n    An element with a ``display`` value of ``block``, ``list-item`` or\n    ``table`` generates a block-level box.\n\n    \"\"\"\n    clearance = None\n\n\nclass BlockContainerBox(ParentBox):\n    \"\"\"A box that contains only block-level boxes or only line boxes.\n\n    A box that either contains only block-level boxes or establishes an inline\n    formatting context and thus contains only line boxes.\n\n    A non-replaced element with a ``display`` value of ``block``,\n    ``list-item``, ``inline-block`` or 'table-cell' generates a block container\n    box.\n\n    \"\"\"\n\n\nclass BlockBox(BlockContainerBox, BlockLevelBox):\n    \"\"\"A block-level box that is also a block container.\n\n    A non-replaced element with a ``display`` value of ``block``, ``list-item``\n    generates a block box.\n\n    \"\"\"\n\n\nclass LineBox(ParentBox):\n    \"\"\"A box that represents a line in an inline formatting context.\n\n    Can only contain inline-level boxes.\n\n    In early stages of building the box tree a single line box contains many\n    consecutive inline boxes. Later, during layout phase, each line boxes will\n    be split into multiple line boxes, one for each actual line.\n\n    \"\"\"\n    text_overflow = 'clip'\n    block_ellipsis = 'none'\n\n    @classmethod\n    def anonymous_from(cls, parent, *args, **kwargs):\n        box = super().anonymous_from(parent, *args, **kwargs)\n        if parent.style['overflow'] != 'visible':\n            box.text_overflow = parent.style['text_overflow']\n        return box\n\n\nclass InlineLevelBox(Box):\n    \"\"\"A box that participates in an inline formatting context.\n\n    An inline-level box that is not an inline box is said to be \"atomic\". Such\n    boxes are inline blocks, replaced elements and inline tables.\n\n    An element with a ``display`` value of ``inline``, ``inline-table``, or\n    ``inline-block`` generates an inline-level box.\n\n    \"\"\"\n    def remove_decoration(self, start, end):\n        if self.style['box_decoration_break'] == 'clone':\n            return\n        ltr = self.style['direction'] == 'ltr'\n        if start:\n            self._reset_spacing('left' if ltr else 'right')\n        if end:\n            self._reset_spacing('right' if ltr else 'left')\n\n\nclass InlineBox(InlineLevelBox, ParentBox):\n    \"\"\"An inline box with inline children.\n\n    A box that participates in an inline formatting context and whose content\n    also participates in that inline formatting context.\n\n    A non-replaced element with a ``display`` value of ``inline`` generates an\n    inline box.\n\n    \"\"\"\n    def hit_area(self):\n        \"\"\"Return the (x, y, w, h) rectangle where the box is clickable.\"\"\"\n        # Use line-height (margin_height) rather than border_height\n        return (self.border_box_x(), self.position_y,\n                self.border_width(), self.margin_height())\n\n\nclass TextBox(InlineLevelBox):\n    \"\"\"A box that contains only text and has no box children.\n\n    Any text in the document ends up in a text box. What CSS calls \"anonymous\n    inline boxes\" are also text boxes.\n\n    \"\"\"\n    justification_spacing = 0\n\n    def __init__(self, element_tag, style, element, text):\n        assert text\n        super().__init__(element_tag, style, element)\n        self.text = text\n\n    def copy_with_text(self, text):\n        \"\"\"Return a new TextBox identical to this one except for the text.\"\"\"\n        assert text\n        new_box = self.copy()\n        new_box.text = text\n        return new_box\n\n\nclass AtomicInlineLevelBox(InlineLevelBox):\n    \"\"\"An atomic box in an inline formatting context.\n\n    This inline-level box cannot be split for line breaks.\n\n    \"\"\"\n\n\nclass InlineBlockBox(AtomicInlineLevelBox, BlockContainerBox):\n    \"\"\"A box that is both inline-level and a block container.\n\n    It behaves as inline on the outside and as a block on the inside.\n\n    A non-replaced element with a 'display' value of 'inline-block' generates\n    an inline-block box.\n\n    \"\"\"\n\n\nclass ReplacedBox(Box):\n    \"\"\"A box whose content is replaced.\n\n    For example, ``<img>`` are replaced: their content is rendered externally\n    and is opaque from CSS’s point of view.\n\n    \"\"\"\n    def __init__(self, element_tag, style, element, replacement):\n        super().__init__(element_tag, style, element)\n        self.replacement = replacement\n\n\nclass BlockReplacedBox(ReplacedBox, BlockLevelBox):\n    \"\"\"A box that is both replaced and block-level.\n\n    A replaced element with a ``display`` value of ``block``, ``liste-item`` or\n    ``table`` generates a block-level replaced box.\n\n    \"\"\"\n\n\nclass InlineReplacedBox(ReplacedBox, AtomicInlineLevelBox):\n    \"\"\"A box that is both replaced and inline-level.\n\n    A replaced element with a ``display`` value of ``inline``,\n    ``inline-table``, or ``inline-block`` generates an inline-level replaced\n    box.\n\n    \"\"\"\n\n\nclass TableBox(BlockLevelBox, ParentBox):\n    \"\"\"Box for elements with ``display: table``\"\"\"\n    # Definitions for the rules generating anonymous table boxes\n    # https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes\n    tabular_container = True\n\n    def all_children(self):\n        return itertools.chain(self.children, self.column_groups)\n\n    def translate(self, dx=0, dy=0, ignore_floats=False):\n        self.column_positions = [\n            position + dx for position in self.column_positions]\n        return super().translate(dx, dy, ignore_floats)\n\n    def page_values(self):\n        return (self.style['page'], self.style['page'])\n\n\nclass InlineTableBox(TableBox):\n    \"\"\"Box for elements with ``display: inline-table``\"\"\"\n\n\nclass TableRowGroupBox(ParentBox):\n    \"\"\"Box for elements with ``display: table-row-group``\"\"\"\n    proper_table_child = True\n    internal_table_or_caption = True\n    tabular_container = True\n    proper_parents = (TableBox, InlineTableBox)\n\n    # Default values. May be overriden on instances.\n    is_header = False\n    is_footer = False\n\n\nclass TableRowBox(ParentBox):\n    \"\"\"Box for elements with ``display: table-row``\"\"\"\n    proper_table_child = True\n    internal_table_or_caption = True\n    tabular_container = True\n    proper_parents = (TableBox, InlineTableBox, TableRowGroupBox)\n\n\nclass TableColumnGroupBox(ParentBox):\n    \"\"\"Box for elements with ``display: table-column-group``\"\"\"\n    proper_table_child = True\n    internal_table_or_caption = True\n    proper_parents = (TableBox, InlineTableBox)\n\n    # Columns groups never have margins or paddings\n    margin_top = 0\n    margin_bottom = 0\n    margin_left = 0\n    margin_right = 0\n\n    padding_top = 0\n    padding_bottom = 0\n    padding_left = 0\n    padding_right = 0\n\n    def get_cells(self):\n        \"\"\"Return cells that originate in the group's columns.\"\"\"\n        return [\n            cell for column in self.children for cell in column.get_cells()]\n\n    @property\n    def span(self):\n        if self.children:\n            return len(self.children)\n        else:\n            try:\n                return max(int(self.element.get('span', '').strip()), 1)\n            except ValueError:\n                return 1\n\n\n# Not really a parent box, but pretending to be removes some corner cases.\nclass TableColumnBox(ParentBox):\n    \"\"\"Box for elements with ``display: table-column``\"\"\"\n    proper_table_child = True\n    internal_table_or_caption = True\n    proper_parents = (TableBox, InlineTableBox, TableColumnGroupBox)\n\n    # Columns never have margins or paddings\n    margin_top = 0\n    margin_bottom = 0\n    margin_left = 0\n    margin_right = 0\n\n    padding_top = 0\n    padding_bottom = 0\n    padding_left = 0\n    padding_right = 0\n\n    def get_cells(self):\n        \"\"\"Return cells that originate in the column.\n\n        Is set on instances.\n\n        \"\"\"\n        raise NotImplementedError\n\n    @property\n    def span(self):\n        try:\n            return max(int(self.element.get('span', '').strip()), 1)\n        except ValueError:\n            return 1\n\n\nclass TableCellBox(BlockContainerBox):\n    \"\"\"Box for elements with ``display: table-cell``\"\"\"\n    internal_table_or_caption = True\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # HTML 4.01 gives special meaning to colspan=0\n        # https://www.w3.org/TR/html401/struct/tables.html#adef-rowspan\n        # but HTML 5 removed it\n        # https://html.spec.whatwg.org/multipage/tables.html#attr-tdth-colspan\n        # rowspan=0 is still there though.\n        try:\n            self.colspan = max(int(self.element.get('colspan', '').strip()), 1)\n        except (AttributeError, ValueError):\n            self.colspan = 1\n        try:\n            self.rowspan = max(int(self.element.get('rowspan', '').strip()), 0)\n        except (AttributeError, ValueError):\n            self.rowspan = 1\n\n\nclass TableCaptionBox(BlockBox):\n    \"\"\"Box for elements with ``display: table-caption``\"\"\"\n    proper_table_child = True\n    internal_table_or_caption = True\n    proper_parents = (TableBox, InlineTableBox)\n\n\nclass PageBox(ParentBox):\n    \"\"\"Box for a page.\n\n    Initially the whole document will be in the box for the root element.\n    During layout a new page box is created after every page break.\n\n    \"\"\"\n    def __init__(self, page_type, style):\n        self.page_type = page_type\n        # Page boxes are not linked to any element.\n        super().__init__(\n            element_tag=None, style=style, element=None, children=[])\n\n    def __repr__(self):\n        return f'<{type(self).__name__} {self.page_type}>'\n\n    @property\n    def bleed(self):\n        return {\n            side: self.style[f'bleed_{side}'].value\n            for side in ('top', 'right', 'bottom', 'left')}\n\n    @property\n    def bleed_area(self):\n        return (\n            -self.bleed['left'], -self.bleed['top'],\n            self.margin_width() + self.bleed['left'] + self.bleed['right'],\n            self.margin_height() + self.bleed['top'] + self.bleed['bottom'])\n\n\nclass MarginBox(BlockContainerBox):\n    \"\"\"Box in page margins, as defined in CSS3 Paged Media\"\"\"\n    def __init__(self, at_keyword, style):\n        self.at_keyword = at_keyword\n        # Margin boxes are not linked to any element.\n        super().__init__(\n            element_tag=None, style=style, element=None, children=[])\n\n    def __repr__(self):\n        return f'<{type(self).__name__} {self.at_keyword}>'\n\n\nclass FootnoteAreaBox(BlockBox):\n    \"\"\"Box displaying footnotes, as defined in GCPM.\"\"\"\n    def __init__(self, page, style):\n        self.page = page\n        # Footnote area boxes are not linked to any element.\n        super().__init__(\n            element_tag=None, style=style, element=None, children=[])\n\n    def __repr__(self):\n        return f'<{type(self).__name__} @footnote>'\n\n\nclass FlexContainerBox(ParentBox):\n    \"\"\"A box that contains only flex-items.\"\"\"\n\n\nclass FlexBox(FlexContainerBox, BlockLevelBox):\n    \"\"\"A box that is both block-level and a flex container.\n\n    It behaves as block on the outside and as a flex container on the inside.\n\n    \"\"\"\n\n\nclass InlineFlexBox(FlexContainerBox, InlineLevelBox):\n    \"\"\"A box that is both inline-level and a flex container.\n\n    It behaves as inline on the outside and as a flex container on the inside.\n\n    \"\"\"\n\n\nclass GridContainerBox(ParentBox):\n    \"\"\"A box that contains only grid-items.\"\"\"\n    def __init__(self, element_tag, style, element, children):\n        super().__init__(element_tag, style, element, children)\n        # TODO: we shouldn’t store this in the box but in the rendering context instead.\n        self.advancements = {}\n\n\nclass GridBox(GridContainerBox, BlockLevelBox):\n    \"\"\"A box that is both block-level and a grid container.\n\n    It behaves as block on the outside and as a grid container on the inside.\n\n    \"\"\"\n\n\nclass InlineGridBox(GridContainerBox, InlineLevelBox):\n    \"\"\"A box that is both inline-level and a grid container.\n\n    It behaves as inline on the outside and as a grid container on the inside.\n\n    \"\"\"\n"
  },
  {
    "path": "weasyprint/formatting_structure/build.py",
    "content": "\"\"\"Turn an element tree with style into a \"before layout\" box tree.\n\nThis includes creating anonymous boxes and processing whitespace as necessary.\n\n\"\"\"\n\nimport re\nimport unicodedata\n\nfrom .. import html\nfrom ..css import properties, targets\nfrom ..layout.table import collapse_table_borders\nfrom ..logger import LOGGER\nfrom ..text.constants import get_lang_quotes\nfrom . import boxes\n\n# Maps values of the ``display`` CSS property to box types.\nBOX_TYPE_FROM_DISPLAY = {\n    ('block', 'flow'): boxes.BlockBox,\n    ('inline', 'flow'): boxes.InlineBox,\n\n    ('block', 'flow-root'): boxes.BlockBox,\n    ('inline', 'flow-root'): boxes.InlineBlockBox,\n\n    ('block', 'table'): boxes.TableBox,\n    ('inline', 'table'): boxes.InlineTableBox,\n\n    ('block', 'flex'): boxes.FlexBox,\n    ('inline', 'flex'): boxes.InlineFlexBox,\n\n    ('block', 'grid'): boxes.GridBox,\n    ('inline', 'grid'): boxes.InlineGridBox,\n\n    ('table-row',): boxes.TableRowBox,\n    ('table-row-group',): boxes.TableRowGroupBox,\n    ('table-header-group',): boxes.TableRowGroupBox,\n    ('table-footer-group',): boxes.TableRowGroupBox,\n    ('table-column',): boxes.TableColumnBox,\n    ('table-column-group',): boxes.TableColumnGroupBox,\n    ('table-cell',): boxes.TableCellBox,\n    ('table-caption',): boxes.TableCaptionBox,\n}\n\n# https://stackoverflow.com/questions/16317534/\nASCII_TO_WIDE = {i: chr(i + 0xfee0) for i in range(0x21, 0x7f)}\nASCII_TO_WIDE.update({0x20: '\\u3000', 0x2D: '\\u2212'})\n\nLINE_FEED_RE = re.compile('\\r\\n?')\nTAB_RE = re.compile('[\\t ]*\\n[\\t ]*')\nSPACE_RE = re.compile('[\\t ]+')\n\n\ndef create_anonymous_boxes(box):\n    \"\"\"Create anonymous boxes in box descendants according to layout rules.\"\"\"\n    box = anonymous_table_boxes(box)\n    box = flex_boxes(box)\n    box = grid_boxes(box)\n    box = inline_in_block(box)\n    box = block_in_inline(box)\n    return box\n\n\ndef build_formatting_structure(element_tree, style_for, get_image_from_uri,\n                               base_url, target_collector, counter_style,\n                               footnotes):\n    \"\"\"Build a formatting structure (box tree) from an element tree.\"\"\"\n    box_list = element_to_box(\n        element_tree, style_for, get_image_from_uri, base_url,\n        target_collector, counter_style, footnotes)\n    if box_list:\n        box, = box_list\n    else:\n        # No root element\n        def root_style_for(element, pseudo_type=None):\n            style = style_for(element, pseudo_type)\n            if style is not None:\n                if element == element_tree:\n                    style['display'] = ('block', 'flow')\n                else:\n                    style['display'] = ('none',)\n            return style\n        box, = element_to_box(\n            element_tree, root_style_for, get_image_from_uri, base_url,\n            target_collector, counter_style, footnotes)\n\n    target_collector.check_pending_targets()\n    process_whitespace(box)\n    process_text_transform(box)\n\n    box.is_for_root_element = True\n    # If this is changed, maybe update weasy.layout.page.make_margin_boxes()\n    box = create_anonymous_boxes(box)\n    box = set_viewport_overflow(box)\n    return box\n\n\ndef make_box(element_tag, style, content, element):\n    return BOX_TYPE_FROM_DISPLAY[style['display'][:2]](\n        element_tag, style, element, content)\n\n\ndef element_to_box(element, style_for, get_image_from_uri, base_url,\n                   target_collector, counter_style, footnotes, state=None):\n    \"\"\"Convert an element and its children into a box with children.\n\n    Return a list of boxes. Most of the time the list will have one item but\n    may have zero or more than one.\n\n    Eg.::\n\n        <p>Some <em>emphasised</em> text.</p>\n\n    gives (not actual syntax)::\n\n        BlockBox[\n            TextBox['Some '],\n            InlineBox[\n                TextBox['emphasised'],\n            ],\n            TextBox[' text.'],\n        ]\n\n    ``TextBox``es are anonymous inline boxes:\n    See https://www.w3.org/TR/CSS21/visuren.html#anonymous\n\n    \"\"\"\n    if not isinstance(element.tag, str):\n        # We ignore comments and XML processing instructions.\n        return []\n\n    style = style_for(element)\n\n    # TODO: should be the used value. When does the used value for `display`\n    # differ from the computer value?\n    display = style['display']\n    if display == ('none',):\n        return []\n\n    if style['float'] == 'footnote':\n        if style['footnote_display'] == 'block':\n            style['display'] = ('block', 'flow')\n        else:\n            # TODO: handle compact footnotes\n            style['display'] = ('inline', 'flow')\n\n    box = make_box(element.tag, style, [], element)\n    box.first_letter_style = style_for(element, 'first-letter')\n    box.first_line_style = style_for(element, 'first-line')\n\n    if state is None:\n        # use a list to have a shared mutable object\n        state = (\n            # Shared mutable objects:\n            [0],  # quote_depth: single integer\n            # TODO: define the footnote counter where it can be updated by page\n            {'footnote': [0]},  # counter_values: name -> stacked/scoped values\n            [{'footnote'}],  # counter_scopes: element depths -> counter names\n            [] # page_groups\n        )\n    quote_depth, counter_values, counter_scopes, _page_groups = state\n\n    update_counters(state, style)\n\n    children = []\n\n    # If this element’s direct children create new scopes, the counter\n    # names will be in this new list\n    counter_scopes.append(set())\n\n    marker_boxes = []\n    if 'list-item' in style['display']:\n        marker_boxes = list(marker_to_box(\n            element, state, style, style_for, get_image_from_uri,\n            target_collector, counter_style))\n        children.extend(marker_boxes)\n\n    children.extend(before_after_to_box(\n        element, 'before', state, style_for, get_image_from_uri,\n        target_collector, counter_style))\n\n    # collect anchor's counter_values, maybe it's a target.\n    # to get the spec-conform counter_values we must do it here,\n    # after the ::before is parsed and before the ::after is\n    if style['anchor']:\n        target_collector.store_target(style['anchor'], counter_values, box)\n\n    text = element.text\n    if text:\n        children.append(boxes.TextBox.anonymous_from(box, text))\n\n    for child_element in element:\n        child_boxes = element_to_box(\n            child_element, style_for, get_image_from_uri, base_url,\n            target_collector, counter_style, footnotes, state)\n\n        if child_boxes and child_boxes[0].style['float'] == 'footnote':\n            footnote = child_boxes[0]\n            footnote.style['float'] = 'none'\n            footnotes.append(footnote)\n            call_style = style_for(footnote.element, 'footnote-call')\n            footnote_call = make_box(\n                f'{footnote.element.tag}::footnote-call', call_style, [],\n                footnote.element)\n            footnote_call.children = content_to_boxes(\n                call_style, footnote_call, quote_depth, counter_values,\n                get_image_from_uri, target_collector, counter_style)\n            footnote_call.footnote = footnote\n            child_boxes = [footnote_call]\n\n        children.extend(child_boxes)\n        text = child_element.tail\n        if text:\n            text_box = boxes.TextBox.anonymous_from(box, text)\n            if children and isinstance(children[-1], boxes.TextBox):\n                children[-1].text += text_box.text\n            else:\n                children.append(text_box)\n\n    children.extend(before_after_to_box(\n        element, 'after', state, style_for, get_image_from_uri,\n        target_collector, counter_style))\n\n    # Scopes created by this element’s children stop here.\n    for name in counter_scopes.pop():\n        counter_values[name].pop()\n        if not counter_values[name]:\n            counter_values.pop(name)\n\n    box.children = children\n    set_content_lists(\n        element, box, style, counter_values, target_collector, counter_style)\n\n    if marker_boxes and len(box.children) == 1:\n        # See https://www.w3.org/TR/css-lists-3/#list-style-position-outside\n        #\n        # \"The size or contents of the marker box may affect the height of the\n        #  principal block box and/or the height of its first line box, and in\n        #  some cases may cause the creation of a new line box; this\n        #  interaction is also not defined.\"\n        #\n        # We decide here to add a zero-width space to have a minimum\n        # height. Adding text boxes is not the best idea, but it's not a good\n        # moment to add an empty line box, and the specification lets us do\n        # almost what we want, so…\n        if style['list_style_position'] == 'outside':\n            box.children.append(boxes.TextBox.anonymous_from(box, '​'))\n\n    if style['float'] == 'footnote':\n        counter_values['footnote'][-1] += 1\n        marker_style = style_for(element, 'footnote-marker')\n        marker = make_box(\n            f'{element.tag}::footnote-marker', marker_style, [], element)\n        marker.children = content_to_boxes(\n            marker_style, marker, quote_depth, counter_values, get_image_from_uri,\n            target_collector, counter_style)\n        box.children.insert(0, marker)\n\n    # Specific handling for the element. (eg. replaced element)\n    return html.handle_element(element, box, get_image_from_uri, base_url)\n\n\ndef before_after_to_box(element, pseudo_type, state, style_for,\n                        get_image_from_uri, target_collector, counter_style):\n    \"\"\"Return the boxes for ::before or ::after pseudo-element.\"\"\"\n    style = style_for(element, pseudo_type)\n    if pseudo_type and style is None:\n        # Pseudo-elements with no style at all do not get a style dict.\n        # Their initial content property computes to 'none'.\n        return []\n\n    # TODO: should be the computed value. When does the used value for\n    # `display` differ from the computer value? It's at least wrong for\n    # `content` where 'normal' computes as 'inhibit' for pseudo elements.\n    display = style['display']\n    if display == ('none',):\n        return []\n    content = style['content']\n    if content in ('normal', 'inhibit', 'none'):\n        return []\n    box = make_box(f'{element.tag}::{pseudo_type}', style, [], element)\n\n    quote_depth, counter_values, _counter_scopes, _page_groups = state\n    update_counters(state, style)\n\n    children = []\n\n    if 'list-item' in display:\n        marker_boxes = list(marker_to_box(\n            element, state, style, style_for, get_image_from_uri,\n            target_collector, counter_style))\n        children.extend(marker_boxes)\n\n    children.extend(content_to_boxes(\n        style, box, quote_depth, counter_values, get_image_from_uri,\n        target_collector, counter_style))\n\n    box.children = children\n\n    # calculate the bookmark-label\n    if style['bookmark_level'] != 'none':\n        _quote_depth, counter_values, _counter_scopes, _page_groups = state\n        compute_bookmark_label(\n            element, box, style['bookmark_label'], counter_values,\n            target_collector, counter_style)\n    return [box]\n\n\ndef marker_to_box(element, state, parent_style, style_for, get_image_from_uri,\n                  target_collector, counter_style):\n    \"\"\"Yield the box for ::marker pseudo-element if there is one.\n\n    https://drafts.csswg.org/css-lists-3/#marker-pseudo\n\n    \"\"\"\n    style = style_for(element, 'marker')\n\n    children = []\n\n    # TODO: should be the computed value. When does the used value for\n    # `display` differ from the computer value? It's at least wrong for\n    # `content` where 'normal' computes as 'inhibit' for pseudo elements.\n    quote_depth, counter_values, _counter_scopes, _page_groups = state\n\n    box = make_box(f'{element.tag}::marker', style, children, element)\n\n    if style['display'] == ('none',):\n        return\n\n    image_type, image = style['list_style_image']\n\n    if style['content'] not in ('normal', 'inhibit'):\n        children.extend(content_to_boxes(\n            style, box, quote_depth, counter_values, get_image_from_uri,\n            target_collector, counter_style))\n\n    else:\n        if image_type == 'url':\n            # image may be None here too, in case the image is not available.\n            image = get_image_from_uri(\n                url=image, orientation=style['image_orientation'])\n            if image is not None:\n                box = boxes.InlineReplacedBox.anonymous_from(box, image)\n                children.append(box)\n\n        if not children and style['list_style_type'] != 'none':\n            counter_value = counter_values.get('list-item', [0])[-1]\n            counter_type = style['list_style_type']\n            if marker_text := counter_style.render_marker(counter_type, counter_value):\n                box = boxes.TextBox.anonymous_from(box, marker_text)\n                box.style['white_space'] = 'pre-wrap'\n                children.append(box)\n\n    if not children:\n        return\n\n    if parent_style['list_style_position'] == 'outside':\n        marker_box = boxes.BlockBox.anonymous_from(box, children)\n        # We can safely edit everything that can't be changed by user style\n        # See https://drafts.csswg.org/css-pseudo-4/#marker-pseudo\n        marker_box.style['position'] = 'absolute'\n        marker_box.is_outside_marker = True\n    else:\n        marker_box = boxes.InlineBox.anonymous_from(box, children)\n    yield marker_box\n\n\ndef compute_content_list(content_list, parent_box, counter_values, css_token,\n                         parse_again, target_collector, counter_style,\n                         get_image_from_uri=None, quote_depth=None,\n                         quote_style=None, lang=None, context=None, page=None,\n                         element=None):\n    \"\"\"Compute and return the boxes corresponding to the ``content_list``.\n\n    ``parse_again`` is called to compute the ``content_list`` again when\n    ``target_collector.lookup_target()`` detected a pending target.\n\n    ``build_formatting_structure`` calls\n    ``target_collector.check_pending_targets()`` after the first pass to do\n    required reparsing.\n\n    \"\"\"\n    # TODO: Some computation done here may be done in computed_values\n    # instead. We currently miss at least style_for, counters and quotes\n    # context in computer. Some work will still need to be done here though,\n    # like box creation for URIs.\n\n    content_boxes = []\n    has_text = set()  # Use a set because variable is modified in add_text\n\n    def add_text(text):\n        has_text.add(True)\n        if text:\n            if content_boxes and isinstance(content_boxes[-1], boxes.TextBox):\n                content_boxes[-1].text += text\n            else:\n                content_boxes.append(\n                    boxes.TextBox.anonymous_from(parent_box, text))\n\n    missing_counters = []\n    missing_target_counters = {}\n    in_page_context = context is not None and page is not None\n\n    # Collect missing counters during build_formatting_structure.\n    # Pointless to collect missing target counters in MarginBoxes.\n    need_collect_missing = target_collector.collecting and not in_page_context\n\n    if parent_box.cached_counter_values is None:\n        # Store the counter_values in the parent_box to make them accessible\n        # in @page context.\n        parent_box.cached_counter_values = {\n            key: value.copy() for key, value in counter_values.items()}\n    for type_, value in content_list:\n        if type_ == 'string':\n            add_text(value)\n        elif type_ == 'url' and get_image_from_uri is not None:\n            origin, uri = value\n            if origin != 'external':\n                # Embedding internal references is impossible.\n                continue\n            image = get_image_from_uri(\n                url=uri, orientation=parent_box.style['image_orientation'])\n            if image is not None:\n                content_boxes.append(\n                    boxes.InlineReplacedBox.anonymous_from(parent_box, image))\n        elif type_ == 'content()':\n            added_text = extract_text(value, parent_box)\n            add_text(added_text)\n        elif type_ == 'string()':\n            if not in_page_context:\n                # string() is currently only valid in @page context.\n                # See issue #723.\n                LOGGER.warning(\n                    '\"string(%s)\" is only allowed in page margins',\n                    ' '.join(value))\n                continue\n            add_text(context.get_string_set_for(page, *value))\n        elif type_ in ('counter()', 'counters()'):\n            counter_name, counter_type = value[0], value[-1]\n            if counter_type == 'none':\n                continue\n            if need_collect_missing:\n                if counter_name not in list(counter_values) + missing_counters:\n                    missing_counters.append(counter_name)\n            if type_ == 'counter()':\n                counter_value = counter_values.get(counter_name, [0])[-1]\n                text = counter_style.render_value(counter_value, counter_type)\n            else:\n                separator = value[1]\n                text = separator.join(\n                    counter_style.render_value(counter_value, counter_type)\n                    for counter_value in counter_values.get(counter_name, [0]))\n            add_text(text)\n        elif type_ in ('target-counter()', 'target-counters()'):\n            (anchor_token, counter_name), counter_type = value[:2], value[-1]\n            if counter_type == 'none':\n                continue\n            lookup_target = target_collector.lookup_target(\n                anchor_token, parent_box, css_token, parse_again)\n            if lookup_target.state != 'up-to-date':\n                break\n            target_values = lookup_target.target_box.cached_counter_values\n            if need_collect_missing and counter_name not in target_values:\n                anchor_name = targets.anchor_name_from_token(anchor_token)\n                missing_counters = missing_target_counters.setdefault(\n                    anchor_name, [])\n                if counter_name not in missing_counters:\n                    missing_counters.append(counter_name)\n            # Mixin target's cached page counters.\n            # cached_page_counter_values are empty during layout.\n            local_counters = lookup_target.cached_page_counter_values.copy()\n            local_counters.update(target_values)\n            if type_ == 'target-counter()':\n                counter_value = local_counters.get(counter_name, [0])[-1]\n                text = counter_style.render_value(counter_value, counter_type)\n            else:\n                separator = value[2]\n                if separator[0] != 'string':\n                    break\n                separator_string = separator[1]\n                text = separator_string.join(\n                    counter_style.render_value(counter_value, counter_type)\n                    for counter_value in local_counters.get(counter_name, [0]))\n            add_text(text)\n        elif type_ == 'target-text()':\n            anchor_token, text_style = value\n            lookup_target = target_collector.lookup_target(\n                anchor_token, parent_box, css_token, parse_again)\n            if lookup_target.state == 'up-to-date':\n                target_box = lookup_target.target_box\n                # TODO: 'before'- and 'after'- content referring missing\n                # counters are not properly set.\n                text = extract_text(text_style, target_box)\n                add_text(text)\n            else:\n                break\n        elif type_ == 'quote' and None not in (quote_depth, quote_style):\n            is_open = 'open' in value\n            insert = not value.startswith('no-') and quote_style != 'none'\n            if not is_open:\n                quote_depth[0] = max(0, quote_depth[0] - 1)\n            if insert:\n                if quote_style == 'auto':\n                    open_quotes, close_quotes = get_lang_quotes(lang)\n                else:\n                    open_quotes, close_quotes = quote_style\n                quotes = open_quotes if is_open else close_quotes\n                add_text(quotes[min(quote_depth[0], len(quotes) - 1)])\n            if is_open:\n                quote_depth[0] += 1\n        elif type_ == 'element()':\n            if not in_page_context:\n                LOGGER.warning(\n                    '\"element(%s)\" is only allowed in page margins',\n                    ' '.join(value))\n                continue\n            new_box = context.get_running_element_for(page, *value)\n            if new_box is None:\n                continue\n            new_box = new_box.deepcopy()\n            new_box.style['position'] = 'static'\n            if isinstance(new_box, boxes.ParentBox):\n                for child in new_box.descendants():\n                    if child.style['content'] in ('normal', 'none'):\n                        continue\n                    child.children = content_to_boxes(\n                        child.style, child, quote_depth, counter_values,\n                        get_image_from_uri, target_collector, counter_style,\n                        context=context, page=page)\n            content_boxes.append(new_box)\n        elif type_ == 'leader()':\n            if not value[1]:\n                continue\n            text_box = boxes.TextBox.anonymous_from(parent_box, value[1])\n            leader_box = boxes.InlineBox.anonymous_from(\n                parent_box, (text_box,))\n            # Avoid breaks inside the leader box\n            leader_box.style['white_space'] = 'pre'\n            # Prevent whitespaces from being removed from the text box\n            text_box.style['white_space'] = 'pre'\n            leader_box.is_leader = True\n            content_boxes.append(leader_box)\n\n    if has_text or content_boxes:\n        # Only add CounterLookupItem if the content_list actually produced text\n        target_collector.collect_missing_counters(\n            parent_box, css_token, parse_again, missing_counters,\n            missing_target_counters)\n        return content_boxes\n\n\ndef content_to_boxes(style, parent_box, quote_depth, counter_values,\n                     get_image_from_uri, target_collector, counter_style,\n                     context=None, page=None):\n    \"\"\"Take the value of a ``content`` property and return boxes.\"\"\"\n    def parse_again(mixin_pagebased_counters=None):\n        \"\"\"Closure to parse the ``parent_boxes`` children all again.\"\"\"\n\n        # Neither alters the mixed-in nor the cached counter values, no\n        # need to deepcopy here\n        if mixin_pagebased_counters is None:\n            local_counters = {}\n        else:\n            local_counters = mixin_pagebased_counters.copy()\n        local_counters.update(parent_box.cached_counter_values)\n\n        local_children = []\n        local_children.extend(content_to_boxes(\n            style, parent_box, orig_quote_depth, local_counters,\n            get_image_from_uri, target_collector, counter_style))\n\n        # TODO: do we need to add markers here?\n        # TODO: redo the formatting structure of the parent instead of hacking\n        # the already formatted structure. Find why inline_in_blocks has\n        # sometimes already been called, and sometimes not.\n        if (len(parent_box.children) == 1 and\n                isinstance(parent_box.children[0], boxes.LineBox)):\n            parent_box.children[0].children = local_children\n        else:\n            parent_box.children = local_children\n\n    if style['content'] == 'inhibit':\n        return []\n\n    orig_quote_depth = quote_depth[:]\n    css_token = 'content'\n    box_list = compute_content_list(\n        style['content'], parent_box, counter_values, css_token, parse_again,\n        target_collector, counter_style, get_image_from_uri, quote_depth,\n        style['quotes'], style['lang'], context, page)\n    return box_list or []\n\n\ndef compute_string_set(element, box, string_name, content_list,\n                       counter_values, target_collector, counter_style):\n    \"\"\"Parse the content-list value of ``string_name`` for ``string-set``.\"\"\"\n    def parse_again(mixin_pagebased_counters=None):\n        \"\"\"Closure to parse the string-set string value all again.\"\"\"\n        # Neither alters the mixed-in nor the cached counter values, no\n        # need to deepcopy here\n        if mixin_pagebased_counters is None:\n            local_counters = {}\n        else:\n            local_counters = mixin_pagebased_counters.copy()\n        local_counters.update(box.cached_counter_values)\n        compute_string_set(\n            element, box, string_name, content_list, local_counters,\n            target_collector, counter_style)\n\n    css_token = f'string-set::{string_name}'\n    box_list = compute_content_list(\n        content_list, box, counter_values, css_token, parse_again,\n        target_collector, counter_style, element=element)\n    if box_list is not None:\n        string = ''.join(\n            box.text for box in box_list if isinstance(box, boxes.TextBox))\n        # Avoid duplicates, care for parse_again and missing counters, don't\n        # change the pointer\n        for string_set_tuple in box.string_set:\n            if string_set_tuple[0] == string_name:\n                box.string_set.remove(string_set_tuple)\n                break\n        box.string_set.append((string_name, string))\n\n\ndef compute_bookmark_label(element, box, content_list, counter_values,\n                           target_collector, counter_style):\n    \"\"\"Parses the content-list value for ``bookmark-label``.\"\"\"\n    def parse_again(mixin_pagebased_counters=None):\n        \"\"\"Closure to parse the bookmark-label all again.\"\"\"\n        # Neither alters the mixed-in nor the cached counter values, no\n        # need to deepcopy here\n        if mixin_pagebased_counters is None:\n            local_counters = {}\n        else:\n            local_counters = mixin_pagebased_counters.copy()\n        local_counters.update(box.cached_counter_values)\n        compute_bookmark_label(\n            element, box, content_list, local_counters, target_collector,\n            counter_style)\n\n    css_token = 'bookmark-label'\n    box_list = compute_content_list(\n        content_list, box, counter_values, css_token, parse_again,\n        target_collector, counter_style, element=element)\n    if box_list:\n        box.bookmark_label = ''.join(box_text(box) for box in box_list)\n\n\ndef set_content_lists(element, box, style, counter_values, target_collector,\n                      counter_style):\n    \"\"\"Set the content-lists values.\n\n    These content-lists are used in GCPM properties like ``string-set`` and\n    ``bookmark-label``.\n\n    \"\"\"\n    box.string_set = []\n    if style['string_set'] != 'none':\n        for string_name, string_values in style['string_set']:\n            compute_string_set(\n                element, box, string_name, string_values, counter_values,\n                target_collector, counter_style)\n    if style['bookmark_level'] != 'none':\n        compute_bookmark_label(\n            element, box, style['bookmark_label'], counter_values,\n            target_collector, counter_style)\n\n\ndef update_counters(state, style):\n    \"\"\"Handle the ``counter-*`` properties.\"\"\"\n    _quote_depth, counter_values, counter_scopes, _page_groups = state\n    sibling_scopes = counter_scopes[-1]\n\n    for name, value in style['counter_reset']:\n        if name in sibling_scopes:\n            counter_values[name].pop()\n        else:\n            sibling_scopes.add(name)\n        counter_values.setdefault(name, []).append(value)\n\n    for name, value in style['counter_set']:\n        values = counter_values.setdefault(name, [])\n        if not values:\n            assert name not in sibling_scopes\n            sibling_scopes.add(name)\n            values.append(0)\n        values[-1] = value\n\n    counter_increment = style['counter_increment']\n    if counter_increment == 'auto':\n        # 'auto' is the initial value but is not valid in stylesheet:\n        # there was no counter-increment declaration for this element.\n        # (Or the winning value was 'initial'.)\n        # https://drafts.csswg.org/css-lists-3/#declaring-a-list-item\n        if 'list-item' in style['display']:\n            counter_increment = [('list-item', 1)]\n        else:\n            counter_increment = []\n    for name, value in counter_increment:\n        values = counter_values.setdefault(name, [])\n        if not values:\n            assert name not in sibling_scopes\n            sibling_scopes.add(name)\n            values.append(0)\n        values[-1] += value\n\n\ndef is_whitespace(box, _has_non_whitespace=re.compile('\\\\S').search):\n    \"\"\"Return True if ``box`` is a TextBox with only whitespace.\"\"\"\n    return isinstance(box, boxes.TextBox) and not _has_non_whitespace(box.text)\n\n\ndef wrap_improper(box, children, wrapper_type, test=None):\n    \"\"\"Wrap consecutive children that do not pass ``test`` in a ``wrapper_type`` box.\n\n    ``test`` defaults to children being of the same type as ``wrapper_type``.\n\n    \"\"\"\n    if test is None:\n        def test(child):\n            return isinstance(child, wrapper_type)\n    improper = []\n    for child in children:\n        if test(child):\n            if improper:\n                wrapper = wrapper_type.anonymous_from(box, children=[])\n                # Apply the rules again on the new wrapper\n                yield table_boxes_children(wrapper, improper)\n                improper = []\n            yield child\n        else:\n            improper.append(child)\n    if improper:\n        wrapper = wrapper_type.anonymous_from(box, children=[])\n        # Apply the rules again on the new wrapper\n        yield table_boxes_children(wrapper, improper)\n\n\ndef anonymous_table_boxes(box):\n    \"\"\"Remove and add boxes according to the table model.\n\n    Take and return a ``Box`` object.\n\n    See https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes\n\n    \"\"\"\n    if not isinstance(box, boxes.ParentBox) or box.is_running():\n        return box\n\n    # Do recursion.\n    children = [anonymous_table_boxes(child) for child in box.children]\n    return table_boxes_children(box, children)\n\n\ndef table_boxes_children(box, children):\n    \"\"\"Internal implementation of anonymous_table_boxes().\"\"\"\n    if isinstance(box, boxes.TableColumnBox):  # rule 1.1\n        # Remove all children.\n        children = []\n    elif isinstance(box, boxes.TableColumnGroupBox):  # rule 1.2\n        # Remove children other than table-column.\n        children = [\n            child for child in children\n            if isinstance(child, boxes.TableColumnBox)\n        ]\n        # Rule XXX (not in the spec): column groups have at least\n        # one column child.\n        if not children:\n            if box.span is None or box.span < 1:\n                span = 1\n            else:\n                span = box.span\n            children = [boxes.TableColumnBox.anonymous_from(box, [])\n                        for _ in range(span)]\n\n    # rule 1.3\n    if box.tabular_container and len(children) >= 2:\n        # TODO: Maybe only remove text if internal is also\n        #       a proper table descendant of box.\n        # This is what the spec says, but maybe not what browsers do:\n        # https://lists.w3.org/Archives/Public/www-style/2011Oct/0567\n\n        # Last child\n        internal, text = children[-2:]\n        if (internal.internal_table_or_caption and is_whitespace(text)):\n            children.pop()\n\n        # First child\n        if len(children) >= 2:\n            text, internal = children[:2]\n            if (internal.internal_table_or_caption and is_whitespace(text)):\n                children.pop(0)\n\n        # Children other than first and last that would be removed by\n        # rule 1.3 are also removed by rule 1.4 below.\n\n    children = [\n        child\n        for prev_child, child, next_child in zip(\n            [None, *children[:-1]],\n            children,\n            [*children[1:], None]\n        )\n        if not (\n            # Ignore some whitespace: rule 1.4\n            prev_child and prev_child.internal_table_or_caption and\n            next_child and next_child.internal_table_or_caption and\n            is_whitespace(child)\n        )\n    ]\n\n    if isinstance(box, boxes.TableBox):\n        # Rule 2.1\n        children = wrap_improper(\n            box, children, boxes.TableRowBox,\n            lambda child: child.proper_table_child)\n    elif isinstance(box, boxes.TableRowGroupBox):\n        # Rule 2.2\n        children = wrap_improper(box, children, boxes.TableRowBox)\n\n    if isinstance(box, boxes.TableRowBox):\n        # Rule 2.3\n        children = wrap_improper(box, children, boxes.TableCellBox)\n    else:\n        # Rule 3.1\n        children = wrap_improper(\n            box, children, boxes.TableRowBox,\n            lambda child: not isinstance(child, boxes.TableCellBox))\n\n    # Rule 3.2\n    if isinstance(box, boxes.InlineBox):\n        children = wrap_improper(\n            box, children, boxes.InlineTableBox,\n            lambda child: not child.proper_table_child)\n    else:\n        parent_type = type(box)\n        children = wrap_improper(\n            box, children, boxes.TableBox,\n            lambda child: (not child.proper_table_child or\n                           parent_type in child.proper_parents))\n\n    if isinstance(box, boxes.TableBox):\n        return wrap_table(box, children)\n    else:\n        box.children = list(children)\n        return box\n\n\ndef wrap_table(box, children):\n    \"\"\"Take a table box and return it in its table wrapper box.\n\n    Also re-order children and assign grid positions to each column and cell.\n\n    Because of colspan/rowspan works, grid_y is implicitly the index of a row,\n    but grid_x is an explicit attribute on cells, columns and column group.\n\n    https://www.w3.org/TR/CSS21/tables.html#model\n    https://www.w3.org/TR/CSS21/tables.html#table-layout\n\n    \"\"\"\n    # Group table children by type\n    columns = []\n    rows = []\n    all_captions = []\n    by_type = {\n        boxes.TableColumnBox: columns,\n        boxes.TableColumnGroupBox: columns,\n        boxes.TableRowBox: rows,\n        boxes.TableRowGroupBox: rows,\n        boxes.TableCaptionBox: all_captions,\n    }\n    for child in children:\n        by_type[type(child)].append(child)\n\n    # Split top and bottom captions\n    captions = {'top': [], 'bottom': []}\n    for caption in all_captions:\n        captions[caption.style['caption_side']].append(caption)\n\n    # Assign X positions on the grid to column boxes\n    column_groups = list(wrap_improper(\n        box, columns, boxes.TableColumnGroupBox))\n    grid_x = 0\n    for group in column_groups:\n        group.grid_x = grid_x\n        if group.children:\n            for column in group.children:\n                # There's no need to take care of group's span, as \"span=x\"\n                # already generates x TableColumnBox children\n                column.grid_x = grid_x\n                grid_x += 1\n        else:\n            grid_x += group.span\n    grid_width = grid_x\n\n    row_groups = wrap_improper(box, rows, boxes.TableRowGroupBox)\n    # Extract the optional header and footer groups.\n    body_row_groups = []\n    header = None\n    footer = None\n    for group in row_groups:\n        display = group.style['display']\n        if display == ('table-header-group',) and header is None:\n            group.is_header = True\n            header = group\n        elif display == ('table-footer-group',) and footer is None:\n            group.is_footer = True\n            footer = group\n        else:\n            body_row_groups.append(group)\n    row_groups = (\n        ([header] if header is not None else []) +\n        body_row_groups +\n        ([footer] if footer is not None else []))\n\n    # Assign a (x,y) position in the grid to each cell.\n    # rowspan can not extend beyond a row group, so each row group\n    # is independent.\n    # https://www.w3.org/TR/CSS21/tables.html#table-layout\n    # Column 0 is on the left if direction is ltr, right if rtl.\n    # This algorithm does not change.\n    grid_height = 0\n    for group in row_groups:\n        # Indexes: row number in the group.\n        # Values: set of cells already occupied by row-spanning cells.\n        occupied_cells_by_row = [set() for row in group.children]\n        for row in group.children:\n            occupied_cells_in_this_row = occupied_cells_by_row.pop(0)\n            # The list is now about rows after this one.\n            grid_x = 0\n            for cell in row.children:\n                # Make sure that the first grid cell is free.\n                while grid_x in occupied_cells_in_this_row:\n                    grid_x += 1\n                cell.grid_x = grid_x\n                new_grid_x = grid_x + cell.colspan\n                # https://www.w3.org/TR/html401/struct/tables.html#adef-rowspan\n                if cell.rowspan != 1:\n                    max_rowspan = len(occupied_cells_by_row) + 1\n                    if cell.rowspan == 0:\n                        # All rows until the end of the group\n                        spanned_rows = occupied_cells_by_row\n                        cell.rowspan = max_rowspan\n                    else:\n                        cell.rowspan = min(cell.rowspan, max_rowspan)\n                        spanned_rows = occupied_cells_by_row[:cell.rowspan - 1]\n                    spanned_columns = range(grid_x, new_grid_x)\n                    for occupied_cells in spanned_rows:\n                        occupied_cells.update(spanned_columns)\n                grid_x = new_grid_x\n                grid_width = max(grid_width, grid_x)\n        grid_height += len(group.children)\n\n    table = box.copy_with_children(row_groups)\n    table.style = table.style.copy()\n    table.column_groups = tuple(column_groups)\n    if table.style['border_collapse'] == 'collapse':\n        table.collapsed_border_grid = collapse_table_borders(\n            table, grid_width, grid_height)\n\n    if isinstance(box, boxes.InlineTableBox):\n        wrapper_type = boxes.InlineBlockBox\n    else:\n        wrapper_type = boxes.BlockBox\n\n    wrapper = wrapper_type.anonymous_from(\n        box, captions['top'] + [table] + captions['bottom'])\n    wrapper.style = wrapper.style.copy()\n    wrapper.is_table_wrapper = True\n    # Non-inherited properties of the table element apply to one\n    # of the wrapper and the table. The other get the initial value.\n    # TODO: put this in a method of the table object\n    for name in properties.TABLE_WRAPPER_BOX_PROPERTIES:\n        wrapper.style[name] = table.style[name]\n        table.style[name] = properties.INITIAL_VALUES[name]\n\n    return wrapper\n\n\ndef blockify(box, layout):\n    \"\"\"Turn an inline box into a block box.\"\"\"\n    # See https://drafts.csswg.org/css-display-4/#blockify.\n    if isinstance(box, boxes.InlineBlockBox):\n        anonymous = boxes.BlockBox.anonymous_from(box, box.children)\n    elif isinstance(box, boxes.InlineReplacedBox):\n        replacement = box.replacement\n        anonymous = boxes.BlockReplacedBox.anonymous_from(box, replacement)\n    elif isinstance(box, boxes.InlineLevelBox):\n        anonymous = boxes.BlockBox.anonymous_from(box, [box])\n        setattr(box, f'is_{layout}_item', False)\n    else:\n        return box\n    anonymous.style = box.style\n    setattr(anonymous, f'is_{layout}_item', True)\n    return anonymous\n\n\ndef flex_boxes(box):\n    \"\"\"Remove and add boxes according to the flex model.\n\n    Take and return a ``Box`` object.\n\n    See https://www.w3.org/TR/css-flexbox-1/#flex-items\n\n    \"\"\"\n    if not isinstance(box, boxes.ParentBox) or box.is_running():\n        return box\n\n    # Do recursion.\n    children = [flex_boxes(child) for child in box.children]\n    box.children = flex_children(box, children)\n    return box\n\n\ndef flex_children(box, children):\n    if isinstance(box, boxes.FlexContainerBox):\n        flex_children = []\n        for child in children:\n            child.is_floated = lambda: False\n            if child.is_in_normal_flow():\n                child.is_flex_item = True\n            if isinstance(child, boxes.TextBox) and not child.text.strip(' '):\n                # TODO: ignore texts only containing \"characters that can be\n                # affected by the white-space property\"\n                # https://www.w3.org/TR/css-flexbox-1/#flex-items\n                continue\n            flex_children.append(blockify(child, 'flex'))\n        return flex_children\n    else:\n        return children\n\n\ndef grid_boxes(box):\n    \"\"\"Remove and add boxes according to the grid model.\n\n    Take and return a ``Box`` object.\n\n    See https://drafts.csswg.org/css-grid-2/#grid-item\n\n    \"\"\"\n    if not isinstance(box, boxes.ParentBox) or box.is_running():\n        return box\n\n    # Do recursion.\n    children = [grid_boxes(child) for child in box.children]\n    box.children = grid_children(box, children)\n    return box\n\n\ndef grid_children(box, children):\n    if isinstance(box, boxes.GridContainerBox):\n        grid_children = []\n        for child in children:\n            if child.is_in_normal_flow():\n                child.is_grid_item = True\n            if isinstance(child, boxes.TextBox) and not child.text.strip(' '):\n                # TODO: ignore texts only containing \"characters that can be\n                # affected by the white-space property\"\n                # https://drafts.csswg.org/css-grid-2/#grid-item\n                continue\n            grid_children.append(blockify(child, 'grid'))\n        return grid_children\n    else:\n        return children\n\n\ndef process_whitespace(box, following_collapsible_space=False):\n    \"\"\"First part of \"The 'white-space' processing model\".\n\n    See https://www.w3.org/TR/CSS21/text.html#white-space-model\n    https://drafts.csswg.org/css-text-3/#white-space-rules\n\n    \"\"\"\n    if isinstance(box, boxes.TextBox):\n        text = box.text\n        if not text:\n            return following_collapsible_space\n\n        # Normalize line feeds\n        text = LINE_FEED_RE.sub('\\n', text)\n\n        new_line_collapse = box.style['white_space'] in ('normal', 'nowrap')\n        space_collapse = box.style['white_space'] in (\n            'normal', 'nowrap', 'pre-line')\n\n        if space_collapse:\n            # \\r characters were removed/converted earlier\n            text = TAB_RE.sub('\\n', text)\n\n        if new_line_collapse:\n            # TODO: this should be language-specific\n            # Could also replace with a zero width space character (U+200B),\n            # or no character\n            # CSS3: https://www.w3.org/TR/css-text-3/#overflow-wrap\n            text = text.replace('\\n', ' ')\n\n        if space_collapse:\n            previous_text = text = SPACE_RE.sub(' ', text)\n            if following_collapsible_space and text.startswith(' '):\n                text = text[1:]\n                box.leading_collapsible_space = True\n            following_collapsible_space = previous_text.endswith(' ')\n        else:\n            following_collapsible_space = False\n\n        box.text = text\n\n    else:\n        for child in box.children:\n            child_collapsible_space = process_whitespace(\n                child, following_collapsible_space)\n            if isinstance(child, (boxes.TextBox, boxes.InlineBox)):\n                following_collapsible_space = child_collapsible_space\n            elif child.is_in_normal_flow():\n                following_collapsible_space = False\n\n    return following_collapsible_space\n\n\ndef process_text_transform(box):\n    # Rules defined in\n    # https://www.unicode.org/versions/latest/core-spec/chapter-3/#G33992\n    # https://www.unicode.org/Public/UCD/latest/ucd/SpecialCasing.txt\n    # https://w3c.github.io/i18n-tests/results/text-transform\n    # Common transformations should be handled by common algorithm in Python, special\n    # casing and tailoring shoud be done here when it depends on the language and not on\n    # only on the glyphs.\n    if isinstance(box, boxes.TextBox):\n        text_transform = box.style['text_transform']\n        lang_code = (box.style['lang'] or '').split('-')[0].lower()\n        if text_transform != 'none':\n            box.text = {\n                'uppercase': uppercase,\n                'lowercase': lowercase,\n                'capitalize': capitalize,\n                'full-width': lambda text, lang_code: text.translate(ASCII_TO_WIDE),\n            }[text_transform](box.text, lang_code)\n        if box.style['hyphens'] == 'none':\n            box.text = box.text.replace('\\u00AD', '')  # U+00AD is soft hyphen\n\n    elif not box.is_running():\n        for child in box.children:\n            process_text_transform(child)\n\ndef uppercase(text, lang_code):\n    mapper = {}\n\n    if lang_code == 'el':\n        # https://w3c.github.io/i18n-tests/css-text/text-transform/\n        #   text-transform-tailoring-003.html\n        # https://en.wikiversity.org/wiki/Greek_Language/Diphthongs\n        mapper = {\n            'άι': 'ΑΪ',\n            'άυ': 'ΑΫ',\n            'όι': 'ΟΪ',\n            'όυ': 'ΟΫ',\n            'έυ': 'ΗΫ',\n        }\n    elif lang_code in ('tr', 'az'):\n        # https://github.com/unicode-org/cldr/blob/main/common/transforms/tr-Upper.xml\n        mapper = {\n            'i': 'İ',\n        }\n\n    for key, value in mapper.items():\n        text = text.replace(key, value)\n\n    if lang_code == 'el':\n        # Remove diacritics in Greek.\n        # https://github.com/unicode-org/cldr/blob/main/common/transforms/el-Upper.xml\n        # TODO: we should keep tonos on disjunctive eta.\n        # https://w3c.github.io/i18n-tests/css-text/text-transform/\n        #   text-transform-tailoring-005.html\n        text = unicodedata.normalize('NFD', text)\n        for char in '\\u0313\\u0314\\u0301\\u0300\\u0306\\u0342\\u0304\\u0345':\n            text = text.replace(char, '')\n        text = unicodedata.normalize('NFC', text)\n\n    return text.upper()\n\n\ndef lowercase(text, lang_code):\n    mapper = {}\n\n    if lang_code in ('tr', 'az'):\n        # https://github.com/unicode-org/cldr/blob/main/common/transforms/tr-Lower.xml\n        mapper = {\n            'I': 'ı',\n            'İ': 'i',\n        }\n    elif lang_code == 'lt':\n        # https://github.com/unicode-org/cldr/blob/main/common/transforms/lt-Lower.xml\n        mapper = {\n            'Ì': 'i̇̀',\n            'Í': 'i̇́',\n            'Ĩ': 'i̇̃',\n        }\n\n    for key, value in mapper.items():\n        text = text.replace(key, value)\n\n    return text.lower()\n\n\ndef capitalize(text, lang_code):\n    \"\"\"Capitalize words according to CSS’s \"text-transform: capitalize\".\"\"\"\n    letter_found = False\n    skip_next_letter = False\n    output = ''\n    for i, letter in enumerate(text):\n        if skip_next_letter:\n            skip_next_letter = False\n            continue\n        category = unicodedata.category(letter)[0]\n        if not letter_found and category in ('L', 'N'):\n            letter_found = True\n            if lang_code == 'nl' and text[i:i+2] == 'ij':\n                skip_next_letter = True\n                letter = 'IJ'\n            elif lang_code in ('tr', 'az'):\n                letter = uppercase(letter, lang_code)\n            else:\n                letter = letter.upper()\n        elif category == 'Z':\n            letter_found = False\n        output += letter\n    return output\n\n\ndef inline_in_block(box):\n    \"\"\"Build the structure of lines inside blocks and return a new box tree.\n\n    Consecutive inline-level boxes in a block container box are wrapped into a\n    line box, itself wrapped into an anonymous block box.\n\n    This line box will be broken into multiple lines later.\n\n    This is the first case in\n    https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level\n\n    Eg.::\n\n        BlockBox[\n            TextBox['Some '],\n            InlineBox[TextBox['text']],\n            BlockBox[\n                TextBox['More text'],\n            ]\n        ]\n\n    is turned into::\n\n        BlockBox[\n            AnonymousBlockBox[\n                LineBox[\n                    TextBox['Some '],\n                    InlineBox[TextBox['text']],\n                ]\n            ]\n            BlockBox[\n                LineBox[\n                    TextBox['More text'],\n                ]\n            ]\n        ]\n\n    \"\"\"\n    if not box.children or box.is_running():\n        return box\n\n    box_children = list(box.children)\n\n    if box_children and box.leading_collapsible_space is False:\n        box.leading_collapsible_space = (\n            box_children[0].leading_collapsible_space)\n\n    children = []\n    trailing_collapsible_space = False\n    for child in box_children:\n        # Keep track of removed collapsing spaces for wrap opportunities, and\n        # remove empty text boxes.\n        # (They may have been emptied by process_whitespace().)\n\n        if trailing_collapsible_space:\n            child.leading_collapsible_space = True\n\n        if isinstance(child, boxes.TextBox) and not child.text:\n            trailing_collapsible_space = child.leading_collapsible_space\n        else:\n            trailing_collapsible_space = False\n            children.append(inline_in_block(child))\n\n    if box.trailing_collapsible_space is False:\n        box.trailing_collapsible_space = trailing_collapsible_space\n\n    if not isinstance(box, boxes.BlockContainerBox):\n        box.children = children\n        return box\n\n    new_line_children = []\n    new_children = []\n\n    for child_box in children:\n        assert not isinstance(child_box, boxes.LineBox)\n        if new_line_children and child_box.is_absolutely_positioned():\n            new_line_children.append(child_box)\n        elif isinstance(child_box, boxes.InlineLevelBox) or (\n                new_line_children and not child_box.is_in_normal_flow()):\n            # Do not append white space at the start of a line:\n            # It would be removed during layout.\n            if new_line_children or not (\n                    isinstance(child_box, boxes.TextBox) and\n                    # Sequence of white-space was collapsed to a single\n                    # space by process_whitespace().\n                    child_box.text == ' ' and\n                    child_box.style['white_space'] in (\n                        'normal', 'nowrap', 'pre-line')):\n                new_line_children.append(child_box)\n        else:\n            if new_line_children:\n                # Inlines are consecutive no more: add this line box\n                # and create a new one.\n                line_box = boxes.LineBox.anonymous_from(box, new_line_children)\n                anonymous = boxes.BlockBox.anonymous_from(box, [line_box])\n                new_children.append(anonymous)\n                new_line_children = []\n            new_children.append(child_box)\n    if new_line_children:\n        # There were inlines at the end\n        line_box = boxes.LineBox.anonymous_from(box, new_line_children)\n        if new_children:\n            anonymous = boxes.BlockBox.anonymous_from(box, [line_box])\n            new_children.append(anonymous)\n        else:\n            # Only inline-level children: one line box\n            new_children.append(line_box)\n\n    box.children = new_children\n    return box\n\n\ndef block_in_inline(box):\n    \"\"\"Build the structure of blocks inside lines.\n\n    Inline boxes containing block-level boxes will be broken in two\n    boxes on each side on consecutive block-level boxes, each side wrapped\n    in an anonymous block-level box.\n\n    This is the second case in\n    https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level\n\n    Eg. if this is given::\n\n        BlockBox[\n            LineBox[\n                InlineBox[\n                    TextBox['Hello.'],\n                ],\n                InlineBox[\n                    TextBox['Some '],\n                    InlineBox[\n                        TextBox['text']\n                        BlockBox[LineBox[TextBox['More text']]],\n                        BlockBox[LineBox[TextBox['More text again']]],\n                    ],\n                    BlockBox[LineBox[TextBox['And again.']]],\n                ]\n            ]\n        ]\n\n    this is returned::\n\n        BlockBox[\n            AnonymousBlockBox[\n                LineBox[\n                    InlineBox[\n                        TextBox['Hello.'],\n                    ],\n                    InlineBox[\n                        TextBox['Some '],\n                        InlineBox[TextBox['text']],\n                    ]\n                ]\n            ],\n            BlockBox[LineBox[TextBox['More text']]],\n            BlockBox[LineBox[TextBox['More text again']]],\n            AnonymousBlockBox[\n                LineBox[\n                    InlineBox[\n                    ]\n                ]\n            ],\n            BlockBox[LineBox[TextBox['And again.']]],\n            AnonymousBlockBox[\n                LineBox[\n                    InlineBox[\n                    ]\n                ]\n            ],\n        ]\n\n    \"\"\"\n    if not box.children or box.is_running():\n        return box\n\n    new_children = []\n    changed = False\n\n    for child in box.children:\n        if isinstance(child, boxes.LineBox):\n            assert len(box.children) == 1, (\n                'Line boxes should have no '\n                'siblings at this stage, got %r.' % box.children)\n            stack = None\n            while True:\n                new_line, block, stack = _inner_block_in_inline(\n                    child, skip_stack=stack)\n                if block is None:\n                    break\n                anon = boxes.BlockBox.anonymous_from(box, [new_line])\n                new_children.append(anon)\n                new_children.append(block_in_inline(block))\n                # Loop with the same child and the new stack.\n            if new_children:\n                # Some children were already added, this became a block\n                # context.\n                new_child = boxes.BlockBox.anonymous_from(box, [new_line])\n            else:\n                # Keep the single line box as-is, without anonymous blocks.\n                new_child = new_line\n        else:\n            # Not in an inline formatting context.\n            new_child = block_in_inline(child)\n\n        if new_child is not child:\n            changed = True\n        new_children.append(new_child)\n\n    if changed:\n        box.children = new_children\n    return box\n\n\ndef _inner_block_in_inline(box, skip_stack=None):\n    \"\"\"Find a block-level box in an inline formatting context.\n\n    If one is found, return ``(new_box, block_level_box, resume_at)``.\n    ``new_box`` contains all of ``box`` content before the block-level box.\n    ``resume_at`` can be passed as ``skip_stack`` in a new call to\n    this function to resume the search just after the block-level box.\n\n    If no block-level box is found after the position marked by\n    ``skip_stack``, return ``(new_box, None, None)``\n\n    \"\"\"\n    new_children = []\n    block_level_box = None\n    resume_at = None\n    changed = False\n\n    is_start = skip_stack is None\n    if is_start:\n        skip = 0\n    else:\n        (skip, skip_stack), = skip_stack.items()\n\n    for i, child in enumerate(box.children[skip:]):\n        index = i + skip\n        if (isinstance(child, boxes.BlockLevelBox) and\n                child.is_in_normal_flow()):\n            assert skip_stack is None  # Should not skip here\n            block_level_box = child\n            index += 1  # Resume *after* the block\n        else:\n            if isinstance(child, boxes.InlineBox):\n                recursion = _inner_block_in_inline(child, skip_stack)\n                skip_stack = None\n                new_child, block_level_box, resume_at = recursion\n            else:\n                assert skip_stack is None  # Should not skip here\n                new_child = block_in_inline(child)\n                # block_level_box is still None.\n            if new_child is not child:\n                changed = True\n            new_children.append(new_child)\n        if block_level_box is not None:\n            resume_at = {index: resume_at}\n            box = box.copy_with_children(new_children)\n            break\n    else:\n        if changed or skip:\n            box = box.copy_with_children(new_children)\n\n    return box, block_level_box, resume_at\n\n\ndef set_viewport_overflow(root_box):\n    \"\"\"\n    Set a ``viewport_overflow`` attribute on the box for the root element.\n\n    Like backgrounds, ``overflow`` on the root element must be propagated\n    to the viewport.\n\n    See https://www.w3.org/TR/CSS21/visufx.html#overflow\n    \"\"\"\n    chosen_box = root_box\n    if (root_box.element_tag.lower() == 'html' and\n            root_box.style['overflow'] == 'visible'):\n        for child in root_box.children:\n            if child.element_tag.lower() == 'body':\n                chosen_box = child\n                break\n\n    root_box.viewport_overflow = chosen_box.style['overflow']\n    chosen_box.style['overflow'] = 'visible'\n    return root_box\n\n\ndef box_text(box):\n    # Stripping may not be the \"right\" way, but it seems to be what users usually want\n    # in this case. The specification asks for the \"text content\", probably as defined\n    # in DOM.\n    box = box.deepcopy()\n    process_whitespace(box)\n    if isinstance(box, boxes.TextBox):\n        return box.text.strip()\n    elif isinstance(box, boxes.ParentBox):\n        return ''.join(\n            child.text for child in box.descendants()\n            if not child.element_tag.endswith('::before') and\n            not child.element_tag.endswith('::after') and\n            not child.element_tag.endswith('::marker') and\n            isinstance(child, boxes.TextBox)).strip()\n    return ''\n\n\ndef extract_text(text_part, box):\n    if text_part in ('text', 'content'):\n        return box_text(box)\n    elif text_part in ('before', 'after'):\n        if isinstance(box, boxes.ParentBox):\n            return ''.join(\n                box_text(child) for child in box.descendants()\n                if child.element_tag.endswith(f'::{text_part}') and\n                not isinstance(child, boxes.ParentBox))\n        return ''\n    elif text_part == 'first-letter':\n        # TODO: use the same code as in inlines.first_letter_to_box\n        character_found = False\n        first_letter = ''\n        text = box_text(box)\n        for letter in text:\n            category = unicodedata.category(letter)\n            if category not in ('Ps', 'Pe', 'Pi', 'Pf', 'Po'):\n                if character_found:\n                    break\n                character_found = True\n            first_letter += letter\n        return first_letter\n"
  },
  {
    "path": "weasyprint/html.py",
    "content": "\"\"\"Specific handling for some HTML elements, especially replaced elements.\n\nReplaced elements (eg. <img> elements) are rendered externally and behave as an\natomic opaque box in CSS. In general, they may or may not have intrinsic\ndimensions. But the only replaced elements currently supported in WeasyPrint\nare images with intrinsic dimensions.\n\n\"\"\"\n\nimport re\nfrom importlib.resources import files\n\nfrom . import CSS, Attachment, css\nfrom .css import get_child_text\nfrom .css.counters import CounterStyle\nfrom .formatting_structure import boxes\nfrom .images import SVGImage\nfrom .logger import LOGGER\nfrom .urls import get_url_attribute\n\nHTML5_UA_COUNTER_STYLE = CounterStyle()\nHTML5_UA = (files(css) / 'html5_ua.css').read_text('utf-8')\nHTML5_UA_FORM = (files(css) / 'html5_ua_form.css').read_text('utf-8')\nHTML5_PH = (files(css) / 'html5_ph.css').read_text('utf-8')\nHTML5_UA_STYLESHEET = CSS(\n    string=HTML5_UA, counter_style=HTML5_UA_COUNTER_STYLE)\nHTML5_UA_FORM_STYLESHEET = CSS(\n    string=HTML5_UA_FORM, counter_style=HTML5_UA_COUNTER_STYLE)\nHTML5_PH_STYLESHEET = CSS(string=HTML5_PH)\n\n# https://html.spec.whatwg.org/multipage/#space-character\nHTML_WHITESPACE = ' \\t\\n\\f\\r'\nHTML_SPACE_SEPARATED_TOKENS_RE = re.compile(f'[^{HTML_WHITESPACE}]+')\n\n\ndef ascii_lower(string):\n    r\"\"\"Transform (only) ASCII letters to lower case: A-Z is mapped to a-z.\n\n    This is used for `ASCII case-insensitive\n    <https://whatwg.org/C#ascii-case-insensitive>`_ matching.\n\n    This is different from the :meth:`str.lower` method of Unicode strings\n    which also affect non-ASCII characters,\n    sometimes mapping them into the ASCII range:\n\n    >>> keyword = 'Bac\\N{KELVIN SIGN}ground'\n    >>> assert keyword.lower() == 'background'\n    >>> assert ascii_lower(keyword) != keyword.lower()\n    >>> assert ascii_lower(keyword) == 'bac\\N{KELVIN SIGN}ground'\n\n    \"\"\"\n    # This turns out to be faster than unicode.translate()\n    return string.encode().lower().decode()\n\n\ndef element_has_link_type(element, link_type):\n    \"\"\"Return whether element has a ``rel`` attribute with given link type.\"\"\"\n    tokens = HTML_SPACE_SEPARATED_TOKENS_RE.findall(element.get('rel', ''))\n    return any(ascii_lower(token) == link_type for token in tokens)\n\n\n# Maps HTML tag names to function taking an HTML element and returning a Box.\nHTML_HANDLERS = {}\n\n\ndef handle_element(element, box, get_image_from_uri, base_url):\n    \"\"\"Handle HTML elements that need special care.\n\n    :returns: a (possibly empty) list of boxes.\n    \"\"\"\n    if box.element_tag in HTML_HANDLERS:\n        return HTML_HANDLERS[element.tag](\n            element, box, get_image_from_uri, base_url)\n    else:\n        return [box]\n\n\ndef handler(tag):\n    \"\"\"Return a decorator registering a function handling ``tag`` elements.\"\"\"\n    def decorator(function):\n        \"\"\"Decorator registering a function handling ``tag`` elements.\"\"\"\n        HTML_HANDLERS[tag] = function\n        return function\n    return decorator\n\n\ndef make_replaced_box(element, box, image):\n    \"\"\"Wrap an image in a replaced box.\n\n    That box is either block-level or inline-level, depending on what the\n    element should be.\n\n    \"\"\"\n    type_ = (\n        boxes.BlockReplacedBox if 'block' in box.style['display']\n        else boxes.InlineReplacedBox)\n    new_box = type_(element.tag, box.style, element, image)\n    # TODO: check other attributes that need to be copied\n    # TODO: find another solution\n    new_box.string_set = box.string_set\n    new_box.bookmark_label = box.bookmark_label\n    return new_box\n\n\n@handler('img')\ndef handle_img(element, box, get_image_from_uri, base_url):\n    \"\"\"Handle ``<img>`` elements.\n\n    Return either an image or the alt-text.\n\n    See: https://www.w3.org/TR/html5/embedded-content-1.html#the-img-element\n\n    \"\"\"\n    src = get_url_attribute(element, 'src', base_url)\n    alt = element.get('alt')\n    if src:\n        image = get_image_from_uri(\n            url=src, orientation=box.style['image_orientation'])\n        if image is not None:\n            return [make_replaced_box(element, box, image)]\n        else:\n            # Invalid image, use the alt-text.\n            if alt:\n                box.children = [boxes.TextBox.anonymous_from(box, alt)]\n                return [box]\n            elif alt == '':\n                # The element represents nothing\n                return []\n            else:\n                assert alt is None\n                # TODO: find some indicator that an image is missing.\n                # For now, just remove the image.\n                return []\n    else:\n        if alt:\n            box.children = [boxes.TextBox.anonymous_from(box, alt)]\n            return [box]\n        else:\n            return []\n\n\n@handler('embed')\ndef handle_embed(element, box, get_image_from_uri, base_url):\n    \"\"\"Handle ``<embed>`` elements, return either an image or nothing.\n\n    See: https://www.w3.org/TR/html5/embedded-content-0.html#the-embed-element\n\n    \"\"\"\n    src = get_url_attribute(element, 'src', base_url)\n    type_ = element.get('type', '').strip()\n    if src:\n        image = get_image_from_uri(\n            url=src, forced_mime_type=type_,\n            orientation=box.style['image_orientation'])\n        if image is not None:\n            return [make_replaced_box(element, box, image)]\n    # No fallback.\n    return []\n\n\n@handler('object')\ndef handle_object(element, box, get_image_from_uri, base_url):\n    \"\"\"Handle ``<object>`` elements, return either an image or the fallback.\n\n    See: https://www.w3.org/TR/html5/embedded-content-0.html#the-object-element\n\n    \"\"\"\n    data = get_url_attribute(element, 'data', base_url)\n    type_ = element.get('type', '').strip()\n    if data:\n        image = get_image_from_uri(\n            url=data, forced_mime_type=type_,\n            orientation=box.style['image_orientation'])\n        if image is not None:\n            return [make_replaced_box(element, box, image)]\n    # The element’s children are the fallback.\n    return [box]\n\n\n@handler('colgroup')\ndef handle_colgroup(element, box, _get_image_from_uri, _base_url):\n    \"\"\"Handle the ``span`` attribute.\"\"\"\n    if isinstance(box, boxes.TableColumnGroupBox):\n        if not any(child.tag == 'col' for child in element):\n            box.children = [\n                boxes.TableColumnBox.anonymous_from(box, [])\n                for _ in range(box.span)]\n    return [box]\n\n\n@handler('col')\ndef handle_col(element, box, _get_image_from_uri, _base_url):\n    \"\"\"Handle the ``span`` attribute.\"\"\"\n    if isinstance(box, boxes.TableColumnBox) and box.span > 1:\n        # Generate multiple boxes\n        # https://lists.w3.org/Archives/Public/www-style/2011Nov/0293.html\n        return [box.copy() for _i in range(box.span)]\n    return [box]\n\n\n@handler('{http://www.w3.org/2000/svg}svg')\ndef handle_svg(element, box, get_image_from_uri, base_url):\n    \"\"\"Handle ``<svg>`` elements.\n\n    Return either an image or the fallback content.\n\n    \"\"\"\n    # TODO: handle href base for inline svg tags\n    url_fetcher = get_image_from_uri.keywords['url_fetcher']\n    context = get_image_from_uri.keywords['context']\n    try:\n        image = SVGImage(element, base_url, url_fetcher, context)\n    except Exception as exception:  # pragma: no cover\n        LOGGER.error('Failed to load inline SVG: %s', exception)\n        LOGGER.debug('Error while loading inline SVG:', exc_info=exception)\n        return []\n    else:\n        return [make_replaced_box(element, box, image)]\n\n\ndef get_html_metadata(html):\n    \"\"\"Get metadata dictionary out of HTML object.\n\n    Relevant specs:\n\n    https://www.whatwg.org/html#the-title-element\n    https://www.whatwg.org/html#standard-metadata-names\n    https://wiki.whatwg.org/wiki/MetaExtensions\n    https://microformats.org/wiki/existing-rel-values#HTML5_link_type_extensions\n\n    \"\"\"\n    title = None\n    description = None\n    generator = None\n    keywords = []\n    authors = []\n    created = None\n    modified = None\n    attachments = []\n    custom = {}\n    lang = html.etree_element.attrib.get('lang', None)\n    for element in html.wrapper_element.query_all('title', 'meta', 'link'):\n        element = element.etree_element\n        if element.tag == 'title' and title is None:\n            title = get_child_text(element)\n        elif element.tag == 'meta':\n            name = ascii_lower(element.get('name', ''))\n            content = element.get('content', '')\n            if name == 'keywords':\n                for keyword in map(strip_whitespace, content.split(',')):\n                    if keyword not in keywords:\n                        keywords.append(keyword)\n            elif name == 'author':\n                authors.append(content)\n            elif name == 'description':\n                if description is None:\n                    description = content\n            elif name == 'generator':\n                if generator is None:\n                    generator = content\n            elif name == 'dcterms.created':\n                if created is None:\n                    created = parse_w3c_date(name, content)\n            elif name == 'dcterms.modified':\n                if modified is None:\n                    modified = parse_w3c_date(name, content)\n            elif name and name not in custom:\n                custom[name] = content\n        elif element.tag == 'link' and element_has_link_type(\n                element, 'attachment'):\n            url = get_url_attribute(element, 'href', html.base_url)\n            attachment_title = element.get('title', None)\n            if url is None:\n                LOGGER.error('Missing href in <link rel=\"attachment\">')\n            else:\n                attachment = Attachment(\n                    url=url, description=attachment_title,\n                    url_fetcher=html.url_fetcher)\n                attachments.append(attachment)\n    return {\n        'title': title,\n        'description': description,\n        'generator': generator,\n        'keywords': keywords,\n        'authors': authors,\n        'created': created,\n        'modified': modified,\n        'attachments': attachments,\n        'lang': lang,\n        'custom': custom,\n    }\n\n\ndef strip_whitespace(string):\n    \"\"\"Use the HTML definition of \"space character\",\n    not all Unicode Whitespace.\n\n    https://www.whatwg.org/html#strip-leading-and-trailing-whitespace\n    https://www.whatwg.org/html#space-character\n\n    \"\"\"\n    return string.strip(HTML_WHITESPACE)\n\n\n# YYYY (eg 1997)\n# YYYY-MM (eg 1997-07)\n# YYYY-MM-DD (eg 1997-07-16)\n# YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)\n# YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)\n# YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)\n\nW3C_DATE_RE = re.compile('''\n    ^\n    [ \\t\\n\\f\\r]*\n    (?P<year>\\\\d\\\\d\\\\d\\\\d)\n    (?:\n        -(?P<month>0\\\\d|1[012])\n        (?:\n            -(?P<day>[012]\\\\d|3[01])\n            (?:\n                T(?P<hour>[01]\\\\d|2[0-3])\n                :(?P<minute>[0-5]\\\\d)\n                (?:\n                    :(?P<second>[0-5]\\\\d)\n                    (?:\\\\.\\\\d+)?  # Second fraction, ignored\n                )?\n                (?:\n                    Z |  # UTC\n                    (?P<tz_hour>[+-](?:[01]\\\\d|2[0-3]))\n                    :(?P<tz_minute>[0-5]\\\\d)\n                )\n            )?\n        )?\n    )?\n    [ \\t\\n\\f\\r]*\n    $\n''', re.VERBOSE)\n\n\ndef parse_w3c_date(meta_name, string):\n    \"\"\"Parse datetimes as defined by the W3C.\n\n    See https://www.w3.org/TR/NOTE-datetime\n\n    \"\"\"\n    if W3C_DATE_RE.match(string):\n        return string\n    else:\n        LOGGER.warning(\n            'Invalid date in <meta name=\"%s\"> %r', meta_name, string)\n"
  },
  {
    "path": "weasyprint/images.py",
    "content": "\"\"\"Fetch and decode images in various formats.\"\"\"\n\nimport io\nimport math\nimport struct\nfrom hashlib import md5\nfrom io import BytesIO\nfrom itertools import cycle\nfrom pathlib import Path\nfrom xml.etree import ElementTree\n\nimport pydyf\nfrom PIL import Image, ImageFile, ImageOps\nfrom tinycss2.color5 import parse_color\n\nfrom . import DEFAULT_OPTIONS\nfrom .layout.percent import percentage\nfrom .logger import LOGGER\nfrom .svg import SVG\nfrom .urls import URLFetchingError, fetch\n\n# Don’t crash when converting truncated images\nImageFile.LOAD_TRUNCATED_IMAGES = True\n\n\nclass ImageLoadingError(ValueError):\n    \"\"\"An error occured when loading an image.\n\n    The image data is probably corrupted or in an invalid format.\n\n    \"\"\"\n\n\nclass RasterImage:\n    def __init__(self, pillow_image, image_id, image_data, filename=None,\n                 cache=None, orientation='none', options=DEFAULT_OPTIONS):\n        # Transpose image\n        original_pillow_image = pillow_image\n        pillow_image = rotate_pillow_image(pillow_image, orientation)\n        if original_pillow_image is not pillow_image:\n            # Keep image format as it is discarded by transposition\n            pillow_image.format = original_pillow_image.format\n            # Discard original data, as the image has been transformed\n            image_data = filename = None\n\n        self.id = image_id\n        self._cache = {} if cache is None else cache\n        self._jpeg_quality = jpeg_quality = options['jpeg_quality']\n        self._dpi = options['dpi']\n\n        if 'transparency' in pillow_image.info:\n            pillow_image = pillow_image.convert('RGBA')\n        elif pillow_image.mode in ('1', 'P', 'I'):\n            pillow_image = pillow_image.convert('RGB')\n\n        self.mode = pillow_image.mode\n        self.width = pillow_image.width\n        self.height = pillow_image.height\n        self.ratio = (self.width / self.height) if self.height != 0 else math.inf\n        self.optimize = optimize = options['optimize_images']\n\n        # The presence of the APP14 segment indicates an Adobe image with\n        # inverted CMYK data. Specify a Decode Array to invert it again back to\n        # normal. See PR #2179.\n        app14 = getattr(original_pillow_image, 'app', {}).get('APP14')\n        self.invert_colors = self.mode == 'CMYK' and app14 is not None\n\n        if pillow_image.format in ('JPEG', 'MPO'):\n            self.format = 'JPEG'\n            if image_data is None or optimize or jpeg_quality is not None:\n                image_file = io.BytesIO()\n                options = {'format': 'JPEG', 'optimize': optimize}\n                if self._jpeg_quality is not None:\n                    options['quality'] = self._jpeg_quality\n                pillow_image.save(image_file, **options)\n                image_data = image_file.getvalue()\n                filename = None\n        else:\n            self.format = 'PNG'\n            if image_data is None or optimize or pillow_image.format != 'PNG':\n                image_file = io.BytesIO()\n                pillow_image.save(image_file, format='PNG', optimize=optimize)\n                image_data = image_file.getvalue()\n                filename = None\n        self.image_data = self.cache_image_data(image_data, filename)\n\n    def get_intrinsic_size(self, resolution, font_size):\n        return self.width / resolution, self.height / resolution, self.ratio\n\n    def draw(self, stream, concrete_width, concrete_height, style):\n        if self.width <= 0 or self.height <= 0:\n            return\n\n        image_rendering = style['image_rendering']\n        interpolate = image_rendering == 'auto'\n        ratio = 1\n        if self._dpi:\n            pt_to_in = 4 / 3 / 96\n            width_inches = abs(concrete_width * stream.ctm[0][0] * pt_to_in)\n            height_inches = abs(concrete_height * stream.ctm[1][1] * pt_to_in)\n            dpi = max(self.width / width_inches, self.height / height_inches)\n            if dpi > self._dpi:\n                ratio = self._dpi / dpi\n        image_name = stream.add_image(self, interpolate, ratio)\n\n        stream.transform(\n            concrete_width, 0, 0, -concrete_height, 0, concrete_height)\n        stream.draw_x_object(image_name)\n\n    def cache_image_data(self, data, filename=None, slot='source'):\n        if filename:\n            return LazyLocalImage(filename)\n        else:\n            key = f'{self.id}-{slot}-{self._dpi or \"\"}'\n            return LazyImage(self._cache, key, data)\n\n    def get_x_object(self, interpolate, dpi_ratio):\n        if dpi_ratio == 1:\n            width, height = self.width, self.height\n        else:\n            thumbnail = Image.open(io.BytesIO(self.image_data.data))\n            width = max(1, round(self.width * dpi_ratio))\n            height = max(1, round(self.height * dpi_ratio))\n            thumbnail.thumbnail((width, height))\n            image_file = io.BytesIO()\n            thumbnail.save(\n                image_file, format=thumbnail.format, optimize=self.optimize)\n            width, height = thumbnail.width, thumbnail.height\n            self.image_data = self.cache_image_data(image_file.getvalue())\n\n        if self.mode in ('RGB', 'RGBA'):\n            color_space = '/DeviceRGB'\n        elif self.mode in ('L', 'LA'):\n            color_space = '/DeviceGray'\n        elif self.mode == 'CMYK':\n            color_space = '/DeviceCMYK'\n        else:\n            LOGGER.warning('Unknown image mode: %s', self.mode)\n            color_space = '/DeviceRGB'\n\n        extra = pydyf.Dictionary({\n            'Type': '/XObject',\n            'Subtype': '/Image',\n            'Width': width,\n            'Height': height,\n            'ColorSpace': color_space,\n            'BitsPerComponent': 8,\n            'Interpolate': 'true' if interpolate else 'false',\n        })\n\n        if self.format == 'JPEG':\n            if self.invert_colors:\n                extra['Decode'] = pydyf.Array((1, 0) * 4)\n            extra['Filter'] = '/DCTDecode'\n            return pydyf.Stream([self.image_data], extra)\n\n        extra['Filter'] = '/FlateDecode'\n        extra['DecodeParms'] = pydyf.Dictionary({\n            # Predictor 15 specifies that we're providing PNG data,\n            # ostensibly using an \"optimum predictor\", but doesn't actually\n            # matter as long as the predictor value is 10+ according to the\n            # spec. (Other PNG predictor values assert that we're using\n            # specific predictors that we don't want to commit to, but\n            # \"optimum\" can vary.)\n            'Predictor': 15,\n            'Columns': width,\n        })\n        if self.mode in ('RGB', 'RGBA'):\n            # Defaults to 1.\n            extra['DecodeParms']['Colors'] = 3\n        if self.mode in ('RGBA', 'LA'):\n            # Remove alpha channel from image\n            pillow_image = Image.open(io.BytesIO(self.image_data.data))\n            alpha = pillow_image.getchannel('A')\n            pillow_image = pillow_image.convert(self.mode[:-1])\n            png_data = self._get_png_data(pillow_image)\n            # Save alpha channel as mask\n            alpha_data = self._get_png_data(alpha)\n            stream = self.cache_image_data(alpha_data, slot='streamalpha')\n            extra['SMask'] = pydyf.Stream([stream], extra={\n                'Filter': '/FlateDecode',\n                'Type': '/XObject',\n                'Subtype': '/Image',\n                'DecodeParms': pydyf.Dictionary({\n                    'Predictor': 15,\n                    'Columns': width,\n                }),\n                'Width': width,\n                'Height': height,\n                'ColorSpace': '/DeviceGray',\n                'BitsPerComponent': 8,\n                'Interpolate': 'true' if interpolate else 'false',\n            })\n        else:\n            png_data = self._get_png_data(\n                Image.open(io.BytesIO(self.image_data.data)))\n\n        return pydyf.Stream([self.cache_image_data(png_data, slot='stream')], extra)\n\n    @staticmethod\n    def _get_png_data(pillow_image):\n        image_file = BytesIO()\n        pillow_image.save(image_file, format='PNG')\n\n        # Read the PNG header, then discard it because we know it's a PNG. If\n        # this weren't just output from Pillow, we should actually check it.\n        image_file.seek(8)\n\n        png_data = []\n        raw_chunk_length = image_file.read(4)\n        # PNG files consist of a series of chunks.\n        while raw_chunk_length:\n            # Each chunk begins with its data length (four bytes, may be zero),\n            # then its type (four ASCII characters), then the data, then four\n            # bytes of a CRC.\n            chunk_length, = struct.unpack('!I', raw_chunk_length)\n            chunk_type = image_file.read(4)\n            if chunk_type == b'IDAT':\n                png_data.append(image_file.read(chunk_length))\n            else:\n                image_file.seek(chunk_length, io.SEEK_CUR)\n            # We aren't checking the CRC, we assume this is a valid PNG.\n            image_file.seek(4, io.SEEK_CUR)\n            raw_chunk_length = image_file.read(4)\n\n        return b''.join(png_data)\n\n\nclass LazyImage(pydyf.Object):\n    def __init__(self, cache, key, data):\n        super().__init__()\n        self._key = key\n        self._cache = cache\n        cache[key] = data\n\n    @property\n    def data(self):\n        return self._cache[self._key]\n\n\nclass LazyLocalImage(pydyf.Object):\n    def __init__(self, filename):\n        super().__init__()\n        self._filename = filename\n\n    @property\n    def data(self):\n        return Path(self._filename).read_bytes()\n\n\nclass SVGImage:\n    def __init__(self, tree, base_url, url_fetcher, context):\n        font_config = context.font_config if context else None\n        self._svg = SVG(tree, base_url, font_config, url_fetcher)\n        self._base_url = base_url\n        self._url_fetcher = url_fetcher\n        self._context = context\n\n    def get_intrinsic_size(self, image_resolution, font_size):\n        width, height = self._svg.get_intrinsic_size(font_size)\n        if None in (width, height):\n            viewbox = self._svg.get_viewbox()\n            if viewbox and viewbox[2] and viewbox[3]:\n                ratio = viewbox[2] / viewbox[3]\n                if width:\n                    height = width / ratio\n                elif height:\n                    width = height * ratio\n            else:\n                ratio = None\n        elif width and height:\n            ratio = width / height\n        else:\n            ratio = 1\n        return width, height, ratio\n\n    def draw(self, stream, concrete_width, concrete_height, _style):\n        try:\n            self._svg.draw(\n                stream, concrete_width, concrete_height, self._base_url,\n                self._context)\n        except BaseException as exception:\n            LOGGER.error('Failed to render SVG image %s', self._base_url)\n            LOGGER.debug('Error while rendering SVG image:', exc_info=exception)\n\n\ndef get_image_from_uri(cache, url_fetcher, options, url, forced_mime_type=None,\n                       context=None, orientation='from-image'):\n    \"\"\"Get an Image instance from an image URI.\"\"\"\n    if url in cache:\n        return cache[url]\n\n    try:\n        with fetch(url_fetcher, url) as response:\n            bytestring = response.read()\n            mime_type = forced_mime_type or response.content_type\n\n        image = None\n        svg_exceptions = []\n        # Try to rely on given mimetype for SVG\n        if mime_type == 'image/svg+xml':\n            try:\n                tree = ElementTree.fromstring(bytestring)\n                image = SVGImage(tree, url, url_fetcher, context)\n            except Exception as svg_exception:\n                svg_exceptions.append(svg_exception)\n        # Try pillow for raster images, or for failing SVG\n        if image is None:\n            try:\n                pillow_image = Image.open(BytesIO(bytestring))\n            except Exception as raster_exception:\n                if mime_type == 'image/svg+xml':\n                    # Tried SVGImage then Pillow for a SVG, abort\n                    raise ImageLoadingError from svg_exceptions[0]\n                try:\n                    # Last chance, try SVG\n                    tree = ElementTree.fromstring(bytestring)\n                    image = SVGImage(tree, url, url_fetcher, context)\n                except Exception:\n                    # Tried Pillow then SVGImage for a raster, abort\n                    raise ImageLoadingError from raster_exception\n            else:\n                # Store image id to enable cache in Stream.add_image\n                image_id = md5(url.encode(), usedforsecurity=False).hexdigest()\n                image = RasterImage(\n                    pillow_image, image_id, bytestring, response.path, cache,\n                    orientation, options)\n\n    except (URLFetchingError, ImageLoadingError) as exception:\n        LOGGER.error('Failed to load image at %r: %s', url, exception)\n        LOGGER.debug('Error while loading image:', exc_info=exception)\n        image = None\n\n    cache[url] = image\n    return image\n\n\ndef rotate_pillow_image(pillow_image, orientation):\n    \"\"\"Return a copy of a Pillow image with modified orientation.\n\n    If orientation is not changed, return the same image.\n\n    \"\"\"\n    image_format = pillow_image.format\n    if orientation == 'from-image':\n        if 'exif' in pillow_image.info:\n            pillow_image = ImageOps.exif_transpose(pillow_image)\n    elif orientation != 'none':\n        angle, flip = orientation\n        if angle > 0:\n            rotation = getattr(Image.Transpose, f'ROTATE_{angle}')\n            pillow_image = pillow_image.transpose(rotation)\n        if flip:\n            pillow_image = pillow_image.transpose(\n                Image.Transpose.FLIP_LEFT_RIGHT)\n\n    # Keep image format as it is discarded by transposition\n    pillow_image.format = image_format\n    return pillow_image\n\n\ndef process_color_stops(vector_length, positions, hints, style):\n    \"\"\"Give color stops positions and hints on the gradient vector.\n\n    ``vector_length`` is the distance between the starting point and ending\n    point of the vector gradient.\n\n    ``positions`` is a list of ``None``, or ``Dimension`` in px or %. 0 is the\n    starting point, 1 the ending point.\n\n    See https://drafts.csswg.org/css-images-3/#color-stop-syntax.\n\n    Return processed color stops, as a list of floats in px.\n\n    \"\"\"\n    # Resolve percentages.\n    positions = [percentage(position, style, vector_length) for position in positions]\n    hints = [percentage(hint, style, vector_length) / vector_length for hint in hints]\n\n    # First and last default to 100%.\n    if positions[0] is None:\n        positions[0] = 0\n    if positions[-1] is None:\n        positions[-1] = vector_length\n\n    # Make sure positions are increasing.\n    previous_pos = positions[0]\n    for i, position in enumerate(positions):\n        if position is not None:\n            if position < previous_pos:\n                positions[i] = previous_pos\n            else:\n                previous_pos = position\n\n    # Assign missing values.\n    previous_i = -1\n    for i, position in enumerate(positions):\n        if position is not None:\n            base = positions[previous_i]\n            increment = (position - base) / (i - previous_i)\n            for j in range(previous_i + 1, i):\n                positions[j] = base + j * increment\n            previous_i = i\n\n    # Calculate exponential value for PDF hints, avoid big numbers.\n    hints = [\n        0 if hint <= 0 else\n        2 ** 32 if hint >= 1 else\n        min(2 ** 32, math.log(0.5, hint)) for hint in hints]\n\n    return positions, hints\n\n\ndef normalize_stop_positions(positions):\n    \"\"\"Normalize stop positions between 0 and 1.\n\n    Return ``(first, last, positions)``.\n\n    first: original position of the first position.\n    last: original position of the last position.\n    positions: list of positions between 0 and 1.\n\n    \"\"\"\n    first, last = positions[0], positions[-1]\n    total_length = last - first\n    if total_length == 0:\n        positions = [0] * len(positions)\n    else:\n        positions = [(pos - first) / total_length for pos in positions]\n    return first, last, positions\n\n\ndef gradient_average_color(colors, positions):\n    \"\"\"\n    https://drafts.csswg.org/css-images-3/#gradient-average-color\n    \"\"\"\n    # TODO: handle color spaces.\n    nb_stops = len(positions)\n    assert nb_stops > 1\n    assert nb_stops == len(colors)\n    total_length = positions[-1] - positions[0]\n    if total_length == 0:\n        positions = list(range(nb_stops))\n        total_length = nb_stops - 1\n    premul_r = [r * a for r, g, b, a in colors]\n    premul_g = [g * a for r, g, b, a in colors]\n    premul_b = [b * a for r, g, b, a in colors]\n    alpha = [a for r, g, b, a in colors]\n    result_r = result_g = result_b = result_a = 0\n    total_weight = 2 * total_length\n    for i, position in enumerate(positions[1:], 1):\n        weight = (position - positions[i - 1]) / total_weight\n        for j in (i - 1, i):\n            result_r += premul_r[j] * weight\n            result_g += premul_g[j] * weight\n            result_b += premul_b[j] * weight\n            result_a += alpha[j] * weight\n    # Un-premultiply.\n    if result_a == 0:\n        return parse_color('transparent')\n    else:\n        return parse_color(\n            f'rgb({result_r / result_a * 255} {result_g / result_a * 255} '\n            f'{result_b / result_a * 255}/{ result_a })')\n\n\nclass Gradient:\n    def __init__(self, color_stops, repeating, color_hints):\n        assert color_stops\n        # List of (r, g, b, a)\n        self.colors = tuple(color for color, _ in color_stops)\n        # List of Dimensions\n        self.stop_positions = tuple(position for _, position in color_stops)\n        # List of Dimensions\n        self.color_hints = color_hints\n        # Boolean\n        self.repeating = repeating\n\n    def get_intrinsic_size(self, image_resolution, font_size):\n        return None, None, None\n\n    def draw(self, stream, concrete_width, concrete_height, style):\n        scale_y, type_, points, positions, colors, color_hints = self.layout(\n            concrete_width, concrete_height, style)\n\n        if type_ == 'solid':\n            stream.rectangle(0, 0, concrete_width, concrete_height)\n            stream.set_color(colors[0])\n            stream.fill()\n            return\n\n        alphas = [color[3] for color in colors]\n        alpha_couples = [\n            [alphas[i], alphas[i + 1], color_hints[i]]\n            for i in range(len(alphas) - 1)]\n        # TODO: handle other color spaces.\n        color_couples = [\n            [colors[i].to('srgb')[:3], colors[i + 1].to('srgb')[:3], color_hints[i]]\n            for i in range(len(colors) - 1)]\n\n        # Premultiply colors\n        for i, alpha in enumerate(alphas):\n            if alpha == 0:\n                if i > 0:\n                    color_couples[i - 1][1] = color_couples[i - 1][0]\n                if i < len(colors) - 1:\n                    color_couples[i][0] = color_couples[i][1]\n        for i, (a0, a1, hint) in enumerate(alpha_couples):\n            if 0 not in (a0, a1) and (a0, a1) != (1, 1):\n                color_couples[i][2] = a0 / a1\n\n        shading_type = 2 if type_ == 'linear' else 3\n        domain = (positions[0], positions[-1])\n        extend = not self.repeating\n        encode = (len(colors) - 1) * (0, 1)\n        bounds = positions[1:-1]\n        sub_functions = (\n            stream.create_interpolation_function((0, 1), c0, c1, hint)\n            for c0, c1, hint in color_couples)\n        function = stream.create_stitching_function(\n            domain, encode, bounds, sub_functions)\n        # TODO: handle other color spaces.\n        shading = stream.add_shading(\n            shading_type, 'RGB', domain, points, extend, function)\n        stream.transform(d=scale_y)\n\n        if any(alpha != 1 for alpha in alphas):\n            alpha_stream = stream.set_alpha_state(\n                0, 0, concrete_width, concrete_height)\n\n            shading_type = 2 if type_ == 'linear' else 3\n            sub_functions = (\n                stream.create_interpolation_function((0, 1), (c0,), (c1,), hint)\n                for c0, c1, hint in alpha_couples)\n            function = stream.create_stitching_function(\n                domain, encode, bounds, sub_functions)\n            alpha_shading = alpha_stream.add_shading(\n                shading_type, 'Gray', domain, points, extend, function)\n            alpha_stream.transform(d=scale_y)\n            alpha_stream.stream = [f'/{alpha_shading.id} sh']\n\n        stream.paint_shading(shading.id)\n\n    def layout(self, width, height, style):\n        \"\"\"Get layout information about the gradient.\n\n        width, height: Gradient box. Top-left is at coordinates (0, 0).\n        style: box computed style.\n\n        Returns (scale_y, type_, points, positions, colors).\n\n        scale_y: vertical scale of the gradient. float, used for ellipses\n                 radial gradients. 1 otherwise.\n        type_: gradient type.\n        points: coordinates of useful points, depending on type_:\n            'solid': None.\n            'linear': (x0, y0, x1, y1)\n                      coordinates of the starting and ending points.\n            'radial': (cx0, cy0, radius0, cx1, cy1, radius1)\n                      coordinates of the starting end ending circles\n        positions: positions of the color stops. list of floats in between 0\n                   and 1 (0 at the starting point, 1 at the ending point).\n        colors: list of (r, g, b, a).\n\n        \"\"\"\n        raise NotImplementedError\n\n\nclass LinearGradient(Gradient):\n    def __init__(self, color_stops, direction, repeating, color_hints):\n        super().__init__(color_stops, repeating, color_hints)\n        # ('corner', keyword) or ('angle', radians)\n        self.direction_type, self.direction = direction\n\n    def layout(self, width, height, style):\n        # Only one color, render the gradient as a solid color\n        if len(self.colors) == 1:\n            return 1, 'solid', None, [], [self.colors[0]], []\n\n        # Define the (dx, dy) unit vector giving the direction of the gradient.\n        # Positive dx: right, positive dy: down.\n        if self.direction_type == 'corner':\n            y, x = self.direction.split('_')\n            factor_x = -1 if x == 'left' else 1\n            factor_y = -1 if y == 'top' else 1\n            diagonal = math.hypot(width, height)\n            # Note the direction swap: dx based on height, dy based on width\n            # The gradient line is perpendicular to a diagonal.\n            dx = factor_x * height / diagonal\n            dy = factor_y * width / diagonal\n        else:\n            assert self.direction_type == 'angle'\n            angle = self.direction  # 0 upwards, then clockwise\n            dx = math.sin(angle)\n            dy = -math.cos(angle)\n\n        # Round dx and dy to avoid floating points errors caused by\n        # trigonometry and angle units conversions\n        dx, dy = round(dx, 9), round(dy, 9)\n\n        # Normalize colors positions\n        colors = list(self.colors)\n        vector_length = abs(width * dx) + abs(height * dy)\n        positions, hints = process_color_stops(\n            vector_length, self.stop_positions, self.color_hints, style)\n        if not self.repeating:\n            # Add explicit colors at boundaries if needed, because PDF doesn’t\n            # extend color stops that are not displayed\n            if positions[0] == positions[1]:\n                positions.insert(0, positions[0] - 1)\n                colors.insert(0, colors[0])\n                hints.insert(0, 1)\n            if positions[-2] == positions[-1]:\n                positions.append(positions[-1] + 1)\n                colors.append(colors[-1])\n                hints.append(1)\n        first, last, positions = normalize_stop_positions(positions)\n\n        if self.repeating:\n            # Render as a solid color if the first and last positions are equal\n            # See https://drafts.csswg.org/css-images-3/#repeating-gradients\n            if first == last:\n                color = gradient_average_color(colors, positions)\n                return 1, 'solid', None, [], [color], []\n\n            # Define defined gradient length and steps between positions\n            stop_length = last - first\n            assert stop_length > 0\n            position_steps = [\n                positions[i + 1] - positions[i]\n                for i in range(len(positions) - 1)]\n\n            # Create cycles used to add colors\n            next_steps = cycle((0, *position_steps))\n            next_colors = cycle(colors)\n            next_hints = cycle(hints)\n            previous_steps = cycle((0, *position_steps[::-1]))\n            previous_colors = cycle(colors[::-1])\n            previous_hints = cycle(hints[::-1])\n\n            # Add colors after last step\n            while last < vector_length:\n                step = next(next_steps)\n                colors.append(next(next_colors))\n                hints.append(next(next_hints))\n                positions.append(positions[-1] + step)\n                last += step * stop_length\n\n            # Add colors before first step\n            while first > 0:\n                step = next(previous_steps)\n                colors.insert(0, next(previous_colors))\n                hints.insert(0, next(previous_hints))\n                positions.insert(0, positions[0] - step)\n                first -= step * stop_length\n\n        # Define the coordinates of the starting and ending points\n        start_x = (width - dx * vector_length) / 2\n        start_y = (height - dy * vector_length) / 2\n        points = (\n            start_x + dx * first, start_y + dy * first,\n            start_x + dx * last, start_y + dy * last)\n\n        return 1, 'linear', points, positions, colors, hints\n\n\nclass RadialGradient(Gradient):\n    def __init__(self, color_stops, shape, size, center, repeating, color_hints):\n        super().__init__(color_stops, repeating, color_hints)\n        # Center of the ending shape. (origin_x, pos_x, origin_y, pos_y)\n        self.center = center\n        # Type of ending shape: 'circle' or 'ellipse'\n        self.shape = shape\n        # size_type: 'keyword'\n        #   size: 'closest-corner', 'farthest-corner',\n        #         'closest-side', or 'farthest-side'\n        # size_type: 'explicit'\n        #   size: (radius_x, radius_y)\n        self.size_type, self.size = size\n\n    def layout(self, width, height, style):\n        # Only one color, render the gradient as a solid color\n        if len(self.colors) == 1:\n            return 1, 'solid', None, [], [self.colors[0]], []\n\n        # Define the center of the gradient\n        origin_x, center_x, origin_y, center_y = self.center\n        center_x = percentage(center_x, style, width)\n        center_y = percentage(center_y, style, height)\n        if origin_x == 'right':\n            center_x = width - center_x\n        if origin_y == 'bottom':\n            center_y = height - center_y\n\n        # Resolve sizes and vertical scale\n        size_x, size_y = self._handle_degenerate(\n            *self._resolve_size(width, height, center_x, center_y, style))\n        scale_y = size_y / size_x\n\n        # Normalize colors positions\n        colors = list(self.colors)\n        positions, hints = process_color_stops(\n            size_x, self.stop_positions, self.color_hints, style)\n        if not self.repeating:\n            # Add explicit colors at boundaries if needed, because PDF doesn’t\n            # extend color stops that are not displayed\n            if positions[0] > 0 and positions[0] == positions[1]:\n                positions.insert(0, 0)\n                colors.insert(0, colors[0])\n                hints.insert(0, 1)\n            if positions[-2] == positions[-1]:\n                positions.append(positions[-1] + 1)\n                colors.append(colors[-1])\n                hints.append(1)\n        if positions[0] < 0:\n            # PDF doesn’t like negative radiuses, shift into the positive realm\n            if self.repeating:\n                # Add vector lengths to first position until positive\n                vector_length = positions[-1] - positions[0]\n                offset = vector_length * (1 + (-positions[0] // vector_length))\n                positions = [position + offset for position in positions]\n            else:\n                # Only keep colors with position >= 0, interpolate if needed\n                if positions[-1] <= 0:\n                    # All stops are negative, fill with the last color\n                    return 1, 'solid', None, [], [self.colors[-1]], []\n                for i, position in enumerate(positions):\n                    if position == 0:\n                        # Keep colors and positions from this rank\n                        colors, positions = colors[i:], positions[i:]\n                        break\n                    if position > 0:\n                        # Interpolate with previous rank to get color at 0\n                        color = colors[i]\n                        previous_color = colors[i - 1]\n                        previous_position = positions[i - 1]\n                        assert previous_position < 0\n                        intermediate_color = gradient_average_color(\n                            [previous_color, previous_color, color, color],\n                            [previous_position, 0, 0, position])\n                        colors = [intermediate_color, *colors[i:]]\n                        positions = [0, *positions[i:]]\n                        break\n        first, last, positions = normalize_stop_positions(positions)\n\n        # Render as a solid color if the first and last positions are the same\n        # See https://drafts.csswg.org/css-images-3/#repeating-gradients\n        if first == last and self.repeating:\n            color = gradient_average_color(colors, positions)\n            return 1, 'solid', None, [], [color], []\n\n        # Define the coordinates of the gradient circles\n        points = (\n            center_x, center_y / scale_y, first,\n            center_x, center_y / scale_y, last)\n\n        if self.repeating:\n            points, positions, colors, hints = self._repeat(\n                width, height, scale_y, points, positions, colors, hints)\n\n        return scale_y, 'radial', points, positions, colors, hints\n\n    def _repeat(self, width, height, scale_y, points, positions, colors, hints):\n        # Keep original lists and values, they’re useful\n        original_colors = colors.copy()\n        original_hints = hints.copy()\n        original_positions = positions.copy()\n        gradient_length = points[5] - points[2]\n\n        # Get the maximum distance between the center and the corners, to find\n        # how many times we have to repeat the colors outside\n        max_distance = max(\n            math.hypot(width - points[0], height / scale_y - points[1]),\n            math.hypot(width - points[0], -points[1] * scale_y),\n            math.hypot(-points[0], height / scale_y - points[1]),\n            math.hypot(-points[0], -points[1] * scale_y))\n        repeat_after = math.ceil((max_distance - points[5]) / gradient_length)\n        if repeat_after > 0:\n            # Repeat colors and extrapolate positions\n            repeat = 1 + repeat_after\n            colors *= repeat\n            hints = ([*hints, 1] * repeat)[:-1]\n            positions = [\n                i + position for i in range(repeat) for position in positions]\n            points = (*points[:5], points[5] + gradient_length * repeat_after)\n\n        if points[2] == 0:\n            # Inner circle has 0 radius, no need to repeat inside, return\n            return points, positions, colors, hints\n\n        # Find how many times we have to repeat the colors inside\n        repeat_before = points[2] / gradient_length\n\n        # Set the inner circle size to 0\n        points = (*points[:2], 0, *points[3:])\n\n        # Find how many times the whole gradient can be repeated\n        full_repeat = int(repeat_before)\n        if full_repeat:\n            # Repeat colors and extrapolate positions\n            colors += original_colors * full_repeat\n            hints += [1, *original_hints] * full_repeat\n            positions = [\n                i - full_repeat + position for i in range(full_repeat)\n                for position in original_positions] + positions\n\n        # Find the ratio of gradient that must be added to reach the center\n        partial_repeat = repeat_before - full_repeat\n        if partial_repeat == 0:\n            # No partial repeat, return\n            return points, positions, colors, hints\n\n        # Iterate through positions in reverse order, from the outer\n        # circle to the original inner circle, to find positions from\n        # the inner circle (including full repeats) to the center\n        assert (original_positions[0], original_positions[-1]) == (0, 1)\n        assert 0 < partial_repeat < 1\n        reverse = original_positions[::-1]\n        ratio = 1 - partial_repeat\n        for i, position in enumerate(reverse, start=1):\n            if position == ratio:\n                # The center is a color of the gradient, truncate original\n                # colors and positions and prepend them\n                colors = original_colors[-i:] + colors\n                hints = [*original_hints[-i:], 1, *hints]\n                new_positions = [\n                    position - full_repeat - 1\n                    for position in original_positions[-i:]]\n                positions = new_positions + positions\n                return points, positions, colors, hints\n            if position < ratio:\n                # The center is between two colors of the gradient,\n                # define the center color as the average of these two\n                # gradient colors\n                color = original_colors[-i]\n                next_color = original_colors[-(i - 1)]\n                next_position = original_positions[-(i - 1)]\n                average_colors = [color, color, next_color, next_color]\n                average_positions = [position, ratio, ratio, next_position]\n                zero_color = gradient_average_color(average_colors, average_positions)\n                colors = [zero_color, *original_colors[-(i-1):], *colors]\n                hints = [1, *original_hints[-(i-1):], 1, *hints]\n                new_positions = [\n                    position - 1 - full_repeat for position\n                    in original_positions[-(i - 1):]]\n                positions = (ratio - 1 - full_repeat, *new_positions, *positions)\n                return points, positions, colors, hints\n\n    def _resolve_size(self, width, height, center_x, center_y, style):\n        \"\"\"Resolve circle size of the radial gradient.\"\"\"\n        if self.size_type == 'explicit':\n            size_x, size_y = self.size\n            size_x = percentage(size_x, style, width)\n            size_y = percentage(size_y, style, height)\n            return size_x, size_y\n        left = abs(center_x)\n        right = abs(width - center_x)\n        top = abs(center_y)\n        bottom = abs(height - center_y)\n        pick = min if self.size.startswith('closest') else max\n        if self.size.endswith('side'):\n            if self.shape == 'circle':\n                size_xy = pick(left, right, top, bottom)\n                return size_xy, size_xy\n            # else: ellipse\n            return pick(left, right), pick(top, bottom)\n        # else: corner\n        if self.shape == 'circle':\n            size_xy = pick(math.hypot(left, top), math.hypot(left, bottom),\n                           math.hypot(right, top), math.hypot(right, bottom))\n            return size_xy, size_xy\n        # else: ellipse\n        corner_x, corner_y = pick(\n            (left, top), (left, bottom), (right, top), (right, bottom),\n            key=lambda a: math.hypot(*a))\n        return corner_x * math.sqrt(2), corner_y * math.sqrt(2)\n\n    def _handle_degenerate(self, size_x, size_y):\n        \"\"\"Handle degenerate radial gradients.\n\n        See https://drafts.csswg.org/css-images-3/#degenerate-radials\n\n        \"\"\"\n        if size_x == size_y == 0:\n            size_x = size_y = 1e-7\n        elif size_x == 0:\n            size_x = 1e-7\n            size_y = 1e7\n        elif size_y == 0:\n            size_x = 1e7\n            size_y = 1e-7\n        return size_x, size_y\n"
  },
  {
    "path": "weasyprint/layout/__init__.py",
    "content": "\"\"\"Transform a \"before layout\" box tree into an \"after layout\" tree.\n\nBreak boxes across lines and pages; determine the size and dimension of each\nbox fragement.\n\nBoxes in the new tree have *used values* in their ``position_x``,\n``position_y``, ``width`` and ``height`` attributes, amongst others.\n\nSee https://www.w3.org/TR/CSS21/cascade.html#used-value\n\n\"\"\"\n\nfrom collections import defaultdict\nfrom functools import partial\nfrom math import inf\n\nfrom ..formatting_structure import boxes, build\nfrom ..logger import PROGRESS_LOGGER\nfrom .absolute import absolute_box_layout, absolute_layout\nfrom .background import layout_backgrounds\nfrom .block import block_level_layout\nfrom .page import make_all_pages, make_margin_boxes\n\n\ndef initialize_page_maker(context, root_box):\n    \"\"\"Initialize ``context.page_maker``.\n\n    Collect the pagination's states required for page based counters.\n\n    \"\"\"\n    context.page_maker = []\n\n    # Special case the root box\n    page_break = root_box.style['break_before']\n\n    # TODO: take care of text direction and writing mode\n    # https://www.w3.org/TR/css-page-3/#progression\n    if page_break == 'right':\n        right_page = True\n    elif page_break == 'left':\n        right_page = False\n    elif page_break == 'recto':\n        right_page = root_box.style['direction'] == 'ltr'\n    elif page_break == 'verso':\n        right_page = root_box.style['direction'] == 'rtl'\n    else:\n        right_page = root_box.style['direction'] == 'ltr'\n    resume_at = None\n    next_page = {'break': 'any', 'page': root_box.page_values()[0]}\n\n    # page_state is prerequisite for filling in missing page based counters\n    # although neither a variable quote_depth nor counter_scopes are needed\n    # in page-boxes -- reusing\n    # `formatting_structure.build.update_counters()` to avoid redundant\n    # code requires a full `state`.\n    # The value of **pages**, of course, is unknown until we return and\n    # might change when 'content_changed' triggers re-pagination...\n    # So we start with an empty state\n    page_state = (\n        # Shared mutable objects:\n        [0],  # quote_depth: single integer\n        {'pages': [0]},\n        [{'pages'}],  # counter_scopes\n        [] # page_groups\n    )\n\n    # Initial values\n    remake_state = {\n        'content_changed': False,\n        'pages_wanted': False,\n        'anchors': [],  # first occurrence of anchor\n        'content_lookups': []  # first occurr. of content-CounterLookupItem\n    }\n    context.page_maker.append((\n        resume_at, next_page, right_page, page_state, remake_state))\n\n\ndef layout_fixed_boxes(context, pages, containing_page):\n    \"\"\"Lay out and yield fixed boxes of ``pages`` on ``containing_page``.\"\"\"\n    for page in pages:\n        for box in page.fixed_boxes:\n            # As replaced boxes are never copied during layout, ensure that we\n            # have different boxes (with a possibly different layout) for\n            # each pages.\n            if isinstance(box, boxes.ReplacedBox):\n                box = box.copy()\n            # Absolute boxes in fixed boxes are rendered as fixed boxes'\n            # children, even when they are fixed themselves.\n            absolute_boxes = []\n            absolute_box, _ = absolute_box_layout(\n                context, box, containing_page, absolute_boxes,\n                bottom_space=-inf, skip_stack=None)\n            yield absolute_box\n            while absolute_boxes:\n                new_absolute_boxes = []\n                for box in absolute_boxes:\n                    absolute_layout(\n                        context, box, containing_page, new_absolute_boxes,\n                        bottom_space=-inf, skip_stack=None)\n                absolute_boxes = new_absolute_boxes\n\n\ndef layout_document(html, root_box, context, max_loops=8):\n    \"\"\"Lay out the whole document.\n\n    This includes line breaks, page breaks, absolute size and position for all\n    boxes. Page based counters might require multiple passes.\n\n    :param root_box:\n        Root of the box tree (formatting structure of the HTML). The page boxes\n        are created from that tree, this structure is not lost during\n        pagination.\n    :returns:\n        A list of laid out Page objects.\n\n    \"\"\"\n    initialize_page_maker(context, root_box)\n    pages = []\n    original_footnotes = []\n    actual_total_pages = 0\n\n    for loop in range(max_loops):\n        if loop > 0:\n            PROGRESS_LOGGER.info(\n                'Step 5 - Creating layout - Repagination #%d', loop)\n            context.footnotes = original_footnotes.copy()\n\n        initial_total_pages = actual_total_pages\n        if loop == 0:\n            original_footnotes = context.footnotes.copy()\n        pages = list(make_all_pages(context, root_box, html, pages))\n        actual_total_pages = len(pages)\n\n        # Check whether another round is required\n        reloop_content = False\n        reloop_pages = False\n        for page_data in context.page_maker:\n            # Update pages\n            _, _, _, page_state, remake_state = page_data\n            page_counter_values = page_state[1]\n            page_counter_values['pages'] = [actual_total_pages]\n            if remake_state['content_changed']:\n                reloop_content = True\n            if remake_state['pages_wanted']:\n                reloop_pages = initial_total_pages != actual_total_pages\n\n        # No need for another loop, stop here\n        if not reloop_content and not reloop_pages:\n            break\n\n    # Calculate string-sets and bookmark-labels containing page based counters\n    # when pagination is finished. No need to do that (maybe multiple times) in\n    # make_page because they dont create boxes, only appear in MarginBoxes and\n    # in the final PDF.\n    # Prevent repetition of bookmarks (see #1145).\n\n    watch_elements = []\n    watch_elements_before = []\n    watch_elements_after = []\n    for i, page in enumerate(pages):\n        # We need the updated page_counter_values\n        _, _, _, page_state, _ = context.page_maker[i + 1]\n        page_counter_values = page_state[1]\n\n        for child in page.descendants():\n            # Only one bookmark per original box\n            if child.bookmark_label:\n                if child.element_tag.endswith('::before'):\n                    checklist = watch_elements_before\n                elif child.element_tag.endswith('::after'):\n                    checklist = watch_elements_after\n                else:\n                    checklist = watch_elements\n                if child.element in checklist:\n                    child.bookmark_label = ''\n                else:\n                    checklist.append(child.element)\n\n            if child.missing_link:\n                for (box, css_token), item in (\n                        context.target_collector.counter_lookup_items.items()):\n                    if child.missing_link == box and css_token != 'content':\n                        if (css_token == 'bookmark-label' and\n                                not child.bookmark_label):\n                            # don't refill it!\n                            continue\n                        item.parse_again(page_counter_values)\n                        # string_set is a pointer, but the bookmark_label is\n                        # just a string: copy it\n                        if css_token == 'bookmark-label':\n                            child.bookmark_label = box.bookmark_label\n            # Collect the string_sets in the LayoutContext\n            string_sets = child.string_set\n            if string_sets and string_sets != 'none':\n                for string_set in string_sets:\n                    string_name, text = string_set\n                    context.string_set[string_name][i+1].append(text)\n\n    # Add margin boxes\n    for i, page in enumerate(pages):\n        root_children = []\n        root, footnote_area = page.children\n        root_children.extend(layout_fixed_boxes(context, pages[:i], page))\n        root_children.extend(root.children)\n        root_children.extend(layout_fixed_boxes(context, pages[i + 1:], page))\n        root.children = root_children\n        context.current_page = i + 1  # page_number starts at 1\n\n        # page_maker's page_state is ready for the MarginBoxes\n        state = context.page_maker[context.current_page][3]\n        page.children = (root,)\n        if footnote_area.children:\n            page.children += (footnote_area,)\n        page.children += tuple(make_margin_boxes(context, page, state))\n        layout_backgrounds(page, context.get_image_from_uri)\n        yield page\n\n\nclass FakeList(list):\n    \"\"\"List in which you can’t append objects.\"\"\"\n    def append(self, item):\n        pass\n\n\nclass LayoutContext:\n    def __init__(self, style_for, get_image_from_uri, font_config,\n                 counter_style, target_collector):\n        self.style_for = style_for\n        self.get_image_from_uri = partial(get_image_from_uri, context=self)\n        self.font_config = font_config\n        self.counter_style = counter_style\n        self.target_collector = target_collector\n        self._excluded_shapes_root_boxes = []\n        self._excluded_shapes = {}\n        self.footnotes = []\n        self.page_footnotes = {}\n        self.current_page_footnotes = []\n        self.reported_footnotes = []\n        self.current_footnote_area = None  # Not initialized yet\n        self.page_bottom = None\n        self.string_set = defaultdict(lambda: defaultdict(list))\n        self.running_elements = defaultdict(lambda: defaultdict(list))\n        self.current_page = None\n        self.forced_break = False\n        self.broken_out_of_flow = {}\n        self.in_column = False\n\n        # Cache\n        self.tables = {}\n        self.dictionaries = {}\n\n    def overflows_page(self, bottom_space, position_y):\n        return self.overflows(self.page_bottom - bottom_space, position_y)\n\n    @staticmethod\n    def overflows(bottom, position_y):\n        # Use a small fudge factor to avoid floating numbers errors.\n        # The 1e-9 value comes from PEP 485.\n        return position_y > bottom * (1 + 1e-9)\n\n    @property\n    def excluded_shapes(self):\n        return self._excluded_shapes[self._excluded_shapes_root_boxes[-1]]\n\n    @excluded_shapes.setter\n    def excluded_shapes(self, excluded_shapes):\n        self._excluded_shapes[self._excluded_shapes_root_boxes[-1]] = excluded_shapes\n\n    def create_block_formatting_context(self, root_box=None, new_list=None):\n        assert root_box not in self._excluded_shapes_root_boxes\n        self._excluded_shapes_root_boxes.append(root_box)\n        if root_box not in self._excluded_shapes:\n            self._excluded_shapes[root_box] = [] if new_list is None else new_list\n\n    def finish_block_formatting_context(self, root_box=None):\n        # See https://www.w3.org/TR/CSS2/visudet.html#root-height\n        if root_box and root_box.style['height'] == 'auto' and self.excluded_shapes:\n            box_bottom = root_box.content_box_y() + root_box.height\n            max_shape_bottom = max([\n                shape.position_y + shape.margin_height()\n                for shape in self.excluded_shapes] + [box_bottom])\n            root_box.height += max_shape_bottom - box_bottom\n        self._excluded_shapes.pop(self._excluded_shapes_root_boxes.pop())\n\n    def create_flex_formatting_context(self, root_box):\n        self.create_block_formatting_context(root_box, FakeList())\n\n    def finish_flex_formatting_context(self, root_box):\n        self.finish_block_formatting_context(root_box)\n\n    def add_broken_out_of_flow(self, new_box, box, containing_block, resume_at):\n        self.broken_out_of_flow[new_box] = (\n            box, containing_block, self._excluded_shapes_root_boxes[-1], resume_at)\n\n    def get_string_set_for(self, page, name, keyword='first'):\n        \"\"\"Resolve value of string function.\"\"\"\n        return self.get_string_or_element_for(\n            self.string_set, page, name, keyword)\n\n    def get_running_element_for(self, page, name, keyword='first'):\n        \"\"\"Resolve value of element function.\"\"\"\n        return self.get_string_or_element_for(\n            self.running_elements, page, name, keyword)\n\n    def get_string_or_element_for(self, store, page, name, keyword):\n        \"\"\"Resolve value of string or element function.\n\n        We'll have something like this that represents all assignments on a\n        given page:\n\n        {1: ['First Header'], 3: ['Second Header'],\n         4: ['Third Header', '3.5th Header']}\n\n        Value depends on current page.\n        https://drafts.csswg.org/css-gcpm/#funcdef-string\n\n        :param dict store:\n            Dictionary where the resolved value is stored.\n        :param page:\n            Current page.\n        :param str name:\n            Name of the named string or running element.\n        :param str keyword:\n            Indicates which value of the named string or running element to\n            use. Default is the first assignment on the current page else the\n            most recent assignment.\n        :returns:\n            Text for string set, box for running element.\n\n        \"\"\"\n        if self.current_page in store[name]:\n            # A value was assigned on this page\n            first_string = store[name][self.current_page][0]\n            last_string = store[name][self.current_page][-1]\n            if keyword == 'first':\n                return first_string\n            elif keyword == 'start':\n                element = page\n                while element:\n                    if element.style['string_set'] != 'none':\n                        for (string_name, _) in element.style['string_set']:\n                            if string_name == name:\n                                return first_string\n                    if element.children:\n                        element = element.children[0]\n                        continue\n                    break\n            elif keyword == 'last':\n                return last_string\n            elif keyword == 'first-except':\n                return\n        # Search backwards through previous pages\n        for previous_page in range(self.current_page - 1, 0, -1):\n            if previous_page in store[name]:\n                return store[name][previous_page][-1]\n\n    def layout_footnote(self, footnote):\n        \"\"\"Add a footnote to the layout for this page.\"\"\"\n        self.footnotes.remove(footnote)\n        self.current_page_footnotes.append(footnote)\n        return self._update_footnote_area()\n\n    def unlayout_footnote(self, footnote):\n        \"\"\"Remove a footnote from the layout and return it to the waitlist.\"\"\"\n        # TODO: Handle unlayouting a footnote that hasn't been laid out yet or\n        # has already been unlayouted\n        if footnote not in self.footnotes:\n            self.footnotes.append(footnote)\n            if footnote in self.current_page_footnotes:\n                self.current_page_footnotes.remove(footnote)\n            elif footnote in self.reported_footnotes:\n                self.reported_footnotes.remove(footnote)\n            self._update_footnote_area()\n\n    def report_footnote(self, footnote):\n        \"\"\"Mark a footnote as being moved to the next page.\"\"\"\n        self.current_page_footnotes.remove(footnote)\n        self.reported_footnotes.append(footnote)\n        self._update_footnote_area()\n\n    def _update_footnote_area(self):\n        \"\"\"Update the page bottom size and our footnote area height.\"\"\"\n        if self.current_footnote_area.height != 'auto' and not self.in_column:\n            self.page_bottom += self.current_footnote_area.margin_height()\n        self.current_footnote_area.children = self.current_page_footnotes\n        if self.current_footnote_area.children:\n            footnote_area = build.create_anonymous_boxes(\n                self.current_footnote_area.deepcopy())\n            footnote_area = block_level_layout(\n                self, footnote_area, -inf, None,\n                self.current_footnote_area.page)[0]\n            self.current_footnote_area.height = footnote_area.height\n            if not self.in_column:\n                self.page_bottom -= footnote_area.margin_height()\n            last_child = footnote_area.children[-1]\n            last_child_bottom = (\n                last_child.position_y + last_child.margin_height() -\n                last_child.margin_bottom)\n            footnote_area_bottom = (\n                footnote_area.position_y + footnote_area.margin_height() -\n                footnote_area.margin_bottom)\n            overflow = last_child_bottom > footnote_area_bottom\n            return overflow\n        else:\n            self.current_footnote_area.height = 0\n            if not self.in_column:\n                self.page_bottom -= self.current_footnote_area.margin_height()\n            return False\n"
  },
  {
    "path": "weasyprint/layout/absolute.py",
    "content": "\"\"\"Absolutely positioned boxes management.\"\"\"\n\nfrom ..formatting_structure import boxes\nfrom .min_max import handle_min_max_width\nfrom .percent import resolve_percentages, resolve_position_percentages\nfrom .preferred import shrink_to_fit\nfrom .replaced import inline_replaced_box_width_height\nfrom .table import table_wrapper_width\n\n\nclass AbsolutePlaceholder:\n    \"\"\"Left where an absolutely-positioned box was taken out of the flow.\"\"\"\n    def __init__(self, box):\n        assert not isinstance(box, AbsolutePlaceholder)\n        # Work around the overloaded __setattr__\n        object.__setattr__(self, '_box', box)\n        object.__setattr__(self, '_layout_done', False)\n\n    def set_laid_out_box(self, new_box):\n        object.__setattr__(self, '_box', new_box)\n        object.__setattr__(self, '_layout_done', True)\n\n    def translate(self, dx=0, dy=0, ignore_floats=False):\n        if dx == dy == 0:\n            return\n        if self._layout_done:\n            self._box.translate(dx, dy, ignore_floats)\n        else:\n            # Descendants do not have a position yet.\n            self._box.position_x += dx\n            self._box.position_y += dy\n\n    def copy(self):\n        new_placeholder = AbsolutePlaceholder(self._box.copy())\n        object.__setattr__(new_placeholder, '_layout_done', self._layout_done)\n        return new_placeholder\n\n    # Pretend to be the box itself\n    def __getattr__(self, name):\n        return getattr(self._box, name)\n\n    def __setattr__(self, name, value):\n        setattr(self._box, name, value)\n\n    def __repr__(self):\n        return '<Placeholder %r>' % self._box\n\n\n@handle_min_max_width\ndef absolute_width(box, context, cb_x, cb_y, cb_width, cb_height):\n    # https://www.w3.org/TR/CSS2/visudet.html#abs-replaced-width\n    ltr = (\n        box.style.parent_style is None or\n        box.style.parent_style['direction'] == 'ltr')\n    paddings_borders = (\n        box.padding_left + box.padding_right +\n        box.border_left_width + box.border_right_width)\n    translate_x = 0\n    translate_box_width = False\n    default_translate_x = cb_x - box.position_x\n\n    if box.left == box.right == box.width == 'auto':\n        if box.margin_left == 'auto':\n            box.margin_left = 0\n        if box.margin_right == 'auto':\n            box.margin_right = 0\n        available_width = cb_width - (\n            paddings_borders + box.margin_left + box.margin_right)\n        box.width = shrink_to_fit(context, box, available_width)\n        if box.is_outside_marker:\n            translate_box_width = ltr\n        elif not ltr:\n            translate_box_width = True\n            translate_x = default_translate_x + available_width\n    elif box.left != 'auto' and box.right != 'auto' and box.width != 'auto':\n        width_for_margins = cb_width - (\n            box.right + box.left + box.width + paddings_borders)\n        if box.margin_left == box.margin_right == 'auto':\n            if box.width + paddings_borders + box.right + box.left <= cb_width:\n                box.margin_left = box.margin_right = width_for_margins / 2\n            else:\n                box.margin_left = 0 if ltr else width_for_margins\n                box.margin_right = width_for_margins if ltr else 0\n        elif box.margin_left == 'auto':\n            box.margin_left = width_for_margins\n        elif box.margin_right == 'auto':\n            box.margin_right = width_for_margins\n        elif ltr:\n            box.margin_right = width_for_margins\n        else:\n            box.margin_left = width_for_margins\n        translate_x = box.left + default_translate_x\n    else:\n        if box.margin_left == 'auto':\n            box.margin_left = 0\n        if box.margin_right == 'auto':\n            box.margin_right = 0\n        spacing = paddings_borders + box.margin_left + box.margin_right\n        if box.left == box.width == 'auto':\n            box.width = shrink_to_fit(\n                context, box, cb_width - spacing - box.right)\n            translate_x = cb_width - box.right - spacing + default_translate_x\n            translate_box_width = True\n        elif box.left == box.right == 'auto':\n            if not ltr:\n                available_width = cb_width - (\n                    paddings_borders + box.margin_left + box.margin_right)\n                translate_box_width = True\n                translate_x = default_translate_x + available_width\n        elif box.width == box.right == 'auto':\n            box.width = shrink_to_fit(\n                context, box, cb_width - spacing - box.left)\n            translate_x = box.left + default_translate_x\n        elif box.left == 'auto':\n            translate_x = cb_width + default_translate_x - (\n                box.right + spacing + box.width)\n        elif box.width == 'auto':\n            box.width = cb_width - box.right - box.left - spacing\n            translate_x = box.left + default_translate_x\n        elif box.right == 'auto':\n            translate_x = box.left + default_translate_x\n\n    return translate_box_width, translate_x\n\n\ndef absolute_height(box, context, cb_x, cb_y, cb_width, cb_height):\n    # https://www.w3.org/TR/CSS2/visudet.html#abs-non-replaced-height\n    paddings_borders = (\n        box.padding_top + box.padding_bottom +\n        box.border_top_width + box.border_bottom_width)\n    translate_y = 0\n    translate_box_height = False\n    default_translate_y = cb_y - box.position_y\n\n    if box.top == box.bottom == box.height == 'auto':\n        # Keep the static position\n        if box.margin_top == 'auto':\n            box.margin_top = 0\n        if box.margin_bottom == 'auto':\n            box.margin_bottom = 0\n    elif 'auto' not in (box.top, box.bottom, box.height):\n        height_for_margins = cb_height - (\n            box.top + box.bottom + box.height + paddings_borders)\n        if box.margin_top == box.margin_bottom == 'auto':\n            box.margin_top = box.margin_bottom = height_for_margins / 2\n        elif box.margin_top == 'auto':\n            box.margin_top = height_for_margins\n        elif box.margin_bottom == 'auto':\n            box.margin_bottom = height_for_margins\n        else:\n            box.margin_bottom = height_for_margins\n        translate_y = box.top + default_translate_y\n    else:\n        if box.margin_top == 'auto':\n            box.margin_top = 0\n        if box.margin_bottom == 'auto':\n            box.margin_bottom = 0\n        spacing = paddings_borders + box.margin_top + box.margin_bottom\n        if box.top == box.height == 'auto':\n            translate_y = (\n                cb_height - box.bottom - spacing + default_translate_y)\n            translate_box_height = True\n        elif box.top == box.bottom == 'auto':\n            pass  # Keep the static position\n        elif box.height == box.bottom == 'auto':\n            translate_y = box.top + default_translate_y\n        elif box.top == 'auto':\n            translate_y = cb_height + default_translate_y - (\n                box.bottom + spacing + box.height)\n        elif box.height == 'auto':\n            box.height = cb_height - box.bottom - box.top - spacing\n            translate_y = box.top + default_translate_y\n        elif box.bottom == 'auto':\n            translate_y = box.top + default_translate_y\n\n    return translate_box_height, translate_y\n\n\ndef absolute_block(context, box, containing_block, fixed_boxes, bottom_space,\n                   skip_stack, cb_x, cb_y, cb_width, cb_height):\n    from .block import block_container_layout\n    from .flex import flex_layout\n    from .grid import grid_layout\n\n    translate_box_width, translate_x = absolute_width(\n        box, context, cb_x, cb_y, cb_width, cb_height)\n    if skip_stack:\n        translate_box_height, translate_y = False, 0\n    else:\n        translate_box_height, translate_y = absolute_height(\n            box, context, cb_x, cb_y, cb_width, cb_height)\n\n    bottom_space += -box.position_y if translate_box_height else translate_y\n\n    # This box is the containing block for absolute descendants.\n    absolute_boxes = []\n\n    if box.is_table_wrapper:\n        table_wrapper_width(context, box, (cb_width, cb_height))\n\n    if isinstance(box, (boxes.BlockBox)):\n        new_box, resume_at, _, _, _, _ = block_container_layout(\n            context, box, bottom_space, skip_stack, page_is_empty=True,\n            absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes,\n            adjoining_margins=None, first_letter_style=None, first_line_style=None,\n            discard=False, max_lines=None)\n    elif isinstance(box, (boxes.FlexContainerBox)):\n        new_box, resume_at, _, _, _ = flex_layout(\n            context, box, bottom_space, skip_stack, containing_block,\n            page_is_empty=True, absolute_boxes=absolute_boxes,\n            fixed_boxes=fixed_boxes, discard=False)\n    elif isinstance(box, (boxes.GridContainerBox)):\n        new_box, resume_at, _, _, _ = grid_layout(\n            context, box, bottom_space, skip_stack, containing_block,\n            page_is_empty=True, absolute_boxes=absolute_boxes,\n            fixed_boxes=fixed_boxes)\n\n    for child_placeholder in absolute_boxes:\n        absolute_layout(\n            context, child_placeholder, new_box, fixed_boxes, bottom_space,\n            skip_stack=None)\n\n    if translate_box_width:\n        translate_x -= new_box.width\n    if translate_box_height:\n        translate_y -= new_box.height\n    new_box.translate(translate_x, translate_y)\n\n    return new_box, resume_at\n\n\ndef absolute_layout(context, placeholder, containing_block, fixed_boxes,\n                    bottom_space, skip_stack):\n    \"\"\"Set the width of absolute positioned ``box``.\"\"\"\n    assert not placeholder._layout_done\n    box = placeholder._box\n    new_box, resume_at = absolute_box_layout(\n        context, box, containing_block, fixed_boxes, bottom_space, skip_stack)\n    placeholder.set_laid_out_box(new_box)\n    if resume_at:\n        context.add_broken_out_of_flow(placeholder, box, containing_block, resume_at)\n\n\ndef absolute_box_layout(context, box, containing_block, fixed_boxes,\n                        bottom_space, skip_stack):\n    # TODO: handle inline boxes (point 10.1.4.1)\n    # https://www.w3.org/TR/CSS2/visudet.html#containing-block-details\n    if isinstance(containing_block, boxes.PageBox):\n        cb_x = containing_block.content_box_x()\n        cb_y = containing_block.content_box_y()\n        cb_width = containing_block.width\n        cb_height = containing_block.height\n    else:\n        cb_x = containing_block.padding_box_x()\n        cb_y = containing_block.padding_box_y()\n        cb_width = containing_block.padding_width()\n        cb_height = containing_block.padding_height()\n\n    resolve_percentages(box, (cb_width, cb_height))\n    resolve_position_percentages(box, (cb_width, cb_height))\n\n    if isinstance(box, boxes.BlockReplacedBox):\n        new_box = absolute_replaced(\n            context, box, cb_x, cb_y, cb_width, cb_height)\n        resume_at = None\n    else:\n        # Absolute tables are wrapped into block boxes\n        new_box, resume_at = absolute_block(\n            context, box, containing_block, fixed_boxes, bottom_space,\n            skip_stack, cb_x, cb_y, cb_width, cb_height)\n\n    return new_box, resume_at\n\n\ndef absolute_replaced(context, box, cb_x, cb_y, cb_width, cb_height):\n    inline_replaced_box_width_height(box, (cb_x, cb_y, cb_width, cb_height))\n    ltr = (\n        box.style.parent_style is None or\n        box.style.parent_style['direction'] == 'ltr')\n\n    # https://www.w3.org/TR/CSS21/visudet.html#abs-replaced-width\n    if box.left == box.right == 'auto':\n        # static position:\n        if ltr:\n            box.left = box.position_x - cb_x\n        else:\n            box.right = cb_x + cb_width - box.position_x\n    if 'auto' in (box.left, box.right):\n        if box.margin_left == 'auto':\n            box.margin_left = 0\n        if box.margin_right == 'auto':\n            box.margin_right = 0\n        remaining = cb_width - box.margin_width()\n        if box.left == 'auto':\n            box.left = remaining - box.right\n        if box.right == 'auto':\n            box.right = remaining - box.left\n    elif 'auto' in (box.margin_left, box.margin_right):\n        remaining = cb_width - (box.border_width() + box.left + box.right)\n        if box.margin_left == box.margin_right == 'auto':\n            if remaining >= 0:\n                box.margin_left = box.margin_right = remaining // 2\n            else:\n                box.margin_left = 0 if ltr else remaining\n                box.margin_right = remaining if ltr else 0\n        elif box.margin_left == 'auto':\n            box.margin_left = remaining\n        else:\n            box.margin_right = remaining\n    else:\n        # Over-constrained\n        if ltr:\n            box.right = cb_width - (box.margin_width() + box.left)\n        else:\n            box.left = cb_width - (box.margin_width() + box.right)\n\n    # https://www.w3.org/TR/CSS21/visudet.html#abs-replaced-height\n    if box.top == box.bottom == 'auto':\n        box.top = box.position_y - cb_y\n    if 'auto' in (box.top, box.bottom):\n        if box.margin_top == 'auto':\n            box.margin_top = 0\n        if box.margin_bottom == 'auto':\n            box.margin_bottom = 0\n        remaining = cb_height - box.margin_height()\n        if box.top == 'auto':\n            box.top = remaining - box.bottom\n        if box.bottom == 'auto':\n            box.bottom = remaining - box.top\n    elif 'auto' in (box.margin_top, box.margin_bottom):\n        remaining = cb_height - (box.border_height() + box.top + box.bottom)\n        if box.margin_top == box.margin_bottom == 'auto':\n            box.margin_top = box.margin_bottom = remaining // 2\n        elif box.margin_top == 'auto':\n            box.margin_top = remaining\n        else:\n            box.margin_bottom = remaining\n    else:\n        # Over-constrained\n        box.bottom = cb_height - (box.margin_height() + box.top)\n\n    # No children for replaced boxes, no need to .translate()\n    box.position_x = cb_x + box.left\n    box.position_y = cb_y + box.top\n    return box\n"
  },
  {
    "path": "weasyprint/layout/background.py",
    "content": "\"\"\"Manage background position and size.\"\"\"\n\nfrom collections import namedtuple\nfrom itertools import cycle\n\nfrom tinycss2.color5 import parse_color\n\nfrom ..formatting_structure import boxes\nfrom . import replaced\nfrom .percent import percentage, resolve_radii_percentages\n\nBackground = namedtuple('Background', 'color, layers, style')\nBackgroundLayer = namedtuple(\n    'BackgroundLayer',\n    'image, size, position, repeat, unbounded, '\n    'painting_area, positioning_area, clipped_boxes')\n\n\ndef box_rectangle(box, which_rectangle):\n    if which_rectangle == 'border-box':\n        return (\n            box.border_box_x(), box.border_box_y(),\n            box.border_width(), box.border_height())\n    elif which_rectangle == 'padding-box':\n        return (\n            box.padding_box_x(), box.padding_box_y(),\n            box.padding_width(), box.padding_height())\n    else:\n        assert which_rectangle == 'content-box', which_rectangle\n        return (\n            box.content_box_x(), box.content_box_y(),\n            box.width, box.height)\n\n\ndef layout_box_backgrounds(page, box, get_image_from_uri, layout_children=True,\n                           style=None):\n    \"\"\"Fetch and position background images.\"\"\"\n    from ..draw.color import get_color\n\n    # Resolve percentages in border-radius properties\n    resolve_radii_percentages(box)\n\n    if layout_children:\n        for child in box.all_children():\n            layout_box_backgrounds(page, child, get_image_from_uri)\n\n    if style is None:\n        style = box.style\n\n    # This is for the border image, not the background, but this is a\n    # convenient place to get the image.\n    if style['border_image_source'][0] != 'none':\n        type_, value = style['border_image_source']\n        if type_ == 'url':\n            box.border_image = get_image_from_uri(url=value)\n        else:\n            box.border_image = value\n\n    if style['mask_border_source'][0] != 'none':\n        type_, value = style['mask_border_source']\n        if type_ == 'url':\n            box.mask_border_image = get_image_from_uri(url=value)\n        else:\n            box.mask_border_image = value\n\n    if style['visibility'] == 'hidden':\n        images = []\n        color = parse_color('transparent')\n    else:\n        orientation = style['image_orientation']\n        images = [\n            get_image_from_uri(url=value, orientation=orientation)\n            if type_ == 'url' else value\n            for type_, value in style['background_image']]\n        color = get_color(style, 'background_color')\n\n    if color.alpha == 0 and not any(images):\n        if box != page:  # Pages need a background for bleed box\n            box.background = None\n            return\n\n    layers = [\n        layout_background_layer(box, page, style['image_resolution'], *layer)\n        for layer in zip(images, *map(cycle, [\n            style['background_size'],\n            style['background_clip'],\n            style['background_repeat'],\n            style['background_origin'],\n            style['background_position'],\n            style['background_attachment']]))]\n    box.background = Background(color, layers, style)\n\n\ndef layout_background_layer(box, page, resolution, image, size, clip, repeat,\n                            origin, position, attachment):\n\n    # TODO: respect box-sizing for table cells?\n    clipped_boxes = []\n    painting_area = 0, 0, 0, 0\n    if box is page:\n        # [The page’s] background painting area is the bleed area […]\n        # regardless of background-clip.\n        # https://drafts.csswg.org/css-page-3/#painting\n        painting_area = page.bleed_area\n        clipped_boxes = []\n    elif isinstance(box, boxes.TableRowGroupBox):\n        clipped_boxes = []\n        total_height = 0\n        for row in box.children:\n            if row.children:\n                clipped_boxes += [\n                    cell.rounded_border_box() for cell in row.children]\n                total_height = max(total_height, max(\n                    cell.border_height() for cell in row.children))\n        painting_area = [\n            box.border_box_x(), box.border_box_y(),\n            box.border_width(), total_height]\n    elif isinstance(box, boxes.TableRowBox):\n        if box.children:\n            clipped_boxes = [\n                cell.rounded_border_box() for cell in box.children]\n            height = max(cell.border_height() for cell in box.children)\n            painting_area = [\n                box.border_box_x(), box.border_box_y(),\n                box.border_width(), height]\n    elif isinstance(box, (boxes.TableColumnGroupBox, boxes.TableColumnBox)):\n        cells = box.get_cells()\n        if cells:\n            clipped_boxes = [cell.rounded_border_box() for cell in cells]\n            min_x = min(cell.border_box_x() for cell in cells)\n            max_x = max(\n                cell.border_box_x() + cell.border_width() for cell in cells)\n            painting_area = [\n                min_x, box.border_box_y(), max_x - min_x, box.border_height()]\n    else:\n        painting_area = box_rectangle(box, clip)\n        if clip == 'border-box':\n            clipped_boxes = [box.rounded_border_box()]\n        elif clip == 'padding-box':\n            clipped_boxes = [box.rounded_padding_box()]\n        else:\n            assert clip == 'content-box', clip\n            clipped_boxes = [box.rounded_content_box()]\n\n    if image is not None:\n        intrinsic_width, intrinsic_height, ratio = image.get_intrinsic_size(\n            resolution, box.style['font_size'])\n    if image is None or 0 in (intrinsic_width, intrinsic_height):\n        return BackgroundLayer(\n            image=None, unbounded=False, painting_area=painting_area,\n            size='unused', position='unused', repeat='unused',\n            positioning_area='unused', clipped_boxes=clipped_boxes)\n\n    if attachment == 'fixed':\n        # Initial containing block\n        if isinstance(box, boxes.PageBox):\n            # […] if background-attachment is fixed then the image is\n            # positioned relative to the page box including its margins […].\n            # https://drafts.csswg.org/css-page/#painting\n            positioning_area = (0, 0, box.margin_width(), box.margin_height())\n        else:\n            positioning_area = box_rectangle(page, 'content-box')\n    else:\n        positioning_area = box_rectangle(box, origin)\n\n    positioning_x, positioning_y, positioning_width, positioning_height = (\n        positioning_area)\n    painting_x, painting_y, painting_width, painting_height = painting_area\n\n    if size == 'cover':\n        image_width, image_height = replaced.cover_constraint_image_sizing(\n            positioning_width, positioning_height, ratio)\n    elif size == 'contain':\n        image_width, image_height = replaced.contain_constraint_image_sizing(\n            positioning_width, positioning_height, ratio)\n    else:\n        size_width, size_height = size\n        image_width, image_height = replaced.default_image_sizing(\n            intrinsic_width, intrinsic_height, ratio,\n            percentage(size_width, box.style, positioning_width),\n            percentage(size_height, box.style, positioning_height),\n            positioning_width, positioning_height)\n\n    origin_x, position_x, origin_y, position_y = position\n    ref_x = positioning_width - image_width\n    ref_y = positioning_height - image_height\n    position_x = percentage(position_x, box.style, ref_x)\n    position_y = percentage(position_y, box.style, ref_y)\n    if origin_x == 'right':\n        position_x = ref_x - position_x\n    if origin_y == 'bottom':\n        position_y = ref_y - position_y\n\n    repeat_x, repeat_y = repeat\n\n    if repeat_x == 'round':\n        n_repeats = max(1, round(positioning_width / image_width))\n        new_width = positioning_width / n_repeats\n        position_x = 0  # Ignore background-position for this dimension\n        if repeat_y != 'round' and size[1] == 'auto':\n            image_height *= new_width / image_width\n        image_width = new_width\n    if repeat_y == 'round':\n        n_repeats = max(1, round(positioning_height / image_height))\n        new_height = positioning_height / n_repeats\n        position_y = 0  # Ignore background-position for this dimension\n        if repeat_x != 'round' and size[0] == 'auto':\n            image_width *= new_height / image_height\n        image_height = new_height\n\n    return BackgroundLayer(\n        image=image,\n        size=(image_width, image_height),\n        position=(position_x, position_y),\n        repeat=repeat,\n        unbounded=False,\n        painting_area=painting_area,\n        positioning_area=positioning_area,\n        clipped_boxes=clipped_boxes)\n\n\ndef layout_backgrounds(page, get_image_from_uri):\n    \"\"\"Layout backgrounds on the page box and on its children.\n\n    This function takes care of the canvas background, taken from the root\n    elememt or a <body> child of the root element.\n\n    See https://www.w3.org/TR/CSS21/colors.html#background\n\n    \"\"\"\n    layout_box_backgrounds(page, page, get_image_from_uri)\n    assert not isinstance(page.children[0], boxes.MarginBox)\n    root_box = page.children[0]\n    chosen_box = root_box\n    if root_box.element_tag.lower() == 'html' and root_box.background is None:\n        for child in root_box.children:\n            if child.element_tag.lower() == 'body':\n                chosen_box = child\n                break\n\n    if chosen_box.background:\n        painting_area = box_rectangle(page, 'border-box')\n        original_background = page.background\n        layout_box_backgrounds(\n            page, page, get_image_from_uri, layout_children=False,\n            style=chosen_box.style)\n        page.canvas_background = page.background._replace(\n            # TODO: background-clip should be updated\n            layers=[\n                layer._replace(painting_area=painting_area)\n                for layer in page.background.layers])\n        page.background = original_background\n        chosen_box.background = None\n    else:\n        page.canvas_background = None\n"
  },
  {
    "path": "weasyprint/layout/block.py",
    "content": "\"\"\"Page breaking and layout for block-level and block-container boxes.\"\"\"\n\nfrom functools import partial\nfrom math import inf\n\nfrom ..formatting_structure import boxes\nfrom .absolute import AbsolutePlaceholder, absolute_layout\nfrom .column import columns_layout\nfrom .flex import flex_layout\nfrom .float import avoid_collisions, float_layout, get_clearance\nfrom .grid import grid_layout\nfrom .inline import iter_line_boxes\nfrom .percent import percentage, resolve_percentages, resolve_position_percentages\nfrom .replaced import block_replaced_box_layout\nfrom .table import table_layout, table_wrapper_width\n\n\ndef block_level_layout(context, box, bottom_space, skip_stack, containing_block,\n                       page_is_empty=True, absolute_boxes=None, fixed_boxes=None,\n                       adjoining_margins=None, first_letter_style=None,\n                       first_line_style=None, discard=False, max_lines=None):\n    \"\"\"Lay out the block-level ``box``.\"\"\"\n    absolute_boxes = [] if absolute_boxes is None else absolute_boxes\n    fixed_boxes = [] if fixed_boxes is None else fixed_boxes\n    adjoining_margins = [] if adjoining_margins is None else adjoining_margins\n\n    if not isinstance(box, boxes.TableBox):\n        resolve_percentages(box, containing_block)\n\n        if box.margin_top == 'auto':\n            box.margin_top = 0\n        if box.margin_bottom == 'auto':\n            box.margin_bottom = 0\n\n        if context.current_page > 1 and page_is_empty:\n            # When an unforced break occurs before or after a block-level box,\n            # any margins adjoining the break are truncated to zero.\n            # TODO: this condition is wrong, it only works for blocks whose\n            # parent breaks collapsing margins. It should work for blocks whose\n            # one of the ancestors breaks collapsing margins.\n            # See test_margin_break_clearance.\n            collapse_with_page = (\n                containing_block.is_for_root_element or adjoining_margins)\n            if collapse_with_page:\n                if box.style['margin_break'] == 'discard':\n                    box.margin_top = 0\n                elif box.style['margin_break'] == 'auto':\n                    if not context.forced_break:\n                        box.margin_top = 0\n\n        collapsed_margin = collapse_margin([*adjoining_margins, box.margin_top])\n        direction = containing_block.style['direction']\n        box.clearance = get_clearance(context, box, direction, collapsed_margin)\n        if box.clearance is not None:\n            top_border_edge = box.position_y + collapsed_margin + box.clearance\n            box.position_y = top_border_edge - box.margin_top\n            adjoining_margins = []\n\n    return block_level_layout_switch(\n        context, box, bottom_space, skip_stack, containing_block, page_is_empty,\n        absolute_boxes, fixed_boxes, adjoining_margins, first_letter_style,\n        first_line_style, discard, max_lines)\n\n\ndef block_level_layout_switch(context, box, bottom_space, skip_stack, containing_block,\n                              page_is_empty, absolute_boxes, fixed_boxes,\n                              adjoining_margins, first_letter_style, first_line_style,\n                              discard, max_lines):\n    \"\"\"Call the layout function corresponding to the ``box`` type.\"\"\"\n    if isinstance(box, boxes.TableBox):\n        result = table_layout(\n            context, box, bottom_space, skip_stack, containing_block,\n            page_is_empty, absolute_boxes, fixed_boxes)\n    elif isinstance(box, boxes.BlockBox):\n        return block_box_layout(\n            context, box, bottom_space, skip_stack, containing_block,\n            page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,\n            first_letter_style, first_line_style, discard, max_lines)\n    elif isinstance(box, boxes.BlockReplacedBox):\n        result = block_replaced_box_layout(context, box, containing_block)\n    elif isinstance(box, boxes.FlexBox):\n        result = flex_layout(\n            context, box, bottom_space, skip_stack, containing_block,\n            page_is_empty, absolute_boxes, fixed_boxes, discard)\n    elif isinstance(box, boxes.GridBox):\n        result = grid_layout(\n            context, box, bottom_space, skip_stack, containing_block,\n            page_is_empty, absolute_boxes, fixed_boxes)\n    else:  # pragma: no cover\n        raise TypeError(f'Layout for {type(box).__name__} not handled yet')\n    return (*result, None)\n\n\ndef block_box_layout(context, box, bottom_space, skip_stack,\n                     containing_block, page_is_empty, absolute_boxes,\n                     fixed_boxes, adjoining_margins, first_letter_style,\n                     first_line_style, discard, max_lines):\n    \"\"\"Lay out the block ``box``.\"\"\"\n    if (box.style['column_width'] != 'auto' or\n            box.style['column_count'] != 'auto'):\n        result = columns_layout(\n            context, box, bottom_space, skip_stack, containing_block, page_is_empty,\n            absolute_boxes, fixed_boxes, adjoining_margins, first_letter_style,\n            first_line_style)\n        resume_at = result[1]\n        # TODO: this condition and the whole relayout are probably wrong\n        if resume_at is None:\n            new_box = result[0]\n            columns_bottom_space = (\n                new_box.margin_bottom + new_box.padding_bottom +\n                new_box.border_bottom_width)\n            if columns_bottom_space:\n                remove_placeholders(\n                    context, [new_box], absolute_boxes, fixed_boxes)\n                bottom_space += columns_bottom_space\n                result = columns_layout(\n                    context, box, bottom_space, skip_stack, containing_block,\n                    page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,\n                    first_letter_style, first_line_style)\n        return (*result, None)\n    elif box.is_table_wrapper:\n        table_wrapper_width(\n            context, box, (containing_block.width, containing_block.height))\n    block_level_width(box, containing_block)\n\n    result = block_container_layout(\n        context, box, bottom_space, skip_stack, page_is_empty, absolute_boxes,\n        fixed_boxes, adjoining_margins, first_letter_style, first_line_style, discard,\n        max_lines)\n    # TODO: columns and flex items shouldn't be block boxes, this condition\n    # would then be useless when this is fixed.\n    if not (new_box := result[0]) or new_box.is_column or new_box.is_flex_item:\n        return result\n    if new_box.is_table_wrapper or new_box.establishes_formatting_context():\n        # Don't collide with floats\n        # https://www.w3.org/TR/CSS21/visuren.html#floats\n        position_x, position_y, _ = avoid_collisions(\n            context, new_box, containing_block, outer=False)\n        new_box.translate(\n            position_x - new_box.position_x, position_y - new_box.position_y)\n    return result\n\n\ndef block_level_width(box, containing_block, with_min_max=True):\n    \"\"\"Set the ``box`` width.\"\"\"\n    # 'cb' stands for 'containing block'\n    if isinstance(containing_block, boxes.Box):\n        cb_width = containing_block.width\n        direction = containing_block.style['direction']\n    else:\n        cb_width = containing_block[0]\n        # TODO: what is the real text direction?\n        direction = 'ltr'\n\n    padding_plus_border = (\n        box.padding_left + box.padding_right +\n        box.border_left_width + box.border_right_width)\n\n    # See https://www.w3.org/TR/CSS21/visudet.html#blockwidth.\n    # Set width. Only margin-left, margin-right and width can be 'auto'.\n    # We want:  width of containing block ==\n    #               margin-left + border-left-width + padding-left + width\n    #               + padding-right + border-right-width + margin-right\n    if box.width == 'auto':\n        box.width = cb_width - padding_plus_border\n        if box.margin_left != 'auto':\n            box.width -= box.margin_left\n        if box.margin_right != 'auto':\n            box.width -= box.margin_right\n    if with_min_max:\n        box.width = max(box.min_width, min(box.max_width, box.width))\n\n    # Set auto margins to 0 for boxes larger than containing block.\n    margin_width = padding_plus_border + box.width\n    if box.margin_left != 'auto':\n        margin_width += box.margin_left\n    if box.margin_right != 'auto':\n        margin_width += box.margin_right\n    if margin_width > cb_width:\n        if box.margin_left == 'auto':\n            box.margin_left = 0\n        if box.margin_right == 'auto':\n            box.margin_right = 0\n\n    # Right-align right-to-left boxes.\n    if direction == 'rtl' and not box.is_column:\n        box.position_x += cb_width - padding_plus_border - box.width\n        if box.margin_left != 'auto':\n            box.position_x -= box.margin_left\n        if box.margin_right != 'auto':\n            box.position_x -= box.margin_right\n\n    # Set margins according to width.\n    margin_sum = cb_width - padding_plus_border - box.width\n    if box.margin_left == box.margin_right == 'auto':\n        box.margin_left = margin_sum / 2\n        box.margin_right = margin_sum / 2\n    elif box.margin_left == 'auto' and box.margin_right != 'auto':\n        box.margin_left = margin_sum - box.margin_right\n    elif box.margin_left != 'auto' and box.margin_right == 'auto':\n        box.margin_right = margin_sum - box.margin_left\n\n\nblock_level_width.without_min_max = partial(block_level_width, with_min_max=False)\n\n\ndef relative_positioning(box, containing_block):\n    \"\"\"Translate the ``box`` if it is relatively positioned.\"\"\"\n    if box.style['position'] == 'relative':\n        resolve_position_percentages(box, containing_block)\n\n        if box.left != 'auto' and box.right != 'auto':\n            if box.style['direction'] == 'ltr':\n                translate_x = box.left\n            else:\n                translate_x = -box.right\n        elif box.left != 'auto':\n            translate_x = box.left\n        elif box.right != 'auto':\n            translate_x = -box.right\n        else:\n            translate_x = 0\n\n        if box.top != 'auto':\n            translate_y = box.top\n        elif box.bottom != 'auto':\n            translate_y = -box.bottom\n        else:\n            translate_y = 0\n\n        box.translate(translate_x, translate_y)\n\n    if isinstance(box, (boxes.InlineBox, boxes.LineBox)):\n        for child in box.children:\n            relative_positioning(child, containing_block)\n\n\ndef _out_of_flow_layout(context, box, index, child, new_children,\n                        page_is_empty, absolute_boxes, fixed_boxes,\n                        adjoining_margins, bottom_space):\n    stop = False  # whether we should stop parent rendering after this layout\n    resume_at = None  # where to resume in-flow rendering\n    new_child = None  # child rendered by this layout\n    out_of_flow_resume_at = None  # where to resume out-of-flow rendering\n\n    # Add the parent’s collapsing margins to shift the child’s position. Don’t\n    # include the out-of-flow child’s top margin because it doesn’t collapse\n    # with its parent.\n    child.position_y += collapse_margin(adjoining_margins)\n\n    # Absolute child layout: create placeholder.\n    if child.is_absolutely_positioned():\n        new_child = placeholder = AbsolutePlaceholder(child)\n        placeholder.index = index\n        new_children.append(placeholder)\n        if child.style['position'] == 'absolute':\n            absolute_boxes.append(placeholder)\n        else:\n            fixed_boxes.append(placeholder)\n\n    # Float child layout.\n    elif child.is_floated():\n        new_child, out_of_flow_resume_at = float_layout(\n            context, child, box, absolute_boxes, fixed_boxes, bottom_space,\n            skip_stack=None)\n\n        # Check that child doesn’t overflow page.\n        page_overflow = context.overflows_page(\n            bottom_space, new_child.position_y + new_child.height)\n        add_child = (\n            (page_is_empty and not new_children) or\n            not page_overflow or\n            box.is_monolithic())\n        if add_child:\n            # Child fits or has to fit, add it.\n            new_child.index = index\n            new_children.append(new_child)\n        else:\n            # Child doesn’t fit and we can break, find where to break and stop\n            # parent rendering.\n            last_in_flow_child = find_last_in_flow_child(new_children)\n            page_break = block_level_page_break(last_in_flow_child, child)\n            resume_at = {index: None}\n            out_of_flow_resume_at = None\n            stop = True\n            if new_children and avoid_page_break(page_break, context):\n                # Can’t break inside float, find an earlier page break.\n                result = find_earlier_page_break(\n                    context, new_children, absolute_boxes, fixed_boxes)\n                if result:\n                    # Earlier page break found, drop whole child rendering.\n                    new_children[:], resume_at = result\n                    new_child = None\n\n    # Running element layout.\n    elif child.is_running():\n        running_name = child.style['position'][1]\n        page = context.current_page\n        context.running_elements[running_name][page].append(child)\n\n    return stop, resume_at, new_child, out_of_flow_resume_at\n\n\ndef _break_line(context, box, line, new_children, needed, page_is_empty, index,\n                skip_stack, resume_at, absolute_boxes, fixed_boxes):\n    \"\"\"Break line where allowed by orphans and widows.\n\n    Return (abort, stop, resume_at).\n\n    \"\"\"\n    over_orphans = len(new_children) - box.style['orphans']\n    if over_orphans < 0 and not page_is_empty:\n        # Reached the bottom of the page before we had\n        # enough lines for orphans, cancel the whole box.\n        remove_placeholders(context, line.children, absolute_boxes, fixed_boxes)\n        return True, False, resume_at\n    # How many lines we need on the next page to satisfy widows\n    # -1 for the current line.\n    if needed > over_orphans and not page_is_empty:\n        # Total number of lines < orphans + widows\n        remove_placeholders(context, line.children, absolute_boxes, fixed_boxes)\n        return True, False, resume_at\n    if needed and needed <= over_orphans:\n        # Remove lines to keep them for the next page\n        for child in new_children[-needed:]:\n            remove_placeholders(\n                context, child.children, absolute_boxes, fixed_boxes)\n        del new_children[-needed:]\n    # Page break here, resume before this line\n    remove_placeholders(context, line.children, absolute_boxes, fixed_boxes)\n    return False, True, {index: skip_stack}\n\n\ndef _linebox_layout(context, box, index, child, new_children, page_is_empty,\n                    absolute_boxes, fixed_boxes, adjoining_margins,\n                    bottom_space, position_y, skip_stack, first_letter_style,\n                    first_line_style, draw_bottom_decoration, max_lines):\n    abort = stop = False\n    resume_at = None\n    new_footnotes = []\n\n    assert len(box.children) == 1, 'line box with siblings before layout'\n\n    if adjoining_margins:\n        position_y += collapse_margin(adjoining_margins)\n    new_containing_block = box\n    lines_iterator = iter_line_boxes(\n        context, child, position_y, bottom_space, skip_stack, new_containing_block,\n        absolute_boxes, fixed_boxes, first_letter_style, first_line_style)\n    for i, (line, resume_at) in enumerate(lines_iterator):\n        # Break box if we reached max-lines\n        if max_lines is not None:\n            if max_lines == 0:\n                new_children[-1].block_ellipsis = box.style['block_ellipsis']\n                break\n            max_lines -= 1\n\n        # Update line resume_at and position_y\n        line.resume_at = resume_at\n        new_position_y = line.position_y + line.height\n\n        # Add bottom padding and border to the bottom position of the\n        # box if needed\n        draw_bottom_decoration |= resume_at is None\n        if draw_bottom_decoration:\n            offset_y = box.border_bottom_width + box.padding_bottom\n        else:\n            offset_y = 0\n\n        # Allow overflow if the first line of the page is higher than the page itself so\n        # that we put *something* on this page and can advance in the context.\n        overflow = (\n            (new_children or not page_is_empty) and\n            context.overflows_page(bottom_space, new_position_y + offset_y))\n        if overflow:\n            # If we couldn’t break the line before but can break now, first try to\n            # report footnotes and see if we don’t overflow.\n            could_break_before = can_break_now = True\n            needed = box.style['widows'] - 1\n            for _ in lines_iterator:\n                needed -= 1\n                # Don’t iterate over all lines as it can be long.\n                if needed == -1:\n                    break\n            if len(new_children) + 1 < box.style['orphans']:\n                can_break_now = False\n            elif needed >= 0:\n                can_break_now = False\n            if len(new_children) < box.style['orphans']:\n                could_break_before = False\n            elif needed > 0:\n                could_break_before = False\n            needed = max(0, needed)\n            report = not context.in_column and can_break_now and not could_break_before\n            reported_footnotes = 0\n            while report and context.current_page_footnotes:\n                context.report_footnote(context.current_page_footnotes[-1])\n                reported_footnotes += 1\n                if not context.overflows_page(bottom_space, new_position_y + offset_y):\n                    new_children.append(line)\n                    stop = True\n                    break\n            else:\n                abort, stop, resume_at = _break_line(\n                    context, box, line, new_children, needed, page_is_empty, index,\n                    skip_stack, resume_at, absolute_boxes, fixed_boxes)\n\n            # Revert reported footnotes, as they’ve been reported starting from the last\n            # one.\n            if reported_footnotes >= 2:\n                extra = context.reported_footnotes[-1:-reported_footnotes-1:-1]\n                context.reported_footnotes[-reported_footnotes:] = extra\n\n            break\n\n        # TODO: this is incomplete.\n        # See https://drafts.csswg.org/css-page-3/#allowed-pg-brk\n        # \"When an unforced page break occurs here, both the adjoining\n        #  ‘margin-top’ and ‘margin-bottom’ are set to zero.\"\n        # See issue #115.\n        elif page_is_empty and context.overflows_page(bottom_space, new_position_y):\n            # Remove the top border when a page is empty and the box is\n            # too high to be drawn in one page\n            new_position_y -= box.margin_top\n            line.translate(0, -box.margin_top)\n            box.margin_top = 0\n\n        if context.footnotes:\n            break_linebox = False\n            footnotes = (\n                descendant.footnote for descendant in line.descendants()\n                if descendant.footnote in context.footnotes)\n            for footnote in footnotes:\n                overflow = context.layout_footnote(footnote)\n                new_footnotes.append(footnote)\n                overflow = (\n                    overflow or\n                    context.reported_footnotes or\n                    context.overflows_page(bottom_space, new_position_y + offset_y))\n                if overflow:\n                    context.report_footnote(footnote)\n                    # If we've put other content on this page, then we may want\n                    # to push this line or block to the next page. Otherwise,\n                    # we can't (and would loop forever if we tried), so don't\n                    # even try.\n                    if new_children or not page_is_empty:\n                        if footnote.style['footnote_policy'] == 'line':\n                            if needed := box.style['widows'] - 1:\n                                for _ in lines_iterator:\n                                    needed -= 1\n                                    # Don’t iterate over all lines as it can be long.\n                                    if needed == 0:\n                                        break\n                            abort, stop, resume_at = _break_line(\n                                context, box, line, new_children, needed, page_is_empty,\n                                index, skip_stack, resume_at, absolute_boxes,\n                                fixed_boxes)\n                            break_linebox = True\n                            break\n                        elif footnote.style['footnote_policy'] == 'block':\n                            abort = break_linebox = True\n                            break\n            if break_linebox:\n                break\n\n        new_children.append(line)\n        position_y = new_position_y\n        skip_stack = resume_at\n\n    if new_children:\n        resume_at = {index: new_children[-1].resume_at}\n\n    return abort, stop, resume_at, position_y, new_footnotes, max_lines\n\n\ndef _in_flow_layout(context, box, index, child, new_children, page_is_empty,\n                    absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,\n                    position_y, skip_stack, first_letter_style, first_line_style,\n                    discard, next_page, max_lines):\n    abort = stop = False\n\n    # Find possible page break between in-flow siblings.\n    last_in_flow_child = find_last_in_flow_child(new_children)\n    if last_in_flow_child is not None:\n        page_break = block_level_page_break(last_in_flow_child, child)\n        page_name = block_level_page_name(last_in_flow_child, child)\n        if page_name or force_page_break(page_break, context):\n            page_name = child.page_values()[0]\n            next_page = {'break': page_break, 'page': page_name}\n            resume_at = {index: None}\n            stop = True\n            return (\n                abort, stop, resume_at, position_y, adjoining_margins,\n                next_page, new_children, max_lines)\n    else:\n        page_break = 'auto'\n\n    # Resolve percentages and collapsing top margins.\n    if not box.is_table_wrapper:\n        resolve_percentages(child, box)\n        if last_in_flow_child is None and box.top_margin_collapses():\n            # TODO: add the adjoining descendants' margin top to\n            # [child.margin_top].\n            old_collapsed_margin = collapse_margin(adjoining_margins)\n            # TODO: the margin-top value is set afterwards in\n            # block_level_layout, we shouldn’t duplicate this code.\n            child_margin_top = child.margin_top\n            if child_margin_top == 'auto':\n                child_margin_top = 0\n            elif context.current_page > 1 and page_is_empty:\n                if box.style['margin_break'] == 'discard':\n                    child_margin_top = 0\n                elif box.style['margin_break'] == 'auto':\n                    if not context.forced_break:\n                        child_margin_top = 0\n            new_collapsed_margin = collapse_margin(\n                [*adjoining_margins, child_margin_top])\n            collapsed_margin_difference = (\n                new_collapsed_margin - old_collapsed_margin)\n            for previous_new_child in new_children:\n                previous_new_child.translate(dy=collapsed_margin_difference)\n            direction = box.style['direction']\n            clearance = get_clearance(context, child, direction, new_collapsed_margin)\n            if clearance is not None:\n                for previous_new_child in new_children:\n                    previous_new_child.translate(\n                        dy=-collapsed_margin_difference)\n\n                collapsed_margin = collapse_margin(adjoining_margins)\n                box.position_y += collapsed_margin - box.margin_top\n                # Count box.margin_top as we emptied adjoining_margins\n                adjoining_margins = []\n                position_y = box.content_box_y()\n\n    # TODO: Merge this with block_container_layout, block_level_layout, _in_flow_layout,\n    # and check code above.\n    if adjoining_margins:\n        if box.is_table_wrapper:  # should not be a special case\n            collapsed_margin = collapse_margin(adjoining_margins)\n            child.position_y += collapsed_margin\n            adjoining_margins = []\n        elif not isinstance(child, boxes.BlockBox):  # blocks handle that themselves\n            if child.style['margin_top'] == 'auto':\n                margin_top = 0\n            else:\n                margin_top = percentage(\n                    child.style['margin_top'], child.style, box.width)\n            adjoining_margins.append(margin_top)\n            offset_y = collapse_margin(adjoining_margins) - margin_top\n            child.position_y += offset_y\n            adjoining_margins = []\n\n    page_is_empty_with_no_children = page_is_empty and not any(\n        child for child in new_children\n        if not isinstance(child, AbsolutePlaceholder))\n\n    (new_child, resume_at, next_page, next_adjoining_margins,\n     collapsing_through, max_lines) = block_level_layout(\n         context, child, bottom_space, skip_stack, box, page_is_empty_with_no_children,\n         absolute_boxes, fixed_boxes, adjoining_margins, first_letter_style,\n         first_line_style, discard, max_lines)\n\n    # Check that child doesn’t overflow and set next position_y.\n    if new_child is not None:\n        if not collapsing_through:\n            # Find content position and check that it doesn’t overflow.\n            new_content_position_y = new_child.content_box_y() + new_child.height\n            content_page_overflow = context.overflows_page(\n                bottom_space, new_content_position_y)\n\n            # Update bottom space to include new child bottom spacing and check that it\n            # doesn’t overflow.\n            bottom_space += new_child.padding_bottom + new_child.border_bottom_width\n            if not box.bottom_margin_collapses():\n                bottom_space += new_child.margin_bottom\n            new_position_y = new_child.border_box_y() + new_child.border_height()\n            border_page_overflow = context.overflows_page(bottom_space, new_position_y)\n\n            can_break = not (page_is_empty_with_no_children or box.is_monolithic())\n            if can_break and content_page_overflow:\n                # Child content overflows the page area, display it on the next page.\n                remove_placeholders(context, [new_child], absolute_boxes, fixed_boxes)\n                new_child = None\n            elif can_break and border_page_overflow:\n                # Child border/padding/margin overflows the page area, do the layout\n                # again with a bottom_space value that includes them.\n                remove_placeholders(context, [new_child], absolute_boxes, fixed_boxes)\n                (new_child, resume_at, next_page, next_adjoining_margins,\n                 collapsing_through, max_lines) = block_level_layout(\n                     context, child, bottom_space, skip_stack, box,\n                     page_is_empty_with_no_children, absolute_boxes, fixed_boxes,\n                     adjoining_margins, discard, max_lines)\n                if new_child:\n                    position_y = new_child.border_box_y() + new_child.border_height()\n            else:\n                position_y = new_position_y\n\n        # Use the new child adjoining margins.\n        adjoining_margins = next_adjoining_margins\n        if new_child:\n            adjoining_margins.append(new_child.margin_bottom)\n\n        # Handle clearance.\n        if new_child and new_child.clearance:\n            position_y = new_child.border_box_y() + new_child.border_height()\n\n    if new_child is None:\n        # Nothing fits in the remaining space of this page: break.\n        if avoid_page_break(page_break, context):\n            # TODO: fill the blank space at the bottom of the page.\n            result = find_earlier_page_break(\n                context, new_children, absolute_boxes, fixed_boxes)\n            if result:\n                new_children, resume_at = result\n                stop = True\n                return (\n                    abort, stop, resume_at, position_y, adjoining_margins,\n                    next_page, new_children, max_lines)\n            else:\n                # We did not find any page break opportunity.\n                if not page_is_empty:\n                    # The page has content *before* this block: cancel the block and try\n                    # to find a break in the parent.\n                    abort = True\n                    return (\n                        abort, stop, resume_at, position_y, adjoining_margins,\n                        next_page, new_children, max_lines)\n                # else:\n                # ignore this 'avoid' and break anyway.\n\n        if all(child.is_absolutely_positioned() for child in new_children):\n            # This box has only rendered absolute children, keep them for the next page.\n            # This is for example useful for list markers.\n            remove_placeholders(context, new_children, absolute_boxes, fixed_boxes)\n            new_children = []\n\n        if new_children:\n            # We already have children, keep them and stop the box rendering.\n            resume_at = {index: None}\n            stop = True\n        else:\n            # This was the first child of this box, cancel the box completly.\n            abort = True\n        return (\n            abort, stop, resume_at, position_y, adjoining_margins, next_page,\n            new_children, max_lines)\n\n    # Index in its non-laid-out parent, not in future new parent.\n    # May be used in find_earlier_page_break().\n    new_child.index = index\n\n    new_children.append(new_child)\n    if resume_at is not None:\n        resume_at = {index: resume_at}\n        stop = True\n\n    return (\n        abort, stop, resume_at, position_y, adjoining_margins, next_page,\n        new_children, max_lines)\n\n\ndef block_container_layout(context, box, bottom_space, skip_stack, page_is_empty,\n                           absolute_boxes, fixed_boxes, adjoining_margins,\n                           first_letter_style, first_line_style, discard, max_lines):\n    \"\"\"Set the ``box`` height.\"\"\"\n    assert isinstance(box, boxes.BlockContainerBox)\n\n    if box.establishes_formatting_context():\n        context.create_block_formatting_context(box)\n\n    # TODO: merge this with _in_flow_layout, flex_layout…\n    is_start = skip_stack is None\n    box.remove_decoration(start=not is_start, end=False)\n\n    discard |= box.style['continue'] == 'discard'\n    draw_bottom_decoration = discard or box.style['box_decoration_break'] == 'clone'\n\n    if adjoining_margins is None:\n        adjoining_margins = []\n\n    if draw_bottom_decoration:\n        bottom_space += box.padding_bottom + box.border_bottom_width + box.margin_bottom\n\n    adjoining_margins.append(box.margin_top)\n    this_box_adjoining_margins = adjoining_margins\n\n    if box.top_margin_collapses():\n        # Not counting margins in adjoining_margins, if any (there are not padding or\n        # borders, see above).\n        position_y = box.position_y\n    else:\n        box.position_y += collapse_margin(adjoining_margins) - box.margin_top\n        adjoining_margins = []\n        position_y = box.content_box_y()\n\n    position_x = box.content_box_x()\n\n    if box.style['position'] == 'relative':\n        # New containing block, use a new absolute list.\n        absolute_boxes = []\n\n    new_children = []\n    next_page = {'break': 'any', 'page': None}\n\n    if box.style['max_lines'] != 'none':\n        max_lines = min(box.style['max_lines'], max_lines or inf)\n\n    if box.first_line_style is not None:\n        box_first_line_style = {\n            key: box.first_line_style[key] for key in box.first_line_style.cascaded}\n        if first_line_style is None:\n            first_line_style = box_first_line_style\n        else:\n            first_line_style |= box_first_line_style\n\n    if box.first_letter_style is not None:\n        box_first_letter_style = {\n            key: box.first_letter_style[key] for key in box.first_letter_style.cascaded}\n        if first_letter_style is None:\n            first_letter_style = box_first_letter_style\n        else:\n            first_letter_style |= box_first_letter_style\n\n    # Layout box children.\n    if is_start:\n        skip = 0\n    else:\n        (skip, skip_stack), = skip_stack.items()\n    for index, child in enumerate(box.children[skip:], start=(skip or 0)):\n        child.position_x = position_x\n        child.position_y = position_y  # doesn’t count adjoining_margins\n        new_footnotes = []\n\n        if not child.is_in_normal_flow():\n            # Layout out-of-flow child.\n            abort = False\n            stop, resume_at, new_child, out_of_flow_resume_at = (\n                _out_of_flow_layout(\n                    context, box, index, child, new_children, page_is_empty,\n                    absolute_boxes, fixed_boxes, adjoining_margins, bottom_space))\n            if out_of_flow_resume_at:\n                context.add_broken_out_of_flow(\n                    new_child, child, box, out_of_flow_resume_at)\n            if child.is_outside_marker:\n                new_child.position_x = box.border_box_x()\n                if child.style['direction'] == 'rtl':\n                    new_child.position_x += box.width + box.padding_right\n\n        elif isinstance(child, boxes.LineBox):\n            # Layout line child.\n            (abort, stop, resume_at, position_y,\n             new_footnotes, max_lines) = _linebox_layout(\n                context, box, index, child, new_children, page_is_empty,\n                absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,\n                position_y, skip_stack, first_letter_style, first_line_style,\n                draw_bottom_decoration, max_lines)\n            draw_bottom_decoration |= resume_at is None\n            adjoining_margins = []\n            first_letter_style = first_line_style = None\n\n        else:\n            # Layout in-flow child.\n            (abort, stop, resume_at, position_y, adjoining_margins,\n             next_page, new_children, new_max_lines) = _in_flow_layout(\n                 context, box, index, child, new_children, page_is_empty,\n                 absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,\n                 position_y, skip_stack, first_letter_style, first_line_style,\n                 discard, next_page, max_lines)\n            skip_stack = None\n            first_letter_style = first_line_style = None\n\n            # Handle max-lines.\n            if None not in (new_max_lines, max_lines):\n                max_lines = new_max_lines\n                if max_lines <= 0:\n                    # Maximum number of lines reached, stop box rendering.\n                    stop = True\n                    last_child = (child == box.children[-1])\n                    if not last_child:\n                        # This is not the last line, recursively find the last line to\n                        # set ellipsis on it.\n                        children = new_children\n                        while children:\n                            last_child = children[-1]\n                            if isinstance(last_child, boxes.LineBox):\n                                last_child.block_ellipsis = box.style['block_ellipsis']\n                            elif isinstance(last_child, boxes.ParentBox):\n                                children = last_child.children\n                                continue\n                            break\n\n        if abort:\n            # Abort the rendering of box.\n            page = child.page_values()[0]\n            remove_placeholders(\n                context, box.children[skip:], absolute_boxes, fixed_boxes)\n            for footnote in new_footnotes:\n                context.unlayout_footnote(footnote)\n            if box.establishes_formatting_context():\n                context.finish_block_formatting_context()\n            return (\n                None, None, {'break': 'any', 'page': page}, [], False,\n                max_lines)\n        elif stop:\n            # Stop after the rendering of child.\n            if box.height != 'auto':\n                box_bottom = box.position_y + box.border_height()\n                bottom_margin = collapse_margin(adjoining_margins)\n                if context.overflows(box_bottom, position_y - bottom_margin):\n                    # Box height is fixed and it overflows the page, forget\n                    # overflowing children.\n                    resume_at = None\n            adjoining_margins = []\n            break\n\n    else:\n        resume_at = None\n\n    box_is_fragmented = resume_at is not None or box.force_fragmentation\n    if box.style['continue'] == 'discard':\n        resume_at = None\n\n    if (box_is_fragmented and\n            avoid_page_break(box.style['break_inside'], context) and\n            not page_is_empty):\n        remove_placeholders(\n            context, [*new_children, *box.children[skip:]], absolute_boxes, fixed_boxes)\n        if box.establishes_formatting_context():\n            context.finish_block_formatting_context()\n        return None, None, {'break': 'any', 'page': None}, [], False, max_lines\n\n    if box.top_margin_collapses():\n        box.position_y += collapse_margin(this_box_adjoining_margins) - box.margin_top\n\n    # Detect collapsing-through situation and collapse margins accordingly.\n    last_in_flow_child = find_last_in_flow_child(new_children)\n    collapsing_through = False\n    if last_in_flow_child is None:\n        # No in-flow child in box, collapse its top and bottom margins.\n        collapsed_margin = collapse_margin(adjoining_margins)\n        direction = box.style['direction']  # TODO: should be parent’s one instead\n        if (box.height in ('auto', 0) and\n            get_clearance(context, box, direction, collapsed_margin) is None and\n            all(value == 0 for value in (\n                box.min_height, box.border_top_width, box.padding_top,\n                box.border_bottom_width, box.padding_bottom))):\n            # Collapse through the box.\n            collapsing_through = True\n        else:\n            # Don’t collapse through the box, update position and reset margins.\n            position_y += collapsed_margin\n            adjoining_margins = []\n    else:\n        # In-flow children, collapse last child bottom margin and box bottom margin.\n        if box.height != 'auto':\n            # Not adjoining, reset margins.\n            adjoining_margins = []\n\n    if not box.bottom_margin_collapses():\n        position_y += collapse_margin(adjoining_margins)\n        adjoining_margins = []\n\n    # Add block ellipsis\n    if box_is_fragmented and new_children:\n        last_child = new_children[-1]\n        if isinstance(last_child, boxes.LineBox):\n            last_child.block_ellipsis = box.style['block_ellipsis']\n\n    new_box = box.copy_with_children(new_children)\n    new_box.remove_decoration(\n        start=not is_start, end=box_is_fragmented and not discard)\n\n    # TODO: See corner cases in\n    # https://www.w3.org/TR/CSS21/visudet.html#normal-block\n    # TODO: See float.float_layout\n    if new_box.height == 'auto':\n        if context.excluded_shapes and new_box.style['overflow'] != 'visible':\n            max_float_position_y = max(\n                float_box.position_y + float_box.margin_height()\n                for float_box in context.excluded_shapes)\n            position_y = max(max_float_position_y, position_y)\n        if position_y == new_box.content_box_y() == inf:\n            new_box.height = 0\n        else:\n            new_box.height = position_y - new_box.content_box_y()\n\n    if new_box.style['position'] == 'relative':\n        # New containing block, resolve the layout of the absolute descendants\n        for absolute_box in absolute_boxes:\n            absolute_layout(\n                context, absolute_box, new_box, fixed_boxes, bottom_space,\n                skip_stack=None)\n\n    for child in new_box.children:\n        relative_positioning(child, (new_box.width, new_box.height))\n\n    if box.establishes_formatting_context():\n        context.finish_block_formatting_context(new_box)\n\n    if discard or not box_is_fragmented:\n        # After finish_block_formatting_context which may increment\n        # new_box.height\n        new_box.height = max(\n            min(new_box.height, new_box.max_height), new_box.min_height)\n    elif bottom_space > -inf and not new_box.is_column:\n        # Make the box fill the blank space at the bottom of the page\n        # https://www.w3.org/TR/css-break-3/#box-splitting\n        new_box_height = (\n            context.page_bottom - bottom_space - new_box.position_y -\n            (new_box.margin_height() - new_box.height))\n        if new_box_height > new_box.height:\n            new_box.height = new_box_height\n            if draw_bottom_decoration:\n                new_box.height += (\n                    box.padding_bottom + box.border_bottom_width +\n                    box.margin_bottom)\n\n    if next_page['page'] is None:\n        next_page['page'] = new_box.page_values()[1]\n\n    return (\n        new_box, resume_at, next_page, adjoining_margins, collapsing_through,\n        max_lines)\n\n\ndef collapse_margin(adjoining_margins):\n    \"\"\"Get the amount of collapsed margin for a list of adjoining margins.\"\"\"\n    margins = [0]  # add 0 to make sure that max/min don’t get an empty list\n    margins.extend(adjoining_margins)\n    positives = (m for m in margins if m >= 0)\n    negatives = (m for m in margins if m <= 0)\n    return max(positives) + min(negatives)\n\n\ndef block_level_page_break(sibling_before, sibling_after):\n    \"\"\"Get the correct page break value between siblings.\n\n    Return the value of ``page-break-before`` or ``page-break-after`` that\n    \"wins\" for boxes that meet at the margin between two sibling boxes.\n\n    For boxes before the margin, the 'page-break-after' value is considered;\n    for boxes after the margin the 'page-break-before' value is considered.\n\n    * 'avoid' takes priority over 'auto'\n    * 'page' takes priority over 'avoid' or 'auto'\n    * 'left' or 'right' take priority over 'always', 'avoid' or 'auto'\n    * Among 'left' and 'right', later values in the tree take priority.\n\n    See https://drafts.csswg.org/css-page-3/#allowed-pg-brk\n\n    \"\"\"\n    values = []\n    # https://drafts.csswg.org/css-break-3/#possible-breaks\n    block_parallel_box_types = (\n        boxes.BlockLevelBox, boxes.TableRowGroupBox, boxes.TableRowBox)\n\n    box = sibling_before\n    while isinstance(box, block_parallel_box_types):\n        values.append(box.style['break_after'])\n        if not box.children:\n            break\n        box = box.children[-1]\n    values.reverse()  # Have them in tree order\n\n    box = sibling_after\n    while isinstance(box, block_parallel_box_types):\n        values.append(box.style['break_before'])\n        if not box.children:\n            break\n        box = box.children[0]\n\n    result = 'auto'\n    for value in values:\n        if value in ('left', 'right', 'recto', 'verso') or (value, result) in (\n                ('page', 'auto'),\n                ('page', 'avoid'),\n                ('page', 'avoid-page'),\n                ('page', 'avoid-column'),\n                ('column', 'auto'),\n                ('column', 'avoid'),\n                ('column', 'avoid-page'),\n                ('column', 'avoid-column'),\n                ('avoid', 'auto'),\n                ('avoid-page', 'auto'),\n                ('avoid-column', 'auto')):\n            result = value\n\n    return result\n\n\ndef block_level_page_name(sibling_before, sibling_after):\n    \"\"\"Return the next page name when siblings don't have the same names.\"\"\"\n    before_page = sibling_before.page_values()[1]\n    after_page = sibling_after.page_values()[0]\n    if before_page != after_page:\n        return after_page\n\n\ndef find_earlier_page_break(context, children, absolute_boxes, fixed_boxes):\n    \"\"\"Find the last possible page break in ``children``.\n\n    Because of a `page-break-before: avoid` or a `page-break-after: avoid` we\n    need to find an earlier page break opportunity inside `children`.\n\n    Absolute or fixed placeholders removed from children should also be\n    removed from `absolute_boxes` or `fixed_boxes`.\n\n    Return (new_children, resume_at).\n\n    \"\"\"\n    if children and isinstance(children[0], boxes.LineBox):\n        # Normally `orphans` and `widows` apply to the block container, but\n        # line boxes inherit them.\n        orphans = children[0].style['orphans']\n        widows = children[0].style['widows']\n        index = len(children) - widows  # how many lines we keep\n        if index < orphans:\n            return None\n        new_children = children[:index]\n        resume_at = {0: new_children[-1].resume_at}\n        remove_placeholders(\n            context, children[index:], absolute_boxes, fixed_boxes)\n        return new_children, resume_at\n\n    previous_in_flow = None\n    for index, child in reversed_enumerate(children):\n        if isinstance(child, boxes.TableRowGroupBox) and (\n                child.is_header or child.is_footer):\n            # We don’t want to break pages before table headers or footers.\n            continue\n        elif child.is_column:\n            # We don’t want to break pages between columns.\n            continue\n\n        if child.is_in_normal_flow():\n            page_break = block_level_page_break(child, previous_in_flow)\n            if previous_in_flow is not None and (\n                    not avoid_page_break(page_break, context)):\n                index += 1  # break after child\n                new_children = children[:index]\n                # Get the index in the original parent\n                resume_at = {children[index].index: None}\n                break\n            previous_in_flow = child\n\n        if child.is_in_normal_flow() and (\n                not avoid_page_break(child.style['break_inside'], context)):\n            breakable_box_types = (\n                boxes.BlockBox, boxes.TableBox, boxes.TableRowGroupBox)\n            if isinstance(child, breakable_box_types):\n                result = find_earlier_page_break(\n                    context, child.children, absolute_boxes, fixed_boxes)\n                if result:\n                    new_grand_children, resume_at = result\n                    new_child = child.copy_with_children(new_grand_children)\n                    new_children = [*children[:index], new_child]\n\n                    # Re-add footer at the end of split table\n                    # TODO: fix table height and footer position\n                    if isinstance(child, boxes.TableRowGroupBox):\n                        for next_child in children[index:]:\n                            if next_child.is_footer:\n                                new_children.append(next_child)\n\n                    # Index in the original parent\n                    resume_at = {new_child.index: resume_at}\n                    index += 1  # Remove placeholders after child\n                    break\n    else:\n        return None\n\n    # TODO: don’t remove absolute and fixed placeholders found in table footers\n    remove_placeholders(context, children[index:], absolute_boxes, fixed_boxes)\n    return new_children, resume_at\n\n\ndef find_last_in_flow_child(children):\n    \"\"\"Find and return the last in-flow child of given ``children``.\"\"\"\n    for child in reversed(children):\n        if child.is_in_normal_flow():\n            return child\n\n\ndef reversed_enumerate(seq):\n    \"\"\"Like reversed(list(enumerate(seq))) without copying the whole seq.\"\"\"\n    return zip(reversed(range(len(seq))), reversed(seq))\n\n\ndef remove_placeholders(context, box_list, absolute_boxes, fixed_boxes):\n    \"\"\"Remove placeholders from absolute and fixed lists.\n\n    For boxes that have been removed in find_earlier_page_break(), remove the\n    matching placeholders in absolute_boxes and fixed_boxes.\n\n    Also takes care of removed footnotes and floats.\n\n    \"\"\"\n    for box in box_list:\n        if isinstance(box, boxes.ParentBox):\n            remove_placeholders(\n                context, box.children, absolute_boxes, fixed_boxes)\n        if box.style['position'] == 'absolute' and box in absolute_boxes:\n            absolute_boxes.remove(box)\n        elif box.style['position'] == 'fixed' and box in fixed_boxes:\n            fixed_boxes.remove(box)\n        if box.footnote:\n            context.unlayout_footnote(box.footnote)\n        if box in context.broken_out_of_flow:\n            context.broken_out_of_flow.pop(box)\n\n\ndef avoid_page_break(page_break, context):\n    \"\"\"Test whether we should avoid breaks.\"\"\"\n    if context.in_column:\n        return page_break in ('avoid', 'avoid-page', 'avoid-column')\n    return page_break in ('avoid', 'avoid-page')\n\n\ndef force_page_break(page_break, context):\n    \"\"\"Test whether we should force breaks.\"\"\"\n    if context.in_column:\n        return page_break in (\n            'page', 'left', 'right', 'recto', 'verso', 'column')\n    return page_break in ('page', 'left', 'right', 'recto', 'verso')\n"
  },
  {
    "path": "weasyprint/layout/column.py",
    "content": "\"\"\"Layout for columns.\"\"\"\n\nfrom math import floor, inf\n\nfrom .absolute import absolute_layout\nfrom .percent import percentage, resolve_percentages\n\n\ndef columns_layout(context, box, bottom_space, skip_stack, containing_block,\n                   page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,\n                   first_letter_style, first_line_style):\n    \"\"\"Lay out a multi-column ``box``.\"\"\"\n    from .block import (  # isort:skip\n        block_box_layout, block_level_layout, block_level_width,\n        collapse_margin, remove_placeholders)\n\n    style = box.style\n    width = style['column_width']\n    count = style['column_count']\n    height = style['height']\n    original_bottom_space = bottom_space\n    context.in_column = True\n\n    if style['position'] == 'relative':\n        # New containing block, use a new absolute list\n        absolute_boxes = []\n\n    box = box.copy_with_children(box.children)\n    box.position_y += collapse_margin(adjoining_margins)\n\n    # Set height if defined\n    if height != 'auto' and height.unit != '%':\n        assert height.unit.lower() == 'px'\n        height_defined = True\n        empty_space = context.page_bottom - box.content_box_y() - height.value\n        bottom_space = max(bottom_space, empty_space)\n    else:\n        height_defined = False\n\n    # TODO: the columns container width can be unknown if the containing block\n    # needs the size of this block to know its own size\n    block_level_width(box, containing_block)\n\n    if style['column_gap'] == 'normal':\n        # 1em because in column context\n        gap = style['font_size']\n    else:\n        gap = percentage(style['column_gap'], box.style, box.width)\n\n    # Define the number of columns and their widths\n    if width == 'auto' and count != 'auto':\n        width = max(0, box.width - (count - 1) * gap) / count\n    elif width != 'auto' and count == 'auto':\n        count = max(1, floor((box.width + gap) / (width + gap)))\n        width = (box.width + gap) / count - gap\n    else:  # overconstrained, with width != 'auto' and count != 'auto'\n        count = max(1, min(count, floor((box.width + gap) / (width + gap))))\n        width = (box.width + gap) / count - gap\n\n    # Handle column-span property with the following structure:\n    # columns_and_blocks = [\n    #     [column_child_1, column_child_2],\n    #     spanning_block,\n    #     …\n    # ]\n    columns_and_blocks = []\n    column_children = []\n    skip, = skip_stack.keys() if skip_stack else (0,)\n    for i, child in enumerate(box.children[skip:], start=skip):\n        if child.style['column_span'] == 'all':\n            if column_children:\n                columns_and_blocks.append(\n                    (i - len(column_children), column_children))\n            columns_and_blocks.append((i, child.copy()))\n            column_children = []\n            continue\n        column_children.append(child.copy())\n    if column_children:\n        columns_and_blocks.append(\n            (i + 1 - len(column_children), column_children))\n\n    if skip_stack:\n        skip_stack = {0: skip_stack[skip]}\n\n    if not box.children:\n        next_page = {'break': 'any', 'page': None}\n        skip_stack = None\n\n    # Find height and balance.\n    #\n    # The current algorithm starts from the total available height, to check\n    # whether the whole content can fit. If it doesn’t fit, we keep the partial\n    # rendering. If it fits, we try to balance the columns starting from the\n    # ideal height (the total height divided by the number of columns). We then\n    # iterate until the last column is not the highest one. At the end of each\n    # loop, we add the minimal height needed to make one direct child at the\n    # top of one column go to the end of the previous column.\n    #\n    # We rely on a real rendering for each loop, and with a stupid algorithm\n    # like this it can last minutes…\n\n    adjoining_margins = []\n    current_position_y = box.content_box_y()\n    new_children = []\n    column_skip_stack = None\n    last_loop = False\n    break_page = False\n    footnote_area_heights = [\n        0 if context.current_footnote_area.height == 'auto'\n        else context.current_footnote_area.margin_height()]\n    last_footnotes_height = 0\n    for index, column_children_or_block in columns_and_blocks:\n        if not isinstance(column_children_or_block, list):\n            # We have a spanning block, we display it like other blocks\n            block = column_children_or_block\n            resolve_percentages(block, containing_block)\n            block.position_x = box.content_box_x()\n            block.position_y = current_position_y\n            new_child, resume_at, next_page, adjoining_margins, _, _ = (\n                block_level_layout(\n                    context, block, original_bottom_space, skip_stack, containing_block,\n                    page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,\n                    first_letter_style, first_line_style))\n            skip_stack = None\n            if new_child is None:\n                last_loop = True\n                break_page = True\n                break\n            new_children.append(new_child)\n            current_position_y = (\n                new_child.border_height() + new_child.border_box_y())\n            adjoining_margins.append(new_child.margin_bottom)\n            if resume_at:\n                last_loop = True\n                break_page = True\n                column_skip_stack = resume_at\n                break\n            page_is_empty = False\n            continue\n\n        # We have a list of children that we have to balance between columns\n        column_children = column_children_or_block\n\n        # Find the total height available for the first run\n        current_position_y += collapse_margin(adjoining_margins)\n        adjoining_margins = []\n        column_box = _create_column_box(\n            box, containing_block, column_children, width, current_position_y)\n        height = max_height = (\n            context.page_bottom - current_position_y - original_bottom_space)\n\n        # Try to render columns until the content fits, increase the column\n        # height step by step\n        column_skip_stack = skip_stack\n        lost_space = inf\n        original_excluded_shapes = context.excluded_shapes[:]\n        original_page_is_empty = page_is_empty\n        page_is_empty = stop_rendering = balancing = False\n        while True:\n            # Remove extra excluded shapes introduced during the previous loop\n            while len(context.excluded_shapes) > len(original_excluded_shapes):\n                context.excluded_shapes.pop()\n\n            # Render the columns\n            column_skip_stack = skip_stack\n            consumed_heights = []\n            new_boxes = []\n            for i in range(count):\n                # Render one column\n                new_box, resume_at, next_page, _, _, _ = block_box_layout(\n                    context, column_box,\n                    context.page_bottom - current_position_y - height,\n                    column_skip_stack, containing_block,\n                    page_is_empty or not balancing, [], [], [], first_letter_style,\n                    first_line_style, discard=False, max_lines=None)\n                if new_box is None:\n                    # We didn't render anything, retry\n                    column_skip_stack = {0: None}\n                    break\n                new_boxes.append(new_box)\n                column_skip_stack = resume_at\n\n                # Calculate consumed height, empty space and next box height\n                in_flow_children = [\n                    child for child in new_box.children\n                    if child.is_in_normal_flow()]\n                if in_flow_children:\n                    # Get the empty space at the bottom of the column box\n                    consumed_height = (\n                        in_flow_children[-1].margin_height() +\n                        in_flow_children[-1].position_y - current_position_y)\n                    empty_space = height - consumed_height\n                    consumed_height -= in_flow_children[-1].margin_bottom\n\n                    # Get the minimum size needed to render the next box\n                    next_box_height = 0\n                    if column_skip_stack:\n                        next_box = block_box_layout(\n                            context, column_box, inf, column_skip_stack,\n                            containing_block, True, [], [], [], first_letter_style,\n                            first_line_style, discard=False, max_lines=None)[0]\n                        for child in next_box.children:\n                            if child.is_in_normal_flow():\n                                next_box_height = child.margin_height()\n                                break\n                        remove_placeholders(context, [next_box], [], [])\n                else:\n                    consumed_height = empty_space = next_box_height = 0\n\n                consumed_heights.append(consumed_height)\n\n                # Append the size needed to render the next box in this\n                # column.\n                #\n                # The next box size may be smaller than the empty space, for\n                # example when the next box can't be separated from its own\n                # next box. In this case we don't try to find the real value\n                # and let the workaround below fix this for us.\n                #\n                # We also want to avoid very small values that may have been\n                # introduced by rounding errors. As the workaround below at\n                # least adds 1 pixel for each loop, we can ignore lost spaces\n                # lower than 1px.\n                if next_box_height - empty_space > 1:\n                    lost_space = min(lost_space, next_box_height - empty_space)\n\n                # Stop if we already rendered the whole content\n                if resume_at is None:\n                    break\n\n            # Remove placeholders but keep the current footnote area height\n            last_footnotes_height = (\n                0 if context.current_footnote_area.height == 'auto'\n                else context.current_footnote_area.margin_height())\n            remove_placeholders(context, new_boxes, [], [])\n\n            if last_loop:\n                break\n\n            if balancing:\n                if column_skip_stack is None:\n                    # We rendered the whole content, stop\n                    break\n\n                # Increase the column heights and render them again\n                add_height = 1 if lost_space == inf else lost_space\n                height += add_height\n\n                if height > max_height:\n                    # We reached max height, stop rendering\n                    height = max_height\n                    stop_rendering = True\n                    break\n            else:\n                if last_footnotes_height not in footnote_area_heights:\n                    # Footnotes have been rendered, try to re-render with the\n                    # new footnote area height\n                    height -= last_footnotes_height - footnote_area_heights[-1]\n                    footnote_area_heights.append(last_footnotes_height)\n                    continue\n\n                everything_fits = (\n                    not column_skip_stack and\n                    max(consumed_heights) <= max_height)\n                if everything_fits:\n                    # Everything fits, start expanding columns at the average\n                    # of the column heights\n                    if (style['column_fill'] == 'balance' or\n                            index < columns_and_blocks[-1][0]):\n                        balancing = True\n                        height = sum(consumed_heights) / count\n                    else:\n                        break\n                else:\n                    # Content overflows even at maximum height, stop now and\n                    # let the columns continue on the next page\n                    stop_rendering = True\n                    break\n\n        # TODO: check style['max']-height\n        bottom_space = max(\n            bottom_space, context.page_bottom - current_position_y - height)\n\n        # Replace the current box children with real columns\n        i = 0\n        max_column_height = 0\n        columns = []\n        while True:\n            column_box = _create_column_box(\n                box, containing_block, column_children, width,\n                current_position_y)\n            if style['direction'] == 'rtl':\n                column_box.position_x += box.width - (i + 1) * width - i * gap\n            else:\n                column_box.position_x += i * (width + gap)\n            new_child, column_skip_stack, column_next_page, _, _, _ = (\n                block_box_layout(\n                    context, column_box, bottom_space, skip_stack, containing_block,\n                    original_page_is_empty, absolute_boxes, fixed_boxes, None,\n                    first_letter_style, first_line_style, discard=False,\n                    max_lines=None))\n            if new_child is None:\n                columns = []\n                break_page = True\n                break\n            next_page = column_next_page\n            skip_stack = column_skip_stack\n            columns.append(new_child)\n            max_column_height = max(\n                max_column_height, new_child.margin_height())\n            if skip_stack is None:\n                bottom_space = original_bottom_space\n                break\n            i += 1\n            if i == count and not height_defined:\n                # [If] a declaration that constrains the column height\n                # (e.g., using height or max-height). In this case,\n                # additional column boxes are created in the inline\n                # direction.\n                break\n\n        # Update the current y position and set the columns’ height\n        current_position_y += min(max_height, max_column_height)\n        for column in columns:\n            column.height = max_column_height\n            new_children.append(column)\n\n        skip_stack = None\n        page_is_empty = False\n\n        if stop_rendering:\n            break\n\n    # Report footnotes above the defined footnotes height\n    _report_footnotes(context, footnote_area_heights[-1])\n\n    if box.children and not new_children:\n        # The box has children but none can be drawn, let's skip the whole box\n        context.in_column = False\n        return None, (0, None), {'break': 'any', 'page': None}, [], False\n\n    # Set the height of the containing box\n    box.children = new_children\n    current_position_y += collapse_margin(adjoining_margins)\n    height = current_position_y - box.content_box_y()\n    if box.height == 'auto':\n        box.height = height\n        height_difference = 0\n    else:\n        height_difference = box.height - height\n\n    # Update the latest columns’ height to respect min-height\n    if box.min_height != 'auto' and box.min_height > box.height:\n        height_difference += box.min_height - box.height\n        box.height = box.min_height\n    for child in new_children[::-1]:\n        if child.is_column:\n            child.height += height_difference\n        else:\n            break\n\n    if style['position'] == 'relative':\n        # New containing block, resolve the layout of the absolute descendants\n        for absolute_box in absolute_boxes:\n            absolute_layout(\n                context, absolute_box, box, fixed_boxes, bottom_space,\n                skip_stack=None)\n\n    # Calculate skip stack\n    if column_skip_stack:\n        skip, = column_skip_stack.keys()\n        skip_stack = {index + skip: column_skip_stack[skip]}\n    elif break_page:\n        skip_stack = {index: None}\n\n    # Update page bottom according to the new footnotes\n    if context.current_footnote_area.height != 'auto':\n        context.page_bottom += footnote_area_heights[0]\n        context.page_bottom -= context.current_footnote_area.margin_height()\n\n    context.in_column = False\n    return box, skip_stack, next_page, [], False\n\n\ndef _report_footnotes(context, footnotes_height):\n    \"\"\"Report footnotes above the defined footnotes height.\"\"\"\n    if not context.current_page_footnotes:\n        return\n\n    # Report and count footnotes\n    reported_footnotes = 0\n    while context.current_footnote_area.margin_height() > footnotes_height:\n        context.report_footnote(context.current_page_footnotes[-1])\n        reported_footnotes += 1\n\n    # Revert reported footnotes, as they’ve been reported starting from the\n    # last one\n    if reported_footnotes >= 2:\n        extra = context.reported_footnotes[-1:-reported_footnotes-1:-1]\n        context.reported_footnotes[-reported_footnotes:] = extra\n\n\ndef _create_column_box(box, containing_block, children, width, position_y):\n    \"\"\"Create a column box including given children.\"\"\"\n    column_box = box.anonymous_from(box, children=children)\n    resolve_percentages(column_box, containing_block)\n    column_box.is_column = True\n    column_box.width = width\n    column_box.position_x = box.content_box_x()\n    column_box.position_y = position_y\n    return column_box\n"
  },
  {
    "path": "weasyprint/layout/flex.py",
    "content": "\"\"\"Layout for flex containers and flex-items.\"\"\"\n\nimport sys\nfrom math import inf, log10\n\nfrom ..css.properties import Dimension\nfrom ..formatting_structure import boxes\nfrom . import percent\nfrom .absolute import AbsolutePlaceholder, absolute_layout\nfrom .preferred import max_content_width, min_content_width, min_max\nfrom .table import find_in_flow_baseline, table_wrapper_width\n\n\nclass FlexLine(list):\n    \"\"\"Flex container line.\"\"\"\n\n\ndef flex_layout(context, box, bottom_space, skip_stack, containing_block, page_is_empty,\n                absolute_boxes, fixed_boxes, discard):\n    from . import block\n\n    # TODO: merge this with block_container_layout.\n    context.create_flex_formatting_context(box)\n    resume_at = None\n    next_page = {'break': 'any', 'page': None}\n\n    is_start = skip_stack is None\n    box.remove_decoration(start=not is_start, end=False)\n\n    discard |= box.style['continue'] == 'discard'\n    draw_bottom_decoration = discard or box.style['box_decoration_break'] == 'clone'\n\n    row_gap, column_gap = box.style['row_gap'], box.style['column_gap']\n\n    if draw_bottom_decoration:\n        bottom_space += box.padding_bottom + box.border_bottom_width + box.margin_bottom\n\n    if box.style['position'] == 'relative':\n        # New containing block, use a new absolute list\n        absolute_boxes = []\n\n    # References are to: https://www.w3.org/TR/css-flexbox-1/#layout-algorithm.\n\n    # 1 Initial setup, done in formatting_structure.build.\n\n    # 2 Determine the available main and cross space for the flex items.\n    if box.style['flex_direction'].startswith('row'):\n        main, cross = 'width', 'height'\n    else:\n        main, cross = 'height', 'width'\n\n    margin_left = 0 if box.margin_left == 'auto' else box.margin_left\n    margin_right = 0 if box.margin_right == 'auto' else box.margin_right\n\n    # Define available main space.\n    # TODO: min- and max-content not implemented.\n    if getattr(box, main) != 'auto':\n        # If that dimension of the flex container’s content box is a definite size…\n        available_main_space = getattr(box, main)\n    else:\n        # Otherwise, subtract the flex container’s margin, border, and padding…\n        if main == 'width':\n            available_main_space = (\n                containing_block.width -\n                margin_left - margin_right -\n                box.padding_left - box.padding_right -\n                box.border_left_width - box.border_right_width)\n        else:\n            available_main_space = inf\n\n    # Same as above for available cross space.\n    # TODO: min- and max-content not implemented.\n    if getattr(box, cross) != 'auto':\n        available_cross_space = getattr(box, cross)\n    else:\n        if cross == 'width':\n            available_cross_space = (\n                containing_block.width -\n                margin_left - margin_right -\n                box.padding_left - box.padding_right -\n                box.border_left_width - box.border_right_width)\n        else:\n            available_cross_space = inf\n\n    # 3 Determine the flex base size and hypothetical main size of each item.\n    parent_box = box.copy()\n    percent.resolve_percentages(parent_box, containing_block)\n    parent_box.remove_decoration(start=not is_start, end=False)\n    block.block_level_width(parent_box, containing_block)\n    children = sorted(box.children, key=lambda item: item.style['order'])\n    if skip_stack is not None:\n        (skip, skip_stack), = skip_stack.items()\n        if box.style['flex_direction'].endswith('-reverse'):\n            children = children[:skip + 1]\n        else:\n            children = children[skip:]\n        skip_stack = skip_stack\n    else:\n        skip, skip_stack = 0, None\n    child_skip_stack = skip_stack\n\n    if row_gap == 'normal':\n        row_gap = 0\n    elif row_gap.unit == '%':\n        if box.height == 'auto':\n            row_gap = 0\n        else:\n            row_gap = row_gap.value / 100 * box.height\n    else:\n        row_gap = row_gap.value\n    if column_gap == 'normal':\n        column_gap = 0\n    elif column_gap.unit == '%':\n        if box.width == 'auto':\n            column_gap = 0\n        else:\n            column_gap = column_gap.value / 100 * box.width\n    else:\n        column_gap = column_gap.value\n    if main == 'width':\n        main_gap, cross_gap = column_gap, row_gap\n    else:\n        main_gap, cross_gap = row_gap, column_gap\n\n    position_x = (\n        parent_box.position_x + parent_box.border_left_width + parent_box.padding_left)\n    if parent_box.margin_left != 'auto':\n        position_x += parent_box.margin_left\n    position_y = (\n        parent_box.position_y + parent_box.border_top_width + parent_box.padding_top)\n    if parent_box.margin_top != 'auto':\n        position_y += parent_box.margin_top\n    for index, child in enumerate(children):\n        if not child.is_flex_item:\n            # Absolute child layout: create placeholder.\n            if child.is_absolutely_positioned():\n                child.position_x = position_x\n                child.position_y = position_y\n                new_child = placeholder = AbsolutePlaceholder(child)\n                placeholder.index = index\n                children[index] = placeholder\n                if child.style['position'] == 'absolute':\n                    absolute_boxes.append(placeholder)\n                else:\n                    fixed_boxes.append(placeholder)\n            elif child.is_running():\n                running_name = child.style['position'][1]\n                page = context.current_page\n                context.running_elements[running_name][page].append(child)\n            continue\n        # See https://www.w3.org/TR/css-flexbox-1/#min-size-auto.\n        if main == 'width':\n            child_containing_block = (available_main_space, parent_box.height)\n        else:\n            child_containing_block = (parent_box.width, available_main_space)\n        percent.resolve_percentages(child, child_containing_block)\n        if child.is_table_wrapper:\n            table_wrapper_width(context, child, child_containing_block)\n        child.position_x = position_x\n        child.position_y = position_y\n        if child.style['min_width'] == 'auto':\n            specified_size = child.width\n            new_child = child.copy()\n            new_child.style = child.style.copy()\n            new_child.style['width'] = 'auto'\n            new_child.style['min_width'] = Dimension(0, 'px')\n            new_child.style['max_width'] = Dimension(inf, 'px')\n            content_size = min_content_width(context, new_child, outer=False)\n            transferred_size = None\n            if isinstance(child, boxes.ReplacedBox):\n                image = child.replacement\n                _, intrinsic_height, intrinsic_ratio = image.get_intrinsic_size(\n                    child.style['image_resolution'], child.style['font_size'])\n                if intrinsic_ratio and intrinsic_height:\n                    transferred_size = intrinsic_height * intrinsic_ratio\n                    content_size = max(\n                        child.min_width, min(child.max_width, content_size))\n            if specified_size != 'auto':\n                child.min_width = min(specified_size, content_size)\n            elif transferred_size is not None:\n                child.min_width = min(transferred_size, content_size)\n            else:\n                child.min_width = content_size\n        if child.style['min_height'] == 'auto':\n            # TODO: avoid calling block_level_layout, write min_content_height instead.\n            specified_size = child.height\n            new_child = child.copy()\n            new_child.style = child.style.copy()\n            new_child.style['height'] = 'auto'\n            new_child.style['min_height'] = Dimension(0, 'px')\n            new_child.style['max_height'] = Dimension(inf, 'px')\n            if new_child.style['width'] == 'auto':\n                new_child_width = max_content_width(context, new_child)\n                new_child.style['width'] = Dimension(new_child_width, 'px')\n            new_child = block.block_level_layout(\n                context, new_child, bottom_space, child_skip_stack, parent_box,\n                page_is_empty)[0]\n            content_size = new_child.height if new_child else 0\n            transferred_size = None\n            if isinstance(child, boxes.ReplacedBox):\n                image = child.replacement\n                intrinsic_width, _, intrinsic_ratio = image.get_intrinsic_size(\n                    child.style['image_resolution'], child.style['font_size'])\n                if intrinsic_ratio and intrinsic_width:\n                    transferred_size = intrinsic_width / intrinsic_ratio\n                    content_size = max(\n                        child.min_height, min(child.max_height, content_size))\n                elif not intrinsic_width:\n                    # TODO: wrongly set by block_level_layout, would be OK with\n                    # min_content_height.\n                    content_size = 0\n            if specified_size != 'auto':\n                child.min_height = min(specified_size, content_size)\n            elif transferred_size is not None:\n                child.min_height = min(transferred_size, content_size)\n            else:\n                child.min_height = content_size\n\n        if child.style['flex_basis'] == 'content':\n            flex_basis = 'content'\n        else:\n            flex_basis = percent.percentage(\n                child.style['flex_basis'], child.style, available_main_space)\n            if flex_basis == 'auto':\n                if (flex_basis := getattr(child, main)) == 'auto':\n                    flex_basis = 'content'\n\n        # 3.A If the item has a definite used flex basis…\n        if flex_basis != 'content':\n            child.flex_base_size = flex_basis\n            if main == 'width':\n                child.main_outer_extra = (\n                    child.border_left_width + child.border_right_width +\n                    child.padding_left + child.padding_right)\n                if child.margin_left != 'auto':\n                    child.main_outer_extra += child.margin_left\n                if child.margin_right != 'auto':\n                    child.main_outer_extra += child.margin_right\n            else:\n                child.main_outer_extra = (\n                    child.border_top_width + child.border_bottom_width +\n                    child.padding_top + child.padding_bottom)\n                if child.margin_top != 'auto':\n                    child.main_outer_extra += child.margin_top\n                if child.margin_bottom != 'auto':\n                    child.main_outer_extra += child.margin_bottom\n        elif False:\n            # TODO: 3.B If the flex item has an intrinsic aspect ratio…\n            # TODO: 3.C If the used flex basis is 'content'…\n            # TODO: 3.D Otherwise, if the used flex basis is 'content'…\n            pass\n        else:\n            # 3.E Otherwise…\n            new_child = child.copy()\n            new_child.style = child.style.copy()\n            if main == 'width':\n                # … the item’s min and max main sizes are ignored.\n                new_child.style['min_width'] = Dimension(0, 'px')\n                new_child.style['max_width'] = Dimension(inf, 'px')\n\n                child.flex_base_size = max_content_width(\n                    context, new_child, outer=False)\n                child.main_outer_extra = (\n                    max_content_width(context, child) - child.flex_base_size)\n            else:\n                # … the item’s min and max main sizes are ignored.\n                new_child.style['min_height'] = Dimension(0, 'px')\n                new_child.style['max_height'] = Dimension(inf, 'px')\n\n                new_child.width = inf\n                new_child, _, _, adjoining_margins, _, _ = block.block_level_layout(\n                    context, new_child, bottom_space, child_skip_stack, parent_box,\n                    page_is_empty, absolute_boxes, fixed_boxes)\n                if new_child:\n                    # As flex items margins never collapse (with other flex items or\n                    # with the flex container), we can add the adjoining margins to the\n                    # child height.\n                    new_child.height += block.collapse_margin(adjoining_margins)\n                    child.flex_base_size = new_child.height\n                    child.main_outer_extra = (\n                        new_child.margin_height() - new_child.height)\n                else:\n                    child.flex_base_size = child.main_outer_extra = 0\n\n        if main == 'width':\n            position_x += child.flex_base_size + child.main_outer_extra\n        else:\n            position_y += child.flex_base_size + child.main_outer_extra\n\n        min_size = getattr(child, f'min_{main}')\n        max_size = getattr(child, f'max_{main}')\n        child.hypothetical_main_size = max(\n            min_size, min(child.flex_base_size, max_size))\n\n        # Skip stack is only for the first child.\n        child_skip_stack = None\n\n    # 4 Determine the main size of the flex container using the rules of the formatting\n    # context in which it participates.\n    original_box_height = box.height\n    if main == 'width':\n        block.block_level_width(box, containing_block)\n    else:\n        if box.height == 'auto':\n            box.height = 0\n            flex_items = (child for child in children if child.is_flex_item)\n            for i, child in enumerate(flex_items):\n                box.height += child.hypothetical_main_size + child.main_outer_extra\n                if i:\n                    box.height += main_gap\n        box.height = max(box.min_height, min(box.height, box.max_height))\n\n    # 5 If the flex container is single-line, collect all the flex items into a single\n    # flex line.\n    flex_lines = []\n    line = []\n    line_size = 0\n    main_size = getattr(box, main)\n    for i, child in enumerate(children, start=skip):\n        if not child.is_flex_item:\n            continue\n        line_size += child.hypothetical_main_size + child.main_outer_extra\n        if i > skip:\n            line_size += main_gap\n        if box.style['flex_wrap'] != 'nowrap' and line_size > main_size:\n            if line:\n                flex_lines.append(FlexLine(line))\n                line = [(i, child)]\n                line_size = child.hypothetical_main_size + child.main_outer_extra\n            else:\n                line.append((i, child))\n                flex_lines.append(FlexLine(line))\n                line = []\n                line_size = 0\n        else:\n            line.append((i, child))\n    if line:\n        flex_lines.append(FlexLine(line))\n\n    # TODO: Handle *-reverse using the terminology from the specification.\n    if box.style['flex_wrap'] == 'wrap-reverse':\n        flex_lines.reverse()\n    if box.style['flex_direction'].endswith('-reverse'):\n        for line in flex_lines:\n            line.reverse()\n\n    # 6 Resolve the flexible lengths of all the flex items to find their used main size.\n    available_main_space = getattr(box, main)\n    for line in flex_lines:\n        # 9.7.1 Determine the used flex factor.\n        hypothetical_main_size = sum(\n            child.hypothetical_main_size + child.main_outer_extra\n            for index, child in line)\n        if hypothetical_main_size < available_main_space:\n            flex_factor_type = 'grow'\n        else:\n            flex_factor_type = 'shrink'\n\n        # 9.7.3 Size inflexible items.\n        for index, child in line:\n            if flex_factor_type == 'grow':\n                child.flex_factor = child.style['flex_grow']\n                flex_condition = child.flex_base_size > child.hypothetical_main_size\n            else:\n                child.flex_factor = child.style['flex_shrink']\n                flex_condition = child.flex_base_size < child.hypothetical_main_size\n            if child.flex_factor == 0 or flex_condition:\n                child.target_main_size = child.hypothetical_main_size\n                child.frozen = True\n            else:\n                child.frozen = False\n\n        # 9.7.4 Calculate initial free space.\n        initial_free_space = available_main_space\n        for i, (index, child) in enumerate(line):\n            if child.frozen:\n                initial_free_space -= child.target_main_size + child.main_outer_extra\n            else:\n                initial_free_space -= child.flex_base_size + child.main_outer_extra\n            if i:\n                initial_free_space -= main_gap\n\n        # 9.7.5.a Check for flexible items.\n        while not all(child.frozen for index, child in line):\n            unfrozen_factor_sum = 0\n            remaining_free_space = available_main_space\n\n            # 9.7.5.b Calculate the remaining free space.\n            for i, (index, child) in enumerate(line):\n                if child.frozen:\n                    remaining_free_space -= (\n                        child.target_main_size + child.main_outer_extra)\n                else:\n                    remaining_free_space -= (\n                        child.flex_base_size + child.main_outer_extra)\n                    unfrozen_factor_sum += child.flex_factor\n                if i:\n                    remaining_free_space -= main_gap\n\n            if unfrozen_factor_sum < 1:\n                initial_free_space *= unfrozen_factor_sum\n\n            if initial_free_space == inf:\n                initial_free_space = sys.maxsize\n            if remaining_free_space == inf:\n                remaining_free_space = sys.maxsize\n\n            initial_magnitude = (\n                int(log10(initial_free_space)) if initial_free_space > 0 else -inf)\n            remaining_magnitude = (\n                int(log10(remaining_free_space)) if remaining_free_space > 0 else -inf)\n            if initial_magnitude < remaining_magnitude:\n                remaining_free_space = initial_free_space\n\n            # 9.7.5.c Distribute free space proportional to the flex factors.\n            if remaining_free_space == 0:\n                # If the remaining free space is zero: \"Do nothing\", but we at least set\n                # the flex_base_size as target_main_size for next step.\n                for index, child in line:\n                    if not child.frozen:\n                        child.target_main_size = child.flex_base_size\n            else:\n                scaled_flex_shrink_factors_sum = 0\n                flex_grow_factors_sum = 0\n                for index, child in line:\n                    if not child.frozen:\n                        child.scaled_flex_shrink_factor = (\n                            child.flex_base_size * child.style['flex_shrink'])\n                        scaled_flex_shrink_factors_sum += (\n                            child.scaled_flex_shrink_factor)\n                        flex_grow_factors_sum += child.style['flex_grow']\n                for index, child in line:\n                    if not child.frozen:\n                        # If using the flex grow factor…\n                        if flex_factor_type == 'grow':\n                            ratio = child.style['flex_grow'] / flex_grow_factors_sum\n                            child.target_main_size = (\n                                child.flex_base_size + remaining_free_space * ratio)\n                        # If using the flex shrink factor…\n                        elif flex_factor_type == 'shrink':\n                            if scaled_flex_shrink_factors_sum == 0:\n                                child.target_main_size = child.flex_base_size\n                            else:\n                                ratio = (\n                                    child.scaled_flex_shrink_factor /\n                                    scaled_flex_shrink_factors_sum)\n                                child.target_main_size = (\n                                    child.flex_base_size + remaining_free_space * ratio)\n                        child.target_main_size = min_max(child, child.target_main_size)\n\n            # 9.7.5.d Fix min/max violations.\n            for index, child in line:\n                child.adjustment = 0\n                if not child.frozen:\n                    min_size = getattr(child, f'min_{main}')\n                    max_size = getattr(child, f'max_{main}')\n                    min_size = max(min_size, min(child.target_main_size, max_size))\n                    if child.target_main_size < min_size:\n                        child.adjustment = min_size - child.target_main_size\n                        child.target_main_size = min_size\n\n            # 9.7.5.e Freeze over-flexed items.\n            adjustments = sum(child.adjustment for index, child in line)\n            for index, child in line:\n                # Zero: Freeze all items.\n                if adjustments == 0:\n                    child.frozen = True\n                # Positive: Freeze all the items with min violations.\n                elif adjustments > 0 and child.adjustment > 0:\n                    child.frozen = True\n                # Negative: Freeze all the items with max violations.\n                elif adjustments < 0 and child.adjustment < 0:\n                    child.frozen = True\n\n        # 9.7.6 Set each item’s used main size to its target main size.\n        for index, child in line:\n            if main == 'width':\n                child.width = child.target_main_size\n            else:\n                child.height = child.target_main_size\n\n    # 7 Determine the hypothetical cross size of each item.\n    # TODO: Handle breaks.\n    new_flex_lines = []\n    child_skip_stack = skip_stack\n    for line in flex_lines:\n        new_flex_line = FlexLine()\n        for index, child in line:\n            # TODO: Fix this value, see test_flex_item_auto_margin_cross.\n            if child.margin_top == 'auto':\n                child.margin_top = 0\n            if child.margin_bottom == 'auto':\n                child.margin_bottom = 0\n            # TODO: Find another way than calling block_level_layout_switch.\n            new_child = child.copy()\n            new_child, _, _, adjoining_margins, _, _ = block.block_level_layout_switch(\n                context, new_child, -inf, child_skip_stack, parent_box, page_is_empty,\n                absolute_boxes, fixed_boxes, adjoining_margins=[],\n                first_letter_style=None, first_line_style=None, discard=discard,\n                max_lines=None)\n            child._baseline = find_in_flow_baseline(new_child) or 0\n            if cross == 'height':\n                child.height = new_child.height\n                # As flex items margins never collapse (with other flex items or\n                # with the flex container), we can add the adjoining margins to the\n                # child height.\n                child.height += block.collapse_margin(adjoining_margins)\n            else:\n                if child.width == 'auto':\n                    min_width = min_content_width(context, child, outer=False)\n                    max_width = max_content_width(context, child, outer=False)\n                    child.width = min(max(min_width, new_child.width), max_width)\n                else:\n                    child.width = new_child.width\n\n            new_flex_line.append((index, child))\n\n            # Skip stack is only for the first child.\n            child_skip_stack = None\n\n        if new_flex_line:\n            new_flex_lines.append(new_flex_line)\n    flex_lines = new_flex_lines\n\n    # 8 Calculate the cross size of each flex line.\n    cross_size = getattr(box, cross)\n    if len(flex_lines) == 1 and cross_size != 'auto':\n        # If the flex container is single-line…\n        flex_lines[0].cross_size = cross_size\n    else:\n        # Otherwise, for each flex line…\n        # 8.1 Collect all the flex items whose inline-axis is parallel to the main-axis…\n        for line in flex_lines:\n            collected_items = []\n            not_collected_items = []\n            for index, child in line:\n                align_self = child.style['align_self']\n                collect = (\n                    box.style['flex_direction'].startswith('row') and\n                    'baseline' in align_self and\n                    'auto' not in (child.margin_top, child.margin_bottom))\n                (collected_items if collect else not_collected_items).append(child)\n            cross_start_distance = cross_end_distance = 0\n            for child in collected_items:\n                baseline = child._baseline - child.position_y\n                cross_start_distance = max(cross_start_distance, baseline)\n                cross_end_distance = max(\n                    cross_end_distance, child.margin_height() - baseline)\n            collected_cross_size = cross_start_distance + cross_end_distance\n            non_collected_cross_size = 0\n            # 8.2 Find the largest outer hypothetical cross size.\n            if not_collected_items:\n                non_collected_cross_size = -inf\n                for child in not_collected_items:\n                    if cross == 'height':\n                        child_cross_size = child.border_height()\n                        if child.margin_top != 'auto':\n                            child_cross_size += child.margin_top\n                        if child.margin_bottom != 'auto':\n                            child_cross_size += child.margin_bottom\n                    else:\n                        child_cross_size = child.border_width()\n                        if child.margin_left != 'auto':\n                            child_cross_size += child.margin_left\n                        if child.margin_right != 'auto':\n                            child_cross_size += child.margin_right\n                    non_collected_cross_size = max(\n                        child_cross_size, non_collected_cross_size)\n            # 8.3 Set the used cross-size of the flex line.\n            line.cross_size = max(collected_cross_size, non_collected_cross_size)\n\n    # 8.3 If the flex container is single-line…\n    if len(flex_lines) == 1:\n        line, = flex_lines\n        min_cross_size = getattr(box, f'min_{cross}')\n        if min_cross_size == 'auto':\n            min_cross_size = -inf\n        max_cross_size = getattr(box, f'max_{cross}')\n        if max_cross_size == 'auto':\n            max_cross_size = inf\n        line.cross_size = max(min_cross_size, min(line.cross_size, max_cross_size))\n\n    # 9 Handle 'align-content: stretch'.\n    align_content = box.style['align_content']\n    if 'normal' in align_content:\n        align_content = ('stretch',)\n    if 'stretch' in align_content:\n        definite_cross_size = None\n        if cross == 'height' and box.height != 'auto':\n            definite_cross_size = box.height\n        elif cross == 'width':\n            if isinstance(box, boxes.FlexBox):\n                if box.width == 'auto':\n                    definite_cross_size = available_cross_space\n                else:\n                    definite_cross_size = box.width\n        if definite_cross_size is not None:\n            extra_cross_size = definite_cross_size\n            extra_cross_size -= sum(line.cross_size for line in flex_lines)\n            extra_cross_size -= (len(flex_lines) - 1) * cross_gap\n            if extra_cross_size:\n                for line in flex_lines:\n                    line.cross_size += extra_cross_size / len(flex_lines)\n\n    # TODO: 10 Collapse 'visibility: collapse' items.\n\n    # 11 Determine the used cross size of each flex item.\n    align_items = box.style['align_items']\n    if 'normal' in align_items:\n        align_items = ('stretch',)\n    for line in flex_lines:\n        for index, child in line:\n            align_self = child.style['align_self']\n            if 'normal' in align_self:\n                align_self = ('stretch',)\n            elif 'auto' in align_self:\n                align_self = align_items\n            if 'stretch' in align_self and child.style[cross] == 'auto':\n                cross_margins = (\n                    (child.style['margin_top'], child.style['margin_bottom'])\n                    if cross == 'height' else\n                    (child.style['margin_left'], child.style['margin_right']))\n                if 'auto' not in cross_margins:\n                    cross_size = line.cross_size\n                    if cross == 'height':\n                        cross_size -= (\n                            child.margin_top + child.margin_bottom +\n                            child.padding_top + child.padding_bottom +\n                            child.border_top_width +\n                            child.border_bottom_width)\n                    else:\n                        cross_size -= (\n                            child.margin_left + child.margin_right +\n                            child.padding_left + child.padding_right +\n                            child.border_left_width +\n                            child.border_right_width)\n                    setattr(child, cross, cross_size)\n            # else: Cross size has been set by step 7.\n\n    # 12 Distribute any remaining free space.\n    original_position_main = (\n        box.content_box_x() if main == 'width'\n        else box.content_box_y())\n    justify_content = box.style['justify_content']\n    if 'normal' in justify_content:\n        justify_content = ('flex-start',)\n    if box.style['flex_direction'].endswith('-reverse'):\n        if 'flex-start' in justify_content:\n            justify_content = ('flex-end',)\n        elif 'flex-end' in justify_content:\n            justify_content = ('flex-start',)\n        elif 'start' in justify_content:\n            justify_content = ('end',)\n        elif 'end' in justify_content:\n            justify_content = ('start',)\n\n    for line in flex_lines:\n        position_main = original_position_main\n        if main == 'width':\n            free_space = box.width\n            for index, child in line:\n                free_space -= child.border_width()\n                if child.margin_left != 'auto':\n                    free_space -= child.margin_left\n                if child.margin_right != 'auto':\n                    free_space -= child.margin_right\n        else:\n            free_space = box.height\n            for index, child in line:\n                free_space -= child.border_height()\n                if child.margin_top != 'auto':\n                    free_space -= child.margin_top\n                if child.margin_bottom != 'auto':\n                    free_space -= child.margin_bottom\n        free_space -= (len(line) - 1) * main_gap\n\n        # 12.1 If the remaining free space is positive…\n        margins = 0\n        for index, child in line:\n            if main == 'width':\n                if child.margin_left == 'auto':\n                    margins += 1\n                if child.margin_right == 'auto':\n                    margins += 1\n            else:\n                if child.margin_top == 'auto':\n                    margins += 1\n                if child.margin_bottom == 'auto':\n                    margins += 1\n        if margins:\n            free_space /= margins\n            for index, child in line:\n                if main == 'width':\n                    if child.margin_left == 'auto':\n                        child.margin_left = free_space\n                    if child.margin_right == 'auto':\n                        child.margin_right = free_space\n                else:\n                    if child.margin_top == 'auto':\n                        child.margin_top = free_space\n                    if child.margin_bottom == 'auto':\n                        child.margin_bottom = free_space\n            free_space = 0\n\n        if box.style['direction'] == 'rtl' and main == 'width':\n            free_space *= -1\n\n        # 12.2 Align the items along the main-axis per justify-content.\n        if {'end', 'flex-end', 'right'} & set(justify_content):\n            position_main += free_space\n        elif 'center' in justify_content:\n            position_main += free_space / 2\n        elif 'space-around' in justify_content:\n            position_main += free_space / len(line) / 2\n        elif 'space-evenly' in justify_content:\n            position_main += free_space / (len(line) + 1)\n\n        growths = sum(child.style['flex_grow'] for child in children)\n        if box.style['direction'] == 'rtl' and main == 'width':\n            main_gap *= -1\n        for i, (index, child) in enumerate(line):\n            if i:\n                position_main += main_gap\n            if main == 'width':\n                child.position_x = position_main\n                if 'stretch' in justify_content and growths:\n                    child.width += free_space * child.style['flex_grow'] / growths\n            else:\n                child.position_y = position_main\n            margin_main = (\n                child.margin_width() if main == 'width' else child.margin_height())\n            if box.style['direction'] == 'rtl' and main == 'width':\n                margin_main *= -1\n            position_main += margin_main\n            if 'space-around' in justify_content:\n                position_main += free_space / len(line)\n            elif 'space-between' in justify_content:\n                if len(line) > 1:\n                    position_main += free_space / (len(line) - 1)\n            elif 'space-evenly' in justify_content:\n                position_main += free_space / (len(line) + 1)\n\n    # 13 Resolve cross-axis auto margins.\n    if cross == 'width':\n        # Make sure width/margins are no longer \"auto\", as we did not do it above in\n        # step 4.\n        block.block_level_width(box, containing_block)\n    position_cross = box.content_box_y() if cross == 'height' else box.content_box_x()\n    for line in flex_lines:\n        line.lower_baseline = -inf\n        # TODO: Don't duplicate this loop.\n        for index, child in line:\n            align_self = child.style['align_self']\n            if 'auto' in align_self:\n                align_self = align_items\n            if 'baseline' in align_self and main == 'width':\n                # TODO: handle vertical text.\n                child.baseline = child._baseline - position_cross\n                line.lower_baseline = max(line.lower_baseline, child.baseline)\n        if line.lower_baseline == -inf:\n            line.lower_baseline = line[0][1]._baseline if line else 0\n        for index, child in line:\n            cross_margins = (\n                (child.style['margin_top'], child.style['margin_bottom'])\n                if cross == 'height' else\n                (child.style['margin_left'], child.style['margin_right']))\n            auto_margins = sum([margin == 'auto' for margin in cross_margins])\n            # If a flex item has auto cross-axis margins…\n            if auto_margins:\n                extra_cross = line.cross_size\n                if cross == 'height':\n                    extra_cross -= child.border_height()\n                    if child.margin_top != 'auto':\n                        extra_cross -= child.margin_top\n                    if child.margin_bottom != 'auto':\n                        extra_cross -= child.margin_bottom\n                else:\n                    extra_cross -= child.border_width()\n                    if child.margin_left != 'auto':\n                        extra_cross -= child.margin_left\n                    if child.margin_right != 'auto':\n                        extra_cross -= child.margin_right\n                if extra_cross > 0:\n                    # If its outer cross size is less than the cross size…\n                    extra_cross /= auto_margins\n                    if cross == 'height':\n                        if child.style['margin_top'] == 'auto':\n                            child.margin_top = extra_cross\n                        if child.style['margin_bottom'] == 'auto':\n                            child.margin_bottom = extra_cross\n                    else:\n                        if child.style['margin_left'] == 'auto':\n                            child.margin_left = extra_cross\n                        if child.style['margin_right'] == 'auto':\n                            child.margin_right = extra_cross\n                else:\n                    # Otherwise…\n                    if cross == 'height':\n                        if child.margin_top == 'auto':\n                            child.margin_top = 0\n                        child.margin_bottom = extra_cross\n                    else:\n                        if child.margin_left == 'auto':\n                            child.margin_left = 0\n                        child.margin_right = extra_cross\n            else:\n                # 14 Align all flex items along the cross-axis.\n                align_self = child.style['align_self']\n                if 'normal' in align_self:\n                    align_self = ('stretch',)\n                elif 'auto' in align_self:\n                    align_self = align_items\n                position = 'position_y' if cross == 'height' else 'position_x'\n                setattr(child, position, position_cross)\n                if {'end', 'self-end', 'flex-end'} & set(align_self):\n                    if cross == 'height':\n                        child.position_y += line.cross_size - child.margin_height()\n                    else:\n                        child.position_x += line.cross_size - child.margin_width()\n                elif 'center' in align_self:\n                    if cross == 'height':\n                        child.position_y += (\n                            line.cross_size - child.margin_height()) / 2\n                    else:\n                        child.position_x += (line.cross_size - child.margin_width()) / 2\n                elif 'baseline' in align_self:\n                    if cross == 'height':\n                        child.position_y += line.lower_baseline - child.baseline\n                    else:\n                        # TODO: Handle vertical text.\n                        pass\n                elif 'stretch' in align_self:\n                    if child.style[cross] == 'auto':\n                        if cross == 'height':\n                            margins = child.margin_top + child.margin_bottom\n                        else:\n                            margins = child.margin_left + child.margin_right\n                        if child.style['box_sizing'] == 'content-box':\n                            if cross == 'height':\n                                margins += (\n                                    child.border_top_width + child.border_bottom_width +\n                                    child.padding_top + child.padding_bottom)\n                            else:\n                                margins += (\n                                    child.border_left_width + child.border_right_width +\n                                    child.padding_left + child.padding_right)\n        position_cross += line.cross_size\n\n    # 15 Determine the flex container’s used cross size.\n    # TODO: Use the updated algorithm.\n    if getattr(box, cross) == 'auto':\n        # Otherwise, use the sum of the flex lines' cross sizes…\n        # TODO: Handle min-max.\n        # TODO: What about align-content here?\n        cross_size = sum(line.cross_size for line in flex_lines)\n        cross_size += (len(flex_lines) - 1) * cross_gap\n        setattr(box, cross, cross_size)\n\n    if len(flex_lines) > 1:\n        # 15 If the cross size property is a definite size, use that…\n        extra_cross_size = getattr(box, cross)\n        extra_cross_size -= sum(line.cross_size for line in flex_lines)\n        extra_cross_size -= (len(flex_lines) - 1) * cross_gap\n        # 16 Align all flex lines per align-content.\n        cross_translate = 0\n        direction = 'position_y' if cross == 'height' else 'position_x'\n        for i, line in enumerate(flex_lines):\n            flex_items = tuple(child for _, child in line if child.is_flex_item)\n            if i:\n                cross_translate += cross_gap\n            for child in flex_items:\n                current_value = getattr(child, direction) + cross_translate\n                setattr(child, direction, current_value)\n            if extra_cross_size == 0:\n                continue\n            for child in flex_items:\n                if {'flex-end', 'end'} & set(align_content):\n                    setattr(child, direction, current_value + extra_cross_size)\n                elif 'center' in align_content:\n                    setattr(child, direction, current_value + extra_cross_size / 2)\n                elif 'space-around' in align_content:\n                    setattr(\n                        child, direction,\n                        current_value + extra_cross_size / len(flex_lines) / 2)\n                elif 'space-evenly' in align_content:\n                    setattr(\n                        child, direction,\n                        current_value + extra_cross_size / (len(flex_lines) + 1))\n            if 'space-between' in align_content:\n                cross_translate += extra_cross_size / (len(flex_lines) - 1)\n            elif 'space-around' in align_content:\n                cross_translate += extra_cross_size / len(flex_lines)\n            elif 'space-evenly' in align_content:\n                cross_translate += extra_cross_size / (len(flex_lines) + 1)\n\n    # Now we are no longer in the flex algorithm.\n    box = box.copy_with_children(\n        [child for child in children if child.is_absolutely_positioned()])\n    child_skip_stack = skip_stack\n    for line in flex_lines:\n        for index, child in line:\n            if child.is_flex_item:\n                # TODO: Don't use block_level_layout_switch.\n                new_child, child_resume_at, next_page = block.block_level_layout_switch(\n                    context, child, bottom_space, child_skip_stack, box, page_is_empty,\n                    absolute_boxes, fixed_boxes, adjoining_margins=[],\n                    first_letter_style=None, first_line_style=None, discard=discard,\n                    max_lines=None)[:3]\n                if new_child is None:\n                    if resume_at:\n                        resume_index, = resume_at\n                        resume_index -= 1\n                    else:\n                        resume_index = 0\n                    resume_at = {resume_index + index: None}\n                else:\n                    page_is_empty = False\n                    box.children.append(new_child)\n                    position_y = new_child.position_y + new_child.border_height()\n                    if child_resume_at is not None:\n                        first_level_skip = 0\n                        if resume_at:\n                            resume_index, = resume_at\n                            first_level_skip = resume_index\n                        resume_at = {first_level_skip + index: child_resume_at}\n                if resume_at:\n                    break\n\n            # Skip stack is only for the first child.\n            child_skip_stack = None\n        if resume_at:\n            break\n\n    if original_box_height != 'auto':\n        # Don’t resume if flex items overflow flex container bottom.\n        box_bottom = box.position_y + box.border_height()\n        if context.overflows(box_bottom, position_y):\n            resume_at = None\n\n    if box.style['position'] == 'relative':\n        # New containing block, resolve the layout of the absolute descendants.\n        for absolute_box in absolute_boxes:\n            absolute_layout(\n                context, absolute_box, box, fixed_boxes, bottom_space, skip_stack=None)\n\n    for child in box.children:\n        block.relative_positioning(child, (box.width, box.height))\n\n    # TODO: Use real algorithm, see https://www.w3.org/TR/css-flexbox-1/#flex-baselines.\n    if isinstance(box, boxes.InlineFlexBox):\n        if main == 'width':  # and main text direction is horizontal\n            box.baseline = flex_lines[0].lower_baseline if flex_lines else 0\n        else:\n            for child in box.children:\n                if child.is_in_normal_flow():\n                    box.baseline = find_in_flow_baseline(child) or 0\n                    break\n            else:\n                box.baseline = 0\n\n    box.remove_decoration(start=False, end=resume_at and not discard)\n\n    context.finish_flex_formatting_context(box)\n\n    return box, resume_at, next_page, [], False\n"
  },
  {
    "path": "weasyprint/layout/float.py",
    "content": "\"\"\"Layout for floating boxes.\"\"\"\n\nfrom math import inf\n\nfrom ..formatting_structure import boxes\nfrom .min_max import handle_min_max_width\nfrom .percent import resolve_percentages, resolve_position_percentages\nfrom .preferred import shrink_to_fit\nfrom .replaced import inline_replaced_box_width_height\nfrom .table import table_wrapper_width\n\n\n@handle_min_max_width\ndef float_width(box, context, containing_block):\n    # Check that box.width is auto even if the caller does it too, because\n    # the handle_min_max_width decorator can change the value\n    if box.width == 'auto':\n        box.width = shrink_to_fit(context, box, containing_block.width)\n\n\ndef float_layout(context, box, containing_block, absolute_boxes, fixed_boxes,\n                 bottom_space, skip_stack):\n    \"\"\"Set the width and position of floating ``box``.\"\"\"\n    from .block import block_container_layout\n    from .flex import flex_layout\n    from .grid import grid_layout\n\n    cb_width, cb_height = (containing_block.width, containing_block.height)\n    resolve_percentages(box, (cb_width, cb_height))\n\n    # TODO: This is only handled later in blocks.block_container_layout\n    # https://www.w3.org/TR/CSS21/visudet.html#normal-block\n    if cb_height == 'auto':\n        cb_height = containing_block.position_y - containing_block.content_box_y()\n\n    resolve_position_percentages(box, (cb_width, cb_height))\n\n    if box.margin_left == 'auto':\n        box.margin_left = 0\n    if box.margin_right == 'auto':\n        box.margin_right = 0\n    if box.margin_top == 'auto':\n        box.margin_top = 0\n    if box.margin_bottom == 'auto':\n        box.margin_bottom = 0\n\n    clearance = get_clearance(context, box, containing_block.style['direction'])\n    if clearance is not None:\n        box.position_y += clearance\n\n    if isinstance(box, boxes.BlockReplacedBox):\n        inline_replaced_box_width_height(box, containing_block)\n    elif box.width == 'auto':\n        float_width(box, context, containing_block)\n\n    if box.is_table_wrapper:\n        table_wrapper_width(context, box, (cb_width, cb_height))\n\n    if isinstance(box, boxes.BlockContainerBox):\n        box, resume_at, _, _, _, _ = block_container_layout(\n            context, box, bottom_space=bottom_space, skip_stack=skip_stack,\n            page_is_empty=True, absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes,\n            adjoining_margins=None, first_letter_style=None, first_line_style=None,\n            discard=False, max_lines=None)\n    elif isinstance(box, boxes.FlexContainerBox):\n        box, resume_at, _, _, _ = flex_layout(\n            context, box, bottom_space=bottom_space,\n            skip_stack=skip_stack, containing_block=containing_block,\n            page_is_empty=True, absolute_boxes=absolute_boxes,\n            fixed_boxes=fixed_boxes, discard=False)\n    elif isinstance(box, boxes.GridContainerBox):\n        box, resume_at, _, _, _ = grid_layout(\n            context, box, bottom_space=bottom_space,\n            skip_stack=skip_stack, containing_block=containing_block,\n            page_is_empty=True, absolute_boxes=absolute_boxes,\n            fixed_boxes=fixed_boxes)\n    else:\n        assert isinstance(box, boxes.BlockReplacedBox)\n        resume_at = None\n\n    box = find_float_position(context, box, containing_block)\n\n    context.excluded_shapes.append(box)\n    return box, resume_at\n\n\ndef find_float_position(context, box, containing_block):\n    \"\"\"Get the right position of the float ``box``.\"\"\"\n    # See https://www.w3.org/TR/CSS2/visuren.html#float-position\n\n    # Point 4 is already handled as box.position_y is set according to the\n    # containing box top position, with collapsing margins handled\n\n    # Points 5 and 6, box.position_y is set to the highest position_y possible\n    if context.excluded_shapes:\n        highest_y = context.excluded_shapes[-1].position_y\n        if box.position_y < highest_y:\n            box.translate(0, highest_y - box.position_y)\n\n    # Points 1 and 2\n    position_x, position_y, available_width = avoid_collisions(\n        context, box, containing_block)\n\n    # Point 9\n    # position_y is set now, let's define position_x\n    # for float: left elements, it's already done!\n    float_right = (\n        box.style['float'] == 'right' or\n        (box.style['direction'] == 'ltr' and box.style['float'] == 'inline-end') or\n        (box.style['direction'] == 'rtl' and box.style['float'] == 'inline-start'))\n    if float_right:\n        position_x += available_width - box.margin_width()\n\n    box.translate(position_x - box.position_x, position_y - box.position_y)\n\n    return box\n\n\ndef get_clearance(context, box, direction, collapsed_margin=0):\n    \"\"\"Return None if there is no clearance, otherwise the clearance value.\"\"\"\n\n    def clear(clear_value, float_value):\n        \"\"\"Closure returning whether clear and float values match.\"\"\"\n        if clear_value == 'inline-start':\n            clear_value = 'left' if direction == 'ltr' else 'right'\n        if clear_value == 'inline-end':\n            clear_value = 'left' if direction == 'rtl' else 'right'\n        if float_value == 'inline-start':\n            float_value = 'left' if direction == 'ltr' else 'right'\n        if float_value == 'inline-end':\n            float_value = 'left' if direction == 'rtl' else 'right'\n        return clear_value in (float_value, 'both')\n\n    # Box should be after shape that’s broken on this page.\n    for broken_shape in context.broken_out_of_flow:\n        if broken_shape.is_floated():\n            if clear(box.style['clear'], broken_shape.style['float']):\n                return inf\n    # Hypothetical position is the position of the top border edge\n    clearance = None\n    hypothetical_position = box.position_y + collapsed_margin\n    for excluded_shape in context.excluded_shapes:\n        if clear(box.style['clear'], excluded_shape.style['float']):\n            y, h = excluded_shape.position_y, excluded_shape.margin_height()\n            if hypothetical_position < y + h:\n                clearance = max((clearance or 0), y + h - hypothetical_position)\n    return clearance\n\n\ndef avoid_collisions(context, box, containing_block, outer=True):\n    excluded_shapes = context.excluded_shapes\n    position_y = box.position_y if outer else box.border_box_y()\n\n    box_width = box.margin_width() if outer else box.border_width()\n    box_height = box.margin_height() if outer else box.border_height()\n\n    if box.border_height() == 0 and box.is_floated():\n        return 0, 0, containing_block.width\n\n    left_keywords = ['left']\n    right_keywords = ['right']\n    if containing_block.style['direction'] == 'ltr':\n        left_keywords.append('inline-start')\n        right_keywords.append('inline-end')\n    else:\n        left_keywords.append('inline-end')\n        right_keywords.append('inline-start')\n\n    while True:\n        colliding_shapes = []\n        for shape in excluded_shapes:\n            # Assign locals to avoid slow attribute lookups.\n            shape_position_y = shape.position_y\n            shape_margin_height = shape.margin_height()\n            if ((shape_position_y < position_y <\n                 shape_position_y + shape_margin_height) or\n                (shape_position_y < position_y + box_height <\n                 shape_position_y + shape_margin_height) or\n                (shape_position_y >= position_y and\n                 shape_position_y + shape_margin_height <=\n                 position_y + box_height)):\n                colliding_shapes.append(shape)\n        left_bounds = [\n            shape.position_x + shape.margin_width()\n            for shape in colliding_shapes\n            if shape.style['float'] in left_keywords]\n        right_bounds = [\n            shape.position_x\n            for shape in colliding_shapes\n            if shape.style['float'] in right_keywords]\n\n        # Set the default maximum bounds\n        max_left_bound = containing_block.content_box_x()\n        max_right_bound = max_left_bound + containing_block.width\n\n        if not outer:\n            max_left_bound += box.margin_left\n            max_right_bound -= box.margin_right\n\n        # Set the real maximum bounds according to sibling float elements\n        if left_bounds or right_bounds:\n            if left_bounds:\n                max_left_bound = max(max(left_bounds), max_left_bound)\n            if right_bounds:\n                max_right_bound = min(min(right_bounds), max_right_bound)\n\n            # Points 3, 7 and 8\n            if box_width > max_right_bound - max_left_bound:\n                # The box does not fit here\n                new_position_y = min(\n                    shape.position_y + shape.margin_height()\n                    for shape in colliding_shapes)\n                if new_position_y > position_y:\n                    # We can find a solution with a higher position_y\n                    position_y = new_position_y\n                    continue\n                # No solution, we must put the box here\n        break\n\n    # See https://www.w3.org/TR/CSS21/visuren.html#floats\n    # Boxes that can’t collide with floats are:\n    # - floats\n    # - line boxes\n    # - table wrappers\n    # - block-level replaced box\n    # - element establishing new formatting contexts\n    assert (\n        box.is_floated() or\n        isinstance(box, boxes.LineBox) or\n        box.is_table_wrapper or\n        isinstance(box, boxes.BlockReplacedBox) or\n        box.establishes_formatting_context())\n\n    # The x-position of the box depends on its type.\n    position_x = max_left_bound\n    if box.style['float'] == 'none':\n        if containing_block.style['direction'] == 'rtl':\n            if isinstance(box, boxes.LineBox):\n                # The position of the line is the position of the cursor, at\n                # the right bound.\n                position_x = max_right_bound\n            elif box.is_table_wrapper:\n                # The position of the right border of the table is at the right\n                # bound.\n                position_x = max_right_bound - box_width\n            else:\n                # The position of the right border of the replaced box or\n                # formatting context is at the right bound.\n                assert (\n                    isinstance(box, boxes.BlockReplacedBox) or\n                    box.establishes_formatting_context())\n                position_x = max_right_bound - box_width\n\n    available_width = max_right_bound - max_left_bound\n\n    if not outer:\n        position_x -= box.margin_left\n        position_y -= box.margin_top\n\n    return position_x, position_y, available_width\n"
  },
  {
    "path": "weasyprint/layout/grid.py",
    "content": "\"\"\"Layout for grid containers and grid-items.\"\"\"\n\nfrom collections import defaultdict\nfrom itertools import count, cycle\nfrom math import inf\n\nfrom ..css.properties import Dimension\nfrom ..formatting_structure import boxes\nfrom ..logger import LOGGER\nfrom .percent import percentage, resolve_percentages\nfrom .preferred import max_content_width, min_content_width\nfrom .table import find_in_flow_baseline\n\n\ndef _is_length(sizing):\n    return isinstance(sizing, Dimension) and sizing.unit.lower() != 'fr'\n\n\ndef _is_fr(sizing):\n    return isinstance(sizing, Dimension) and sizing.unit.lower() == 'fr'\n\n\ndef _intersect(position_1, size_1, position_2, size_2):\n    return (\n        position_1 < position_2 + size_2 and\n        position_2 < position_1 + size_1)\n\n\ndef _intersect_with_children(x, y, width, height, positions):\n    for full_x, full_y, full_width, full_height in positions:\n        x_intersect = _intersect(x, width, full_x, full_width)\n        y_intersect = _intersect(y, height, full_y, full_height)\n        if x_intersect and y_intersect:\n            return True\n    return False\n\n\ndef _get_line(line, lines, side):\n    span, number, ident = line\n    if ident and span is None and number is None:\n        for coord, line in enumerate(lines):\n            if f'{ident}-{side}' in line:\n                break\n        else:\n            number = 1\n    if number is not None and span is None:\n        if ident is None:\n            coord = number - 1\n        else:\n            step = 1 if number > 0 else -1\n            for coord, line in enumerate(lines[::step]):\n                if ident in line:\n                    number -= step\n                    break\n                if number == 0:\n                    break\n            else:\n                coord += abs(number)\n            if step == -1:\n                coord = len(lines) - 1 - coord\n    if span is not None:\n        coord = None\n    return span, number, ident, coord\n\n\ndef _get_placement(start, end, lines):\n    # Input coordinates are 1-indexed, returned coordinates are 0-indexed.\n    if start == 'auto' or start[0] == 'span':\n        if end == 'auto' or end[0] == 'span':\n            return\n    if start != 'auto':\n        span, number, ident, coord = _get_line(start, lines, 'start')\n        if span is not None:\n            size = number or 1\n            span_ident = ident\n    else:\n        size = 1\n        span_ident = coord = None\n    if end != 'auto':\n        span, number, ident, coord_end = _get_line(end, lines, 'end')\n        if span is not None:\n            size = span_number = number or 1\n            span_ident = ident\n            if span_ident is not None:\n                for size, line in enumerate(lines[coord+1:], start=1):\n                    if span_ident in line:\n                        span_number -= 1\n                    if span_number == 0:\n                        break\n                else:\n                    size += span_number\n        elif coord is not None:\n            size = coord_end - coord\n        if coord is None:\n            if span_ident is None:\n                coord = coord_end - size\n            else:\n                number = number or 1\n                if coord_end > 0:\n                    iterable = enumerate(lines[coord_end-1::-1])\n                    for coord, line in iterable:\n                        if span_ident in line:\n                            number -= 1\n                        if number == 0:\n                            coord = coord_end - 1 - coord\n                            break\n                    else:\n                        coord = -number\n                else:\n                    coord = -number\n            size = coord_end - coord\n    else:\n        size = 1\n    if size < 0:\n        size = -size\n        coord -= size\n    if size == 0:\n        size = 1\n    return (coord, size)\n\n\ndef _get_span(place):\n    # TODO: Handle lines.\n    span = 1\n    if place[0] == 'span':\n        span = place[1] or 1\n    return span\n\n\ndef _get_second_placement(first_placement, second_start, second_end,\n                          second_tracks, children_positions, first_flow, dense):\n    occupied_tracks = set()\n    for x, y, width, height in children_positions.values():\n        # Test whether cells overlap.\n        if first_flow == 'row':\n            if _intersect(y, height, *first_placement):\n                for x in range(x, x + width):\n                    occupied_tracks.add(x)\n        else:\n            if _intersect(x, width, *first_placement):\n                for y in range(y, y + height):\n                    occupied_tracks.add(y)\n    if dense:\n        for track in count():\n            if track in occupied_tracks:\n                continue\n            if second_start == 'auto':\n                placement = _get_placement(\n                    (None, track + 1, None), second_end, second_tracks)\n            else:\n                assert second_start[0] == 'span'\n                # If the placement contains two spans, remove the one\n                # contributed by the end grid-placement property.\n                # https://drafts.csswg.org/css-grid/#grid-placement-errors\n                assert second_start == 'auto' or second_start[0] == 'span'\n                span = _get_span(second_start)\n                placement = _get_placement(\n                    second_start, (None, track + 1 + span, None), second_tracks)\n            tracks = range(placement[0], placement[0] + placement[1])\n            if not set(tracks) & occupied_tracks:\n                return placement\n    else:\n        track = max(occupied_tracks or [0]) + 1\n        if second_start == 'auto':\n            return _get_placement(\n                (None, track + 1, None), second_end, second_tracks)\n        else:\n            assert second_start[0] == 'span'\n            # If the placement contains two spans, remove the one contributed\n            # by the end grid-placement property.\n            # https://drafts.csswg.org/css-grid/#grid-placement-errors\n            assert second_start == 'auto' or second_start[0] == 'span'\n            for end_track in count(track + 1):\n                placement = _get_placement(\n                    second_start, (None, end_track + 1, None), second_tracks)\n                if placement[0] >= track:\n                    return placement\n\n\ndef _get_sizing_functions(size):\n    min_sizing = max_sizing = size\n    if size[0] == 'minmax()':\n        min_sizing, max_sizing = size[1:]\n    if min_sizing[0] == 'fit-content()':\n        min_sizing = 'auto'\n    elif _is_fr(min_sizing):\n        min_sizing = 'auto'\n    return (min_sizing, max_sizing)\n\n\ndef _get_template_tracks(tracks):\n    if tracks == 'none':\n        tracks = ((),)\n    if 'subgrid' in tracks:\n        # TODO: Support subgrids.\n        LOGGER.warning('Subgrids are unsupported')\n        return [[]]\n    tracks_list = []\n    for i, track in enumerate(tracks):\n        if i % 2:\n            # Track size.\n            if track[0] == 'repeat()':\n                repeat_number, repeat_track_list = track[1:]\n                if not isinstance(repeat_number, int):\n                    # TODO: Respect auto-fit and auto-fill.\n                    LOGGER.warning(\n                        '\"auto-fit\" and \"auto-fill\" are unsupported in repeat()')\n                    repeat_number = 1\n                for _ in range(repeat_number):\n                    for j, repeat_track in enumerate(repeat_track_list):\n                        if j % 2:\n                            # Track size in repeat.\n                            tracks_list.append(repeat_track)\n                        else:\n                            # Line names in repeat.\n                            if len(tracks_list) % 2:\n                                tracks_list[-1].extend(repeat_track)\n                            else:\n                                tracks_list.append(list(repeat_track))\n            else:\n                tracks_list.append(track)\n        else:\n            # Line names.\n            if len(tracks_list) % 2:\n                tracks_list[-1].extend(track)\n            else:\n                tracks_list.append(list(track))\n    return tracks_list\n\n\ndef _distribute_extra_space(affected_sizes, affected_tracks_types, size_contribution,\n                            tracks_children, sizing_functions, tracks_sizes, span,\n                            direction, context):\n    assert affected_sizes in ('min', 'max')\n    assert affected_tracks_types in (\n        'intrinsic', 'content-based', 'max-content')\n    assert size_contribution in ('minimum', 'min-content', 'max-content')\n    assert direction in 'xy'\n\n    # 1. Maintain separately for each affected track a planned increase.\n    planned_increases = [0] * len(tracks_sizes)\n\n    # 2. Distribute space.\n    affected_tracks = []\n    affected_size_index = 0 if affected_sizes == 'min' else 1\n    current_span = 0\n    for children, functions in zip(tracks_children, sizing_functions):\n        if children:\n            current_span = span\n        if not current_span:\n            affected_tracks.append(False)\n            continue\n        current_span -= 1\n        function = functions[affected_size_index]\n        if affected_tracks_types == 'intrinsic':\n            if (function in ('min-content', 'max-content', 'auto') or\n                    function[0] == 'fit-content()'):\n                affected_tracks.append(True)\n                continue\n        elif affected_tracks_types == 'content-based':\n            if function in ('min-content', 'max-content'):\n                affected_tracks.append(True)\n                continue\n        elif affected_tracks_types == 'max-content':\n            if function in ('max-content', 'auto'):\n                affected_tracks.append(True)\n                continue\n        affected_tracks.append(False)\n    for i, children in enumerate(tracks_children):\n        if not children:\n            continue\n        for item, parent in children:\n            # 2.1 Find the space distribution.\n            # TODO: Differenciate minimum and min-content values.\n            # TODO: Find a better way to get height.\n            if direction == 'x':\n                if size_contribution in ('minimum', 'min-content'):\n                    space = min_content_width(context, item)\n                else:\n                    space = max_content_width(context, item)\n            else:\n                from .block import block_level_layout\n                item = item.deepcopy()\n                item.position_x = 0\n                item.position_y = 0\n                item, _, _, _, _, _ = block_level_layout(\n                    context, item, bottom_space=-inf, skip_stack=None,\n                    containing_block=parent)\n                space = item.margin_height()\n            for sizes in tracks_sizes[i:i+span]:\n                space -= sizes[affected_size_index]\n            space = max(0, space)\n            # 2.2 Distribute space up to limits.\n            tracks_numbers = list(\n                enumerate(affected_tracks[i:i+span], start=i))\n            item_incurred_increases = [0] * len(sizing_functions)\n            affected_tracks_numbers = [\n                j for j, affected in tracks_numbers if affected]\n            distributed_space = space / (len(affected_tracks_numbers) or 1)\n            for track_number in affected_tracks_numbers:\n                base_size, growth_limit = tracks_sizes[track_number]\n                item_incurred_increase = distributed_space\n                affected_size = tracks_sizes[track_number][affected_size_index]\n                limit = tracks_sizes[track_number][1]\n                if affected_size + item_incurred_increase >= limit:\n                    extra = (\n                        item_incurred_increase + affected_size - limit)\n                    item_incurred_increase -= extra\n                space -= item_incurred_increase\n                item_incurred_increases[track_number] = item_incurred_increase\n            # 2.3 Distribute space to non-affected tracks.\n            if space and affected_tracks_numbers:\n                unaffected_tracks_numbers = [\n                    j for j, affected in tracks_numbers if not affected]\n                distributed_space = (\n                    space / (len(unaffected_tracks_numbers) or 1))\n                for track_number in unaffected_tracks_numbers:\n                    base_size, growth_limit = tracks_sizes[track_number]\n                    item_incurred_increase = distributed_space\n                    affected_size = (\n                        tracks_sizes[track_number][affected_size_index])\n                    limit = tracks_sizes[track_number][1]\n                    if affected_size + item_incurred_increase >= limit:\n                        extra = (\n                            item_incurred_increase + affected_size - limit)\n                        item_incurred_increase -= extra\n                    space -= item_incurred_increase\n                    item_incurred_increases[track_number] = (\n                        item_incurred_increase)\n            # 2.4 Distribute space beyond limits.\n            if space:\n                # TODO: Distribute space beyond limits.\n                pass\n            # 2.5. Set the track’s planned increase.\n            for k, extra in enumerate(item_incurred_increases):\n                if extra > planned_increases[k]:\n                    planned_increases[k] = extra\n\n    # 3. Update the tracks’ affected size.\n    iterator = zip(affected_tracks, tracks_sizes, planned_increases)\n    for affected, track_sizes, increase in iterator:\n        if not affected:\n            continue\n        if affected_sizes == 'max' and track_sizes[1] is inf:\n            track_sizes[1] = track_sizes[0] + increase\n        else:\n            track_sizes[affected_size_index] += increase\n\n\ndef _resolve_tracks_sizes(sizing_functions, box_size, children_positions,\n                          implicit_start, direction, gap, context, containing_block,\n                          orthogonal_sizes=None):\n    assert direction in 'xy'\n    tracks_sizes = []\n    # TODO: Check that auto box size is 0 for percentages.\n    percent_box_size = 0 if box_size == 'auto' else box_size\n    # 1.1 Initialize track sizes.\n    for min_function, max_function in sizing_functions:\n        base_size = None\n        if _is_length(min_function):\n            base_size = percentage(\n                min_function, containing_block.style, percent_box_size)\n        elif (min_function in ('min-content', 'max-content', 'auto') or\n              min_function[0] == 'fit-content()'):\n            base_size = 0\n        growth_limit = None\n        if _is_length(max_function):\n            growth_limit = percentage(\n                max_function, containing_block.style, percent_box_size)\n        elif (max_function in ('min-content', 'max-content', 'auto') or\n              max_function[0] == 'fit-content()' or _is_fr(max_function)):\n            growth_limit = inf\n        if None not in (base_size, growth_limit):\n            growth_limit = max(base_size, growth_limit)\n        tracks_sizes.append([base_size, growth_limit])\n\n    # 1.2 Resolve intrinsic track sizes.\n    # 1.2.1 Shim baseline-aligned items.\n    # TODO: Shim items.\n    # 1.2.2 Size tracks to fit non-spanning items.\n    tracks_children = [[] for _ in range(len(tracks_sizes))]\n    for child, (x, y, width, height) in children_positions.items():\n        coord, size = (x, width) if direction == 'x' else (y, height)\n        if size != 1:\n            continue\n        tracks_children[coord - implicit_start].append(child)\n    iterable = zip(tracks_children, sizing_functions, tracks_sizes)\n    for children, (min_function, max_function), sizes in iterable:\n        if not children:\n            continue\n        if direction == 'y':\n            # TODO: Find a better way to get height.\n            from .block import block_level_layout\n            height = 0\n            for child in children:\n                x, y, width, _ = children_positions[child]\n                width = sum(orthogonal_sizes[x:x+width])\n                child = child.deepcopy()\n                child.position_x = 0\n                child.position_y = 0\n                parent = boxes.BlockContainerBox.anonymous_from(containing_block, ())\n                resolve_percentages(parent, containing_block)\n                parent.position_x = child.position_x\n                parent.position_y = child.position_y\n                parent.width = width\n                parent.height = height\n                bottom_space = -inf\n                child, _, _, _, _, _ = block_level_layout(\n                    context, child, bottom_space, skip_stack=None,\n                    containing_block=parent)\n                height = max(height, child.margin_height())\n            if min_function in ('min-content', 'max_content', 'auto'):\n                sizes[0] = height\n            if max_function in ('min-content', 'max_content'):\n                sizes[1] = height\n            if None not in sizes:\n                sizes[1] = max(sizes)\n            continue\n        if min_function == 'min-content':\n            sizes[0] = max(0, *(\n                min_content_width(context, child) for child in children))\n        elif min_function == 'max-content':\n            sizes[0] = max(0, *(\n                max_content_width(context, child) for child in children))\n        elif min_function == 'auto':\n            # TODO: Handle min-/max-content constrained parents.\n            # TODO: Use real \"minimum contributions\".\n            sizes[0] = max(0, *(\n                min_content_width(context, child) for child in children))\n        if max_function == 'min-content':\n            sizes[1] = max(\n                min_content_width(context, child) for child in children)\n        elif (max_function in ('auto', 'max-content') or\n              max_function[0] == 'fit_content()'):\n            sizes[1] = max(\n                max_content_width(context, child) for child in children)\n        if None not in sizes:\n            sizes[1] = max(sizes)\n    # 1.2.3 Increase sizes to accommodate items spanning content-sized tracks.\n    spans = sorted({\n        width if direction == 'x' else height\n        for (_, _, width, height) in children_positions.values()\n        if (width if direction == 'x' else height) >= 2})\n    for span in spans:\n        tracks_children = [[] for _ in range(len(sizing_functions))]\n        iterable = enumerate(children_positions.items())\n        for i, (child, (x, y, width, height)) in iterable:\n            coord, size = (x, width) if direction == 'x' else (y, height)\n            if size != span:\n                continue\n            for _, max_function in sizing_functions[i:i+span+1]:\n                if _is_fr(max_function):\n                    break\n            else:\n                parent = boxes.BlockContainerBox.anonymous_from(containing_block, ())\n                resolve_percentages(parent, containing_block)\n                if direction == 'y':\n                    parent.width = sum(orthogonal_sizes[x:x+width])\n                tracks_children[coord - implicit_start].append((child, parent))\n        # 1.2.3.1 For intrinsic minimums.\n        # TODO: Respect min-/max-content constraint.\n        _distribute_extra_space(\n            'min', 'intrinsic', 'minimum', tracks_children,\n            sizing_functions, tracks_sizes, span, direction, context)\n        # 1.2.3.2 For content-based minimums.\n        _distribute_extra_space(\n            'min', 'content-based', 'min-content', tracks_children,\n            sizing_functions, tracks_sizes, span, direction, context)\n        # 1.2.3.3 For max-content minimums.\n        # TODO: Respect max-content constraint.\n        _distribute_extra_space(\n            'min', 'max-content', 'max-content', tracks_children,\n            sizing_functions, tracks_sizes, span, direction, context)\n        # 1.2.3.4 Increase growth limit.\n        # TODO: Increase growth limit.\n        # 1.2.3.5 For intrinsic maximums.\n        _distribute_extra_space(\n            'max', 'intrinsic', 'min-content', tracks_children,\n            sizing_functions, tracks_sizes, span, direction, context)\n        # 1.2.3.6 For max-content maximums.\n        _distribute_extra_space(\n            'max', 'max-content', 'max-content', tracks_children,\n            sizing_functions, tracks_sizes, span, direction, context)\n    # 1.2.4 Increase sizes to accommodate items spanning flexible tracks.\n    # TODO: Support spans for flexible tracks.\n    # 1.2.5 Fix infinite growth limits.\n    for sizes in tracks_sizes:\n        if sizes[1] is inf:\n            sizes[1] = sizes[0]\n    # 1.3 Maximize tracks.\n    if box_size == 'auto':\n        free_space = None\n    else:\n        free_space = (\n            box_size -\n            sum(size[0] for size in tracks_sizes) -\n            (len(tracks_sizes) - 1) * gap)\n    if free_space is not None and free_space > 0:\n        distributed_free_space = free_space / len(tracks_sizes)\n        for i, sizes in enumerate(tracks_sizes):\n            base_size, growth_limit = sizes\n            if base_size + distributed_free_space > growth_limit:\n                sizes[0] = growth_limit\n                free_space -= growth_limit - base_size\n            else:\n                sizes[0] += distributed_free_space\n                free_space -= distributed_free_space\n    # TODO: Respect max-width/-height.\n    # 1.4 Expand flexible tracks.\n    inflexible_tracks = set()\n    if free_space is not None and free_space <= 0:\n        # TODO: Respect min-content constraint.\n        flex_fraction = 0\n    elif free_space is not None:\n        stop = False\n        while not stop:\n            leftover_space = free_space\n            flex_factor_sum = 0\n            iterable = enumerate(zip(tracks_sizes, sizing_functions))\n            for i, (sizes, (_, max_function)) in iterable:\n                if _is_fr(max_function):\n                    leftover_space += sizes[0]\n                    if i not in inflexible_tracks:\n                        flex_factor_sum += max_function.value\n            flex_factor_sum = max(1, flex_factor_sum)\n            hypothetical_fr_size = leftover_space / flex_factor_sum\n            stop = True\n            iterable = enumerate(zip(tracks_sizes, sizing_functions))\n            for i, (sizes, (_, max_function)) in iterable:\n                if i not in inflexible_tracks and _is_fr(max_function):\n                    if hypothetical_fr_size * max_function.value < sizes[0]:\n                        inflexible_tracks.add(i)\n                        free_space -= sizes[0]\n                        stop = free_space > 0\n        flex_fraction = hypothetical_fr_size\n    else:\n        flex_fraction = 0\n        iterable = zip(tracks_sizes, sizing_functions)\n        for sizes, (_, max_function) in iterable:\n            if _is_fr(max_function):\n                if max_function.value > 1:\n                    flex_fraction = max(\n                        flex_fraction, max_function.value * sizes[0])\n                else:\n                    flex_fraction = max(flex_fraction, sizes[0])\n        # TODO: Respect grid items max-content contribution.\n        # TODO: Respect min-* constraint.\n    iterable = enumerate(zip(tracks_sizes, sizing_functions))\n    for i, (sizes, (_, max_function)) in iterable:\n        if _is_fr(max_function) and i not in inflexible_tracks:\n            if flex_fraction * max_function.value > sizes[0]:\n                if free_space is not None:\n                    free_space -= flex_fraction * max_function.value\n                sizes[0] = flex_fraction * max_function.value\n    # 1.5 Expand stretched auto tracks.\n    justify_content = containing_block.style['justify_content']\n    align_content = containing_block.style['align_content']\n    x_stretch = (\n        direction == 'x' and set(justify_content) & {'normal', 'stretch'})\n    y_stretch = (\n        direction == 'y' and set(align_content) & {'normal', 'stretch'})\n    if (x_stretch or y_stretch) and free_space is not None and free_space > 0:\n        auto_tracks_sizes = [\n            sizes for sizes, (min_function, _)\n            in zip(tracks_sizes, sizing_functions)\n            if min_function == 'auto']\n        if auto_tracks_sizes:\n            distributed_free_space = free_space / len(auto_tracks_sizes)\n            for sizes in auto_tracks_sizes:\n                sizes[0] += distributed_free_space\n\n    return tracks_sizes\n\n\ndef grid_layout(context, box, bottom_space, skip_stack, containing_block,\n                page_is_empty, absolute_boxes, fixed_boxes):\n    context.create_block_formatting_context(box)\n\n    if skip_stack and box.style['box_decoration_break'] != 'clone':\n        box.remove_decoration(start=True, end=False)\n\n    if box.style['position'] == 'relative':\n        # New containing block, use a new absolute list\n        absolute_boxes = []\n\n    # Define explicit grid\n    grid_areas = box.style['grid_template_areas']\n    flow = box.style['grid_auto_flow']\n    auto_rows = cycle(box.style['grid_auto_rows'])\n    auto_columns = cycle(box.style['grid_auto_columns'])\n    auto_rows_back = cycle(box.style['grid_auto_rows'][::-1])\n    auto_columns_back = cycle(box.style['grid_auto_columns'][::-1])\n    column_gap = box.style['column_gap']\n    if column_gap == 'normal':\n        column_gap = 0\n    else:\n        refer_to = containing_block.width if box.width == 'auto' else box.width\n        column_gap = percentage(column_gap, box.style, refer_to)\n    row_gap = box.style['row_gap']\n    if row_gap == 'normal':\n        row_gap = 0\n    else:\n        refer_to = 0 if box.height == 'auto' else box.height\n        row_gap = percentage(row_gap, box.style, refer_to)\n\n    if grid_areas == 'none':\n        grid_areas = ((None,),)\n    grid_areas = [list(row) for row in grid_areas]\n\n    rows = _get_template_tracks(box.style['grid_template_rows'])\n    columns = _get_template_tracks(box.style['grid_template_columns'])\n\n    # Adjust rows number\n    grid_areas_columns = len(grid_areas[0]) if grid_areas else 0\n    rows_diff = int((len(rows) - 1) / 2) - len(grid_areas)\n    if rows_diff > 0:\n        for _ in range(rows_diff):\n            grid_areas.append([None] * grid_areas_columns)\n    elif rows_diff < 0:\n        for _ in range(-rows_diff):\n            rows.append(next(auto_rows))\n            rows.append([])\n\n    # Adjust columns number\n    columns_diff = int((len(columns) - 1) / 2) - grid_areas_columns\n    if columns_diff > 0:\n        for row in grid_areas:\n            for _ in range(columns_diff):\n                row.append(None)\n    elif columns_diff < 0:\n        for _ in range(-columns_diff):\n            columns.append(next(auto_columns))\n            columns.append([])\n\n    # Add implicit line names\n    for y, row in enumerate(grid_areas):\n        for x, area_name in enumerate(row):\n            if area_name is None:\n                continue\n            start_name = f'{area_name}-start'\n            names = [name for row in rows[::2] for name in row]\n            if start_name not in names:\n                rows[2*y].append(start_name)\n            names = [name for column in columns[::2] for name in column]\n            if start_name not in names:\n                columns[2*x].append(start_name)\n    for y, row in enumerate(grid_areas[::-1]):\n        for x, area_name in enumerate(row[::-1]):\n            if area_name is None:\n                continue\n            end_name = f'{area_name}-end'\n            names = [name for row in rows[::2] for name in row]\n            if end_name not in names:\n                rows[-2*y-1].append(end_name)\n            names = [name for column in columns[::2] for name in column]\n            if end_name not in names:\n                columns[-2*x-1].append(end_name)\n\n    # 1. Run the grid placement algorithm.\n\n    first_flow = 'column' if 'column' in flow else 'row'  # auto flow axis\n    second_flow = 'row' if 'column' in flow else 'column'  # other axis\n    first_tracks = rows if first_flow == 'row' else columns\n    second_tracks = rows if second_flow == 'row' else columns\n\n    # 1.1 Position anything that’s not auto-positioned.\n    children = sorted(box.children, key=lambda item: item.style['order'])\n    children_positions = {}\n    for child in children:\n        column_start = child.style['grid_column_start']\n        column_end = child.style['grid_column_end']\n        row_start = child.style['grid_row_start']\n        row_end = child.style['grid_row_end']\n\n        column_placement = _get_placement(\n            column_start, column_end, columns[::2])\n        row_placement = _get_placement(row_start, row_end, rows[::2])\n\n        if column_placement and row_placement:\n            x, width = column_placement\n            y, height = row_placement\n            children_positions[child] = (x, y, width, height)\n\n    # 1.2 Process the items locked to a given row (resp. column).\n    for child in children:\n        if child in children_positions:\n            continue\n        first_start = child.style[f'grid_{first_flow}_start']\n        first_end = child.style[f'grid_{first_flow}_end']\n        first_placement = _get_placement(first_start, first_end, first_tracks[::2])\n        if not first_placement:\n            continue\n        second_start = child.style[f'grid_{second_flow}_start']\n        second_end = child.style[f'grid_{second_flow}_end']\n        second_placement = _get_second_placement(\n            first_placement, second_start, second_end, second_tracks,\n            children_positions, first_flow, 'dense' in flow)\n        if first_flow == 'row':\n            y, height = first_placement\n            x, width = second_placement\n        else:\n            x, width = first_placement\n            y, height = second_placement\n        children_positions[child] = (x, y, width, height)\n\n    # 1.3 Determine the columns (resp. rows) in the implicit grid.\n    # 1.3.1 Start with the columns (resp. rows) from the explicit grid.\n    implicit_second_1 = 0\n    if second_flow == 'column':\n        implicit_second_2 = len(grid_areas[0]) if grid_areas else 0\n    else:\n        implicit_second_2 = len(grid_areas)\n    # 1.3.2 Add columns (resp. rows) to the beginning and end of the implicit grid.\n    remaining_grid_items = []\n    for child in children:\n        if child in children_positions:\n            if second_flow == 'column':\n                i, _, size, _ = children_positions[child]\n            else:\n                _, i, _, size = children_positions[child]\n        else:\n            second_start = child.style[f'grid_{second_flow}_start']\n            second_end = child.style[f'grid_{second_flow}_end']\n            second_placement = _get_placement(\n                second_start, second_end, second_tracks[::2])\n            remaining_grid_items.append(child)\n            if second_placement:\n                i, size = second_placement\n            else:\n                continue\n        implicit_second_1 = min(i, implicit_second_1)\n        implicit_second_2 = max(i + size, implicit_second_2)\n    # 1.3.3 Add columns (resp. rows) to accommodate max track span.\n    for child in remaining_grid_items:\n        second_start = child.style[f'grid_{second_flow}_start']\n        second_end = child.style[f'grid_{second_flow}_end']\n        span = 1\n        if second_start != 'auto' and second_start[0] == 'span':\n            span = second_start[1]\n        elif second_end != 'auto' and second_end[0] == 'span':\n            span = second_end[1]\n        implicit_second_2 = max(implicit_second_1 + (span or 1), implicit_second_2)\n\n    # 1.4 Position the remaining grid items.\n    implicit_first_1 = 0\n    if first_flow == 'row':\n        implicit_first_2 = len(grid_areas)\n    else:\n        implicit_first_2 = len(grid_areas[0]) if grid_areas else 0\n    for position in children_positions.values():\n        if first_flow == 'row':\n            _, i, _, size = position\n        else:\n            i, _, size, _ = position\n        implicit_first_1 = min(i, implicit_first_1)\n        implicit_first_2 = max(i + size, implicit_first_2)\n    cursor_first, cursor_second = implicit_first_1, implicit_second_1\n    if 'dense' in flow:\n        for child in remaining_grid_items:\n            first_start = child.style[f'grid_{first_flow}_start']\n            first_end = child.style[f'grid_{first_flow}_end']\n            second_start = child.style[f'grid_{second_flow}_start']\n            second_end = child.style[f'grid_{second_flow}_end']\n            second_placement = _get_placement(\n                second_start, second_end, second_tracks[::2])\n            if second_placement:\n                # 1. Set the row (resp. column) position of the cursor.\n                cursor_first = implicit_first_1\n                second_i, second_size = second_placement\n                cursor_second = second_i\n                # 2. Increment the cursor’s row (resp. column) position.\n                for first_i in count(cursor_first):\n                    if first_start == 'auto':\n                        first_i, first_size = _get_placement(\n                            (None, first_i + 1, None), first_end, first_tracks[::2])\n                    else:\n                        assert first_start[0] == 'span'\n                        span = _get_span(first_start)\n                        first_i, first_size = _get_placement(\n                            first_start, (None, first_i + 1 + span, None),\n                            first_tracks[::2])\n                    if first_i < cursor_first:\n                        continue\n                    for _ in range(first_i, first_i + first_size):\n                        if first_flow == 'row':\n                            x, y = second_i, first_i\n                            width, height = second_size, first_size\n                        else:\n                            x, y = first_i, second_i\n                            width, height = first_size, second_size\n                        intersect = _intersect_with_children(\n                            x, y, width, height, children_positions.values())\n                        if intersect:\n                            # Child intersects with a positioned child on\n                            # current row.\n                            break\n                    else:\n                        # Child doesn’t intersect with any positioned child on\n                        # any row.\n                        break\n                first_diff = first_i + first_size - implicit_first_2\n                if first_diff > 0:\n                    implicit_first_2 += first_diff\n                # 3. Set the item’s row-start line.\n                if first_flow == 'row':\n                    x, y = second_i, first_i\n                    width, height = second_size, first_size\n                else:\n                    x, y = first_i, second_i\n                    width, height = first_size, second_size\n                children_positions[child] = (x, y, width, height)\n            else:\n                # 1. Set the cursor’s row and column positions.\n                cursor_first, cursor_second = implicit_first_1, implicit_second_1\n                while True:\n                    # 2. Increment the column (resp. row) position of the cursor.\n                    first_i = cursor_first\n                    for second_i in range(cursor_second, implicit_second_2):\n                        if first_start == 'auto':\n                            first_i, first_size = _get_placement(\n                                (None, first_i + 1, None), first_end, first_tracks[::2])\n                        else:\n                            assert first_start[0] == 'span'\n                            span = _get_span(first_start)\n                            first_i, first_size = _get_placement(\n                                first_start, (None, first_i + 1 + span, None),\n                                first_tracks[::2])\n                        if second_start == 'auto':\n                            second_i, second_size = _get_placement(\n                                (None, second_i + 1, None), second_end,\n                                second_tracks[::2])\n                        else:\n                            span = _get_span(second_start)\n                            second_i, second_size = _get_placement(\n                                second_start, (None, second_i + 1 + span, None),\n                                second_tracks[::2])\n                        if first_flow == 'row':\n                            x, y = second_i, first_i\n                            width, height = second_size, first_size\n                        else:\n                            x, y = first_i, second_i\n                            width, height = first_size, second_size\n                        intersect = _intersect_with_children(\n                            x, y, width, height, children_positions.values())\n                        overflow = second_i + second_size > implicit_second_2\n                        if intersect or overflow:\n                            # Child intersects with a positioned child or overflows.\n                            continue\n                        else:\n                            # Free place found.\n                            # 3. Set the item’s row-/column-start lines.\n                            children_positions[child] = (x, y, width, height)\n                            first_diff = (\n                                cursor_first + first_size - 1 - implicit_first_2)\n                            if first_diff > 0:\n                                implicit_first_2 += first_diff\n                            break\n                    else:\n                        # No room found.\n                        # 2. Return to the previous step.\n                        cursor_first += 1\n                        first_diff = cursor_first + 1 - implicit_first_2\n                        if first_diff > 0:\n                            implicit_first_2 += first_diff\n                        cursor_second = implicit_second_1\n                        continue\n                    break\n    else:\n        for child in remaining_grid_items:\n            first_start = child.style[f'grid_{first_flow}_start']\n            first_end = child.style[f'grid_{first_flow}_end']\n            second_start = child.style[f'grid_{second_flow}_start']\n            second_end = child.style[f'grid_{second_flow}_end']\n            second_placement = _get_placement(\n                second_start, second_end, second_tracks[::2])\n            if second_placement:\n                # 1. Set the column (resp. row) position of the cursor.\n                second_i, second_size = second_placement\n                if second_i < cursor_second:\n                    cursor_first += 1\n                cursor_second = second_i\n                # 2. Increment the cursor’s row (resp. column) position.\n                for cursor_first in count(cursor_first):\n                    if first_start == 'auto':\n                        first_i, first_size = _get_placement(\n                            (None, cursor_first + 1, None), first_end,\n                            first_tracks[::2])\n                    else:\n                        assert first_start[0] == 'span'\n                        span = _get_span(first_start)\n                        first_i, first_size = _get_placement(\n                            first_start, (None, first_i + 1 + span, None),\n                            first_tracks[::2])\n                    if first_i < cursor_first:\n                        continue\n                    for row in range(first_i, first_i + first_size):\n                        if first_flow == 'row':\n                            x, y = second_i, first_i\n                            width, height = second_size, first_size\n                        else:\n                            x, y = first_i, second_i\n                            width, height = first_size, second_size\n                        intersect = _intersect_with_children(\n                            x, y, width, height, children_positions.values())\n                        if intersect:\n                            # Child intersects with a positioned child on\n                            # current row.\n                            break\n                    else:\n                        # Child doesn’t intersect with any positioned child on\n                        # any row.\n                        break\n                first_diff = first_i + first_size - implicit_first_2\n                if first_diff > 0:\n                    implicit_first_2 += first_diff\n                # 3. Set the item’s row-start line.\n                children_positions[child] = (x, y, width, height)\n            else:\n                while True:\n                    # 1. Increment the column position of the cursor.\n                    first_i = cursor_first\n                    for second_i in range(cursor_second, implicit_second_2):\n                        if first_start == 'auto':\n                            first_i, first_size = _get_placement(\n                                (None, first_i + 1, None), first_end, first_tracks[::2])\n                        else:\n                            span = _get_span(first_start)\n                            first_i, first_size = _get_placement(\n                                first_start, (None, first_i + 1 + span, None),\n                                first_tracks[::2])\n                        if second_start == 'auto':\n                            second_i, second_size = _get_placement(\n                                (None, second_i + 1, None), second_end,\n                                second_tracks[::2])\n                        else:\n                            span = _get_span(second_start)\n                            second_i, second_size = _get_placement(\n                                second_start, (None, second_i + 1 + span, None),\n                                second_tracks[::2])\n                        if first_flow == 'row':\n                            x, y = second_i, first_i\n                            width, height = second_size, first_size\n                        else:\n                            x, y = first_i, second_i\n                            width, height = first_size, second_size\n                        intersect = _intersect_with_children(\n                            x, y, width, height, children_positions.values())\n                        overflow = second_i + second_size > implicit_second_2\n                        if intersect or overflow:\n                            # Child intersects with a positioned child or overflows.\n                            continue\n                        else:\n                            # Free place found.\n                            # 2. Set the item’s row-/column-start lines.\n                            children_positions[child] = (x, y, width, height)\n                            break\n                    else:\n                        # No room found.\n                        # 2. Return to the previous step.\n                        cursor_first += 1\n                        first_diff = cursor_first + 1 - implicit_first_2\n                        if first_diff > 0:\n                            implicit_first_2 += first_diff\n                        cursor_second = implicit_second_1\n                        continue\n                    break\n\n    if first_flow == 'row':\n        implicit_x1, implicit_x2 = implicit_second_1, implicit_second_2\n        implicit_y1, implicit_y2 = implicit_first_1, implicit_first_2\n    else:\n        implicit_x1, implicit_x2 = implicit_first_1, implicit_first_2\n        implicit_y1, implicit_y2 = implicit_second_1, implicit_second_2\n    for _ in range(0 - implicit_x1):\n        columns.insert(0, next(auto_columns_back))\n        columns.insert(0, [])\n    for _ in range(len(grid_areas[0]) if grid_areas else 0, implicit_x2):\n        columns.append(next(auto_columns))\n        columns.append([])\n    for _ in range(0 - implicit_y1):\n        rows.insert(0, next(auto_rows_back))\n        rows.insert(0, [])\n    for _ in range(len(grid_areas), implicit_y2):\n        rows.append(next(auto_rows))\n        rows.append([])\n\n    page_breaks_by_row = defaultdict(\n        lambda: {'before': 'auto', 'after': 'auto', 'inside': 'auto'})\n    for child, (x, y, width, height) in children_positions.items():\n        row_page_break = page_breaks_by_row[y]\n        if child.style['break_inside'] in ('avoid', 'avoid-page'):\n            row_page_break['inside'] = 'avoid'\n        for side in ('before', 'after'):\n            if child.style[f'break_{side}'] in ('avoid', 'avoid-page'):\n                if row_page_break[side] != 'page':\n                    row_page_break[side] = 'avoid'\n            elif child.style[f'break_{side}'] in ('page', 'always'):\n                row_page_break[side] = 'page'\n            elif child.style[f'break_{side}'] in ('left', 'right', 'recto', 'verso'):\n                row_page_break[side] = child.style[f'break_{side}']\n\n    # 2. Find the size of the grid container.\n\n    if isinstance(box, boxes.GridBox):\n        from .block import block_level_width\n        block_level_width(box, containing_block)\n    else:\n        assert isinstance(box, boxes.InlineGridBox)\n        from .inline import inline_block_width\n        inline_block_width(box, context, containing_block)\n    if box.width == 'auto':\n        # TODO: Calculate max-width.\n        box.width = containing_block.width\n\n    # 3. Run the grid sizing algorithm.\n\n    # 3.0 List min/max sizing functions.\n    row_sizing_functions = [_get_sizing_functions(row) for row in rows[1::2]]\n    column_sizing_functions = [\n        _get_sizing_functions(column) for column in columns[1::2]]\n\n    # 3.1 Resolve the sizes of the grid columns.\n    columns_sizes = _resolve_tracks_sizes(\n        column_sizing_functions, box.width, children_positions, implicit_second_1,\n        'x', column_gap, context, box)\n\n    # 3.2 Resolve the sizes of the grid rows.\n    vertical_sizes = [size for size, _ in columns_sizes]\n    rows_sizes = _resolve_tracks_sizes(\n        row_sizing_functions, box.height, children_positions, implicit_y1, 'y',\n        row_gap, context, box, vertical_sizes)\n\n    # 3.3 Re-resolve the sizes of the grid columns with min-/max-content.\n    # TODO: Re-resolve.\n\n    # 3.4 Re-re-resolve the sizes of the grid columns with min-/max-content.\n    # TODO: Re-re-resolve.\n\n    # 3.5 Align the tracks within the grid container.\n    # TODO: Support safe/unsafe.\n    justify_content = set(box.style['justify_content'])\n    x = box.content_box_x()\n    free_width = max(0, box.width - sum(size for size, _ in columns_sizes))\n    columns_positions = []\n    columns_number = len(columns_sizes)\n    if justify_content & {'center'}:\n        x += free_width / 2\n        for size, _ in columns_sizes:\n            columns_positions.append(x)\n            x += size + column_gap\n    elif justify_content & {'right', 'end', 'flex-end'}:\n        x += free_width\n        for size, _ in columns_sizes:\n            columns_positions.append(x)\n            x += size + column_gap\n    elif justify_content & {'space-around'}:\n        x += free_width / 2 / columns_number\n        for size, _ in columns_sizes:\n            columns_positions.append(x)\n            x += size + free_width / columns_number + column_gap\n    elif justify_content & {'space-between'}:\n        for size, _ in columns_sizes:\n            columns_positions.append(x)\n            if columns_number >= 2:\n                x += size + free_width / (columns_number - 1) + column_gap\n    elif justify_content & {'space-evenly'}:\n        x += free_width / (columns_number + 1)\n        for size, _ in columns_sizes:\n            columns_positions.append(x)\n            x += size + free_width / (columns_number + 1) + column_gap\n    else:\n        for size, _ in columns_sizes:\n            columns_positions.append(x)\n            x += size + column_gap\n\n    align_content = set(box.style['align_content'])\n    y = box.content_box_y()\n    if box.height == 'auto':\n        free_height = 0\n    else:\n        free_height = (\n            box.height -\n            sum(size for size, _ in rows_sizes) -\n            (len(rows_sizes) - 1) * row_gap)\n        free_height = max(0, free_height)\n    rows_positions = []\n    rows_number = len(rows_sizes)\n    if align_content & {'center'}:\n        y += free_height / 2\n        for size, _ in rows_sizes:\n            rows_positions.append(y)\n            y += size + row_gap\n    elif align_content & {'right', 'end', 'flex-end'}:\n        y += free_height\n        for size, _ in rows_sizes:\n            rows_positions.append(y)\n            y += size + row_gap\n    elif align_content & {'space-around'}:\n        y += free_height / 2 / rows_number\n        for size, _ in rows_sizes:\n            rows_positions.append(y)\n            y += size + free_height / rows_number + row_gap\n    elif align_content & {'space-between'}:\n        for size, _ in rows_sizes:\n            rows_positions.append(y)\n            if rows_number >= 2:\n                y += size + free_height / (rows_number - 1) + row_gap\n    elif align_content & {'space-evenly'}:\n        y += free_height / (rows_number + 1)\n        for size, _ in rows_sizes:\n            rows_positions.append(y)\n            y += size + free_height / (rows_number + 1) + row_gap\n    else:\n        if align_content & {'baseline'}:\n            # TODO: Support baseline value.\n            LOGGER.warning('Baseline alignment is not supported for grid layout')\n        for size, _ in rows_sizes:\n            rows_positions.append(y)\n            y += size + row_gap\n\n    # 4. Lay out the grid items into their respective containing blocks.\n\n    # Find resume_at row.\n    this_page_children = []\n    resume_row = None\n    extra_skip_height = 0\n    if skip_stack:\n        from .block import block_level_layout\n        first_skip_row = min(skip_stack)\n        last_skip_row = max(skip_stack)\n        skip_height = (\n            sum(size for size, _ in rows_sizes[:last_skip_row]) +\n            (len(rows_sizes[:last_skip_row]) - 1) * row_gap)\n        for child, (x, y, width, height) in children_positions.items():\n            if (advancement := box.advancements.get((x, y))) is None:\n                continue\n            span = _get_span(child.style['grid_row_start'])\n            span_height = (\n                sum(size for size, _ in rows_sizes[y:y+span]) +\n                (span - 1) * row_gap)\n            index = tuple(children_positions).index(child)\n            width = sum(vertical_sizes[x:x+width])\n            child = child.deepcopy()\n            child.position_x = 0\n            child.position_y = 0\n            if y in skip_stack:\n                child_skip_stack = skip_stack[y].get(index)\n            else:\n                child_skip_stack = None\n            parent = boxes.BlockContainerBox.anonymous_from(containing_block, ())\n            resolve_percentages(parent, containing_block)\n            parent.position_x = 0\n            parent.position_y = 0\n            parent.width = width\n            parent.height = 0\n            child, _, _, _, _, _ = block_level_layout(\n                context, child, bottom_space=-inf, skip_stack=child_skip_stack,\n                containing_block=parent)\n            skip_stack_advancement = span_height - child.margin_height()\n            if skip_stack_advancement < advancement:\n                extra_skip_height = max(\n                    extra_skip_height, advancement - skip_stack_advancement)\n        for (x, y), advancement in box.advancements.items():\n            if y != last_skip_row:\n                continue\n            skip_height += advancement\n            break\n        else:\n            extra_skip_height = 0\n        skip_height -= extra_skip_height\n    else:\n        first_skip_row = last_skip_row = skip_height = 0\n    resume_at = None\n    total_height = (\n        sum(size for size, _ in rows_sizes[last_skip_row:]) +\n        (len(rows_sizes[last_skip_row:]) - 1) * row_gap)\n\n    def _add_page_children(max_row=inf):\n        for j, child in enumerate(children):\n            _, y, _, _ = children_positions[child]\n            if not first_skip_row <= y < max_row:\n                # Item in previous or next rows.\n                continue\n            if skip_stack is None:\n                # No skip stack, draw on this page.\n                this_page_children.append((j, child))\n            elif y > last_skip_row:\n                # Row after the skip stack, draw on this page.\n                this_page_children.append((j, child))\n            elif y in skip_stack:\n                child_skip_stack = skip_stack[y]\n                if child_skip_stack is None or j in child_skip_stack:\n                    # Child in skip stack, draw on this page.\n                    this_page_children.append((j, child))\n\n    row_lines_positions = (\n        [*rows_positions[first_skip_row + 1:], box.content_box_y() + total_height])\n    for i, row_y in enumerate(row_lines_positions, start=first_skip_row):\n        # TODO: handle break-before and break-after for rows.\n        if not context.overflows_page(bottom_space, row_y - skip_height):\n            # No need to force rendering, at least a whole row fits in page.\n            page_is_empty = False\n            continue\n        resume_row = i\n        if box.style['break_inside'] == 'avoid' and not page_is_empty:\n            # Avoid breaks inside grid container, break before.\n            context.finish_block_formatting_context(box)\n            return None, None, {'break': 'any', 'page': None}, [], False\n        if page_breaks_by_row[i]['inside'] == 'avoid' and not page_is_empty:\n            # Break before current row.\n            if resume_row == 0:\n                # First row, break before grid container.\n                context.finish_block_formatting_context(box)\n                return None, None, {'break': 'any', 'page': None}, [], False\n            # Mark all children before and in current row as drawn on the page.\n            _add_page_children(resume_row)\n            resume_at = {resume_row: None}\n        else:\n            # Break inside current row.\n            # Mark all children before current row as drawn on the page.\n            _add_page_children(resume_row + 1)\n        break\n    else:\n        # Mark all children as drawn on the page.\n        _add_page_children()\n    if box.height == 'auto':\n        box.height = (\n            sum(size for size, _ in rows_sizes[:resume_row]) +\n            (len(rows_sizes[:resume_row]) - 1) * row_gap) - skip_height\n\n    # Lay out grid items.\n    justify_items = set(box.style['justify_items'])\n    align_items = set(box.style['align_items'])\n    new_children = []\n    new_children_by_rows = defaultdict(list)\n    baseline = None\n    next_page = {'break': 'any', 'page': None}\n    from .block import block_level_layout\n    for i, child in this_page_children:\n        x, y, width, height = children_positions[child]\n        index = children.index(child)\n        if skip_stack and skip_stack.get(y):\n            if index in skip_stack[y]:\n                child_skip_stack = skip_stack[y][index]\n            else:\n                assert isinstance(child, boxes.ParentBox)\n                child_skip_stack = {len(child.children): None}\n        else:\n            child_skip_stack = None\n        child = child.deepcopy()\n        child.position_x = columns_positions[x]\n        child.position_y = rows_positions[y] - skip_height\n        resolve_percentages(child, box)\n        width = (\n            sum(size for size, _ in columns_sizes[x:x+width]) +\n            (width - 1) * column_gap)\n        height = (\n            sum(size for size, _ in rows_sizes[y:y+height]) +\n            (height - 1) * row_gap)\n        if skip_stack and (x, y) in box.advancements:\n            child.position_y += box.advancements[x, y] - extra_skip_height\n            height -= box.advancements[x, y] - extra_skip_height\n\n        # TODO: Apply auto margin.\n        if child.margin_top == 'auto':\n            child.margin_top = 0\n        if child.margin_right == 'auto':\n            child.margin_right = 0\n        if child.margin_bottom == 'auto':\n            child.margin_bottom = 0\n        if child.margin_left == 'auto':\n            child.margin_left = 0\n\n        child_border_width = width - (child.margin_left + child.margin_right)\n        child_content_width = child_border_width - (\n                child.border_left_width + child.padding_left +\n                child.border_right_width + child.padding_right)\n        child_border_height = height - child.margin_bottom\n        child_content_height = child_border_height - (\n            child.border_bottom_width + child.padding_bottom)\n        if not child_skip_stack or child.style['box_decoration_break'] == 'clone':\n            child_border_height -= child.margin_top\n            child_content_height -= (\n                child.margin_top + child.border_top_width + child.padding_top)\n\n        justify_self = set(child.style['justify_self'])\n        if justify_self & {'auto'}:\n            justify_self = justify_items\n        if justify_self & {'normal', 'stretch'}:\n            if child.style['width'] == 'auto':\n                child.style = child.style.copy()\n                child_width = (\n                    child_content_width if child.style['box_sizing'] == 'content-box'\n                    else child_border_width)\n                child.style['width'] = Dimension(child_width, 'px')\n        align_self = set(child.style['align_self'])\n        if align_self & {'auto'}:\n            align_self = align_items\n        if align_self & {'normal', 'stretch'}:\n            if child.style['height'] == 'auto':\n                child.style = child.style.copy()\n                child_height = (\n                    child_content_height if child.style['box_sizing'] == 'content-box'\n                    else child_border_height)\n                child.style['height'] = Dimension(child_height, 'px')\n\n        # TODO: Find a better solution for the layout.\n        parent = boxes.BlockContainerBox.anonymous_from(box, ())\n        resolve_percentages(parent, containing_block)\n        parent.position_x = child.position_x\n        parent.position_y = child.position_y\n        parent.width = width\n        parent.height = height\n        new_child, child_resume_at, child_next_page = block_level_layout(\n            context, child, bottom_space, child_skip_stack, parent,\n            page_is_empty, absolute_boxes, fixed_boxes)[:3]\n        if new_child:\n            broken_child = False\n            span = _get_span(child.style['grid_row_start'])\n            if child_resume_at:\n                broken_child = True\n            elif resume_row is not None and y + span >= resume_row + 1:\n                broken_child = True\n            if broken_child:\n                # Child is broken, add row to resume_at.\n                if resume_at is None:\n                    resume_at = {}\n                if y not in resume_at:\n                    resume_at[y] = {}\n            if child_resume_at:\n                # There is some content left for next page, save the cell’s resume_at.\n                resume_at[y][i] = child_resume_at\n            elif broken_child:\n                # Everything fits but the cell overflows. Only display the bottom of an\n                # empty cell on next page, set the cell’s resume_at after the cell’s\n                # last child.\n                assert isinstance(new_child, boxes.ParentBox)\n                previous_skip_child = max(child_skip_stack) if child_skip_stack else 0\n                if not page_is_empty:\n                    resume_at[y][i] = {\n                        previous_skip_child + len(new_child.children): None}\n            page_is_empty = False\n        else:\n            if resume_at is None:\n                resume_at = {}\n            if y not in resume_at:\n                resume_at[y] = {}\n            resume_at[y][i] = None\n            continue\n\n        if justify_self & {'normal', 'stretch'}:\n            new_child.width = max(child_content_width, new_child.width)\n        else:\n            if new_child.style['width'] == 'auto':\n                new_child.width = max_content_width(context, new_child, outer=False)\n            diff = child_content_width - new_child.width\n            if justify_self & {'center'}:\n                new_child.translate(diff / 2, 0)\n            elif justify_self & {'right', 'end', 'flex-end', 'self-end'}:\n                new_child.translate(diff, 0)\n\n        # TODO: Apply auto margins.\n        if align_self & {'normal', 'stretch'}:\n            new_child.height = max(child_content_height, new_child.height)\n        else:\n            diff = child_content_height - new_child.height\n            if align_self & {'center'}:\n                new_child.translate(0, diff / 2)\n            elif align_self & {'end', 'flex-end', 'self-end'}:\n                new_child.translate(0, diff)\n\n        new_children.append(new_child)\n        new_children_by_rows[y].append((x, new_child))\n        if baseline is None and y == implicit_y1:\n            baseline = find_in_flow_baseline(new_child)\n\n    # Abort whole grid rendering if no child fits.\n    if this_page_children and not new_children:\n        context.finish_block_formatting_context(box)\n        return None, None, {'break': 'any', 'page': None}, [], False\n\n    old_advancements = box.advancements.copy()\n    advancements = box.advancements\n    advancements.clear()\n    box = box.copy_with_children(new_children)\n    if isinstance(box, boxes.InlineGridBox):\n        # TODO: Synthetize a real baseline value.\n        LOGGER.warning('Inline grids are not supported')\n        box.baseline = baseline or 0\n\n    from .absolute import absolute_layout\n    from .block import relative_positioning\n\n    if box.style['position'] == 'relative':\n        # New containing block, resolve the layout of the absolute descendants\n        for absolute_box in absolute_boxes:\n            absolute_layout(\n                context, absolute_box, box, fixed_boxes, bottom_space,\n                skip_stack=None)\n\n    for child in box.children:\n        relative_positioning(child, (box.width, box.height))\n\n    # Resume early when there’s no resume_at.\n    if not resume_at:\n        context.finish_block_formatting_context(box)\n        return box, resume_at, next_page, [], False\n\n    # Set broken rows’ bottom at the bottom of the page.\n    last_page_row = max(new_children_by_rows)\n    next_page_first_row = min(resume_at)\n    next_page_last_row = max(resume_at)\n    extra_advancement = 0\n    for y in range(last_page_row, next_page_first_row - 1, -1):\n        for x, child in new_children_by_rows[y]:\n            span = _get_span(child.style['grid_row_start'])\n            if y + span < last_page_row + 1:\n                # Child finishing before the last row, do nothing.\n                continue\n            broken_child = y + span >= next_page_last_row + 1\n            if broken_child and child.style['box_decoration_break'] != 'clone':\n                child.remove_decoration(start=False, end=True)\n            child.height = max(0, (\n                context.page_bottom - bottom_space - child.position_y -\n                child.margin_top - child.border_top_width - child.padding_top -\n                child.margin_bottom - child.border_bottom_width - child.padding_bottom))\n            if broken_child:\n                # Child not fully drawn, keep advancement.\n                advancements[x, y] = child.margin_height()\n                if (x, y) in old_advancements:\n                    advancements[x, y] += old_advancements[x, y] - extra_skip_height\n            else:\n                # Child fully drawn, save the extra height added to reach the bottom of\n                # the page to substract it from the advancements.\n                extra_advancement = max(\n                    extra_advancement, child.height - child_content_height)\n\n    # Substract the extra height added to reach the bottom of the page from all the\n    # advancements.\n    if extra_advancement:\n        for x, y in advancements:\n            advancements[x, y] -= extra_advancement\n\n    # Set box height and remove bottom decoration.\n    box.height = (\n        context.page_bottom - bottom_space - box.position_y -\n        box.margin_top - box.border_top_width - box.padding_top)\n    if box.style['box_decoration_break'] != 'clone':\n        box.remove_decoration(start=False, end=True)\n        box.height -= box.margin_bottom + box.border_bottom_width + box.padding_bottom\n\n    context.finish_block_formatting_context(box)\n    return box, resume_at, next_page, [], False\n"
  },
  {
    "path": "weasyprint/layout/inline.py",
    "content": "\"\"\"Layout for inline-level boxes.\"\"\"\n\nimport unicodedata\nfrom math import inf\n\nfrom ..css import AnonymousStyle, Pending\nfrom ..css.properties import INHERITED\nfrom ..formatting_structure import boxes, build\nfrom .absolute import AbsolutePlaceholder, absolute_layout\nfrom .flex import flex_layout\nfrom .float import avoid_collisions, float_layout\nfrom .grid import grid_layout\nfrom .leader import handle_leader\nfrom .min_max import handle_min_max_width\nfrom .percent import percentage, resolve_one_percentage, resolve_percentages\nfrom .preferred import inline_min_content_width, shrink_to_fit, trailing_whitespace_size\nfrom .replaced import inline_replaced_box_layout\nfrom .table import find_in_flow_baseline, table_wrapper_width\n\nfrom ..text.line_break import (  # isort:skip\n    can_break_text, character_ratio, create_layout, split_first_line, strut)\n\n\ndef iter_line_boxes(context, box, position_y, bottom_space, skip_stack,\n                    containing_block, absolute_boxes, fixed_boxes,\n                    first_letter_style, first_line_style):\n    \"\"\"Return an iterator of ``(line, resume_at)``.\n\n    ``line`` is a laid-out LineBox with as much content as possible that\n    fits in the available width.\n\n    \"\"\"\n    resolve_percentages(box, containing_block)\n    if skip_stack is None:\n        # TODO: wrong, see issue #679.\n        resolve_one_percentage(box, 'text_indent', containing_block.width)\n    else:\n        box.text_indent = 0\n    while True:\n        line, resume_at = get_next_linebox(\n            context, box, position_y, bottom_space, skip_stack, containing_block,\n            absolute_boxes, fixed_boxes, first_letter_style, first_line_style)\n        first_line_style = None\n        if line:\n            handle_leader(context, line, containing_block)\n            position_y = line.position_y + line.height\n        if line is None:\n            return\n        yield line, resume_at\n        if resume_at is None:\n            return\n        skip_stack = resume_at\n        box.text_indent = 0\n        first_letter_style = None\n\n\ndef get_next_linebox(context, linebox, position_y, bottom_space, skip_stack,\n                     containing_block, absolute_boxes, fixed_boxes,\n                     first_letter_style, first_line_style):\n    \"\"\"Return next line from given linebox.\n\n    Return ``(line, resume_at)``, where ``line`` is a new linebox copied from the\n    original one, with replaced children.\n\n    This function takes care of excluded floating shapes to avoid collisions.\n\n    \"\"\"\n\n    skip_stack = skip_first_whitespace(linebox, skip_stack)\n    if skip_stack == 'continue':\n        return None, None\n\n    skip_stack = first_letter_to_box(linebox, skip_stack, first_letter_style)\n\n    linebox.position_y = position_y\n\n    if context.excluded_shapes:\n        # Width and height must be calculated to avoid floats\n        linebox.width = inline_min_content_width(\n            context, linebox, skip_stack=skip_stack, first_line=True)\n        linebox.height, _ = strut(linebox.style)\n    else:\n        # No float, width and height will be set by the lines\n        linebox.width = linebox.height = 0\n    position_x, position_y, available_width = avoid_collisions(\n        context, linebox, containing_block, outer=False)\n\n    candidate_height = linebox.height\n\n    excluded_shapes = context.excluded_shapes.copy()\n\n    while True:\n        original_position_x = linebox.position_x = position_x\n        original_position_y = linebox.position_y = position_y\n        original_width = linebox.width\n        max_x = position_x + available_width\n        position_x += linebox.text_indent\n\n        line_placeholders = []\n        line_absolutes = []\n        line_fixed = []\n        waiting_floats = []\n        line_children = []\n\n        (line, resume_at, preserved_line_break, first_letter,\n         last_letter, float_width) = split_inline_box(\n             context, linebox, position_x, max_x, bottom_space, skip_stack,\n             containing_block, line_absolutes, line_fixed, line_placeholders,\n             waiting_floats, line_children, first_letter_style, first_line_style)\n        linebox.width, linebox.height = line.width, line.height\n\n        if is_phantom_linebox(line) and not preserved_line_break:\n            line.height = 0\n            break\n\n        remove_last_whitespace(context, line)\n\n        new_position_x, _, new_available_width = avoid_collisions(\n            context, linebox, containing_block, outer=False)\n        offset_x = text_align(\n            context, line, new_available_width,\n            last=(resume_at is None or preserved_line_break))\n        if containing_block.style['direction'] == 'rtl':\n            offset_x *= -1\n            offset_x -= line.width\n\n        bottom, top = line_box_verticality(line)\n        assert top is not None\n        assert bottom is not None\n        line.baseline = -top\n        line.position_y = top\n        line.height = bottom - top\n        offset_y = position_y - top\n        line.margin_top = 0\n        line.margin_bottom = 0\n\n        line.translate(offset_x, offset_y)\n        # Avoid floating point errors, as position_y - top + top != position_y\n        # Removing this line breaks the position == linebox.position test below\n        # See issue #583.\n        line.position_y = position_y\n\n        if line.height <= candidate_height:\n            break\n        candidate_height = line.height\n\n        new_excluded_shapes = context.excluded_shapes\n        context.excluded_shapes = excluded_shapes\n        position_x, position_y, available_width = avoid_collisions(\n            context, line, containing_block, outer=False)\n\n        if first_line_style:\n            first_line_box = line.copy_with_children(line.children)\n            first_line_box.element_tag += '::first-line'\n            first_line_box.style = first_line_box.style.copy()\n            for key, value in first_line_style.items():\n                first_line_box.style[key] = value\n            line.children = [first_line_box]\n            _adjust_line_height(first_line_box)\n\n        if containing_block.style['direction'] == 'ltr':\n            condition = (position_x, position_y) == (\n                original_position_x, original_position_y)\n        else:\n            condition = (position_x + line.width, position_y) == (\n                original_position_x + original_width, original_position_y)\n        if condition:\n            context.excluded_shapes = new_excluded_shapes\n            break\n\n    absolute_boxes.extend(line_absolutes)\n    fixed_boxes.extend(line_fixed)\n\n    for placeholder in line_placeholders:\n        if 'inline' in placeholder.style.specified['display']:\n            # Inline-level static position:\n            placeholder.translate(0, position_y - placeholder.position_y)\n        else:\n            # Block-level static position: at the start of the next line\n            placeholder.translate(\n                line.position_x - placeholder.position_x,\n                position_y + line.height - placeholder.position_y)\n\n    float_children = []\n    waiting_floats_y = line.position_y + line.height\n    for waiting_float in waiting_floats:\n        waiting_float.position_y = waiting_floats_y\n        new_waiting_float, waiting_float_resume_at = float_layout(\n            context, waiting_float, containing_block, absolute_boxes,\n            fixed_boxes, bottom_space, skip_stack=None)\n        float_children.append(new_waiting_float)\n        if waiting_float_resume_at:\n            context.add_broken_out_of_flow(\n                new_waiting_float, waiting_float, containing_block,\n                waiting_float_resume_at)\n    if float_children:\n        line.children += tuple(float_children)\n\n    return line, resume_at\n\n\ndef skip_first_whitespace(box, skip_stack):\n    \"\"\"Return ``skip_stack`` to start just after removable leading spaces.\n\n    See https://www.w3.org/TR/CSS21/text.html#white-space-model\n\n    \"\"\"\n    if skip_stack is None:\n        index = 0\n        next_skip_stack = None\n    else:\n        (index, next_skip_stack), = skip_stack.items()\n\n    if isinstance(box, boxes.TextBox):\n        assert next_skip_stack is None\n        white_space = box.style['white_space']\n        text = box.text.encode()\n        if index == len(text):\n            # Starting a the end of the TextBox, no text to see: Continue\n            return 'continue'\n        if white_space in ('normal', 'nowrap', 'pre-line'):\n            text = text[index:]\n            while text and text.startswith(b' '):\n                index += 1\n                text = text[1:]\n        return {index: None} if index else None\n\n    if isinstance(box, (boxes.LineBox, boxes.InlineBox)):\n        if index == 0 and not box.children:\n            return None\n        result = skip_first_whitespace(box.children[index], next_skip_stack)\n        if result == 'continue':\n            index += 1\n            if index >= len(box.children):\n                return 'continue'\n            result = skip_first_whitespace(box.children[index], None)\n        return {index: result} if (index or result) else None\n\n    assert skip_stack is None, f'unexpected skip inside {box}'\n    return None\n\n\ndef remove_last_whitespace(context, line):\n    \"\"\"Remove in place space characters at the end of a line.\n\n    This also reduces the width and position of the inline parents of the\n    modified text.\n\n    \"\"\"\n    ancestors = []\n    box = line\n    while isinstance(box, (boxes.LineBox, boxes.InlineBox)):\n        ancestors.append(box)\n        if not box.children:\n            return\n        box = box.children[-1]\n    if not (isinstance(box, boxes.TextBox) and\n            box.style['white_space'] in ('normal', 'nowrap', 'pre-line')):\n        return\n    new_text = box.text.rstrip(' ')\n    if new_text:\n        if len(new_text) == len(box.text):\n            return\n        box.text = new_text\n        new_box, resume, _ = split_text_box(context, box, None, 0)\n        assert new_box is not None\n        assert resume is None\n        space_width = box.width - new_box.width\n        box.width = new_box.width\n    else:\n        space_width = box.width\n        box.width = 0\n        box.text = ''\n\n    # RTL line, the trailing space is at the left of the box. We have to translate the\n    # box to align the stripped text with the right edge of the box.\n    if box.pango_layout.first_line_direction % 2:\n        for child in line.children:\n            child.translate(dx=-space_width, ignore_floats=True)\n\n    for ancestor in ancestors:\n        ancestor.width -= space_width\n\n    # TODO: All tabs (U+0009) are rendered as a horizontal shift that\n    # lines up the start edge of the next glyph with the next tab stop.\n    # Tab stops occur at points that are multiples of 8 times the width\n    # of a space (U+0020) rendered in the block's font from the block's\n    # starting content edge.\n\n    # TODO: If spaces (U+0020) or tabs (U+0009) at the end of a line have\n    # 'white-space' set to 'pre-wrap', UAs may visually collapse them.\n\n\ndef first_letter_to_box(box, skip_stack, first_letter_style):\n    \"\"\"Create a box for the ::first-letter selector.\"\"\"\n    if first_letter_style and box.children:\n        # Some properties must be ignored in first-letter boxes.\n        # https://drafts.csswg.org/selectors-3/#application-in-css\n        # At least, position is ignored to avoid layout troubles.\n        first_letter_style['position'] = 'static'\n\n        first_letter = ''\n        child = box.children[0]\n        if isinstance(child, boxes.TextBox):\n            letter_style = box.style.copy()\n            for key, value in first_letter_style.items():\n                letter_style[key] = value\n            if child.element_tag.endswith('::first-letter'):\n                letter_box = boxes.InlineBox(\n                    f'{box.element_tag}::first-letter', letter_style,\n                    box.element, [child])\n                box.children = ((letter_box, *box.children[1:]))\n            elif child.text:\n                character_found = False\n                if skip_stack:\n                    child_skip_stack, = skip_stack.values()\n                    if child_skip_stack:\n                        index, = child_skip_stack\n                        child.text = child.text[index:]\n                        skip_stack = None\n                while child.text:\n                    next_letter = child.text[0]\n                    category = unicodedata.category(next_letter)\n                    if category not in ('Ps', 'Pe', 'Pi', 'Pf', 'Po'):\n                        if character_found:\n                            break\n                        character_found = True\n                    first_letter += next_letter\n                    child.text = child.text[1:]\n                if first_letter.lstrip('\\n'):\n                    # \"This type of initial letter is similar to an\n                    # inline-level element if its 'float' property is 'none',\n                    # otherwise it is similar to a floated element.\"\n                    children_style = AnonymousStyle(letter_style)\n                    if letter_style['float'] == 'none':\n                        letter_box = boxes.InlineBox(\n                            f'{box.element_tag}::first-letter',\n                            letter_style, box.element, [])\n                        text_box = boxes.TextBox(\n                            f'{box.element_tag}::first-letter', children_style,\n                            box.element, first_letter)\n                        letter_box.children = (text_box,)\n                        box.children = (letter_box, *box.children)\n                    else:\n                        letter_box = boxes.BlockBox(\n                            f'{box.element_tag}::first-letter',\n                            letter_style, box.element, [])\n                        line_box = boxes.LineBox(\n                            f'{box.element_tag}::first-letter', children_style,\n                            box.element, [])\n                        letter_box.children = (line_box,)\n                        text_box = boxes.TextBox(\n                            f'{box.element_tag}::first-letter', children_style,\n                            box.element, first_letter)\n                        line_box.children = (text_box,)\n                        box.children = (letter_box, *box.children)\n                    build.process_text_transform(text_box)\n                    if skip_stack and child_skip_stack:\n                        index, = skip_stack\n                        (child_index, grandchild_skip_stack), = child_skip_stack.items()\n                        skip_stack = {index: {child_index + 1: grandchild_skip_stack}}\n        elif isinstance(child, boxes.ParentBox):\n            if skip_stack:\n                child_skip_stack, = skip_stack.values()\n            else:\n                child_skip_stack = None\n            child_skip_stack = first_letter_to_box(\n                child, child_skip_stack, first_letter_style)\n            if skip_stack:\n                index, = skip_stack\n                skip_stack = {index: child_skip_stack}\n    return skip_stack\n\n\ndef atomic_box(context, box, position_x, skip_stack, containing_block,\n               absolute_boxes, fixed_boxes):\n    \"\"\"Compute the width and the height of the atomic ``box``.\"\"\"\n    if isinstance(box, boxes.ReplacedBox):\n        box = box.copy()\n        inline_replaced_box_layout(box, containing_block)\n        box.baseline = box.margin_height()\n    elif isinstance(box, boxes.InlineBlockBox):\n        if box.is_table_wrapper:\n            containing_size = (containing_block.width, containing_block.height)\n            table_wrapper_width(context, box, containing_size)\n            width, min_width, max_width = box.width, box.min_width, box.max_width\n        box = inline_block_box_layout(\n            context, box, position_x, skip_stack, containing_block,\n            absolute_boxes, fixed_boxes)\n        if box.is_table_wrapper:\n            box.width, box.min_width, box.max_width = width, min_width, max_width\n    else:  # pragma: no cover\n        raise TypeError(f'Layout for {type(box).__name__} not handled yet')\n    return box\n\n\ndef inline_block_box_layout(context, box, position_x, skip_stack,\n                            containing_block, absolute_boxes, fixed_boxes):\n    from .block import block_container_layout\n\n    resolve_percentages(box, containing_block)\n\n    # https://www.w3.org/TR/CSS21/visudet.html#inlineblock-width\n    if box.margin_left == 'auto':\n        box.margin_left = 0\n    if box.margin_right == 'auto':\n        box.margin_right = 0\n    # https://www.w3.org/TR/CSS21/visudet.html#block-root-margin\n    if box.margin_top == 'auto':\n        box.margin_top = 0\n    if box.margin_bottom == 'auto':\n        box.margin_bottom = 0\n\n    inline_block_width(box, context, containing_block)\n\n    box.position_x = position_x\n    box.position_y = 0\n    box, _, _, _, _, _ = block_container_layout(\n        context, box, bottom_space=-inf, skip_stack=skip_stack, page_is_empty=True,\n        absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes, adjoining_margins=None,\n        first_letter_style=None, first_line_style=None, discard=False, max_lines=None)\n    box.baseline = inline_block_baseline(box)\n    return box\n\n\ndef inline_block_baseline(box):\n    \"\"\"Return the y position of the baseline for an inline block.\n\n    Position is taken from the top of its margin box.\n\n    https://www.w3.org/TR/CSS21/visudet.html#propdef-vertical-align\n\n    \"\"\"\n    if box.is_table_wrapper:\n        # Inline table's baseline is its first row's baseline\n        for child in box.children:\n            if isinstance(child, boxes.TableBox):\n                if child.children and child.children[0].children:\n                    first_row = child.children[0].children[0]\n                    return first_row.baseline\n    elif box.style['overflow'] == 'visible':\n        result = find_in_flow_baseline(box, last=True)\n        if result:\n            return result\n    return box.position_y + box.margin_height()\n\n\n@handle_min_max_width\ndef inline_block_width(box, context, containing_block):\n    available_content_width = containing_block.width - (\n        box.margin_left + box.margin_right +\n        box.border_left_width + box.border_right_width +\n        box.padding_left + box.padding_right)\n    if box.width == 'auto':\n        box.width = shrink_to_fit(context, box, available_content_width)\n\n\ndef split_inline_level(context, box, position_x, max_x, bottom_space,\n                       skip_stack, containing_block, absolute_boxes,\n                       fixed_boxes, line_placeholders, waiting_floats,\n                       line_children, first_letter_style, first_line_style):\n    \"\"\"Fit as much content as possible from an inline-level box in a width.\n\n    Return ``(new_box, resume_at, preserved_line_break, first_letter, last_letter)``.\n    ``resume_at`` is ``None`` if all of the content fits. Otherwise it can be passed as\n    a ``skip_stack`` parameter to resume where we left off.\n\n    ``new_box`` is non-empty (unless the box is empty) and as big as possible\n    while respecting ``max_x``, if possible (may overflow is no split is possible.)\n\n    \"\"\"\n    if first_line_style:\n        box = box.copy()\n        box.style = box.style.copy()\n        for key, value in first_line_style.items():\n            if key in INHERITED:\n                box.style[key] = value\n        build.process_text_transform(box)\n    resolve_percentages(box, containing_block)\n    float_widths = {'left': 0, 'right': 0}\n    if isinstance(box, boxes.TextBox):\n        box.position_x = position_x\n        if skip_stack is None:\n            skip = 0\n        else:\n            (skip, skip_stack), = skip_stack.items()\n            skip = skip or 0\n            assert skip_stack is None\n\n        is_line_start = len(line_children) == 0\n        new_box, skip, preserved_line_break = split_text_box(\n            context, box, max_x - position_x, skip,\n            is_line_start=is_line_start)\n\n        if skip is None:\n            resume_at = None\n        else:\n            resume_at = {skip: None}\n        if box.text:\n            first_letter = box.text[0]\n            if skip is None:\n                last_letter = box.text[-1]\n            else:\n                last_letter = box.text.encode()[:skip].decode()[-1]\n        else:\n            first_letter = last_letter = None\n    elif isinstance(box, boxes.InlineBox):\n        if box.margin_left == 'auto':\n            box.margin_left = 0\n        if box.margin_right == 'auto':\n            box.margin_right = 0\n        (new_box, resume_at, preserved_line_break, first_letter,\n         last_letter, float_widths) = split_inline_box(\n             context, box, position_x, max_x, bottom_space, skip_stack,\n             containing_block, absolute_boxes, fixed_boxes, line_placeholders,\n             waiting_floats, line_children, first_letter_style, first_line_style)\n    elif isinstance(box, boxes.AtomicInlineLevelBox):\n        new_box = atomic_box(\n            context, box, position_x, skip_stack, containing_block,\n            absolute_boxes, fixed_boxes)\n        new_box.position_x = position_x\n        resume_at = None\n        preserved_line_break = False\n        # See https://www.w3.org/TR/css-text-3/#line-breaking\n        # Atomic inlines behave like ideographic characters.\n        first_letter = '\\u2e80'\n        last_letter = '\\u2e80'\n    elif isinstance(box, boxes.InlineFlexBox):\n        box.position_x = position_x\n        box.position_y = 0\n        for side in ('top', 'right', 'bottom', 'left'):\n            if getattr(box, f'margin_{side}') == 'auto':\n                setattr(box, f'margin_{side}', 0)\n        new_box, resume_at, _, _, _ = flex_layout(\n            context, box, -inf, skip_stack, containing_block, False,\n            absolute_boxes, fixed_boxes, False)\n        preserved_line_break = False\n        first_letter = '\\u2e80'\n        last_letter = '\\u2e80'\n    elif isinstance(box, boxes.InlineGridBox):\n        box.position_x = position_x\n        box.position_y = 0\n        for side in ('top', 'right', 'bottom', 'left'):\n            if getattr(box, f'margin_{side}') == 'auto':\n                setattr(box, f'margin_{side}', 0)\n        new_box, resume_at, _, _, _ = grid_layout(\n            context, box, -inf, skip_stack, containing_block, False,\n            absolute_boxes, fixed_boxes)\n        preserved_line_break = False\n        first_letter = '\\u2e80'\n        last_letter = '\\u2e80'\n    else:  # pragma: no cover\n        raise TypeError(f'Layout for {type(box).__name__} not handled yet')\n    return (\n        new_box, resume_at, preserved_line_break, first_letter, last_letter,\n        float_widths)\n\n\ndef _out_of_flow_layout(context, box, containing_block, index, child,\n                        children, line_children, waiting_children,\n                        waiting_floats, absolute_boxes, fixed_boxes,\n                        line_placeholders, float_widths, max_x, position_x,\n                        bottom_space):\n    if child.is_absolutely_positioned():\n        child.position_x = position_x\n        placeholder = AbsolutePlaceholder(child)\n        line_placeholders.append(placeholder)\n        waiting_children.append((index, placeholder, child))\n        if child.style['position'] == 'absolute':\n            absolute_boxes.append(placeholder)\n        else:\n            fixed_boxes.append(placeholder)\n\n    elif child.is_floated():\n        child.position_x = position_x\n        float_width = shrink_to_fit(context, child, containing_block.width)\n\n        # To retrieve the real available space for floats, we must remove\n        # the trailing whitespaces from the line\n        non_floating_children = [\n            child_ for _, child_, _ in (children + waiting_children)\n            if not child_.is_floated()]\n        if non_floating_children:\n            float_width -= trailing_whitespace_size(\n                context, non_floating_children[-1])\n\n        if float_width > max_x - position_x or waiting_floats:\n            # TODO: the absolute and fixed boxes in the floats must be\n            # added here, and not in iter_line_boxes\n            waiting_floats.append(child)\n        else:\n            new_child, float_resume_at = float_layout(\n                context, child, containing_block, absolute_boxes, fixed_boxes,\n                bottom_space, skip_stack=None)\n            if float_resume_at:\n                context.add_broken_out_of_flow(\n                    child, child, containing_block, float_resume_at)\n            waiting_children.append((index, new_child, child))\n            child = new_child\n\n            # Translate previous line children\n            dx = max(child.margin_width(), 0)\n            float_widths[child.style['float']] += dx\n\n            float_left = child.style['float'] == 'left'\n            float_right = child.style['float'] == 'right'\n            if box.style['direction'] == 'ltr':\n                if child.style['float'] == 'inline-start':\n                    float_left = True\n                if child.style['float'] == 'inline-end':\n                    float_right = True\n            else:\n                if child.style['float'] == 'inline-start':\n                    float_right = True\n                if child.style['float'] == 'inline-end':\n                    float_left = True\n\n            if float_left:\n                if isinstance(box, boxes.LineBox):\n                    # The parent is the line, update the current position\n                    # for the next child. When the parent is not the line\n                    # (it is an inline block), the current position of the\n                    # line is updated by the box itself (see next\n                    # split_inline_level call).\n                    position_x += dx\n            elif float_right:\n                # Update the maximum x position for the next children\n                max_x -= dx\n            for _, old_child in line_children:\n                if not old_child.is_in_normal_flow():\n                    continue\n                float_align = (\n                    (float_left and box.style['direction'] == 'ltr') or\n                    (float_right and box.style['direction'] == 'rtl'))\n                if float_align:\n                    old_child.translate(dx=dx)\n\n    elif child.is_running():\n        running_name = child.style['position'][1]\n        page = context.current_page\n        context.running_elements[running_name][page].append(child)\n\n\ndef _break_waiting_children(context, box, max_x, bottom_space, initial_skip_stack,\n                            absolute_boxes, fixed_boxes, line_placeholders,\n                            waiting_floats, line_children, children, waiting_children,\n                            first_letter_style, first_line_style):\n    if waiting_children:\n        # Too wide, try to cut inside waiting children, starting from the end.\n        # TODO: we should take care of children added into absolute_boxes,\n        # fixed_boxes and other lists.\n        waiting_children_copy = waiting_children.copy()\n        while waiting_children_copy:\n            child_index, child, original_child = waiting_children_copy.pop()\n            if not child.is_in_normal_flow() or not can_break_inside(child):\n                continue\n\n            if initial_skip_stack and child_index in initial_skip_stack:\n                child_skip_stack = initial_skip_stack[child_index]\n            else:\n                child_skip_stack = None\n\n            # Break the waiting child at its last possible breaking point.\n            # TODO: The dirty solution chosen here is to decrease the\n            # actual size by 1 and render the waiting child again with this\n            # constraint. We may find a better way.\n            max_x = child.position_x + child.margin_width() - 1\n            while max_x > child.position_x:\n                new_child, child_resume_at, _, _, _, _ = split_inline_level(\n                    context, original_child, child.position_x, max_x,\n                    bottom_space, child_skip_stack, box, absolute_boxes,\n                    fixed_boxes, line_placeholders, waiting_floats,\n                    line_children, first_letter_style, first_line_style)\n                if child_resume_at:\n                    break\n                max_x -= 1\n            else:\n                # No line break found\n                continue\n\n            children.extend(waiting_children_copy)\n            if new_child is None:\n                # May be None where we have an empty TextBox.\n                assert isinstance(child, boxes.TextBox)\n            else:\n                children.append((child_index, new_child, child))\n\n            return {child_index: child_resume_at}\n\n    if children:\n        # Too wide, can't break waiting children and the inline is\n        # non-empty: put child entirely on the next line.\n        return {children[-1][0] + 1: None}\n\n\ndef _adjust_line_height(box):\n    \"\"\"Set margins to the half leading to respect line height.\n\n    Also compensate for borders and padding, we want margin_height() == line_height.\n\n    \"\"\"\n    line_height, box.baseline = strut(box.style)\n    box.height = box.style['font_size']\n    half_leading = (line_height - box.height) / 2\n    box.margin_top = half_leading - box.border_top_width - box.padding_top\n    box.margin_bottom = half_leading - box.border_bottom_width - box.padding_bottom\n\n\ndef split_inline_box(context, box, position_x, max_x, bottom_space, skip_stack,\n                     containing_block, absolute_boxes, fixed_boxes, line_placeholders,\n                     waiting_floats, line_children, first_letter_style,\n                     first_line_style):\n    \"\"\"Fit as much content as possible from an inline box in a width.\n\n    Return ``(new_box, resume_at, preserved_line_break, first_letter, last_letter)``.\n    ``resume_at`` is ``None`` if all of the content fits. Otherwise it can be passed as\n    a ``skip_stack`` parameter to resume where we left off.\n\n    ``new_box`` is non-empty (unless the box is empty) and as big as possible while\n    respecting ``max_x``, if possible (may overflow is no split is possible.)\n\n    This is the recursive step that loops over the children of the box; base steps of\n    recursion are handled by ``split_inline_level()``.\n\n    In this phase, excluded floating shapes are ignored.\n\n    \"\"\"\n\n    # In some cases (shrink-to-fit result being the preferred width)\n    # max_x is coming from Pango itself,\n    # but floating point errors have accumulated:\n    #   width2 = (width + X) - X   # in some cases, width2 < width\n    # Increase the value a bit to compensate and not introduce\n    # an unexpected line break. The 1e-9 value comes from PEP 485.\n    max_x *= 1 + 1e-9\n\n    is_start = skip_stack is None\n    initial_position_x = position_x\n    initial_skip_stack = skip_stack\n    assert isinstance(box, (boxes.LineBox, boxes.InlineBox))\n    left_spacing = (\n        box.padding_left + box.margin_left + box.border_left_width)\n    right_spacing = (\n        box.padding_right + box.margin_right + box.border_right_width)\n    content_box_left = position_x\n\n    children = []\n    waiting_children = []\n    preserved_line_break = False\n    first_letter = last_letter = None\n    float_widths = {'left': 0, 'right': 0}\n    float_resume_index = 0\n\n    if box.style['position'] == 'relative':\n        absolute_boxes = []\n\n    if is_start:\n        skip = 0\n    else:\n        (skip, skip_stack), = skip_stack.items()\n\n    for i, child in enumerate(box.children[skip:]):\n        index = i + skip\n        child.position_y = box.position_y\n\n        if not child.is_in_normal_flow():\n            _out_of_flow_layout(\n                context, box, containing_block, index, child, children,\n                line_children, waiting_children, waiting_floats,\n                absolute_boxes, fixed_boxes, line_placeholders, float_widths,\n                max_x, position_x, bottom_space)\n            if child.is_floated():\n                float_resume_index = index + 1\n                if child not in waiting_floats:\n                    max_x -= child.margin_width()\n            continue\n\n        is_last_child = (index == len(box.children) - 1)\n        available_width = max_x\n        child_waiting_floats = []\n        new_child, resume_at, preserved, first, last, new_float_widths = (\n            split_inline_level(\n                context, child, position_x, available_width, bottom_space,\n                skip_stack, containing_block, absolute_boxes, fixed_boxes,\n                line_placeholders, child_waiting_floats, line_children,\n                first_letter_style, first_line_style))\n        if box.style['direction'] == 'rtl':\n            end_spacing = left_spacing\n            max_x -= new_float_widths['left']\n        else:\n            end_spacing = right_spacing\n            max_x -= new_float_widths['right']\n        if is_last_child and end_spacing and resume_at is None:\n            # TODO: we should take care of children added into absolute_boxes,\n            # fixed_boxes and other lists.\n            available_width -= end_spacing\n            new_child, resume_at, preserved, first, last, new_float_widths = (\n                split_inline_level(\n                    context, child, position_x, available_width, bottom_space,\n                    skip_stack, containing_block, absolute_boxes, fixed_boxes,\n                    line_placeholders, child_waiting_floats, line_children,\n                    first_letter_style, first_line_style))\n\n        float_widths['left'] = max(float_widths['left'], new_float_widths['left'])\n        float_widths['right'] = max(float_widths['right'], new_float_widths['right'])\n\n        skip_stack = None\n        if preserved:\n            preserved_line_break = True\n\n        can_break = None\n        if last_letter is True:\n            last_letter = ' '\n        elif last_letter is False:\n            last_letter = ' '  # no-break space\n        elif box.style['white_space'] in ('pre', 'nowrap'):\n            can_break = False\n        if can_break is None:\n            if None in (last_letter, first):\n                can_break = False\n            elif first in (True, False):\n                can_break = first\n            else:\n                can_break = can_break_text(\n                    last_letter + first, child.style['lang'])\n\n        if can_break:\n            children.extend(waiting_children)\n            waiting_children = []\n\n        if first_letter is None:\n            first_letter = first\n        if child.trailing_collapsible_space:\n            last_letter = True\n        else:\n            last_letter = last\n\n        if new_child is None:\n            # May be None where we have an empty TextBox.\n            assert isinstance(child, boxes.TextBox)\n        else:\n            # Store lines to get previous break points.\n            if isinstance(box, boxes.LineBox):\n                line_children.append((index, new_child))\n\n            # Check that text doesn’t overflow.\n            new_position_x = new_child.position_x + new_child.margin_width()\n            if new_position_x - trailing_whitespace_size(context, new_child) > max_x:\n                # Text overflows, find previous break point.\n                previous_resume_at = _break_waiting_children(\n                    context, containing_block, max_x, bottom_space, initial_skip_stack,\n                    absolute_boxes, fixed_boxes, line_placeholders, waiting_floats,\n                    line_children, children, waiting_children, first_letter_style,\n                    first_line_style)\n                if previous_resume_at:\n                    resume_at = previous_resume_at\n                    break\n\n            position_x = new_position_x\n            waiting_children.append((index, new_child, child))\n\n        waiting_floats.extend(child_waiting_floats)\n        if resume_at is not None:\n            children.extend(waiting_children)\n            resume_at = {index: resume_at}\n            break\n    else:\n        children.extend(waiting_children)\n        resume_at = None\n\n    # Reorder inline blocks when direction is rtl\n    if box.style['direction'] == 'rtl' and len(children) > 1:\n        in_flow_children = [\n            box_child for _, box_child, _ in children\n            if box_child.is_in_normal_flow()]\n        position_x = in_flow_children[0].position_x\n        for child in in_flow_children[::-1]:\n            child.translate(\n                dx=(position_x - child.position_x), ignore_floats=True)\n            position_x += child.margin_width()\n\n    is_end = resume_at is None\n    new_box = box.copy_with_children(\n        [box_child for index, box_child, _ in children])\n    new_box.remove_decoration(start=not is_start, end=not is_end)\n    if isinstance(box, boxes.LineBox):\n        # We must reset line box width according to its new children\n        new_box.width = 0\n        children = new_box.children\n        if new_box.style['direction'] == 'ltr':\n            children = children[::-1]\n        for child in children:\n            if child.is_in_normal_flow():\n                new_box.width = (\n                    child.position_x + child.margin_width() -\n                    new_box.position_x)\n                break\n    else:\n        new_box.position_x = initial_position_x\n        if box.style['box_decoration_break'] == 'clone':\n            translation_needed = True\n        else:\n            translation_needed = (\n                is_start if box.style['direction'] == 'ltr' else is_end)\n        if translation_needed:\n            for child in new_box.children:\n                child.translate(dx=left_spacing)\n        new_box.width = position_x - content_box_left\n        new_box.translate(dx=float_widths['left'], ignore_floats=True)\n\n    _adjust_line_height(new_box)\n\n    if new_box.style['position'] == 'relative':\n        for absolute_box in absolute_boxes:\n            absolute_layout(\n                context, absolute_box, new_box, fixed_boxes, bottom_space,\n                skip_stack=None)\n\n    if resume_at is not None:\n        index = next(iter(resume_at))\n        if index < float_resume_index:\n            resume_at = {float_resume_index: None}\n\n    if box.is_leader:\n        first_letter = True\n        last_letter = False\n\n    return (\n        new_box, resume_at, preserved_line_break, first_letter, last_letter,\n        float_widths)\n\n\ndef split_text_box(context, box, available_width, skip, is_line_start=True):\n    \"\"\"Keep as much text as possible from a TextBox in a limited width.\n\n    Try not to overflow but always have some text in ``new_box``.\n\n    Return ``(new_box, skip, preserved_line_break)``. ``skip`` is the number of\n    UTF-8 bytes to skip form the start of the TextBox for the next line, or\n    ``None`` if all of the text fits.\n\n    Also break on preserved line breaks.\n\n    \"\"\"\n    assert isinstance(box, boxes.TextBox)\n    font_size = box.style['font_size']\n    text = box.text.encode()[skip:]\n    if font_size == 0 or not text:\n        return None, None, False\n    layout, length, resume_index, width, height, baseline = split_first_line(\n        text.decode(), box.style, context, available_width,\n        box.justification_spacing, is_line_start=is_line_start)\n    assert resume_index != 0\n\n    if length > 0:\n        box = box.copy_with_text(layout.text)\n        box.width = width\n        box.pango_layout = layout\n        # \"The height of the content area should be based on the font,\n        #  but this specification does not specify how.\"\n        # https://www.w3.org/TR/CSS21/visudet.html#inline-non-replaced\n        # We trust Pango and use the height of the LayoutLine.\n        box.height = height\n        # \"only the 'line-height' is used when calculating the height\n        #  of the line box.\"\n        # Set margins so that margin_height() == line_height\n        line_height, _ = strut(box.style)\n        half_leading = (line_height - height) / 2\n        box.margin_top = half_leading\n        box.margin_bottom = half_leading\n        # form the top of the content box\n        box.baseline = baseline\n        # form the top of the margin box\n        box.baseline += box.margin_top\n    else:\n        box = None\n\n    if resume_index is None:\n        preserved_line_break = False\n    else:\n        between = text[length:resume_index].decode()\n        preserved_line_break = (\n            (length != resume_index) and between.strip(' '))\n        if preserved_line_break:\n            # See https://unicode.org/reports/tr14/\n            # \\r is already handled by process_whitespace\n            line_breaks = ('\\n', '\\t', '\\f', '\\u0085', '\\u2028', '\\u2029')\n            assert between in line_breaks, (\n                'Got %r between two lines. '\n                'Expected nothing or a preserved line break' % (between,))\n        resume_index += skip\n\n    return box, resume_index, preserved_line_break\n\n\ndef line_box_verticality(box):\n    \"\"\"Handle ``vertical-align`` within a :class:`LineBox`.\n\n    Place all boxes vertically assuming that the baseline of ``box``\n    is at `y = 0`.\n\n    Return ``(max_y, min_y)``, the maximum and minimum vertical position\n    of margin boxes.\n\n    \"\"\"\n    top_bottom_subtrees = []\n    max_y, min_y = aligned_subtree_verticality(\n        box, top_bottom_subtrees, baseline_y=0)\n    subtrees_with_min_max = [\n        (subtree, sub_max_y, sub_min_y)\n        for subtree in top_bottom_subtrees\n        for sub_max_y, sub_min_y in [\n            (None, None) if subtree.is_floated()\n            else aligned_subtree_verticality(\n                subtree, top_bottom_subtrees, baseline_y=0)]]\n\n    if subtrees_with_min_max:\n        sub_positions = [\n            sub_max_y - sub_min_y\n            for subtree, sub_max_y, sub_min_y in subtrees_with_min_max\n            if not subtree.is_floated()]\n        if sub_positions:\n            highest_sub = max(sub_positions)\n            max_y = max(max_y, min_y + highest_sub)\n\n    for subtree, sub_max_y, sub_min_y in subtrees_with_min_max:\n        if subtree.is_floated():\n            dy = min_y - subtree.position_y\n        elif subtree.style['vertical_align'] == 'top':\n            dy = min_y - sub_min_y\n        else:\n            assert subtree.style['vertical_align'] == 'bottom'\n            dy = max_y - sub_max_y\n        translate_subtree(subtree, dy)\n    return max_y, min_y\n\n\ndef translate_subtree(box, dy):\n    if isinstance(box, boxes.InlineBox):\n        box.position_y += dy\n        if box.style['vertical_align'] in ('top', 'bottom'):\n            for child in box.children:\n                translate_subtree(child, dy)\n    else:\n        # Text or atomic boxes\n        box.translate(dy=dy)\n\n\ndef aligned_subtree_verticality(box, top_bottom_subtrees, baseline_y):\n    max_y, min_y = inline_box_verticality(box, top_bottom_subtrees, baseline_y)\n    # Account for the line box itself:\n    top = baseline_y - box.baseline\n    bottom = top + box.margin_height()\n    if min_y is None or top < min_y:\n        min_y = top\n    if max_y is None or bottom > max_y:\n        max_y = bottom\n\n    return max_y, min_y\n\n\ndef inline_box_verticality(box, top_bottom_subtrees, baseline_y):\n    \"\"\"Handle ``vertical-align`` within an :class:`InlineBox`.\n\n    Place all boxes vertically assuming that the baseline of ``box``\n    is at `y = baseline_y`.\n\n    Return ``(max_y, min_y)``, the maximum and minimum vertical position\n    of margin boxes.\n\n    \"\"\"\n    max_y = None\n    min_y = None\n    if not isinstance(box, (boxes.LineBox, boxes.InlineBox)):\n        return max_y, min_y\n\n    for child in box.children:\n        if not child.is_in_normal_flow():\n            if child.is_floated():\n                top_bottom_subtrees.append(child)\n            continue\n        vertical_align = child.style['vertical_align']\n        if vertical_align == 'baseline':\n            child_baseline_y = baseline_y\n        elif vertical_align == 'middle':\n            one_ex = box.style['font_size'] * character_ratio(box.style, 'ex')\n            top = baseline_y - (one_ex + child.margin_height()) / 2\n            child_baseline_y = top + child.baseline\n        elif vertical_align == 'text-top':\n            # align top with the top of the parent’s content area\n            top = (baseline_y - box.baseline + box.margin_top +\n                   box.border_top_width + box.padding_top)\n            child_baseline_y = top + child.baseline\n        elif vertical_align == 'text-bottom':\n            # align bottom with the bottom of the parent’s content area\n            bottom = (baseline_y - box.baseline + box.margin_top +\n                      box.border_top_width + box.padding_top + box.height)\n            child_baseline_y = bottom - child.margin_height() + child.baseline\n        elif vertical_align in ('top', 'bottom'):\n            # TODO: actually implement vertical-align: top and bottom\n            # Later, we will assume for this subtree that its baseline\n            # is at y=0.\n            child_baseline_y = 0\n        elif isinstance(vertical_align, Pending):\n            height, _ = strut(box.style)\n            child_baseline_y = baseline_y - percentage(\n                vertical_align, box.style, height)\n        else:\n            # Numeric value: The child’s baseline is `vertical_align` above\n            # (lower y) the parent’s baseline.\n            child_baseline_y = baseline_y - vertical_align\n\n        # the child’s `top` is `child.baseline` above (lower y) its baseline.\n        top = child_baseline_y - child.baseline\n        box_types = (boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)\n        if isinstance(child, box_types):\n            # This also includes table wrappers for inline tables.\n            child.translate(dy=top - child.position_y)\n        else:\n            child.position_y = top\n            # grand-children for inline boxes are handled below\n\n        if vertical_align in ('top', 'bottom'):\n            # top or bottom are special, they need to be handled in\n            # a later pass.\n            top_bottom_subtrees.append(child)\n            continue\n\n        bottom = top + child.margin_height()\n        if min_y is None or top < min_y:\n            min_y = top\n        if max_y is None or bottom > max_y:\n            max_y = bottom\n        if isinstance(child, boxes.InlineBox):\n            children_max_y, children_min_y = inline_box_verticality(\n                child, top_bottom_subtrees, child_baseline_y)\n            if children_min_y is not None and children_min_y < min_y:\n                min_y = children_min_y\n            if children_max_y is not None and children_max_y > max_y:\n                max_y = children_max_y\n    return max_y, min_y\n\n\ndef text_align(context, line, available_width, last):\n    \"\"\"Return the line horizontal offset according to ``text-align``.\"\"\"\n\n    # \"When the total width of the inline-level boxes on a line is less than\n    # the width of the line box containing them, their horizontal distribution\n    # within the line box is determined by the 'text-align' property.\"\n    if line.width >= available_width:\n        return 0\n\n    align = line.style['text_align_all']\n    if last:\n        align_last = line.style['text_align_last']\n        align = align if align_last == 'auto' else align_last\n    space_collapse = line.style['white_space'] in (\n        'normal', 'nowrap', 'pre-line')\n    if align in ('left', 'right'):\n        if (align == 'left') ^ (line.style['direction'] == 'rtl'):\n            align = 'start'\n        else:\n            align = 'end'\n    if align == 'start':\n        return 0\n    offset = available_width - line.width\n    if align == 'justify':\n        if space_collapse:\n            # Justification of texts where white space is not collapsing is\n            # - forbidden by CSS 2, and\n            # - not required by CSS 3 Text.\n            justify_line(context, line, offset)\n        return 0\n    if align == 'center':\n        return offset / 2\n    else:\n        assert align == 'end'\n        return offset\n\n\ndef justify_line(context, line, extra_width):\n    # TODO: We should use a better algorithm here, see\n    # https://www.w3.org/TR/css-text-3/#justify-algos\n    if (nb_spaces := count_expandable_spaces(line)):\n        add_word_spacing(context, line, extra_width / nb_spaces, 0)\n\n\ndef count_expandable_spaces(box):\n    \"\"\"Count expandable spaces (space and nbsp) for justification.\"\"\"\n    if isinstance(box, boxes.TextBox):\n        # TODO: remove trailing spaces correctly\n        return box.text.count(' ') + box.text.count('\\u00a0')\n    elif isinstance(box, (boxes.LineBox, boxes.InlineBox)):\n        return sum(count_expandable_spaces(child) for child in box.children)\n    else:\n        return 0\n\n\ndef add_word_spacing(context, box, justification_spacing, x_advance):\n    if isinstance(box, boxes.TextBox):\n        box.justification_spacing = justification_spacing\n        box.position_x += x_advance\n        nb_spaces = count_expandable_spaces(box)\n        if nb_spaces > 0:\n            layout = create_layout(\n                box.text, box.style, context, max_width=None,\n                justification_spacing=box.justification_spacing)\n            layout.deactivate()\n            extra_space = justification_spacing * nb_spaces\n            x_advance += extra_space\n            box.width += extra_space\n            box.pango_layout = layout\n    elif isinstance(box, (boxes.LineBox, boxes.InlineBox)):\n        box.position_x += x_advance\n        previous_x_advance = x_advance\n        children = box.children\n        if box.style['direction'] == 'rtl':\n            children = children[::-1]\n        for child in children:\n            if child.is_in_normal_flow():\n                x_advance = add_word_spacing(\n                    context, child, justification_spacing, x_advance)\n        box.width += x_advance - previous_x_advance\n    else:\n        # Atomic inline-level box\n        box.translate(x_advance, 0)\n    return x_advance\n\n\ndef is_phantom_linebox(linebox):\n    # See https://www.w3.org/TR/CSS21/visuren.html#phantom-line-box\n    for child in linebox.children:\n        if isinstance(child, boxes.InlineBox):\n            if not is_phantom_linebox(child):\n                return False\n            for side in ('top', 'right', 'bottom', 'left'):\n                if (getattr(child.style[f'margin_{side}'], 'value', None) or\n                        child.style[f'border_{side}_width'] or\n                        child.style[f'padding_{side}'].value):\n                    return False\n        elif child.is_in_normal_flow():\n            return False\n    return True\n\n\ndef can_break_inside(box):\n    # See https://www.w3.org/TR/css-text-3/#white-space-property\n    text_wrap = box.style['white_space'] in ('normal', 'pre-wrap', 'pre-line')\n    if isinstance(box, boxes.AtomicInlineLevelBox):\n        return False\n    elif text_wrap and isinstance(box, boxes.TextBox):\n        return can_break_text(box.text, box.style['lang'])\n    elif text_wrap and isinstance(box, boxes.ParentBox):\n        return any(can_break_inside(child) for child in box.children)\n    return False\n"
  },
  {
    "path": "weasyprint/layout/leader.py",
    "content": "\"\"\"Leaders management.\"\"\"\n\nfrom ..formatting_structure import boxes\n\n\ndef leader_index(box):\n    \"\"\"Get the index of the first leader box in ``box``.\"\"\"\n    for i, child in enumerate(box.children):\n        if child.is_leader:\n            return (i, None), child\n        if isinstance(child, boxes.ParentBox):\n            child_leader_index, child_leader = leader_index(child)\n            if child_leader_index is not None:\n                return (i, child_leader_index), child_leader\n    return None, None\n\n\ndef handle_leader(context, line, containing_block):\n    \"\"\"Find a leader box in ``line`` and handle its text and its position.\"\"\"\n    index, leader_box = leader_index(line)\n    extra_width = 0\n    if index is not None and leader_box.children:\n        text_box, = leader_box.children\n\n        # Abort if the leader text has no width\n        if text_box.width <= 0:\n            return\n\n        # Extra width is the additional width taken by the leader box\n        extra_width = containing_block.width - sum(\n            child.margin_width() for child in line.children\n            if child.is_in_normal_flow())\n\n        # Take care of excluded shapes\n        for shape in context.excluded_shapes:\n            if shape.position_y + shape.height > line.position_y:\n                extra_width -= shape.width\n\n        # Available width is the width available for the leader box\n        available_width = extra_width + text_box.width\n        line.width = containing_block.width\n\n        # Add text boxes into the leader box\n        number_of_leaders = int(line.width // text_box.width)\n        position_x = line.position_x + line.width\n        children = []\n        for i in range(number_of_leaders):\n            position_x -= text_box.width\n            if position_x < leader_box.position_x:\n                # Don’t add leaders behind the text on the left\n                continue\n            elif (position_x + text_box.width >\n                    leader_box.position_x + available_width):\n                # Don’t add leaders behind the text on the right\n                continue\n            text_box = text_box.copy()\n            text_box.position_x = position_x\n            children.append(text_box)\n        leader_box.children = tuple(children)\n\n        if line.style['direction'] == 'rtl':\n            leader_box.translate(dx=-extra_width)\n\n    # Widen leader parent boxes and translate following boxes\n    box = line\n    while index is not None:\n        for child in box.children[index[0] + 1:]:\n            if child.is_in_normal_flow():\n                if line.style['direction'] == 'ltr':\n                    child.translate(dx=extra_width)\n                else:\n                    child.translate(dx=-extra_width)\n        box = box.children[index[0]]\n        box.width += extra_width\n        index = index[1]\n"
  },
  {
    "path": "weasyprint/layout/min_max.py",
    "content": "\"\"\"Decorators handling min- and max- widths and heights.\"\"\"\n\nimport functools\n\n\ndef handle_min_max_width(function):\n    \"\"\"Decorate a function setting used width, handling {min,max}-width.\"\"\"\n    @functools.wraps(function)\n    def wrapper(box, *args):\n        result = function(box, *args)\n        if box.width > box.max_width:\n            box.width = box.max_width\n            result = function(box, *args)\n        if box.width < box.min_width:\n            box.width = box.min_width\n            result = function(box, *args)\n        return result\n    wrapper.without_min_max = function\n    return wrapper\n\n\ndef handle_min_max_height(function):\n    \"\"\"Decorate a function setting used height, handling {min,max}-height.\"\"\"\n    @functools.wraps(function)\n    def wrapper(box, *args):\n        result = function(box, *args)\n        if box.height > box.max_height:\n            box.height = box.max_height\n            result = function(box, *args)\n        if box.height < box.min_height:\n            box.height = box.min_height\n            result = function(box, *args)\n        return result\n    wrapper.without_min_max = function\n    return wrapper\n"
  },
  {
    "path": "weasyprint/layout/page.py",
    "content": "\"\"\"Layout for pages and CSS3 margin boxes.\"\"\"\n\nimport copy\nfrom collections import defaultdict, namedtuple\nfrom math import inf\n\nfrom ..css import AnonymousStyle\nfrom ..formatting_structure import boxes, build\nfrom ..logger import PROGRESS_LOGGER\nfrom .absolute import absolute_box_layout, absolute_layout\nfrom .block import block_container_layout, block_level_layout\nfrom .float import float_layout\nfrom .min_max import handle_min_max_height, handle_min_max_width\nfrom .percent import resolve_percentages\nfrom .preferred import max_content_width, min_content_width\n\nPageType = namedtuple('PageType', ['side', 'blank', 'name', 'index', 'groups'])\n\n\nclass OrientedBox:\n    @property\n    def sugar(self):\n        return self.padding_plus_border + self.margin_a + self.margin_b\n\n    @property\n    def outer(self):\n        return self.sugar + self.inner\n\n    @outer.setter\n    def outer(self, new_outer_width):\n        self.inner = min(\n            max(self.min_content_size, new_outer_width - self.sugar),\n            self.max_content_size)\n\n    @property\n    def outer_min_content_size(self):\n        return self.sugar + (\n            self.min_content_size if self.inner == 'auto' else self.inner)\n\n    @property\n    def outer_max_content_size(self):\n        return self.sugar + (\n            self.max_content_size if self.inner == 'auto' else self.inner)\n\n\nclass VerticalBox(OrientedBox):\n    def __init__(self, context, box):\n        self.context = context\n        self.box = box\n        # Inner dimension: that of the content area, as opposed to the\n        # outer dimension: that of the margin area.\n        self.inner = box.height\n        self.margin_a = box.margin_top\n        self.margin_b = box.margin_bottom\n        self.padding_plus_border = (\n            box.padding_top + box.padding_bottom +\n            box.border_top_width + box.border_bottom_width)\n\n    def restore_box_attributes(self):\n        box = self.box\n        box.height = self.inner\n        box.margin_top = self.margin_a\n        box.margin_bottom = self.margin_b\n\n    # TODO: Define what are the min-content and max-content heights\n    @property\n    def min_content_size(self):\n        return 0\n\n    @property\n    def max_content_size(self):\n        return 1e6\n\n\nclass HorizontalBox(OrientedBox):\n    def __init__(self, context, box):\n        self.context = context\n        self.box = box\n        self.inner = box.width\n        self.margin_a = box.margin_left\n        self.margin_b = box.margin_right\n        self.padding_plus_border = (\n            box.padding_left + box.padding_right +\n            box.border_left_width + box.border_right_width)\n        self._min_content_size = None\n        self._max_content_size = None\n\n    def restore_box_attributes(self):\n        box = self.box\n        box.width = self.inner\n        box.margin_left = self.margin_a\n        box.margin_right = self.margin_b\n\n    @property\n    def min_content_size(self):\n        if self._min_content_size is None:\n            self._min_content_size = min_content_width(\n                self.context, self.box, outer=False)\n        return self._min_content_size\n\n    @property\n    def max_content_size(self):\n        if self._max_content_size is None:\n            self._max_content_size = max_content_width(\n                self.context, self.box, outer=False)\n        return self._max_content_size\n\n\ndef compute_fixed_dimension(context, box, outer, vertical, top_or_left):\n    \"\"\"Compute and set a margin box fixed dimension on ``box``.\n\n    Described in: https://drafts.csswg.org/css-page-3/#margin-constraints\n\n    :param box:\n        The margin box to work on\n    :param outer:\n        The target outer dimension (value of a page margin)\n    :param vertical:\n        True to set height, margin-top and margin-bottom; False for width,\n        margin-left and margin-right\n    :param top_or_left:\n        True if the margin box in if the top half (for vertical==True) or\n        left half (for vertical==False) of the page.\n        This determines which margin should be 'auto' if the values are\n        over-constrained. (Rule 3 of the algorithm.)\n    \"\"\"\n    box = (VerticalBox if vertical else HorizontalBox)(context, box)\n\n    # Rule 2\n    total = box.padding_plus_border + sum(\n        value for value in (box.margin_a, box.margin_b, box.inner)\n        if value != 'auto')\n    if total > outer:\n        if box.margin_a == 'auto':\n            box.margin_a = 0\n        if box.margin_b == 'auto':\n            box.margin_b = 0\n        if box.inner == 'auto':\n            # XXX this is not in the spec, but without it box.inner\n            # would end up with a negative value.\n            # Instead, this will trigger rule 3 below.\n            # https://lists.w3.org/Archives/Public/www-style/2012Jul/0006.html\n            box.inner = 0\n    # Rule 3\n    if 'auto' not in [box.margin_a, box.margin_b, box.inner]:\n        # Over-constrained\n        if top_or_left:\n            box.margin_a = 'auto'\n        else:\n            box.margin_b = 'auto'\n    # Rule 4\n    if [box.margin_a, box.margin_b, box.inner].count('auto') == 1:\n        if box.inner == 'auto':\n            box.inner = (outer - box.padding_plus_border -\n                         box.margin_a - box.margin_b)\n        elif box.margin_a == 'auto':\n            box.margin_a = (outer - box.padding_plus_border -\n                            box.margin_b - box.inner)\n        elif box.margin_b == 'auto':\n            box.margin_b = (outer - box.padding_plus_border -\n                            box.margin_a - box.inner)\n    # Rule 5\n    if box.inner == 'auto':\n        if box.margin_a == 'auto':\n            box.margin_a = 0\n        if box.margin_b == 'auto':\n            box.margin_b = 0\n        box.inner = (outer - box.padding_plus_border -\n                     box.margin_a - box.margin_b)\n    # Rule 6\n    if box.margin_a == box.margin_b == 'auto':\n        box.margin_a = box.margin_b = (\n            outer - box.padding_plus_border - box.inner) / 2\n\n    assert 'auto' not in [box.margin_a, box.margin_b, box.inner]\n\n    box.restore_box_attributes()\n\n\ndef compute_variable_dimension(context, side_boxes, vertical, available_size):\n    \"\"\"Compute and set a margin box fixed dimension on ``box``\n\n    Described in: https://drafts.csswg.org/css-page-3/#margin-dimension\n\n    :param side_boxes:\n        Three boxes on a same side (as opposed to a corner).\n        A list of:\n        - A @*-left or @*-top margin box\n        - A @*-center or @*-middle margin box\n        - A @*-right or @*-bottom margin box\n    :param vertical:\n        ``True`` to set height, margin-top and margin-bottom;\n        ``False`` for width, margin-left and margin-right.\n    :param available_size:\n        The distance between the page box’s left right border edges\n\n    \"\"\"\n    box_class = VerticalBox if vertical else HorizontalBox\n    side_boxes = [box_class(context, box) for box in side_boxes]\n    box_a, box_b, box_c = side_boxes\n\n    for box in side_boxes:\n        if box.margin_a == 'auto':\n            box.margin_a = 0\n        if box.margin_b == 'auto':\n            box.margin_b = 0\n\n    if not box_b.box.is_generated:\n        # Non-generated boxes get zero for every box-model property\n        assert box_b.inner == 0\n        if box_a.inner == box_c.inner == 'auto':\n            # A and C both have 'width: auto'\n            if available_size > (\n                    box_a.outer_max_content_size +\n                    box_c.outer_max_content_size):\n                # sum of the outer max-content widths\n                # is less than the available width\n                flex_space = (\n                    available_size -\n                    box_a.outer_max_content_size -\n                    box_c.outer_max_content_size)\n                flex_factor_a = box_a.outer_max_content_size\n                flex_factor_c = box_c.outer_max_content_size\n                flex_factor_sum = flex_factor_a + flex_factor_c\n                if flex_factor_sum == 0:\n                    flex_factor_sum = 1\n                box_a.outer = box_a.max_content_size + (\n                    flex_space * flex_factor_a / flex_factor_sum)\n                box_c.outer = box_c.max_content_size + (\n                    flex_space * flex_factor_c / flex_factor_sum)\n            elif available_size > (\n                    box_a.outer_min_content_size +\n                    box_c.outer_min_content_size):\n                # sum of the outer min-content widths\n                # is less than the available width\n                flex_space = (\n                    available_size -\n                    box_a.outer_min_content_size -\n                    box_c.outer_min_content_size)\n                flex_factor_a = (\n                    box_a.max_content_size - box_a.min_content_size)\n                flex_factor_c = (\n                    box_c.max_content_size - box_c.min_content_size)\n                flex_factor_sum = flex_factor_a + flex_factor_c\n                if flex_factor_sum == 0:\n                    flex_factor_sum = 1\n                box_a.outer = box_a.min_content_size + (\n                    flex_space * flex_factor_a / flex_factor_sum)\n                box_c.outer = box_c.min_content_size + (\n                    flex_space * flex_factor_c / flex_factor_sum)\n            else:\n                # otherwise\n                flex_space = (\n                    available_size -\n                    box_a.outer_min_content_size -\n                    box_c.outer_min_content_size)\n                flex_factor_a = box_a.min_content_size\n                flex_factor_c = box_c.min_content_size\n                flex_factor_sum = flex_factor_a + flex_factor_c\n                if flex_factor_sum == 0:\n                    flex_factor_sum = 1\n                box_a.outer = box_a.min_content_size + (\n                    flex_space * flex_factor_a / flex_factor_sum)\n                box_c.outer = box_c.min_content_size + (\n                    flex_space * flex_factor_c / flex_factor_sum)\n        else:\n            # only one box has 'width: auto'\n            if box_a.inner == 'auto':\n                box_a.outer = available_size - box_c.outer\n            elif box_c.inner == 'auto':\n                box_c.outer = available_size - box_a.outer\n    else:\n        if box_b.inner == 'auto':\n            # resolve any auto width of the middle box (B)\n            ac_max_content_size = 2 * max(\n                box_a.outer_max_content_size, box_c.outer_max_content_size)\n            if available_size > (\n                    box_b.outer_max_content_size + ac_max_content_size):\n                flex_space = (\n                    available_size -\n                    box_b.outer_max_content_size -\n                    ac_max_content_size)\n                flex_factor_b = box_b.outer_max_content_size\n                flex_factor_ac = ac_max_content_size\n                flex_factor_sum = flex_factor_b + flex_factor_ac\n                if flex_factor_sum == 0:\n                    flex_factor_sum = 1\n                box_b.outer = box_b.max_content_size + (\n                    flex_space * flex_factor_b / flex_factor_sum)\n            else:\n                ac_min_content_size = 2 * max(\n                    box_a.outer_min_content_size, box_c.outer_min_content_size)\n                if available_size > (\n                        box_b.outer_min_content_size + ac_min_content_size):\n                    flex_space = (\n                        available_size -\n                        box_b.outer_min_content_size -\n                        ac_min_content_size)\n                    flex_factor_b = (\n                        box_b.max_content_size - box_b.min_content_size)\n                    flex_factor_ac = ac_max_content_size - ac_min_content_size\n                    flex_factor_sum = flex_factor_b + flex_factor_ac\n                    if flex_factor_sum == 0:\n                        flex_factor_sum = 1\n                    box_b.outer = box_b.min_content_size + (\n                        flex_space * flex_factor_b / flex_factor_sum)\n                else:\n                    flex_space = (\n                        available_size -\n                        box_b.outer_min_content_size -\n                        ac_min_content_size)\n                    flex_factor_b = box_b.min_content_size\n                    flex_factor_ac = ac_min_content_size\n                    flex_factor_sum = flex_factor_b + flex_factor_ac\n                    if flex_factor_sum == 0:\n                        flex_factor_sum = 1\n                    box_b.outer = box_b.min_content_size + (\n                        flex_space * flex_factor_b / flex_factor_sum)\n        if box_a.inner == 'auto':\n            box_a.outer = (available_size - box_b.outer) / 2\n        if box_c.inner == 'auto':\n            box_c.outer = (available_size - box_b.outer) / 2\n\n    # And, we’re done!\n    assert 'auto' not in [box.inner for box in side_boxes]\n    # Set the actual attributes back.\n    for box in side_boxes:\n        box.restore_box_attributes()\n\n\ndef _standardize_page_based_counters(style, pseudo_type):\n    \"\"\"Drop 'pages' counter from style in @page and @margin context.\n\n    Ensure `counter-increment: page` for @page context if not otherwise\n    manipulated by the style.\n\n    \"\"\"\n    page_counter_touched = False\n    for propname in ('counter_set', 'counter_reset', 'counter_increment'):\n        if style[propname] == 'auto':\n            style[propname] = ()\n            continue\n        justified_values = []\n        for name, value in style[propname]:\n            if name == 'page':\n                page_counter_touched = True\n            if name != 'pages':\n                justified_values.append((name, value))\n        style[propname] = tuple(justified_values)\n\n    if pseudo_type is None and not page_counter_touched:\n        style['counter_increment'] = (\n            ('page', 1),) + style['counter_increment']\n\n\ndef make_margin_boxes(context, page, state):\n    \"\"\"Yield laid-out margin boxes for this page.\n\n    ``state`` is the actual, up-to-date page-state from\n    ``context.page_maker[context.current_page]``.\n\n    \"\"\"\n    # This is a closure only to make calls shorter\n    def make_box(at_keyword, containing_block):\n        \"\"\"Return a margin box with resolved percentages.\n\n        The margin box may still have 'auto' values.\n\n        Return ``None`` if this margin box should not be generated.\n\n        :param at_keyword:\n            Which margin box to return, e.g. '@top-left'\n        :param containing_block:\n            As expected by :func:`resolve_percentages`.\n\n        \"\"\"\n        style = context.style_for(page.page_type, at_keyword)\n        if style is None:\n            # doesn't affect counters\n            style = AnonymousStyle(page.style)\n        _standardize_page_based_counters(style, at_keyword)\n        box = boxes.MarginBox(at_keyword, style)\n        # Empty boxes should not be generated, but they may be needed for\n        # the layout of their neighbors.\n        # TODO: should be the computed value.\n        box.is_generated = style['content'] not in (\n            'normal', 'inhibit', 'none')\n        # TODO: get actual counter values at the time of the last page break\n        if box.is_generated:\n            # @margins mustn't manipulate page-context counters\n            margin_state = copy.deepcopy(state)\n            quote_depth, counter_values, counter_scopes, _page_groups = margin_state\n            # TODO: check this, probably useless\n            counter_scopes.append(set())\n            build.update_counters(margin_state, box.style)\n            box.children = build.content_to_boxes(\n                box.style, box, quote_depth, counter_values,\n                context.get_image_from_uri, context.target_collector,\n                context.counter_style, context, page)\n            build.process_whitespace(box)\n            build.process_text_transform(box)\n            box = build.create_anonymous_boxes(box)\n        resolve_percentages(box, containing_block)\n        if not box.is_generated:\n            box.width = box.height = 0\n            for side in ('top', 'right', 'bottom', 'left'):\n                box._reset_spacing(side)\n        return box\n\n    margin_top = page.margin_top\n    margin_bottom = page.margin_bottom\n    margin_left = page.margin_left\n    margin_right = page.margin_right\n    max_box_width = page.border_width()\n    max_box_height = page.border_height()\n\n    # bottom right corner of the border box\n    page_end_x = margin_left + max_box_width\n    page_end_y = margin_top + max_box_height\n\n    # Margin box dimensions, described in\n    # https://drafts.csswg.org/css-page-3/#margin-box-dimensions\n    generated_boxes = []\n\n    for prefix, vertical, containing_block, position_x, position_y in (\n        ('top', False, (max_box_width, margin_top),\n            margin_left, 0),\n        ('bottom', False, (max_box_width, margin_bottom),\n            margin_left, page_end_y),\n        ('left', True, (margin_left, max_box_height),\n            0, margin_top),\n        ('right', True, (margin_right, max_box_height),\n            page_end_x, margin_top),\n    ):\n        if vertical:\n            suffixes = ['top', 'middle', 'bottom']\n            fixed_outer, variable_outer = containing_block\n        else:\n            suffixes = ['left', 'center', 'right']\n            variable_outer, fixed_outer = containing_block\n        side_boxes = [\n            make_box(f'@{prefix}-{suffix}', containing_block)\n            for suffix in suffixes]\n        if not any(box.is_generated for box in side_boxes):\n            continue\n        # We need the three boxes together for the variable dimension:\n        compute_variable_dimension(\n            context, side_boxes, vertical, variable_outer)\n        for box, offset in zip(side_boxes, [0, 0.5, 1]):\n            if not box.is_generated:\n                continue\n            box.position_x = position_x\n            box.position_y = position_y\n            if vertical:\n                box.position_y += offset * (\n                    variable_outer - box.margin_height())\n            else:\n                box.position_x += offset * (\n                    variable_outer - box.margin_width())\n            compute_fixed_dimension(\n                context, box, fixed_outer, not vertical,\n                prefix in ('top', 'left'))\n            generated_boxes.append(box)\n\n    # Corner boxes\n\n    for at_keyword, cb_width, cb_height, position_x, position_y in (\n        ('@top-left-corner', margin_left, margin_top, 0, 0),\n        ('@top-right-corner', margin_right, margin_top, page_end_x, 0),\n        ('@bottom-left-corner', margin_left, margin_bottom, 0, page_end_y),\n        ('@bottom-right-corner', margin_right, margin_bottom,\n            page_end_x, page_end_y),\n    ):\n        box = make_box(at_keyword, (cb_width, cb_height))\n        if not box.is_generated:\n            continue\n        box.position_x = position_x\n        box.position_y = position_y\n        compute_fixed_dimension(\n            context, box, cb_height, True, 'top' in at_keyword)\n        compute_fixed_dimension(\n            context, box, cb_width, False, 'left' in at_keyword)\n        generated_boxes.append(box)\n\n    for box in generated_boxes:\n        yield margin_box_content_layout(context, page, box)\n\n\ndef margin_box_content_layout(context, page, box):\n    \"\"\"Layout a margin box’s content once the box has dimensions.\"\"\"\n    positioned_boxes = []\n    box, resume_at, next_page, _, _, _ = block_container_layout(\n        context, box, bottom_space=-inf, skip_stack=None, page_is_empty=True,\n        absolute_boxes=positioned_boxes, fixed_boxes=positioned_boxes,\n        adjoining_margins=None, first_letter_style=None, first_line_style=None,\n        discard=False, max_lines=None)\n    assert resume_at is None\n    for absolute_box in positioned_boxes:\n        absolute_layout(\n            context, absolute_box, box, positioned_boxes, bottom_space=0,\n            skip_stack=None)\n\n    vertical_align = box.style['vertical_align']\n    # Every other value is read as 'top', ie. no change.\n    if vertical_align in ('middle', 'bottom') and box.children:\n        first_child = box.children[0]\n        last_child = box.children[-1]\n        top = first_child.position_y\n        # Not always exact because floating point errors\n        # assert top == box.content_box_y()\n        bottom = last_child.position_y + last_child.margin_height()\n        content_height = bottom - top\n        offset = box.height - content_height\n        if vertical_align == 'middle':\n            offset /= 2\n        for child in box.children:\n            child.translate(0, offset)\n    return box\n\n\ndef page_width_or_height(box, containing_block_size):\n    \"\"\"Take a :class:`OrientedBox` object and set either width, margin-left\n    and margin-right; or height, margin-top and margin-bottom.\n\n    \"The width and horizontal margins of the page box are then calculated\n     exactly as for a non-replaced block element in normal flow. The height\n     and vertical margins of the page box are calculated analogously (instead\n     of using the block height formulas). In both cases if the values are\n     over-constrained, instead of ignoring any margins, the containing block\n     is resized to coincide with the margin edges of the page box.\"\n\n    https://drafts.csswg.org/css-page-3/#page-box-page-rule\n    https://www.w3.org/TR/CSS21/visudet.html#blockwidth\n\n    \"\"\"\n    remaining = containing_block_size - box.padding_plus_border\n    if box.inner == 'auto':\n        if box.margin_a == 'auto':\n            box.margin_a = 0\n        if box.margin_b == 'auto':\n            box.margin_b = 0\n        box.inner = remaining - box.margin_a - box.margin_b\n    elif box.margin_a == box.margin_b == 'auto':\n        box.margin_a = box.margin_b = (remaining - box.inner) / 2\n    elif box.margin_a == 'auto':\n        box.margin_a = remaining - box.inner - box.margin_b\n    elif box.margin_b == 'auto':\n        box.margin_b = remaining - box.inner - box.margin_a\n    box.restore_box_attributes()\n\n\n@handle_min_max_width\ndef page_width(box, context, containing_block_width):\n    page_width_or_height(HorizontalBox(context, box), containing_block_width)\n\n\n@handle_min_max_height\ndef page_height(box, context, containing_block_height):\n    page_width_or_height(VerticalBox(context, box), containing_block_height)\n\n\ndef make_page(context, root_box, page_type, resume_at, page_number,\n              page_state):\n    \"\"\"Take just enough content from the beginning to fill one page.\n\n    Return ``(page, finished)``. ``page`` is a laid out PageBox object\n    and ``resume_at`` indicates where in the document to start the next page,\n    or is ``None`` if this was the last page.\n\n    :param int page_number:\n        Page number, starts at 1 for the first page.\n    :param resume_at:\n        As returned by ``make_page()`` for the previous page, or ``None`` for\n        the first page.\n\n    \"\"\"\n    style = context.style_for(page_type)\n\n    # Propagated from the root or <body>.\n    style['overflow'] = root_box.viewport_overflow\n    page = boxes.PageBox(page_type, style)\n\n    device_size = page.style['size']\n\n    resolve_percentages(page, device_size)\n\n    page.position_x = 0\n    page.position_y = 0\n    cb_width, cb_height = device_size\n    page_width(page, context, cb_width)\n    page_height(page, context, cb_height)\n\n    if page_number == 1:\n        context.style_for.initial_page_sizes['box'] = device_size\n        context.style_for.initial_page_sizes['area'] = (page.width, page.height)\n\n    root_box.position_x = page.content_box_x()\n    root_box.position_y = page.content_box_y()\n    context.page_bottom = root_box.position_y + page.height\n    initial_containing_block = page\n\n    footnote_area_style = context.style_for(page_type, '@footnote')\n    footnote_area = boxes.FootnoteAreaBox(page, footnote_area_style)\n    resolve_percentages(footnote_area, page)\n    footnote_area.position_x = page.content_box_x()\n    footnote_area.position_y = context.page_bottom\n\n    if page_type.blank:\n        previous_resume_at = resume_at\n        root_box = root_box.copy_with_children([])\n\n    # https://www.w3.org/TR/css-display-4/#root\n    assert isinstance(root_box, boxes.BlockLevelBox)\n    context.create_block_formatting_context()\n    context.current_page = page_number\n    context.current_page_footnotes = []\n    context.current_footnote_area = footnote_area\n\n    reported_footnotes = context.reported_footnotes\n    context.reported_footnotes = []\n    for i, reported_footnote in enumerate(reported_footnotes):\n        context.footnotes.append(reported_footnote)\n        overflow = context.layout_footnote(reported_footnote)\n        if overflow and i != 0:\n            context.report_footnote(reported_footnote)\n            context.reported_footnotes = reported_footnotes[i:]\n            break\n\n    # Display out-of-flow boxes broken on the previous page.\n    # TODO: we shouldn’t separate broken in-flow and out-of-flow layout.\n    page_is_empty = True\n    adjoining_margins = []\n    positioned_boxes = []  # Mixed absolute and fixed\n    out_of_flow_boxes = []\n    excluded_shapes = defaultdict(list)\n    broken_out_of_flow = {}\n    context_out_of_flow = context.broken_out_of_flow.values()\n    context.broken_out_of_flow = broken_out_of_flow\n    for box, containing_block, context_box, skip_stack in context_out_of_flow:\n        if context_box:\n            context.create_block_formatting_context(context_box)\n        box.position_y = root_box.content_box_y()\n        if box.is_floated():\n            out_of_flow_box, out_of_flow_resume_at = float_layout(\n                context, box, containing_block, positioned_boxes,\n                positioned_boxes, 0, skip_stack)\n            excluded_shapes[context_box].append(out_of_flow_box)\n        else:\n            assert box.is_absolutely_positioned()\n            out_of_flow_box, out_of_flow_resume_at = absolute_box_layout(\n                context, box, containing_block, positioned_boxes, 0,\n                skip_stack)\n        out_of_flow_boxes.append(out_of_flow_box)\n        page_is_empty = False\n        if out_of_flow_resume_at:\n            context.add_broken_out_of_flow(\n                out_of_flow_box, box, containing_block, out_of_flow_resume_at)\n        if context_box:\n            context.finish_block_formatting_context()\n\n    # Set excluded shapes from broken out-of-flow for in-flow content.\n    for context_box, shapes in excluded_shapes.items():\n        context._excluded_shapes[context_box] = shapes\n\n    # Display in-flow content.\n    initial_root_box = root_box\n    initial_resume_at = resume_at\n    root_box, resume_at, next_page, _, _, _ = block_level_layout(\n        context, root_box, 0, resume_at, initial_containing_block,\n        page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins)\n    if not root_box:\n        # In-flow page rendering didn’t progress, only out-of-flow did. Render empty box\n        # at skip_stack and force fragmentation to make the root box and its descendants\n        # cover the whole page height.\n        assert not page_is_empty\n        box = parent = initial_root_box = initial_root_box.deepcopy()\n        skip_stack = initial_resume_at\n        while skip_stack and len(skip_stack) == 1:\n            (skip, skip_stack), = skip_stack.items()\n            box, parent = box.children[skip], box\n        parent.children = []\n        parent.force_fragmentation = True\n        root_box, _, _, _, _, _ = block_level_layout(\n            context, initial_root_box, 0, initial_resume_at, initial_containing_block,\n            True, positioned_boxes, positioned_boxes, adjoining_margins)\n        resume_at = initial_resume_at\n    root_box.children = out_of_flow_boxes + root_box.children\n\n    footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy())\n    footnote_area = block_level_layout(\n        context, footnote_area, bottom_space=-inf, skip_stack=None,\n        containing_block=footnote_area.page, page_is_empty=True,\n        absolute_boxes=positioned_boxes, fixed_boxes=positioned_boxes)[0]\n    footnote_area.translate(dy=-footnote_area.margin_height())\n\n    page.fixed_boxes = [\n        placeholder._box for placeholder in positioned_boxes\n        if placeholder._box.style['position'] == 'fixed']\n    for absolute_box in positioned_boxes:\n        absolute_layout(\n            context, absolute_box, page, positioned_boxes, bottom_space=0,\n            skip_stack=None)\n\n    context.finish_block_formatting_context()\n\n    page.children = [root_box, footnote_area]\n\n    # Update page counter values\n    _standardize_page_based_counters(style, None)\n    build.update_counters(page_state, style)\n    page_counter_values = page_state[1]\n    # page_counter_values will be cached in the page_maker\n\n    target_collector = context.target_collector\n    page_maker = context.page_maker\n\n    # remake_state tells the make_all_pages-loop in layout_document()\n    # whether and what to re-make.\n    remake_state = page_maker[page_number - 1][-1]\n\n    # Evaluate and cache page values only once (for the first LineBox)\n    # otherwise we suffer endless loops when the target/pseudo-element\n    # spans across multiple pages\n    cached_anchors = []\n    cached_lookups = []\n    for (_, _, _, _, x_remake_state) in page_maker[:page_number - 1]:\n        cached_anchors.extend(x_remake_state.get('anchors', []))\n        cached_lookups.extend(x_remake_state.get('content_lookups', []))\n\n    for child in page.descendants(placeholders=True):\n        # Cache target's page counters\n        anchor = child.style['anchor']\n        if anchor and anchor not in cached_anchors:\n            remake_state['anchors'].append(anchor)\n            cached_anchors.append(anchor)\n            # Re-make of affected targeting boxes is inclusive\n            target_collector.cache_target_page_counters(\n                anchor, page_counter_values, page_number - 1, page_maker)\n\n        # string-set and bookmark-labels don't create boxes, only `content`\n        # requires another call to make_page. There is maximum one 'content'\n        # item per box.\n        if child.missing_link:\n            # A CounterLookupItem exists for the css-token 'content'\n            counter_lookup = target_collector.counter_lookup_items.get(\n                (child.missing_link, 'content'))\n        else:\n            counter_lookup = None\n\n        # Resolve missing (page based) counters\n        if counter_lookup is not None:\n            call_parse_again = False\n\n            # Prevent endless loops\n            counter_lookup_id = id(counter_lookup)\n            refresh_missing_counters = counter_lookup_id not in cached_lookups\n            if refresh_missing_counters:\n                remake_state['content_lookups'].append(counter_lookup_id)\n                cached_lookups.append(counter_lookup_id)\n                counter_lookup.page_maker_index = page_number - 1\n\n            # Step 1: page based back-references\n            # Marked as pending by target_collector.cache_target_page_counters\n            if counter_lookup.pending:\n                if (page_counter_values !=\n                        counter_lookup.cached_page_counter_values):\n                    counter_lookup.cached_page_counter_values = copy.deepcopy(\n                        page_counter_values)\n                counter_lookup.pending = False\n                call_parse_again = True\n\n            # Step 2: local counters\n            # If the box mixed-in page counters changed, update the content\n            # and cache the new values.\n            missing_counters = counter_lookup.missing_counters\n            if missing_counters:\n                if 'pages' in missing_counters:\n                    remake_state['pages_wanted'] = True\n                if refresh_missing_counters and page_counter_values != \\\n                        counter_lookup.cached_page_counter_values:\n                    counter_lookup.cached_page_counter_values = \\\n                        copy.deepcopy(page_counter_values)\n                    for counter_name in missing_counters:\n                        counter_value = page_counter_values.get(\n                            counter_name, None)\n                        if counter_value is not None:\n                            call_parse_again = True\n                            # no need to loop them all\n                            break\n\n            # Step 3: targeted counters\n            target_missing = counter_lookup.missing_target_counters\n            for anchor_name, missed_counters in target_missing.items():\n                if 'pages' not in missed_counters:\n                    continue\n                # Adjust 'pages_wanted'\n                item = target_collector.target_lookup_items.get(\n                    anchor_name, None)\n                page_maker_index = item.page_maker_index\n                if page_maker_index >= 0 and anchor_name in cached_anchors:\n                    page_maker[page_maker_index][-1]['pages_wanted'] = True\n                # 'content_changed' is triggered in\n                # targets.cache_target_page_counters()\n\n            if call_parse_again:\n                remake_state['content_changed'] = True\n                counter_lookup.parse_again(page_counter_values)\n\n    if page_type.blank:\n        resume_at = previous_resume_at\n        next_page = page_maker[page_number - 1][1]\n\n    return page, resume_at, next_page\n\n\ndef set_page_type_computed_styles(page_type, html, style_for):\n    \"\"\"Set style for page types and pseudo-types matching ``page_type``.\"\"\"\n    style_for.add_page_declarations(page_type)\n\n    # Apply style for page\n    style_for.set_computed_styles(\n        page_type,\n        # @page inherits from the root element:\n        # https://lists.w3.org/Archives/Public/www-style/2012Jan/1164.html\n        root=html.etree_element, parent=html.etree_element,\n        base_url=html.base_url)\n\n    # Apply style for page pseudo-elements (margin boxes)\n    for element, pseudo_type in style_for.get_cascaded_styles():\n        if pseudo_type and element == page_type:\n            style_for.set_computed_styles(\n                element, pseudo_type=pseudo_type,\n                # The pseudo-element inherits from the element.\n                root=html.etree_element, parent=element,\n                base_url=html.base_url)\n\n\ndef _includes_resume_at(resume_at, page_group_resume_at):\n    if not page_group_resume_at:\n        return True\n    (page_child_index, page_child_resume_at), = page_group_resume_at.items()\n    if resume_at is None or page_child_index not in resume_at:\n        return False\n    if page_child_resume_at is None:\n        return True\n    return _includes_resume_at(resume_at[page_child_index], page_child_resume_at)\n\n\ndef _update_page_groups(page_groups, resume_at, next_page, root_box, blank):\n    # https://www.w3.org/TR/css-gcpm-3/#document-sequence-selectors\n\n    # Remove or increment page groups.\n    page_groups_length = len(page_groups)\n    for i, page_group in enumerate(page_groups[:]):\n        if _includes_resume_at(resume_at, page_group[2]):\n            page_group[1] += 1\n        else:\n            page_groups.pop(i - page_groups_length)\n\n    # Add page groups.\n    if not blank:\n        if (resume_at and next_page['break'] == 'any') or not next_page['page']:\n            # We don’t have a forced page break or a named page.\n            return next_page['page']\n        if page_groups and page_groups[-1][0] == next_page['page']:\n            # We’re already in an element whose page name is the next page name.\n            return next_page['page']\n\n    # Find the box that has the named page. It is a first in-flow child of the\n    # element corresponding to resume_at.\n\n    # Find element corrensponding to resume_at.\n    current_resume_at = page_group_resume_at = copy.deepcopy(resume_at) or {0: None}\n    current_element = root_box\n    while True:\n        child_index, child_resume_at = tuple(current_resume_at.items())[-1]\n        parent_element = current_element\n        current_element = current_element.children[child_index]\n        if child_resume_at is None:\n            break\n        current_resume_at = child_resume_at\n\n    if blank:\n        # Page is blank, don’t create a new page group and return parent’s page name.\n        return parent_element.style['page']\n\n    # Find the descendant with named page.\n    while True:\n        if current_element.style['page'] == next_page['page']:\n            page_groups.append([next_page['page'], 0, page_group_resume_at])\n            return next_page['page']\n        if not isinstance(current_element, boxes.ParentBox):\n            # Shouldn’t happen.\n            return next_page['page']\n        for i, child in enumerate(current_element.children):\n            if not child.is_in_normal_flow():\n                continue\n            current_resume_at[child_index] = {i: None}\n            current_resume_at = current_resume_at[child_index]\n            child_index = i\n            current_element = child\n            break\n        else:\n            # Shouldn’t happen.\n            return next_page['page']\n\n\ndef remake_page(index, context, root_box, html):\n    \"\"\"Return one laid out page without margin boxes.\n\n    Start with the initial values from ``context.page_maker[index]``.\n    The resulting values / initial values for the next page are stored in\n    the ``page_maker``.\n\n    As the function's name suggests: the plan is not to make all pages\n    repeatedly when a missing counter was resolved, but rather re-make the\n    single page where the ``content_changed`` happened.\n\n    \"\"\"\n    page_maker = context.page_maker\n    resume_at, next_page, right_page, page_state, _ = page_maker[index]\n\n    # PageType for current page, values for page_maker[index + 1].\n    # Don't modify actual page_maker[index] values!\n    page_state = copy.deepcopy(page_state)\n    if next_page['break'] in ('left', 'right'):\n        next_page_side = next_page['break']\n    elif next_page['break'] in ('recto', 'verso'):\n        direction_ltr = root_box.style['direction'] == 'ltr'\n        break_verso = next_page['break'] == 'verso'\n        next_page_side = 'right' if direction_ltr ^ break_verso else 'left'\n    else:\n        next_page_side = None\n    blank = bool(\n        (next_page_side == 'left' and right_page) or\n        (next_page_side == 'right' and not right_page) or\n        (context.reported_footnotes and resume_at is None))\n    side = 'right' if right_page else 'left'\n    page_groups = page_state[3]\n    name = _update_page_groups(page_groups, resume_at, next_page, root_box, blank)\n    groups = tuple((name, index) for name, index, _ in page_groups)\n    page_type = PageType(side, blank, name, index, groups)\n    set_page_type_computed_styles(page_type, html, context.style_for)\n\n    context.forced_break = (next_page['break'] != 'any' or next_page['page'])\n    context.margin_clearance = False\n\n    # make_page wants a page_number of index + 1\n    page_number = index + 1\n    page, resume_at, next_page = make_page(\n        context, root_box, page_type, resume_at, page_number, page_state)\n    assert next_page\n    right_page = not right_page\n\n    # Check whether we need to append or update the next page_maker item\n    if index + 1 >= len(page_maker):\n        # New page\n        page_maker_next_changed = True\n    else:\n        # Check whether something changed\n        next_resume_at, next_next_page, next_right_page, next_page_state, _ = (\n            page_maker[index + 1])\n        page_maker_next_changed = (\n            next_resume_at != resume_at or\n            next_next_page != next_page or\n            next_right_page != right_page or\n            next_page_state != page_state)\n\n    if page_maker_next_changed:\n        # Reset remake_state\n        remake_state = {\n            'content_changed': False,\n            'pages_wanted': False,\n            'anchors': [],\n            'content_lookups': [],\n        }\n        # Setting content_changed to True ensures remake.\n        # If resume_at is None (last page) it must be False to prevent endless\n        # loops and list index out of range (see #794).\n        remake_state['content_changed'] = resume_at is not None\n        item = resume_at, next_page, right_page, page_state, remake_state\n        if index + 1 >= len(page_maker):\n            page_maker.append(item)\n        else:\n            page_maker[index + 1] = item\n\n    return page, resume_at\n\n\ndef make_all_pages(context, root_box, html, pages):\n    \"\"\"Return a list of laid out pages without margin boxes.\n\n    Re-make pages only if necessary.\n\n    \"\"\"\n    i = 0\n    reported_footnotes = None\n    while True:\n        remake_state = context.page_maker[i][-1]\n        if (len(pages) == 0 or\n                remake_state['content_changed'] or\n                remake_state['pages_wanted']):\n            PROGRESS_LOGGER.info('Step 5 - Creating layout - Page %d', i + 1)\n            # Reset remake_state\n            remake_state['content_changed'] = False\n            remake_state['pages_wanted'] = False\n            remake_state['anchors'] = []\n            remake_state['content_lookups'] = []\n            page, resume_at = remake_page(i, context, root_box, html)\n            reported_footnotes = context.reported_footnotes\n            yield page\n        else:\n            PROGRESS_LOGGER.info(\n                'Step 5 - Creating layout - Page %d (up-to-date)', i + 1)\n            resume_at = context.page_maker[i + 1][0]\n            reported_footnotes = None\n            yield pages[i]\n\n        i += 1\n        if resume_at is None and not reported_footnotes:\n            # Throw away obsolete pages and content\n            context.page_maker = context.page_maker[:i + 1]\n            context.broken_out_of_flow.clear()\n            context.reported_footnotes.clear()\n            return\n"
  },
  {
    "path": "weasyprint/layout/percent.py",
    "content": "\"\"\"Resolve percentages into fixed values.\"\"\"\n\nfrom math import inf\n\nfrom ..css import resolve_math\nfrom ..css.functions import check_math\nfrom ..formatting_structure import boxes\n\n\ndef percentage(value, computed, refer_to):\n    \"\"\"Return the percentage of the reference value, or the value unchanged.\n\n    ``refer_to`` is the length for 100%.\n\n    \"\"\"\n    if check_math(value):\n        value = resolve_math(value, computed, refer_to=refer_to)\n    if value is None or value == 'auto':\n        return value\n    elif value.unit.lower() == 'px':\n        return value.value\n    else:\n        assert value.unit == '%'\n        return refer_to * value.value / 100\n\n\ndef resolve_one_percentage(box, property_name, refer_to):\n    \"\"\"Set a used length value from a computed length value.\n\n    ``refer_to`` is the length for 100%. If ``refer_to`` is not a number, it\n    just replaces percentages.\n\n    \"\"\"\n    # box.style has computed values\n    value = box.style[property_name]\n    # box attributes are used values\n    percent = percentage(value, box.style, refer_to)\n    setattr(box, property_name, percent)\n    if property_name in ('min_width', 'min_height') and percent == 'auto':\n        setattr(box, property_name, 0)\n\n\ndef resolve_position_percentages(box, containing_block):\n    cb_width, cb_height = containing_block\n    resolve_one_percentage(box, 'left', cb_width)\n    resolve_one_percentage(box, 'right', cb_width)\n    resolve_one_percentage(box, 'top', cb_height)\n    resolve_one_percentage(box, 'bottom', cb_height)\n\n\ndef resolve_percentages(box, containing_block):\n    \"\"\"Set used values as attributes of the box object.\"\"\"\n    if isinstance(containing_block, boxes.Box):\n        # cb is short for containing block\n        cb_width = containing_block.width\n        cb_height = containing_block.height\n    else:\n        cb_width, cb_height = containing_block\n    if isinstance(box, boxes.PageBox):\n        maybe_height = cb_height\n    else:\n        maybe_height = cb_width\n    resolve_one_percentage(box, 'margin_left', cb_width)\n    resolve_one_percentage(box, 'margin_right', cb_width)\n    resolve_one_percentage(box, 'margin_top', maybe_height)\n    resolve_one_percentage(box, 'margin_bottom', maybe_height)\n    resolve_one_percentage(box, 'padding_left', cb_width)\n    resolve_one_percentage(box, 'padding_right', cb_width)\n    resolve_one_percentage(box, 'padding_top', maybe_height)\n    resolve_one_percentage(box, 'padding_bottom', maybe_height)\n    resolve_one_percentage(box, 'width', cb_width)\n    resolve_one_percentage(box, 'min_width', cb_width)\n    resolve_one_percentage(box, 'max_width', cb_width)\n\n    # XXX later: top, bottom, left and right on positioned elements\n\n    if cb_height == 'auto':\n        # Special handling when the height of the containing block\n        # depends on its content.\n        height = box.style['height']\n        if height == 'auto' or check_math(height) or height.unit == '%':\n            box.height = 'auto'\n        else:\n            assert height.unit.lower() == 'px'\n            box.height = height.value\n        resolve_one_percentage(box, 'min_height', 0)\n        resolve_one_percentage(box, 'max_height', inf)\n    else:\n        resolve_one_percentage(box, 'height', cb_height)\n        resolve_one_percentage(box, 'min_height', cb_height)\n        resolve_one_percentage(box, 'max_height', cb_height)\n\n    collapse = box.style['border_collapse'] == 'collapse'\n    # Used value == computed value\n    for side in ('top', 'right', 'bottom', 'left'):\n        prop = f'border_{side}_width'\n        # border-{side}-width would have been resolved\n        # during border conflict resolution for collapsed-borders\n        if not (collapse and hasattr(box, prop)):\n            setattr(box, prop, box.style[prop])\n\n    # Shrink *content* widths and heights according to box-sizing\n    adjust_box_sizing(box, 'width')\n    adjust_box_sizing(box, 'height')\n\n\ndef resolve_radii_percentages(box):\n    for corner in ('top_left', 'top_right', 'bottom_right', 'bottom_left'):\n        property_name = f'border_{corner}_radius'\n        computed = box.style[property_name]\n        rx, ry = computed\n\n        # Short track for common case\n        if (0, 'px') in (rx, ry):\n            setattr(box, property_name, (0, 0))\n            continue\n\n        for side in corner.split('_'):\n            if side in box.remove_decoration_sides:\n                setattr(box, property_name, (0, 0))\n                break\n        else:\n            rx = percentage(rx, box.style, box.border_width())\n            ry = percentage(ry, box.style, box.border_height())\n            setattr(box, property_name, (rx, ry))\n\n\ndef adjust_box_sizing(box, axis):\n    if box.style['box_sizing'] == 'border-box':\n        if axis == 'width':\n            delta = (\n                box.padding_left + box.padding_right +\n                box.border_left_width + box.border_right_width)\n        else:\n            delta = (\n                box.padding_top + box.padding_bottom +\n                box.border_top_width + box.border_bottom_width)\n    elif box.style['box_sizing'] == 'padding-box':\n        if axis == 'width':\n            delta = box.padding_left + box.padding_right\n        else:\n            delta = box.padding_top + box.padding_bottom\n    else:\n        assert box.style['box_sizing'] == 'content-box'\n        delta = 0\n\n    # Keep at least min_* >= 0 to prevent funny output in case box.width or\n    # box.height become negative.\n    # Restricting max_* seems reasonable, too.\n    if delta > 0:\n        if getattr(box, axis) != 'auto':\n            setattr(box, axis, max(0, getattr(box, axis) - delta))\n        setattr(box, f'max_{axis}', max(0, getattr(box, f'max_{axis}') - delta))\n        if getattr(box, f'min_{axis}') != 'auto':\n            setattr(box, f'min_{axis}', max(0, getattr(box, f'min_{axis}') - delta))\n"
  },
  {
    "path": "weasyprint/layout/preferred.py",
    "content": "\"\"\"Preferred and minimum preferred width.\n\nAlso known as max-content and min-content width, also known as the\nshrink-to-fit algorithm.\n\nTerms used (max-content width, min-content width) are defined in David\nBaron's unofficial draft (https://dbaron.org/css/intrinsic/).\n\n\"\"\"\n\nimport sys\nfrom functools import cache\nfrom math import inf\n\nfrom ..css import resolve_math\nfrom ..css.functions import check_math\nfrom ..css.validation import validate_non_shorthand\nfrom ..formatting_structure import boxes\nfrom ..text.line_break import can_break_text, split_first_line\nfrom .replaced import default_image_sizing\n\n\ndef shrink_to_fit(context, box, available_content_width):\n    \"\"\"Return the shrink-to-fit width of ``box``.\n\n    *Warning:* both available_content_width and the return value are\n    for width of the *content area*, not margin area.\n\n    https://www.w3.org/TR/CSS21/visudet.html#float-width\n\n    \"\"\"\n    return min(\n        max(\n            min_content_width(context, box, outer=False),\n            available_content_width),\n        max_content_width(context, box, outer=False))\n\n\ndef min_content_width(context, box, outer=True):\n    \"\"\"Return the min-content width for ``box``.\n\n    This is the width by breaking at every line-break opportunity.\n\n    \"\"\"\n    if box.is_table_wrapper:\n        return table_and_columns_preferred_widths(context, box, outer)[0]\n    elif isinstance(box, boxes.TableCellBox):\n        return table_cell_min_content_width(context, box, outer)\n    elif isinstance(box, (boxes.BlockContainerBox, boxes.TableColumnBox)):\n        return block_min_content_width(context, box, outer)\n    elif isinstance(box, boxes.TableColumnGroupBox):\n        return column_group_content_width(context, box)\n    elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):\n        return inline_min_content_width(context, box, outer, is_line_start=True)\n    elif isinstance(box, boxes.ReplacedBox):\n        return replaced_min_content_width(box, outer)\n    elif isinstance(box, boxes.FlexContainerBox):\n        return flex_min_content_width(context, box, outer)\n    elif isinstance(box, boxes.GridContainerBox):\n        # TODO: Get real grid size.\n        return block_min_content_width(context, box, outer)\n    else:\n        raise TypeError(f'min-content width for {type(box).__name__} not handled yet')\n\n\ndef max_content_width(context, box, outer=True):\n    \"\"\"Return the max-content width for ``box``.\n\n    This is the width by only breaking at forced line breaks.\n\n    \"\"\"\n    if box.is_table_wrapper:\n        return table_and_columns_preferred_widths(context, box, outer)[1]\n    elif isinstance(box, boxes.TableCellBox):\n        return table_cell_min_max_content_width(context, box, outer)[1]\n    elif isinstance(box, (boxes.BlockContainerBox, boxes.TableColumnBox)):\n        return block_max_content_width(context, box, outer)\n    elif isinstance(box, boxes.TableColumnGroupBox):\n        return column_group_content_width(context, box)\n    elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):\n        return inline_max_content_width(context, box, outer, is_line_start=True)\n    elif isinstance(box, boxes.ReplacedBox):\n        return replaced_max_content_width(box, outer)\n    elif isinstance(box, boxes.FlexContainerBox):\n        return flex_max_content_width(context, box, outer)\n    elif isinstance(box, boxes.GridContainerBox):\n        # TODO: Get real grid size.\n        return block_max_content_width(context, box, outer)\n    else:\n        raise TypeError(f'max-content width for {type(box).__name__} not handled yet')\n\n\ndef _block_content_width(context, box, function, outer):\n    \"\"\"Helper to create ``block_*_content_width.``\"\"\"\n    width = box.style['width']\n    if width == 'auto' or check_math(width) or width.unit == '%':\n        # \"percentages on the following properties are treated instead as\n        # though they were the following: width: auto\"\n        # https://dbaron.org/css/intrinsic/#outer-intrinsic\n        children_widths = [\n            function(context, child, outer=True) for child in box.children\n            if not child.is_absolutely_positioned()]\n        width = max(children_widths) if children_widths else 0\n    elif box.style['box_sizing'] == 'content-box':\n        width = width.value\n    else:\n        width = width.value\n        percentages = 0\n\n        for value in ('padding_left', 'padding_right'):\n            style_value = box.style[value]\n            if style_value != 'auto' and not check_math(style_value):\n                if style_value.unit.lower() == 'px':\n                    width -= style_value.value\n                else:\n                    assert style_value.unit == '%'\n                    percentages += style_value.value\n\n        # Same as margin_width().\n        collapse = box.style['border_collapse'] == 'collapse'\n        if collapse and hasattr(box, 'border_left_width'):\n            width -= box.border_left_width\n        else:\n            width -= box.style['border_left_width']\n        if collapse and hasattr(box, 'border_right_width'):\n            width -= box.border_right_width\n        else:\n            width -= box.style['border_right_width']\n        width = (100 - min(100, percentages)) * max(0, width) / 100\n\n    return adjust(box, outer, width)\n\n\ndef min_max(box, width):\n    \"\"\"Get box width from given width and box min- and max-widths.\"\"\"\n    min_width = box.style['min_width']\n    max_width = box.style['max_width']\n    min_pending = check_math(min_width)\n    max_pending = check_math(max_width)\n    if min_width == 'auto' or min_pending or min_width.unit == '%':\n        min_width = 0\n    else:\n        min_width = min_width.value\n    if max_width == 'auto' or max_pending or max_width.unit == '%':\n        max_width = inf\n    else:\n        max_width = max_width.value\n\n    if isinstance(box, boxes.ReplacedBox):\n        _, _, ratio = box.replacement.get_intrinsic_size(\n            1, box.style['font_size'])\n        if ratio is not None:\n            min_height = box.style['min_height']\n            max_height = box.style['max_height']\n            min_pending = check_math(min_height)\n            max_pending = check_math(max_height)\n            if min_height != 'auto' and not min_pending and min_height.unit != '%':\n                min_width = max(min_width, min_height.value * ratio)\n            if max_height != 'auto' and not min_pending and max_height.unit != '%':\n                max_width = min(max_width, max_height.value * ratio)\n\n    return max(min_width, min(width, max_width))\n\n\ndef margin_width(box, width, left=True, right=True):\n    \"\"\"Add box paddings, borders and margins to ``width``.\"\"\"\n    percentages = 0\n\n    # See https://drafts.csswg.org/css-tables-3/#cell-intrinsic-offsets\n    # It is a set of computed values for border-left-width, padding-left,\n    # padding-right, and border-right-width (along with zero values for\n    # margin-left and margin-right)\n    for value in (\n        (['margin_left', 'padding_left'] if left else []) +\n        (['margin_right', 'padding_right'] if right else [])\n    ):\n        style_value = box.style[value]\n        if style_value != 'auto' and not check_math(style_value):\n            if style_value.unit.lower() == 'px':\n                width += style_value.value\n            else:\n                assert style_value.unit == '%'\n                percentages += style_value.value\n\n    collapse = box.style['border_collapse'] == 'collapse'\n    if left:\n        if collapse and hasattr(box, 'border_left_width'):\n            # In collapsed-borders mode: the computed horizontal padding of the\n            # cell and, for border values, the used border-width values of the\n            # cell (half the winning border-width)\n            width += box.border_left_width\n        else:\n            # In separated-borders mode: the computed horizontal padding and\n            # border of the table-cell\n            width += box.style['border_left_width']\n    if right:\n        if collapse and hasattr(box, 'border_right_width'):\n            # [...] the used border-width values of the cell\n            width += box.border_right_width\n        else:\n            # [...] the computed border of the table-cell\n            width += box.style['border_right_width']\n\n    if percentages < 100:\n        return width / (1 - percentages / 100)\n    else:\n        # Pathological case, ignore\n        return 0\n\n\ndef adjust(box, outer, width, left=True, right=True):\n    \"\"\"Respect min/max and adjust width depending on ``outer``.\n\n    If ``outer`` is set to ``True``, return margin width, else return content\n    width.\n\n    \"\"\"\n    fixed = min_max(box, width)\n\n    if outer:\n        return margin_width(box, fixed, left, right)\n    else:\n        return fixed\n\n\ndef block_min_content_width(context, box, outer=True):\n    \"\"\"Return the min-content width for a ``BlockBox``.\"\"\"\n    return _block_content_width(\n        context, box, min_content_width, outer)\n\n\ndef block_max_content_width(context, box, outer=True):\n    \"\"\"Return the max-content width for a ``BlockBox``.\"\"\"\n    return _block_content_width(context, box, max_content_width, outer)\n\n\ndef inline_min_content_width(context, box, outer=True, skip_stack=None,\n                             first_line=False, is_line_start=False):\n    \"\"\"Return the min-content width for an ``InlineBox``.\n\n    The width is calculated from the lines from ``skip_stack``. If\n    ``first_line`` is ``True``, only the first line minimum width is\n    calculated.\n\n    \"\"\"\n    widths = inline_line_widths(\n        context, box, outer, is_line_start, minimum=True,\n        skip_stack=skip_stack, first_line=first_line)\n    width = next(widths) if first_line else max(widths)\n    return adjust(box, outer, width)\n\n\ndef inline_max_content_width(context, box, outer=True, is_line_start=False):\n    \"\"\"Return the max-content width for an ``InlineBox``.\"\"\"\n    widths = list(\n        inline_line_widths(context, box, outer, is_line_start, minimum=False))\n    # Remove trailing space, as split_first_line keeps trailing spaces when\n    # max_width is not set.\n    widths[-1] -= trailing_whitespace_size(context, box)\n    return adjust(box, outer, max(widths))\n\n\ndef column_group_content_width(context, box):\n    \"\"\"Return the *-content width for a ``TableColumnGroupBox``.\"\"\"\n    width = box.style['width']\n    if width == 'auto' or check_math(width) or width.unit == '%':\n        width = 0\n    else:\n        assert width.unit.lower() == 'px'\n        width = width.value\n\n    return adjust(box, False, width)\n\n\ndef table_cell_min_content_width(context, box, outer):\n    \"\"\"Return the min-content width for a ``TableCellBox``.\"\"\"\n    # See https://www.w3.org/TR/css-tables-3/#outer-min-content\n    # The outer min-content width of a table-cell is\n    # max(min-width, min-content width) adjusted by\n    # the cell intrinsic offsets.\n    children_widths = [\n        min_content_width(context, child)\n        for child in box.children\n        if not child.is_absolutely_positioned()]\n    children_min_width = adjust(\n        box,\n        outer,\n        max(children_widths) if children_widths else 0)\n\n    return children_min_width\n\n\ndef table_cell_min_max_content_width(context, box, outer=True):\n    \"\"\"Return the min- and max-content width for a ``TableCellBox``.\"\"\"\n    # This is much faster than calling min and max separately.\n    min_width = table_cell_min_content_width(context, box, outer)\n    max_width = max(min_width, block_max_content_width(context, box, outer))\n    return min_width, max_width\n\n\ndef inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=None,\n                       first_line=False):\n    \"\"\"Yield line width for each line.\"\"\"\n\n    # Set text indent.\n    text_indent = 0\n    if isinstance(box, boxes.LineBox):\n        indent_token = box.style['text_indent']\n        if check_math(indent_token):\n            # Ignore percentages by setting refer_to to 0.\n            result = resolve_math(indent_token, box.style, 'text_indent', refer_to=0)\n            value = validate_non_shorthand((result,), 'text-indent')[0][1]\n            if value and value.unit != '%':\n                text_indent = value.value\n        elif indent_token.unit != '%':\n            text_indent = box.style['text_indent'].value\n\n    # Yield widths for each line.\n    current_line = 0\n    if skip_stack is None:\n        skip = 0\n    else:\n        (skip, skip_stack), = skip_stack.items()\n    for child in box.children[skip:]:\n        # Skip absolutely positioned elements.\n        if child.is_absolutely_positioned():\n            continue\n\n        # None is used in \"lines\" to track line breaks, transformed to 0 when yielded.\n        if isinstance(child, boxes.InlineBox):\n            # Inline box, call function recursively.\n            lines = inline_line_widths(\n                context, child, outer, is_line_start, minimum, skip_stack, first_line)\n            if first_line:\n                lines = [next(lines) or None]\n            else:\n                lines = [line or None for line in lines]\n            if len(lines) == 1:\n                lines[0] = adjust(child, outer, lines[0] or 0)\n            else:\n                lines[0] = adjust(child, outer, lines[0] or 0, right=False) or None\n                lines[-1] = adjust(child, outer, lines[-1] or 0, left=False) or None\n        elif isinstance(child, boxes.TextBox):\n            # Text box, split into lines.\n            white_space = child.style['white_space']\n            space_collapse = white_space in ('normal', 'nowrap', 'pre-line')\n            text_wrap = white_space in ('normal', 'pre-wrap', 'pre-line')\n            if skip_stack is None:\n                skip = 0\n            else:\n                (skip, skip_stack), = skip_stack.items()\n                assert skip_stack is None\n            child_text = child.text.encode()[(skip or 0):]\n            if is_line_start and space_collapse:\n                child_text = child_text.lstrip(b' ')\n            max_width = 0 if minimum else None\n            lines = []\n            resume_index = new_resume_index = 0\n            while new_resume_index is not None:\n                resume_index += new_resume_index\n                _, _, new_resume_index, width, _, _ = split_first_line(\n                    child_text[resume_index:].decode(), child.style, context, max_width,\n                    child.justification_spacing, is_line_start=is_line_start,\n                    minimum=True)\n                lines.append(width or None)\n                if first_line:\n                    break\n            if first_line and new_resume_index:\n                # We only need the first line, break early.\n                current_line += lines[0] or 0\n                break\n            # TODO: use the real next character instead of 'a' to detect line breaks.\n            last_letter = child_text.decode()[-1:]\n            can_break = can_break_text(last_letter + 'a', child.style['lang'])\n            if minimum and text_wrap and can_break:\n                # Add all possible line breaks for minimal width.\n                lines.append(None)\n        else:\n            # Replaced elements, inline blocks…\n            # https://www.w3.org/TR/css-text-3/#overflow-wrap\n            # \"The line breaking behavior of a replaced element\n            #  or other atomic inline is equivalent to that\n            #  of the Object Replacement Character (U+FFFC).\"\n            # https://www.unicode.org/reports/tr14/#DescriptionOfProperties\n            # \"By default, there is a break opportunity\n            #  both before and after any inline object.\"\n            if minimum:\n                # \"For soft wrap opportunities defined by the boundary between two\n                # characters or atomic inlines, the white-space property on the nearest\n                # common ancestor of the two characters controls breaking; which\n                # elements’ line-break, word-break, and overflow-wrap properties control\n                # the determination of soft wrap opportunities at such boundaries is\n                # undefined in this level.\" We choose to always follow the parent’s\n                # value here, other parts of the line-breaking algorithm do the same.\n                if box.style['white_space'] in ('normal', 'pre-wrap', 'pre-line'):\n                    lines = [None, min_content_width(context, child), None]\n                else:\n                    lines = [min_content_width(context, child)]\n            else:\n                lines = [max_content_width(context, child)]\n        # The first text line goes on the current line.\n        current_line += lines[0] or 0\n        if len(lines) > 1:\n            # Forced line break(s).\n            yield current_line + text_indent\n            text_indent = 0\n            if len(lines) > 2:\n                for line in lines[1:-1]:\n                    yield line or 0\n            current_line = lines[-1] or 0\n        is_line_start = lines[-1] is None\n        skip_stack = None\n    yield current_line + text_indent\n\n\ndef _percentage_contribution(box):\n    \"\"\"Return the percentage contribution of a cell, column or column group.\n\n    https://dbaron.org/css/intrinsic/#pct-contrib\n\n    \"\"\"\n    min_width = box.style['min_width']\n    min_width = (\n        min_width.value if min_width != 'auto' and\n        not check_math(min_width) and min_width.unit == '%' else 0)\n    max_width = box.style['max_width']\n    max_width = (\n        max_width.value if max_width != 'auto' and\n        not check_math(max_width) and max_width.unit == '%' else inf)\n    width = box.style['width']\n    width = (\n        width.value if width != 'auto' and\n        not check_math(width) and width.unit == '%' else 0)\n    return max(min_width, min(width, max_width))\n\n\ndef table_and_columns_preferred_widths(context, box, outer=True):\n    \"\"\"Return content widths for the auto layout table and its columns.\n\n    The tuple returned is\n    ``(table_min_content_width, table_max_content_width,\n       column_min_content_widths, column_max_content_widths,\n       column_intrinsic_percentages, constrainedness,\n       total_horizontal_border_spacing, grid)``\n\n    https://dbaron.org/css/intrinsic/\n\n    \"\"\"\n    from .table import distribute_excess_width\n\n    table = box.get_wrapped_table()\n    result = context.tables.get(table)\n    if result:\n        return result[outer]\n\n    # Create the grid\n    grid_width, grid_height = 0, 0\n    row_number = 0\n    for row_group in table.children:\n        for row in row_group.children:\n            for cell in row.children:\n                grid_width = max(cell.grid_x + cell.colspan, grid_width)\n                grid_height = max(row_number + cell.rowspan, grid_height)\n            row_number += 1\n    grid = [[None] * grid_width for i in range(grid_height)]\n    row_number = 0\n    for row_group in table.children:\n        for row in row_group.children:\n            for cell in row.children:\n                grid[row_number][cell.grid_x] = cell\n            row_number += 1\n\n    zipped_grid = list(zip(*grid))\n\n    # Define the total horizontal border spacing\n    if table.style['border_collapse'] == 'separate' and grid_width > 0:\n        total_horizontal_border_spacing = (\n            table.style['border_spacing'][0] *\n            (1 + len([column for column in zipped_grid if any(column)])))\n    else:\n        total_horizontal_border_spacing = 0\n\n    if grid_width == 0 or grid_height == 0:\n        table.children = []\n        min_width = block_min_content_width(context, table, outer=False)\n        max_width = block_max_content_width(context, table, outer=False)\n        outer_min_width = adjust(\n            box, outer=True, width=block_min_content_width(context, table))\n        outer_max_width = adjust(\n            box, outer=True, width=block_max_content_width(context, table))\n        result = ([], [], [], [], total_horizontal_border_spacing, [])\n        context.tables[table] = result = {\n            False: (min_width, max_width, *result),\n            True: (outer_min_width, outer_max_width, *result),\n        }\n        return result[outer]\n\n    column_groups = [None] * grid_width\n    columns = [None] * grid_width\n    column_number = 0\n    for column_group in table.column_groups:\n        for column in column_group.children:\n            column_groups[column_number] = column_group\n            columns[column_number] = column\n            column_number += 1\n            if column_number == grid_width:\n                break\n        else:\n            continue\n        break\n\n    colspan_cells = []\n    colspans = set()\n\n    # Define the intermediate content widths\n    min_content_widths = [0] * grid_width\n    max_content_widths = [0] * grid_width\n    intrinsic_percentages = [0] * grid_width\n\n    # Intermediate content widths for span 1\n    for i in range(grid_width):\n        for groups in (column_groups, columns):\n            if group := groups[i]:\n                min_content_widths[i] = max(\n                    min_content_widths[i], min_content_width(context, group))\n                max_content_widths[i] = max(\n                    max_content_widths[i], max_content_width(context, group))\n                intrinsic_percentages[i] = max(\n                    intrinsic_percentages[i], _percentage_contribution(group))\n        for cell in zipped_grid[i]:\n            if not cell:\n                continue\n            if cell.colspan == 1:\n                min_width, max_width = table_cell_min_max_content_width(context, cell)\n                min_content_widths[i] = max(min_content_widths[i], min_width)\n                max_content_widths[i] = max(max_content_widths[i], max_width)\n                intrinsic_percentages[i] = max(\n                    intrinsic_percentages[i], _percentage_contribution(cell))\n            else:\n                colspan_cells.append(cell)\n                colspans.add(cell.colspan - 1)\n\n    # Intermediate content widths for span > 1 is wrong in the 4.1 section, as\n    # explained in its third issue. Min- and max-content widths are handled by\n    # the excess width distribution method, and percentages do not distribute\n    # widths to columns that have originating cells.\n\n    # Intermediate intrinsic percentage widths for span > 1\n    rows_origins = []\n    for y, row in enumerate(grid):\n        origin = None\n        rows_origins.append(row_origins := [])\n        for x, cell in enumerate(row):\n            if cell:\n                origin = x\n            row_origins.append(origin)\n\n    @cache\n    def get_percentage_contribution(origin_cell, origin, max_content_width):\n        # Cached for big colspan values, see #1155.\n        cell_slice = slice(origin, origin + origin_cell.colspan)\n        baseline_percentage = sum(intrinsic_percentages[cell_slice])\n        cell_percentage_contribution = _percentage_contribution(origin_cell)\n        diff = max(0, cell_percentage_contribution - baseline_percentage)\n        other_columns_contributions = [\n            max_content_widths[j]\n            for j in range(origin, origin + origin_cell.colspan)\n            if intrinsic_percentages[j] == 0]\n        other_columns_contributions_sum = sum(other_columns_contributions)\n        if other_columns_contributions_sum == 0:\n            ratio = 1 / (len(other_columns_contributions) or 1)\n        else:\n            ratio = max_content_width / other_columns_contributions_sum\n        return diff * ratio\n\n    for span in sorted(colspans):\n        percentage_contributions = []\n        for i in range(grid_width):\n            if percentage_contribution := intrinsic_percentages[i]:\n                percentage_contributions.append(percentage_contribution)\n                continue\n            for row, row_origins in zip(grid, rows_origins):\n                if (origin := row_origins[i]) is None:\n                    continue\n                origin_cell = row[origin]\n                if origin_cell.colspan - 1 != span:\n                    continue\n                cell_percentage_contribution = get_percentage_contribution(\n                    origin_cell, origin, max_content_widths[i])\n                percentage_contribution = max(\n                    percentage_contribution, cell_percentage_contribution)\n\n            percentage_contributions.append(percentage_contribution)\n\n        intrinsic_percentages = percentage_contributions\n\n    # Define constrainedness\n    constrainedness = [False for i in range(grid_width)]\n    for i in range(grid_width):\n        if column_groups[i]:\n            width = column_groups[i].style['width']\n            if width != 'auto' and not check_math(width) and width.unit != '%':\n                constrainedness[i] = True\n                continue\n        if columns[i]:\n            width = columns[i].style['width']\n            if width != 'auto' and not check_math(width) and width.unit != '%':\n                constrainedness[i] = True\n                continue\n        for cell in zipped_grid[i]:\n            if cell and cell.colspan == 1:\n                width = cell.style['width']\n                if width != 'auto' and not check_math(width) and width.unit != '%':\n                    constrainedness[i] = True\n                    break\n\n    intrinsic_percentages = [\n        min(percentage, 100 - sum(intrinsic_percentages[:i]))\n        for i, percentage in enumerate(intrinsic_percentages)]\n\n    # Max- and min-content widths for span > 1\n    for cell in colspan_cells:\n        min_content = min_content_width(context, cell)\n        max_content = max_content_width(context, cell)\n        column_slice = slice(cell.grid_x, cell.grid_x + cell.colspan)\n        columns_min_content = sum(min_content_widths[column_slice])\n        columns_max_content = sum(max_content_widths[column_slice])\n        if table.style['border_collapse'] == 'separate':\n            spacing = (cell.colspan - 1) * table.style['border_spacing'][0]\n        else:\n            spacing = 0\n\n        if min_content > columns_min_content + spacing:\n            excess_width = min_content - (columns_min_content + spacing)\n            distribute_excess_width(\n                context, zipped_grid, excess_width, min_content_widths,\n                constrainedness, intrinsic_percentages, max_content_widths,\n                column_slice)\n\n        if max_content > columns_max_content + spacing:\n            excess_width = max_content - (columns_max_content + spacing)\n            distribute_excess_width(\n                context, zipped_grid, excess_width, max_content_widths,\n                constrainedness, intrinsic_percentages, max_content_widths,\n                column_slice)\n\n    # Calculate the max- and min-content widths of table and columns\n    small_percentage_contributions = [\n        max_content_widths[i] / (intrinsic_percentages[i] / 100)\n        for i in range(grid_width)\n        if intrinsic_percentages[i]]\n    large_percentage_contribution_numerator = sum(\n        max_content_widths[i] for i in range(grid_width)\n        if intrinsic_percentages[i] == 0)\n    large_percentage_contribution_denominator = (\n        (100 - sum(intrinsic_percentages)) / 100)\n    if large_percentage_contribution_denominator == 0:\n        if large_percentage_contribution_numerator == 0:\n            large_percentage_contribution = 0\n        else:\n            # \"the large percentage contribution of the table [is] an\n            # infinitely large number if the numerator is nonzero [and] the\n            # denominator of that ratio is 0.\"\n            #\n            # https://dbaron.org/css/intrinsic/#autotableintrinsic\n            #\n            # Please note that \"an infinitely large number\" is not \"infinite\",\n            # and that's probably not a coincindence: putting 'inf' here breaks\n            # some cases (see #305).\n            large_percentage_contribution = sys.maxsize\n    else:\n        large_percentage_contribution = (\n            large_percentage_contribution_numerator /\n            large_percentage_contribution_denominator)\n\n    table_min_content_width = (\n        total_horizontal_border_spacing + sum(min_content_widths))\n    table_max_content_width = (\n        total_horizontal_border_spacing + max([\n            sum(max_content_widths), large_percentage_contribution,\n            *small_percentage_contributions]))\n\n    width = table.style['width']\n    if width != 'auto' and not check_math(width) and width.unit.lower() == 'px':\n        # \"percentages on the following properties are treated instead as\n        # though they were the following: width: auto\"\n        # https://dbaron.org/css/intrinsic/#outer-intrinsic\n        table_min_width = table_max_width = table.style['width'].value\n    else:\n        table_min_width = table_min_content_width\n        table_max_width = table_max_content_width\n\n    table_min_content_width = max(\n        table_min_content_width, adjust(\n            table, outer=False, width=table_min_width))\n    table_max_content_width = max(\n        table_max_content_width, adjust(\n            table, outer=False, width=table_max_width))\n    table_outer_min_content_width = margin_width(\n        table, margin_width(box, table_min_content_width))\n    table_outer_max_content_width = margin_width(\n        table, margin_width(box, table_max_content_width))\n\n    result = (\n        min_content_widths, max_content_widths, intrinsic_percentages,\n        constrainedness, total_horizontal_border_spacing, zipped_grid)\n    context.tables[table] = result = {\n        False: (table_min_content_width, table_max_content_width, *result),\n        True: (table_outer_min_content_width, table_outer_max_content_width, *result),\n    }\n    return result[outer]\n\n\ndef replaced_min_content_width(box, outer=True):\n    \"\"\"Return the min-content width for an ``InlineReplacedBox``.\"\"\"\n    width = box.style['width']\n    if width == 'auto':\n        height = box.style['height']\n        if height == 'auto' or check_math(height) or height.unit == '%':\n            height = 'auto'\n        else:\n            assert height.unit.lower() == 'px'\n            height = height.value\n        unknown_max_width = (\n            box.style['max_width'] != 'auto' and\n            not check_math(box.style['max_width']) and\n            box.style['max_width'].unit == '%')\n        if unknown_max_width:\n            # See https://drafts.csswg.org/css-sizing/#intrinsic-contribution\n            width = 0\n        else:\n            image = box.replacement\n            intrinsic_width, intrinsic_height, intrinsic_ratio = (\n                image.get_intrinsic_size(\n                    box.style['image_resolution'], box.style['font_size']))\n            width, _ = default_image_sizing(\n                intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto',\n                height, default_width=0, default_height=0)\n    elif check_math(box.style['width']) or box.style['width'].unit == '%':\n        # See https://drafts.csswg.org/css-sizing/#intrinsic-contribution\n        width = 0\n    else:\n        assert width.unit.lower() == 'px'\n        width = width.value\n    return adjust(box, outer, width)\n\n\ndef replaced_max_content_width(box, outer=True):\n    \"\"\"Return the max-content width for an ``InlineReplacedBox``.\"\"\"\n    width = box.style['width']\n    if width == 'auto':\n        height = box.style['height']\n        if height == 'auto' or check_math(height) or height.unit == '%':\n            height = 'auto'\n        else:\n            assert height.unit.lower() == 'px'\n            height = height.value\n        image = box.replacement\n        intrinsic_width, intrinsic_height, intrinsic_ratio = (\n            image.get_intrinsic_size(\n                box.style['image_resolution'], box.style['font_size']))\n        width, _ = default_image_sizing(\n            intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto', height,\n            default_width=300, default_height=150)\n    elif check_math(box.style['width']) or box.style['width'].unit == '%':\n        # See https://drafts.csswg.org/css-sizing/#intrinsic-contribution\n        width = 0\n    else:\n        assert width.unit.lower() == 'px'\n        width = width.value\n    return adjust(box, outer, width)\n\n\ndef flex_min_content_width(context, box, outer=True):\n    \"\"\"Return the min-content width for an ``FlexContainerBox``.\"\"\"\n    # TODO: use real values, see\n    # https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes\n    min_contents = [\n        min_content_width(context, child)\n        for child in box.children if child.is_flex_item]\n    if not min_contents:\n        return adjust(box, outer, 0)\n    if (box.style['flex_direction'].startswith('row') and\n            box.style['flex_wrap'] == 'nowrap'):\n        return adjust(box, outer, sum(min_contents))\n    else:\n        return adjust(box, outer, max(min_contents))\n\n\ndef flex_max_content_width(context, box, outer=True):\n    \"\"\"Return the max-content width for an ``FlexContainerBox``.\"\"\"\n    # TODO: use real values, see\n    # https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes\n    max_contents = [\n        max_content_width(context, child)\n        for child in box.children if child.is_flex_item]\n    if not max_contents:\n        return adjust(box, outer, 0)\n    if box.style['flex_direction'].startswith('row'):\n        return adjust(box, outer, sum(max_contents))\n    else:\n        return adjust(box, outer, max(max_contents))\n\n\ndef trailing_whitespace_size(context, box):\n    \"\"\"Return the size of the trailing whitespace of ``box``.\"\"\"\n    from .inline import split_first_line, split_text_box\n\n    # Find last box child, keep last parent to remove nested trailing spaces.\n    last_parent = None\n    while isinstance(box, (boxes.InlineBox, boxes.LineBox)):\n        if not box.children:\n            return 0\n        last_parent, box = box, box.children[-1]\n\n    # Return early if possible.\n    if not isinstance(box, boxes.TextBox) or not box.text:\n        # There’s no text in last child.\n        return 0\n    elif box.style['white_space'] not in ('normal', 'nowrap', 'pre-line'):\n        # Spaces don’t collapse.\n        return 0\n    elif box.style['font_size'] == 0:\n        # Trailing spaces take no space.\n        return 0\n    elif not box.text.endswith(' '):\n        # No trailing space.\n        return 0\n\n    # Strip text.\n    if stripped_text := box.text.rstrip(' '):\n        # Stripped text is not empty, calculate width difference.\n        resume = 0\n        while resume is not None:\n            old_resume = resume\n            old_box, resume, _ = split_text_box(context, box, None, resume)\n        assert old_box\n        stripped_box = box.copy_with_text(stripped_text)\n        stripped_box, resume, _ = split_text_box(\n            context, stripped_box, None, old_resume)\n        if stripped_box is None:\n            # Old box is split just before the trailing spaces.\n            return old_box.width\n        else:\n            # Return difference between old width and stripped width.\n            assert resume is None\n            return old_box.width - stripped_box.width\n    else:\n        # Stripped text is empty, render spaces to get width.\n        _, _, _, width, _, _ = split_first_line(\n            box.text, box.style, context, None, box.justification_spacing)\n        # Remove possible trailing spaces from previous child.\n        if last_parent and len(last_parent.children) >= 2:\n            width += trailing_whitespace_size(context, last_parent.children[-2])\n        return width\n"
  },
  {
    "path": "weasyprint/layout/replaced.py",
    "content": "\"\"\"Layout for images and other replaced elements.\n\nSee https://drafts.csswg.org/css-images-3/#sizing\n\n\"\"\"\n\nfrom .min_max import handle_min_max_height, handle_min_max_width\nfrom .percent import percentage\n\n\ndef default_image_sizing(intrinsic_width, intrinsic_height, intrinsic_ratio,\n                         specified_width, specified_height,\n                         default_width, default_height):\n    \"\"\"Default sizing algorithm for the concrete object size.\n\n    Return a ``(concrete_width, concrete_height)`` tuple.\n\n    See https://drafts.csswg.org/css-images-3/#default-sizing\n\n    \"\"\"\n    if specified_width == 'auto':\n        specified_width = None\n    if specified_height == 'auto':\n        specified_height = None\n\n    if specified_width is not None and specified_height is not None:\n        return specified_width, specified_height\n    elif specified_width is not None:\n        return specified_width, (\n            specified_width / intrinsic_ratio if intrinsic_ratio is not None\n            else intrinsic_height if intrinsic_height is not None\n            else default_height)\n    elif specified_height is not None:\n        return (\n            specified_height * intrinsic_ratio if intrinsic_ratio is not None\n            else intrinsic_width if intrinsic_width is not None\n            else default_width\n        ), specified_height\n    else:\n        if intrinsic_width is not None or intrinsic_height is not None:\n            return default_image_sizing(\n                intrinsic_width, intrinsic_height, intrinsic_ratio,\n                intrinsic_width, intrinsic_height, default_width,\n                default_height)\n        else:\n            return contain_constraint_image_sizing(\n                default_width, default_height, intrinsic_ratio)\n\n\ndef contain_constraint_image_sizing(constraint_width, constraint_height,\n                                    intrinsic_ratio):\n    \"\"\"Contain constraint sizing algorithm for the concrete object size.\n\n    Return a ``(concrete_width, concrete_height)`` tuple.\n\n    See https://drafts.csswg.org/css-images-3/#contain-constraint\n\n    \"\"\"\n    return _constraint_image_sizing(\n        constraint_width, constraint_height, intrinsic_ratio, cover=False)\n\n\ndef cover_constraint_image_sizing(constraint_width, constraint_height,\n                                  intrinsic_ratio):\n    \"\"\"Cover constraint sizing algorithm for the concrete object size.\n\n    Return a ``(concrete_width, concrete_height)`` tuple.\n\n    See https://drafts.csswg.org/css-images-3/#cover-constraint\n\n    \"\"\"\n    return _constraint_image_sizing(\n        constraint_width, constraint_height, intrinsic_ratio, cover=True)\n\n\ndef _constraint_image_sizing(constraint_width, constraint_height,\n                             intrinsic_ratio, cover):\n    if intrinsic_ratio is None:\n        return constraint_width, constraint_height\n    elif cover ^ (constraint_width > constraint_height * intrinsic_ratio):\n        return constraint_height * intrinsic_ratio, constraint_height\n    else:\n        return constraint_width, constraint_width / intrinsic_ratio\n\n\ndef replacedbox_layout(box):\n    # TODO: respect box-sizing ?\n    object_fit = box.style['object_fit']\n    position = box.style['object_position']\n\n    image = box.replacement\n    intrinsic_width, intrinsic_height, intrinsic_ratio = (\n        image.get_intrinsic_size(\n            box.style['image_resolution'], box.style['font_size']))\n    if None in (intrinsic_width, intrinsic_height):\n        intrinsic_width, intrinsic_height = contain_constraint_image_sizing(\n            box.width, box.height, intrinsic_ratio)\n\n    if object_fit == 'fill':\n        draw_width, draw_height = box.width, box.height\n    else:\n        if object_fit in ('contain', 'scale-down'):\n            draw_width, draw_height = contain_constraint_image_sizing(\n                box.width, box.height, intrinsic_ratio)\n        elif object_fit == 'cover':\n            draw_width, draw_height = cover_constraint_image_sizing(\n                box.width, box.height, intrinsic_ratio)\n        else:\n            assert object_fit == 'none', object_fit\n            draw_width, draw_height = intrinsic_width, intrinsic_height\n\n        if object_fit == 'scale-down':\n            draw_width = min(draw_width, intrinsic_width)\n            draw_height = min(draw_height, intrinsic_height)\n\n    origin_x, position_x, origin_y, position_y = position[0]\n    ref_x = box.width - draw_width\n    ref_y = box.height - draw_height\n\n    position_x = percentage(position_x, box.style, ref_x)\n    position_y = percentage(position_y, box.style, ref_y)\n    if origin_x == 'right':\n        position_x = ref_x - position_x\n    if origin_y == 'bottom':\n        position_y = ref_y - position_y\n\n    position_x += box.content_box_x()\n    position_y += box.content_box_y()\n\n    return draw_width, draw_height, position_x, position_y\n\n\n@handle_min_max_width\ndef replaced_box_width(box, containing_block):\n    \"\"\"Set the used width for replaced boxes.\"\"\"\n    from .block import block_level_width\n\n    width, height, ratio = box.replacement.get_intrinsic_size(\n        box.style['image_resolution'], box.style['font_size'])\n\n    # This algorithm simply follows the different points of the specification:\n    # https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width\n    if box.height == box.width == 'auto':\n        if width is not None:\n            # Point #1\n            box.width = width\n        elif ratio is not None:\n            if height is not None:\n                # Point #2 first part\n                box.width = height * ratio\n            else:\n                # Point #3\n                block_level_width(box, containing_block)\n\n    if box.width == 'auto':\n        if ratio is not None:\n            # Point #2 second part\n            box.width = box.height * ratio\n        elif width is not None:\n            # Point #4\n            box.width = width\n        else:\n            # Point #5\n            # It's pretty useless to rely on device size to set width.\n            box.width = 300\n\n\n@handle_min_max_height\ndef replaced_box_height(box):\n    \"\"\"Compute and set the used height for replaced boxes.\"\"\"\n    # https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height\n    width, height, ratio = box.replacement.get_intrinsic_size(\n        box.style['image_resolution'], box.style['font_size'])\n\n    # Test 'auto' on the computed width, not the used width\n    if box.height == box.width == 'auto':\n        box.height = height\n    elif box.height == 'auto' and ratio:\n        box.height = box.width / ratio\n\n    if box.height == box.width == 'auto' and height is not None:\n        box.height = height\n    elif ratio is not None and box.height == 'auto':\n        box.height = box.width / ratio\n    elif box.height == 'auto' and height is not None:\n        box.height = height\n    elif box.height == 'auto':\n        # It's pretty useless to rely on device size to set width.\n        box.height = 150\n\n\ndef inline_replaced_box_layout(box, containing_block):\n    \"\"\"Lay out an inline :class:`boxes.ReplacedBox` ``box``.\"\"\"\n    for side in ('top', 'right', 'bottom', 'left'):\n        if getattr(box, f'margin_{side}') == 'auto':\n            setattr(box, f'margin_{side}', 0)\n    inline_replaced_box_width_height(box, containing_block)\n\n\ndef inline_replaced_box_width_height(box, containing_block):\n    if box.style['width'] == box.style['height'] == 'auto':\n        replaced_box_width.without_min_max(box, containing_block)\n        replaced_box_height.without_min_max(box)\n        min_max_auto_replaced(box)\n    else:\n        replaced_box_width(box, containing_block)\n        replaced_box_height(box)\n\n\ndef min_max_auto_replaced(box):\n    \"\"\"Resolve min/max constraints on replaced elements with 'auto' sizes.\"\"\"\n    width = box.width\n    height = box.height\n    min_width = box.min_width\n    min_height = box.min_height\n    max_width = max(min_width, box.max_width)\n    max_height = max(min_height, box.max_height)\n\n    # (violation_width, violation_height)\n    violations = (\n        'min' if width < min_width else 'max' if width > max_width else '',\n        'min' if height < min_height else 'max' if height > max_height else '')\n\n    # Work around divisions by zero. These are pathological cases anyway.\n    # TODO: is there a cleaner way?\n    if width == 0:\n        width = 1e-6\n    if height == 0:\n        height = 1e-6\n\n    # ('', ''): nothing to do\n    if violations == ('max', ''):\n        box.width = max_width\n        box.height = max(max_width * height / width, min_height)\n    elif violations == ('min', ''):\n        box.width = min_width\n        box.height = min(min_width * height / width, max_height)\n    elif violations == ('', 'max'):\n        box.width = max(max_height * width / height, min_width)\n        box.height = max_height\n    elif violations == ('', 'min'):\n        box.width = min(min_height * width / height, max_width)\n        box.height = min_height\n    elif violations == ('max', 'max'):\n        if max_width / width <= max_height / height:\n            box.width = max_width\n            box.height = max(min_height, max_width * height / width)\n        else:\n            box.width = max(min_width, max_height * width / height)\n            box.height = max_height\n    elif violations == ('min', 'min'):\n        if min_width / width <= min_height / height:\n            box.width = min(max_width, min_height * width / height)\n            box.height = min_height\n        else:\n            box.width = min_width\n            box.height = min(max_height, min_width * height / width)\n    elif violations == ('min', 'max'):\n        box.width = min_width\n        box.height = max_height\n    elif violations == ('max', 'min'):\n        box.width = max_width\n        box.height = min_height\n\n\ndef block_replaced_box_layout(context, box, containing_block):\n    \"\"\"Lay out the block :class:`boxes.ReplacedBox` ``box``.\"\"\"\n    from .block import block_level_width\n    from .float import avoid_collisions\n\n    box = box.copy()\n    if box.style['width'] == box.style['height'] == 'auto':\n        computed_margins = box.margin_left, box.margin_right\n        block_replaced_width.without_min_max(\n            box, containing_block)\n        replaced_box_height.without_min_max(box)\n        min_max_auto_replaced(box)\n        box.margin_left, box.margin_right = computed_margins\n        block_level_width.without_min_max(box, containing_block)\n    else:\n        block_replaced_width(box, containing_block)\n        replaced_box_height(box)\n\n    # TODO: flex items shouldn't be block boxes, this condition\n    # would then be useless when this is fixed.\n    if not box.is_flex_item:\n        # Don't collide with floats\n        # https://www.w3.org/TR/CSS21/visuren.html#floats\n        box.position_x, box.position_y, _ = avoid_collisions(\n            context, box, containing_block, outer=False)\n    resume_at = None\n    next_page = {'break': 'any', 'page': None}\n    adjoining_margins = []\n    collapsing_through = False\n    return box, resume_at, next_page, adjoining_margins, collapsing_through\n\n\n@handle_min_max_width\ndef block_replaced_width(box, containing_block):\n    from .block import block_level_width\n\n    # https://www.w3.org/TR/CSS21/visudet.html#block-replaced-width\n    replaced_box_width.without_min_max(box, containing_block)\n    block_level_width.without_min_max(box, containing_block)\n"
  },
  {
    "path": "weasyprint/layout/table.py",
    "content": "\"\"\"Layout for tables and internal table boxes.\"\"\"\n\nfrom math import inf\n\nimport tinycss2.color5\n\nfrom ..formatting_structure import boxes\nfrom ..logger import LOGGER\nfrom .percent import resolve_one_percentage, resolve_percentages\nfrom .preferred import table_and_columns_preferred_widths\n\n\ndef table_layout(context, table, bottom_space, skip_stack, containing_block,\n                 page_is_empty, absolute_boxes, fixed_boxes):\n    \"\"\"Layout for a table box.\"\"\"\n    from .block import (  # isort:skip\n        avoid_page_break, block_container_layout, block_level_page_break,\n        find_earlier_page_break, force_page_break, remove_placeholders)\n\n    # Remove top and bottom decorations for split tables.\n    has_header = table.children and table.children[0].is_header\n    has_footer = table.children and table.children[-1].is_footer\n    collapse = table.style['border_collapse'] == 'collapse'\n    remove_start_decoration = skip_stack is not None and not has_header\n    table.remove_decoration(remove_start_decoration, end=False)\n\n    # Set border spacings.\n    if collapse:\n        border_spacing_x = border_spacing_y = 0\n    else:\n        border_spacing_x, border_spacing_y = table.style['border_spacing']\n\n    # Define column positions.\n    column_widths = table.column_widths\n    column_positions = table.column_positions = []\n    rows_left_x = table.content_box_x() + border_spacing_x\n    if table.style['direction'] == 'ltr':\n        position_x = table.content_box_x()\n        rows_x = position_x + border_spacing_x\n        for width in column_widths:\n            position_x += border_spacing_x\n            column_positions.append(position_x)\n            position_x += width\n        rows_width = position_x - rows_x\n    else:\n        position_x = table.content_box_x() + table.width\n        rows_x = position_x - border_spacing_x\n        for width in column_widths:\n            position_x -= border_spacing_x\n            position_x -= width\n            column_positions.append(position_x)\n        rows_width = rows_x - position_x\n\n    # Set border top width on tables with collapsed borders and split cells.\n    if collapse:\n        table.skip_cell_border_top = False\n        table.skip_cell_border_bottom = False\n        split_cells = False\n        if skip_stack:\n            (skipped_groups, group_skip_stack), = skip_stack.items()\n            if group_skip_stack:\n                (skipped_rows, cells_skip_stack), = group_skip_stack.items()\n                if cells_skip_stack:\n                    split_cells = True\n            else:\n                skipped_rows = 0\n            for group in table.children[:skipped_groups]:\n                skipped_rows += len(group.children)\n        else:\n            skipped_rows = 0\n        if not split_cells and not has_header:\n            _, horizontal_borders = table.collapsed_border_grid\n            if horizontal_borders:\n                table.border_top_width = max(\n                    width for _, (_, width, _)\n                    in horizontal_borders[skipped_rows]) / 2\n\n    # Make this a sub-function so that many local variables like rows_x\n    # don't need to be passed as parameters.\n    def group_layout(group, position_y, bottom_space, page_is_empty, skip_stack):\n        resume_at = None\n        next_page = {'break': 'any', 'page': None}\n        original_page_is_empty = page_is_empty\n        resolve_percentages(group, containing_block=table)\n        group.position_x = rows_left_x\n        group.position_y = position_y\n        group.width = rows_width\n        new_group_children = []\n        # For each row, cells for which this is the last row (with rowspan).\n        ending_cells_by_row = [[] for row in group.children]\n\n        is_group_start = skip_stack is None\n        if is_group_start:\n            skip = 0\n        else:\n            (skip, skip_stack), = skip_stack.items()\n        for index_row, row in enumerate(group.children[skip:], start=skip):\n            row.index = index_row\n\n            if new_group_children:\n                page_break = block_level_page_break(\n                    new_group_children[-1], row)\n                if force_page_break(page_break, context):\n                    next_page['break'] = page_break\n                    resume_at = {index_row: None}\n                    break\n\n            resolve_percentages(row, containing_block=table)\n            row.position_x = rows_left_x\n            row.position_y = position_y\n            row.width = rows_width\n            # Place cells at the top of the row and layout their content.\n            new_row_children = []\n            for index_cell, cell in enumerate(row.children):\n                spanned_widths = column_widths[cell.grid_x:][:cell.colspan]\n                # In the fixed layout the grid width is set by cells in\n                # the first row and column elements.\n                # This may be less than the previous value of cell.colspan\n                # if that would bring the cell beyond the grid width.\n                cell.colspan = len(spanned_widths)\n                if cell.colspan == 0:\n                    # The cell is entierly beyond the grid width, remove it\n                    # entierly. Subsequent cells in the same row have greater\n                    # grid_x, so they are beyond too.\n                    cell_index = row.children.index(cell)\n                    ignored_cells = row.children[cell_index:]\n                    LOGGER.warning(\n                        'This table row has more columns than the table, '\n                        f'ignored {len(ignored_cells)} cells: {ignored_cells}')\n                    break\n                resolve_percentages(cell, containing_block=table)\n                if table.style['direction'] == 'ltr':\n                    cell.position_x = column_positions[cell.grid_x]\n                else:\n                    cell.position_x = column_positions[cell.grid_x + cell.colspan - 1]\n                cell.position_y = row.position_y\n                cell.margin_top = 0\n                cell.margin_left = 0\n                cell.width = 0\n                borders_plus_padding = cell.border_width()  # with width==0\n                # TODO: we should remove the number of columns with no\n                # originating cells to cell.colspan, see test_layout_table_auto_49.\n                cell.width = (\n                    sum(spanned_widths) +\n                    border_spacing_x * (cell.colspan - 1) -\n                    borders_plus_padding)\n                if skip_stack:\n                    if index_cell in skip_stack:\n                        cell_skip_stack = skip_stack[index_cell]\n                    else:\n                        cell_skip_stack = {len(cell.children): None}\n                else:\n                    cell_skip_stack = None\n\n                # Adapt cell and table collapsing borders when a row is split.\n                if cell_skip_stack and collapse:\n                    if has_header:\n                        # We have a header, we have to adapt the position of\n                        # the split cell to match the header’s bottom border.\n                        header_rows = table.children[0].children\n                        if header_rows and header_rows[-1].children:\n                            cell.position_y += max(\n                                header.border_bottom_width\n                                for header in header_rows[-1].children)\n                    else:\n                        # We don’t have a header, we have to skip the\n                        # decoration at the top of the table when it’s drawn.\n                        table.skip_cell_border_top = True\n\n                # First try to render content as if there was already something\n                # on the page to avoid hitting block_level_layout’s TODO. Then\n                # force to render something if the page is actually empty, or\n                # just draw an empty cell otherwise. See\n                # test_table_break_children_margin.\n                # Pretend that height is not set, keeping computed height as a minimum.\n                cell.computed_height = cell.height\n                cell.height = 'auto'\n                original_style = cell.style\n                if cell.style['height'] != 'auto':\n                    style_copy = cell.style.copy()\n                    style_copy['height'] = 'auto'\n                    cell.style = style_copy\n                new_cell, cell_resume_at, _, _, _, _ = block_container_layout(\n                    context, cell, bottom_space, cell_skip_stack,\n                    page_is_empty=page_is_empty, absolute_boxes=absolute_boxes,\n                    fixed_boxes=fixed_boxes, adjoining_margins=None,\n                    first_letter_style=None, first_line_style=None, discard=False,\n                    max_lines=None)\n                cell.style = original_style\n                if new_cell is None:\n                    cell = cell.copy_with_children([])\n                    cell, _, _, _, _, _ = block_container_layout(\n                        context, cell, bottom_space, cell_skip_stack,\n                        page_is_empty=True, absolute_boxes=[], fixed_boxes=[],\n                        adjoining_margins=None, first_letter_style=None,\n                        first_line_style=None, discard=False, max_lines=None)\n                    cell_resume_at = {0: None}\n                else:\n                    cell = new_cell\n\n                cell.remove_decoration(start=cell_skip_stack is not None, end=False)\n                if cell_resume_at:\n                    if resume_at is None:\n                        resume_at = {index_row: {}}\n                    resume_at[index_row][index_cell] = cell_resume_at\n                cell.empty = not any(\n                    child.is_floated() or child.is_in_normal_flow()\n                    for child in cell.children)\n                cell.content_height = cell.height\n                if cell.computed_height != 'auto':\n                    cell.height = max(cell.height, cell.computed_height)\n                new_row_children.append(cell)\n\n            if resume_at and not page_is_empty:\n                # Avoid break when \"break-inside: avoid\" is set on row or any\n                # on its cells.\n                avoid_break = (\n                    avoid_page_break(row.style['break_inside'], context) or any(\n                        avoid_page_break(cell.style['break_inside'], context)\n                        for cell in row.children))\n                if avoid_break:\n                    resume_at = {index_row: {}}\n                    remove_placeholders(\n                        context, new_row_children, absolute_boxes, fixed_boxes)\n                    break\n\n            if resume_at:\n                # Remove bottom decoration if row is split.\n                for cell in new_row_children:\n                    cell.remove_decoration(start=False, end=True)\n\n            row = row.copy_with_children(new_row_children)\n\n            # Table height algorithm\n            # https://www.w3.org/TR/CSS21/tables.html#height-layout\n\n            # Set row baseline with cells with vertical-align: baseline.\n            baseline_cells = []\n            for cell in row.children:\n                vertical_align = cell.style['vertical_align']\n                if vertical_align in ('top', 'middle', 'bottom'):\n                    cell.vertical_align = vertical_align\n                else:\n                    # Assume 'baseline' for any other value\n                    cell.vertical_align = 'baseline'\n                    cell.baseline = cell_baseline(cell)\n                    baseline_cells.append(cell)\n            if baseline_cells:\n                row.baseline = max(cell.baseline for cell in baseline_cells)\n                for cell in baseline_cells:\n                    extra = row.baseline - cell.baseline\n                    if cell.baseline != row.baseline and extra:\n                        add_top_padding(cell, extra)\n\n            # Set row height.\n            for cell in row.children:\n                ending_cells_by_row[cell.rowspan - 1].append(cell)\n            ending_cells = ending_cells_by_row.pop(0)\n            if ending_cells:  # in this row\n                if row.height == 'auto':\n                    row_bottom_y = max(\n                        cell.position_y + cell.border_height()\n                        for cell in ending_cells)\n                    row.height = max(row_bottom_y - row.position_y, 0)\n                else:\n                    row.height = max(row.height, max(\n                        row_cell.border_height() for row_cell in ending_cells))\n                    row_bottom_y = row.position_y + row.height\n            else:\n                row_bottom_y = row.position_y\n                row.height = 0\n\n            if not baseline_cells:\n                row.baseline = row_bottom_y\n\n            # Add extra padding to make the cells the same height as the row\n            # and honor vertical-align.\n            for cell in ending_cells:\n                cell_bottom_y = cell.position_y + cell.border_height()\n                extra = row_bottom_y - cell_bottom_y\n                if extra:\n                    if cell.vertical_align == 'bottom':\n                        add_top_padding(cell, extra)\n                    elif cell.vertical_align == 'middle':\n                        extra /= 2\n                        add_top_padding(cell, extra)\n                        cell.padding_bottom += extra\n                    else:\n                        cell.padding_bottom += extra\n                if cell.computed_height != 'auto':\n                    vertical_align_shift = 0\n                    if cell.vertical_align == 'middle':\n                        vertical_align_shift = (\n                            cell.computed_height - cell.content_height) / 2\n                    elif cell.vertical_align == 'bottom':\n                        vertical_align_shift = (\n                            cell.computed_height - cell.content_height)\n                    if vertical_align_shift > 0:\n                        for child in cell.children:\n                            child.translate(dy=vertical_align_shift)\n\n            next_position_y = row.position_y + row.height\n            if resume_at is None:\n                next_position_y += border_spacing_y\n\n            # Break if one cell was broken.\n            break_cell = False\n            if resume_at:\n                if all(child.empty for child in row.children):\n                    # No cell was displayed, give up row.\n                    next_position_y = inf\n                    page_is_empty = False\n                    resume_at = None\n                else:\n                    break_cell = True\n\n            # Break if this row overflows the page, unless there is no\n            # other content on the page.\n            overflow = context.overflows_page(bottom_space, next_position_y)\n            if not page_is_empty and overflow:\n                remove_placeholders(context, row.children, absolute_boxes, fixed_boxes)\n                if new_group_children:\n                    previous_row = new_group_children[-1]\n                    page_break = block_level_page_break(previous_row, row)\n                    if avoid_page_break(page_break, context):\n                        earlier_page_break = find_earlier_page_break(\n                            context, new_group_children, absolute_boxes, fixed_boxes)\n                        if earlier_page_break:\n                            new_group_children, resume_at = earlier_page_break\n                            break\n                    else:\n                        resume_at = {index_row: None}\n                        break\n                if original_page_is_empty:\n                    resume_at = {index_row: None}\n                else:\n                    return None, None, next_page\n                break\n\n            new_group_children.append(row)\n            position_y = next_position_y\n            page_is_empty = False\n            skip_stack = None\n\n            if break_cell and collapse and not has_footer:\n                table.skip_cell_border_bottom = True\n\n            if break_cell or resume_at:\n                break\n\n        # Do not keep the row group if we made a page break\n        # before any of its rows or with 'avoid'.\n        abort = (\n            resume_at and\n            not original_page_is_empty and (\n                avoid_page_break(group.style['break_inside'], context) or\n                not new_group_children))\n        if abort:\n            remove_placeholders(\n                context, new_group_children, absolute_boxes, fixed_boxes)\n            return None, None, next_page\n\n        group = group.copy_with_children(new_group_children)\n        group.remove_decoration(start=not is_group_start, end=resume_at is not None)\n\n        # Set missing baselines in a second loop because of rowspan.\n        for row in group.children:\n            if row.baseline is None:\n                if row.children:\n                    # Set baseline to lowest bottom content edge.\n                    row.baseline = max(\n                        cell.content_box_y() + cell.height\n                        for cell in row.children) - row.position_y\n                else:\n                    row.baseline = 0\n        group.height = position_y - group.position_y\n        if group.children:\n            # The last border spacing is outside of the group.\n            group.height -= border_spacing_y\n\n        return group, resume_at, next_page\n\n    def body_groups_layout(skip_stack, position_y, bottom_space, page_is_empty):\n        if skip_stack is None:\n            skip = 0\n        else:\n            (skip, skip_stack), = skip_stack.items()\n        new_table_children = []\n        resume_at = None\n        next_page = {'break': 'any', 'page': None}\n\n        for i, group in enumerate(table.children[skip:]):\n            if group.is_header or group.is_footer:\n                continue\n\n            # Index is useless for headers and footers, as we never want to\n            # break pages after the header or before the footer.\n            index_group = i + skip\n            group.index = index_group\n\n            if new_table_children:\n                page_break = block_level_page_break(new_table_children[-1], group)\n                if force_page_break(page_break, context):\n                    next_page['break'] = page_break\n                    resume_at = {index_group: None}\n                    break\n\n            new_group, resume_at, next_page = group_layout(\n                group, position_y, bottom_space, page_is_empty, skip_stack)\n            skip_stack = None\n\n            if new_group is None:\n                if new_table_children:\n                    previous_group = new_table_children[-1]\n                    page_break = block_level_page_break(previous_group, group)\n                    if avoid_page_break(page_break, context):\n                        earlier_page_break = find_earlier_page_break(\n                            context, new_table_children, absolute_boxes, fixed_boxes)\n                        if earlier_page_break is None:\n                            remove_placeholders(\n                                context, new_table_children, absolute_boxes,\n                                fixed_boxes)\n                            return None, None, next_page, position_y\n                        new_table_children, resume_at = earlier_page_break\n                        break\n                    resume_at = {index_group: None}\n                else:\n                    return None, None, next_page, position_y\n                break\n\n            new_table_children.append(new_group)\n            position_y += new_group.height + border_spacing_y\n            page_is_empty = False\n\n            if resume_at:\n                resume_at = {index_group: resume_at}\n                break\n\n        return new_table_children, resume_at, next_page, position_y\n\n    # Layout row groups, rows and cells.\n    position_y = table.content_box_y()\n    if skip_stack is None:\n        position_y += border_spacing_y\n    initial_position_y = position_y\n    table_rows = [\n        child for child in table.children\n        if not child.is_header and not child.is_footer]\n\n    def all_groups_layout():\n        # If the page is not empty, we try to render the header and the footer\n        # on it. If the table does not fit on the page, we try to render it on\n        # the next page.\n\n        # If the page is empty and the header and footer are too big, there\n        # are not rendered. If no row can be rendered because of the header and\n        # the footer, the header and/or the footer are not rendered.\n\n        if page_is_empty:\n            header_footer_bottom_space = bottom_space\n        else:\n            header_footer_bottom_space = -inf\n\n        if has_header:\n            header = table.children[0]\n            header, resume_at, next_page = group_layout(\n                header, position_y, header_footer_bottom_space,\n                skip_stack=None, page_is_empty=False)\n            if header and not resume_at:\n                header_height = header.height + border_spacing_y\n            else:\n                # Header too big for the page.\n                header = None\n        else:\n            header = None\n\n        if has_footer:\n            footer = table.children[-1]\n            footer, resume_at, next_page = group_layout(\n                footer, position_y, header_footer_bottom_space,\n                skip_stack=None, page_is_empty=False)\n            if footer and not resume_at:\n                footer_height = footer.height + border_spacing_y\n            else:\n                # Footer too big for the page.\n                footer = None\n        else:\n            footer = None\n\n        # Don't remove headers and footers if breaks are avoided in line groups\n        if skip_stack:\n            skip, = skip_stack\n        else:\n            skip = 0\n        avoid_breaks = False\n        for group in table.children[skip:]:\n            if not group.is_header and not group.is_footer:\n                avoid_breaks = avoid_page_break(group.style['break_inside'], context)\n                break\n\n        if header and footer:\n            # Try with both the header and footer.\n            new_table_children, resume_at, next_page, end_position_y = (\n                body_groups_layout(\n                    skip_stack, position_y + header_height,\n                    bottom_space + footer_height, page_is_empty=avoid_breaks))\n            if new_table_children or not table_rows or not page_is_empty:\n                footer.translate(dy=end_position_y - footer.position_y)\n                end_position_y += footer_height\n                return (\n                    header, new_table_children, footer, end_position_y, resume_at,\n                    next_page)\n            else:\n                # We could not fit any content, drop the footer.\n                footer = None\n\n        if header and not footer:\n            # Try with just the header.\n            new_table_children, resume_at, next_page, end_position_y = (\n                body_groups_layout(\n                    skip_stack, position_y + header_height, bottom_space,\n                    page_is_empty=avoid_breaks))\n            if new_table_children or not table_rows or not page_is_empty:\n                return (\n                    header, new_table_children, footer, end_position_y, resume_at,\n                    next_page)\n            else:\n                # We could not fit any content, drop the header.\n                header = None\n\n        if footer and not header:\n            # Try with just the footer.\n            new_table_children, resume_at, next_page, end_position_y = (\n                body_groups_layout(\n                    skip_stack, position_y, bottom_space + footer_height,\n                    page_is_empty=avoid_breaks))\n            if new_table_children or not table_rows or not page_is_empty:\n                footer.translate(dy=end_position_y - footer.position_y)\n                end_position_y += footer_height\n                return (\n                    header, new_table_children, footer, end_position_y, resume_at,\n                    next_page)\n            else:\n                # We could not fit any content, drop the footer.\n                footer = None\n\n        assert not header\n        assert not footer\n        new_table_children, resume_at, next_page, end_position_y = (\n            body_groups_layout(skip_stack, position_y, bottom_space, page_is_empty))\n        return header, new_table_children, footer, end_position_y, resume_at, next_page\n\n    def get_column_cells(table, column):\n        \"\"\"Return closure getting the column cells.\"\"\"\n        return lambda: [\n            cell\n            for row_group in table.children\n            for row in row_group.children\n            for cell in row.children\n            if cell.grid_x == column.grid_x]\n\n    header, new_table_children, footer, position_y, resume_at, next_page = (\n        all_groups_layout())\n\n    if new_table_children is None:\n        assert resume_at is None\n        table = None\n        adjoining_margins = []\n        collapsing_through = False\n        return table, resume_at, next_page, adjoining_margins, collapsing_through\n\n    table = table.copy_with_children(\n        ([header] if header is not None else []) +\n        new_table_children +\n        ([footer] if footer is not None else []))\n    table.column_groups = tuple(\n        column_group.deepcopy() for column_group in table.column_groups)\n    remove_end_decoration = resume_at is not None and not has_footer\n    table.remove_decoration(remove_start_decoration, remove_end_decoration)\n    if collapse:\n        table.skipped_rows = skipped_rows\n\n    # If the height property has a bigger value, just add blank space\n    # below the last row group.\n    table.height = max(\n        table.height if table.height != 'auto' else 0,\n        position_y - table.content_box_y())\n\n    # Layout column groups and columns.\n    columns_height = position_y - initial_position_y\n    if table.children:\n        # The last border spacing is below the columns.\n        columns_height -= border_spacing_y\n    for group in table.column_groups:\n        for column in group.children:\n            resolve_percentages(column, containing_block=table)\n            if column.grid_x < len(column_positions):\n                column.position_x = column_positions[column.grid_x]\n                column.position_y = initial_position_y\n                column.width = column_widths[column.grid_x]\n                column.height = columns_height\n            else:\n                # Ignore extra empty columns.\n                column.position_x = 0\n                column.position_y = 0\n                column.width = 0\n                column.height = 0\n            resolve_percentages(group, containing_block=table)\n            column.get_cells = get_column_cells(table, column)\n        first = group.children[0]\n        last = group.children[-1]\n        group.position_x = first.position_x\n        group.position_y = initial_position_y\n        group.width = last.position_x + last.width - first.position_x\n        group.height = columns_height\n\n    # Invert columns for drawing.\n    if table.style['direction'] == 'rtl':\n        column_widths.reverse()\n        column_positions.reverse()\n\n    avoid_break = avoid_page_break(table.style['break_inside'], context)\n    if resume_at and not page_is_empty and avoid_break:\n        remove_placeholders(context, [table], absolute_boxes, fixed_boxes)\n        table = None\n        resume_at = None\n    adjoining_margins = []\n    collapsing_through = False\n\n    return table, resume_at, next_page, adjoining_margins, collapsing_through\n\n\ndef add_top_padding(box, extra_padding):\n    \"\"\"Increase the top padding of a box.\n\n    This also translates the children.\n\n    \"\"\"\n    box.padding_top += extra_padding\n    for child in box.children:\n        child.translate(dy=extra_padding)\n\n\ndef fixed_table_layout(box):\n    \"\"\"Run the fixed table layout and return a list of column widths.\n\n    https://www.w3.org/TR/CSS21/tables.html#fixed-table-layout\n\n    \"\"\"\n    table = box.get_wrapped_table()\n    assert table.width != 'auto'\n\n    all_columns = [\n        column for column_group in table.column_groups\n        for column in column_group.children]\n    if table.children and table.children[0].children:\n        first_rowgroup = table.children[0]\n        first_row_cells = first_rowgroup.children[0].children\n    else:\n        first_row_cells = []\n    num_columns = max(len(all_columns), sum(cell.colspan for cell in first_row_cells))\n    # ``None`` means not know yet.\n    column_widths = [None] * num_columns\n\n    # Set width on column boxes.\n    for i, column in enumerate(all_columns):\n        resolve_one_percentage(column, 'width', table.width)\n        if column.width != 'auto':\n            column_widths[i] = column.width\n\n    if table.style['border_collapse'] == 'separate':\n        border_spacing_x, _ = table.style['border_spacing']\n    else:\n        border_spacing_x = 0\n\n    # Set width on cells of the first row.\n    i = 0\n    for cell in first_row_cells:\n        resolve_percentages(cell, table)\n        if cell.width != 'auto':\n            width = cell.border_width()\n            width -= border_spacing_x * (cell.colspan - 1)\n            # In the general case, this width affects several columns (through\n            # colspan) some of which already have a width. Subtract these\n            # known widths and divide among remaining columns.\n            columns_without_width = []  # and occupied by this cell\n            for j in range(i, i + cell.colspan):\n                if column_widths[j] is None:\n                    columns_without_width.append(j)\n                else:\n                    width -= column_widths[j]\n            if columns_without_width:\n                width_per_column = width / len(columns_without_width)\n                for j in columns_without_width:\n                    column_widths[j] = width_per_column\n        i += cell.colspan\n\n    # Distribute the remaining space equally on columns that do not have\n    # a width yet.\n    all_border_spacing = border_spacing_x * (num_columns + 1)\n    min_table_width = (sum(w for w in column_widths if w is not None) +\n                       all_border_spacing)\n    columns_without_width = [i for i, w in enumerate(column_widths)\n                             if w is None]\n    if columns_without_width and table.width >= min_table_width:\n        remaining_width = table.width - min_table_width\n        width_per_column = remaining_width / len(columns_without_width)\n        for i in columns_without_width:\n            column_widths[i] = width_per_column\n    else:\n        # This is bad, but we were given a broken table.\n        for i in columns_without_width:\n            column_widths[i] = 0\n\n    # If the sum is less than the table width, distribute the remaining space\n    # equally.\n    extra_width = table.width - sum(column_widths) - all_border_spacing\n    if extra_width <= 0:\n        # Substract a negative: widen the table.\n        table.width -= extra_width\n    elif num_columns:\n        extra_per_column = extra_width / num_columns\n        column_widths = [w + extra_per_column for w in column_widths]\n\n    # Now we have table.width == sum(column_widths) + all_border_spacing\n    # with possible floating point rounding errors (unless there is zero column).\n    table.column_widths = column_widths\n\n\ndef auto_table_layout(context, box, containing_block):\n    \"\"\"Run the auto table layout and return a list of column widths.\n\n    https://www.w3.org/TR/CSS21/tables.html#auto-table-layout\n\n    \"\"\"\n    table = box.get_wrapped_table()\n    (table_min_content_width, table_max_content_width,\n     column_min_content_widths, column_max_content_widths,\n     column_intrinsic_percentages, constrainedness,\n     total_horizontal_border_spacing, grid) = table_and_columns_preferred_widths(\n         context, box, outer=False)\n\n    margins = 0\n    if box.margin_left != 'auto':\n        margins += box.margin_left\n    if box.margin_right != 'auto':\n        margins += box.margin_right\n    paddings = table.padding_left + table.padding_right\n    borders = table.border_left_width + table.border_right_width\n\n    cb_width, _ = containing_block\n    available_width = cb_width - margins - paddings - borders\n\n    if table.width == 'auto':\n        if available_width <= table_min_content_width:\n            table.width = table_min_content_width\n        elif available_width < table_max_content_width:\n            table.width = available_width\n        else:\n            table.width = table_max_content_width\n    else:\n        if table.width < table_min_content_width:\n            table.width = table_min_content_width\n\n    if not grid:\n        table.column_widths = []\n        return\n\n    assignable_width = table.width - total_horizontal_border_spacing\n    min_content_guess = column_min_content_widths[:]\n    min_content_percentage_guess = column_min_content_widths[:]\n    min_content_specified_guess = column_min_content_widths[:]\n    max_content_guess = column_max_content_widths[:]\n    guesses = (\n        min_content_guess, min_content_percentage_guess,\n        min_content_specified_guess, max_content_guess)\n    # See https://www.w3.org/TR/css-tables-3/#width-distribution-algorithm.\n    for i in range(len(grid)):\n        if column_intrinsic_percentages[i]:\n            min_content_percentage_guess[i] = max(\n                column_intrinsic_percentages[i] / 100 * assignable_width,\n                column_min_content_widths[i])\n            min_content_specified_guess[i] = min_content_percentage_guess[i]\n            max_content_guess[i] = min_content_percentage_guess[i]\n        elif constrainedness[i]:\n            # Any other column that is constrained is assigned its max-content\n            # width.\n            min_content_specified_guess[i] = column_max_content_widths[i]\n\n    if assignable_width < sum(max_content_guess):\n        # Default values shouldn't be used, but we never know.\n        # See issue #770.\n        lower_guess = guesses[0]\n        upper_guess = guesses[-1]\n\n        # We have to work around floating point rounding errors here.\n        # The 1e-9 value comes from PEP 485.\n        for guess in guesses:\n            if sum(guess) <= assignable_width * (1 + 1e-9):\n                lower_guess = guess\n            else:\n                break\n        for guess in guesses[::-1]:\n            if sum(guess) >= assignable_width * (1 - 1e-9):\n                upper_guess = guess\n            else:\n                break\n        if upper_guess == lower_guess:\n            table.column_widths = upper_guess\n        else:\n            added_widths = [\n                upper_guess[i] - lower_guess[i] for i in range(len(grid))]\n            available_ratio = (assignable_width - sum(lower_guess)) / sum(added_widths)\n            table.column_widths = [\n                lower_guess[i] + added_widths[i] * available_ratio\n                for i in range(len(grid))]\n    else:\n        table.column_widths = max_content_guess\n        excess_width = assignable_width - sum(max_content_guess)\n        distribute_excess_width(\n            context, grid, excess_width, table.column_widths, constrainedness,\n            column_intrinsic_percentages, column_max_content_widths)\n\n\ndef table_wrapper_width(context, wrapper, containing_block):\n    \"\"\"Find the width of each column and derive the wrapper width.\"\"\"\n    table = wrapper.get_wrapped_table()\n    resolve_percentages(table, containing_block)\n\n    if table.style['table_layout'] == 'fixed' and table.width != 'auto':\n        fixed_table_layout(wrapper)\n    else:\n        auto_table_layout(context, wrapper, containing_block)\n\n    wrapper.width = table.border_width()\n\n\ndef cell_baseline(cell):\n    \"\"\"Return the y position of a cell baseline from the top of its border box.\n\n    See https://www.w3.org/TR/CSS21/tables.html#height-layout\n\n    \"\"\"\n    baseline_types = (boxes.LineBox, boxes.TableRowBox)\n    result = find_in_flow_baseline(cell, baseline_types=baseline_types)\n    if result is not None:\n        return result - cell.position_y\n    else:\n        # Default to the bottom of the content area.\n        return cell.border_top_width + cell.padding_top + cell.height\n\n\ndef find_in_flow_baseline(box, last=False, baseline_types=(boxes.LineBox,)):\n    \"\"\"Return the absolute y position for the first (or last) in-flow baseline.\n\n    If there’s no in-flow baseline, return None.\n\n    \"\"\"\n    # TODO: synthetize baseline when needed.\n    # See https://www.w3.org/TR/css-align-3/#synthesize-baseline.\n    if isinstance(box, baseline_types):\n        return box.position_y + box.baseline\n    elif isinstance(box, boxes.TableCaptionBox):\n        return\n    children = reversed(box.children) if last else box.children\n    for child in children:\n        if child.is_in_normal_flow():\n            result = find_in_flow_baseline(child, last, baseline_types)\n            if result is not None:\n                return result\n\n\ndef distribute_excess_width(context, grid, excess_width, column_widths, constrainedness,\n                            column_intrinsic_percentages, column_max_content_widths,\n                            column_slice=slice(0, None)):\n    \"\"\"Distribute available width to columns.\n\n    See https://www.w3.org/TR/css-tables-3/#distributing-width-to-columns\n\n    \"\"\"\n    # First group.\n    columns = [\n        i for i, _ in enumerate(grid[column_slice], start=column_slice.start)\n        if not constrainedness[i] and\n        column_intrinsic_percentages[i] == 0 and\n        column_max_content_widths[i] > 0]\n    if columns:\n        sum_max_content_widths = sum(column_max_content_widths[i] for i in columns)\n        ratio = excess_width / sum_max_content_widths\n        for i in columns:\n            column_widths[i] += column_max_content_widths[i] * ratio\n        return\n\n    # Second group.\n    columns = [\n        i for i, _ in enumerate(grid[column_slice], start=column_slice.start)\n        if not constrainedness[i] and column_intrinsic_percentages[i] == 0]\n    if columns:\n        for i in columns:\n            column_widths[i] += excess_width / len(columns)\n        return\n\n    # Third group.\n    columns = [\n        i for i, _ in enumerate(grid[column_slice], start=column_slice.start)\n        if constrainedness[i] and\n        column_intrinsic_percentages[i] == 0 and\n        column_max_content_widths[i] > 0]\n    if columns:\n        sum_max_content_widths = sum(column_max_content_widths[i] for i in columns)\n        ratio = excess_width / sum_max_content_widths\n        for i in columns:\n            column_widths[i] += column_max_content_widths[i] * ratio\n        return\n\n    # Fourth group.\n    columns = [\n        i for i, _ in enumerate(grid[column_slice], start=column_slice.start)\n        if column_intrinsic_percentages[i] > 0 and column_max_content_widths[i] > 0]\n    if columns:\n        sum_intrinsic_percentages = sum(\n            column_intrinsic_percentages[i] for i in columns)\n        ratio = excess_width / sum_intrinsic_percentages\n        for i in columns:\n            column_widths[i] += column_intrinsic_percentages[i] * ratio\n        return\n\n    # Fifth group.\n    columns = [\n        i for i, column in enumerate(grid[column_slice], start=column_slice.start)\n        if column]\n    if columns:\n        for i in columns:\n            column_widths[i] += excess_width / len(columns)\n        return\n\n    # Sixth group.\n    columns = [i for i, _ in enumerate(grid[column_slice], start=column_slice.start)]\n    for i in columns:\n        column_widths[i] += excess_width / len(columns)\n\n\nTRANSPARENT = tinycss2.color5.parse_color('transparent')\n\n\ndef collapse_table_borders(table, grid_width, grid_height):\n    \"\"\"Resolve border conflicts for a table in the collapsing border model.\n\n    Take a :class:`TableBox`; set appropriate border widths on the table,\n    column group, column, row group, row, and cell boxes; and return\n    a data structure for the resolved collapsed border grid.\n\n    \"\"\"\n    if not (grid_width and grid_height):\n        # Don’t bother with empty tables.\n        return [], []\n\n    styles = reversed([\n        'hidden', 'double', 'solid', 'dashed', 'dotted', 'ridge', 'outset',\n        'groove', 'inset', 'none'])\n    style_scores = {style: score for score, style in enumerate(styles)}\n    style_map = {'inset': 'ridge', 'outset': 'groove'}\n    weak_null_border = ((0, 0, style_scores['none']), ('none', 0, TRANSPARENT))\n\n    # Borders are always stored left to right, top to bottom.\n    vertical_borders = [\n        [weak_null_border] * (grid_width + 1) for _ in range(grid_height)]\n    horizontal_borders = [\n        [weak_null_border] * grid_width for _ in range(grid_height + 1)]\n\n    def set_one_border(border_grid, box_style, side, grid_x, grid_y):\n        from ..draw.color import get_color\n\n        style = box_style[f'border_{side}_style']\n        width = box_style[f'border_{side}_width']\n        color = get_color(box_style, f'border_{side}_color')\n\n        # See https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution.\n        score = ((1 if style == 'hidden' else 0), width, style_scores[style])\n\n        style = style_map.get(style, style)\n        previous_score, _ = border_grid[grid_y][grid_x]\n        # Strict < so that the earlier call wins in case of a tie.\n        if previous_score < score:\n            border_grid[grid_y][grid_x] = (score, (style, width, color))\n\n    def set_borders(box, x, y, w, h):\n        style = box.style\n\n        # x and y are logical (possibly rtl), but borders are graphical (always ltr).\n        if table.style['direction'] == 'ltr':\n            for yy in range(y, y + h):\n                set_one_border(vertical_borders, style, 'left', x, yy)\n                set_one_border(vertical_borders, style, 'right', x + w, yy)\n            for xx in range(x, x + w):\n                set_one_border(horizontal_borders, style, 'top', xx, y)\n                set_one_border(horizontal_borders, style, 'bottom', xx, y + h)\n        else:\n            for yy in range(y, y + h):\n                set_one_border(vertical_borders, style, 'left', -1 - w - x, yy)\n                set_one_border(vertical_borders, style, 'right', -1 - x, yy)\n            for xx in range(-1 - x, -1 - x - w, -1):\n                set_one_border(horizontal_borders, style, 'top', xx, y)\n                set_one_border(horizontal_borders, style, 'bottom', xx, y + h)\n\n    # Set cell borders. The order is important here:\n    # \"A style set on a cell wins over one on a row, which wins over a\n    #  row group, column, column group and, lastly, table\"\n    # See https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution.\n    strong_null_border = ((1, 0, style_scores['hidden']), ('hidden', 0, TRANSPARENT))\n    grid_y = 0\n    for row_group in table.children:\n        for row in row_group.children:\n            for cell in row.children:\n                # Force null border inside of a cell with rowspan or colspan.\n                grid_x, colspan, rowspan = cell.grid_x, cell.colspan, cell.rowspan\n                if table.style['direction'] == 'ltr':\n                    vertical_x_range = range(grid_x + 1, grid_x + colspan)\n                    horizontal_x_range = range(grid_x, grid_x + colspan)\n                else:\n                    vertical_x_range = range(-2 - grid_x, -1 - grid_x - colspan, -1)\n                    horizontal_x_range = range(-1 - grid_x, -1 - grid_x - colspan, -1)\n                for xx in vertical_x_range:\n                    for yy in range(grid_y, grid_y + rowspan):\n                        vertical_borders[yy][xx] = strong_null_border\n                for xx in horizontal_x_range:\n                    for yy in range(grid_y + 1, grid_y + rowspan):\n                        horizontal_borders[yy][xx] = strong_null_border\n                # Set cell border.\n                set_borders(cell, grid_x, grid_y, colspan, rowspan)\n            grid_y += 1\n\n    # Set row borders.\n    grid_y = 0\n    for row_group in table.children:\n        for row in row_group.children:\n            set_borders(row, 0, grid_y, grid_width, 1)\n            grid_y += 1\n\n    # Set row group borders.\n    grid_y = 0\n    for row_group in table.children:\n        rowspan = len(row_group.children)\n        set_borders(row_group, 0, grid_y, grid_width, rowspan)\n        grid_y += rowspan\n\n    # Set column borders.\n    for column_group in table.column_groups:\n        for column in column_group.children:\n            set_borders(column, column.grid_x, 0, 1, grid_height)\n\n    # Set column group group borders.\n    for column_group in table.column_groups:\n        set_borders(\n            column_group, column_group.grid_x, 0, column_group.span, grid_height)\n\n    # Set table borders.\n    set_borders(table, 0, 0, grid_width, grid_height)\n\n    # Now that all conflicts are resolved, set transparent borders of the\n    # correct widths on each box. The actual border grid will be painted\n    # separately.\n    def set_border_used_width(box, side, twice_width):\n        prop = f'border_{side}_width'\n        setattr(box, prop, twice_width / 2)\n\n    def remove_borders(box):\n        set_border_used_width(box, 'top', 0)\n        set_border_used_width(box, 'right', 0)\n        set_border_used_width(box, 'bottom', 0)\n        set_border_used_width(box, 'left', 0)\n\n    def max_vertical_width(x, y1, y2):\n        return max(grid_row[x][1][1] for grid_row in vertical_borders[y1:y2])\n\n    def max_horizontal_width(x1, y, x2):\n        return max(width for _, (_, width, _) in horizontal_borders[y][x1:x2])\n\n    grid_y = 0\n    for row_group in table.children:\n        remove_borders(row_group)\n        for row in row_group.children:\n            remove_borders(row)\n            for cell in row.children:\n                x, y = cell.grid_x, grid_y\n                colspan, rowspan = cell.colspan, cell.rowspan\n                if table.style['direction'] == 'ltr':\n                    top = max_horizontal_width(x, y, x + colspan)\n                    bottom = max_horizontal_width(x, y + rowspan, x + colspan)\n                    left = max_vertical_width(x, y, y + rowspan)\n                    right = max_vertical_width(x + colspan, y, y + rowspan)\n                else:\n                    top = max_horizontal_width(-colspan - x, y, -x or None)\n                    bottom = max_horizontal_width(-colspan - x, y + rowspan, -x or None)\n                    left = max_vertical_width(-1 - colspan - x, y, y + rowspan)\n                    right = max_vertical_width(-1 - x, y, y + rowspan)\n                set_border_used_width(cell, 'top', top)\n                set_border_used_width(cell, 'bottom', bottom)\n                set_border_used_width(cell, 'left', left)\n                set_border_used_width(cell, 'right', right)\n            grid_y += 1\n\n    for column_group in table.column_groups:\n        remove_borders(column_group)\n        for column in column_group.children:\n            remove_borders(column)\n\n    set_border_used_width(table, 'top', max_horizontal_width(0, 0, grid_width))\n    set_border_used_width(\n        table, 'bottom', max_horizontal_width(0, grid_height, grid_width))\n    # \"UAs must compute an initial left and right border width for the table\n    #  by examining the first and last cells in the first row of the table.\"\n    # https://www.w3.org/TR/CSS21/tables.html#collapsing-borders\n    # ... so h=1, not grid_height:\n    set_border_used_width(table, 'left', max_vertical_width(0, 0, 1))\n    set_border_used_width(table, 'right', max_vertical_width(grid_width, 0, 1))\n\n    return vertical_borders, horizontal_borders\n"
  },
  {
    "path": "weasyprint/logger.py",
    "content": "\"\"\"Logging setup.\n\nThe rest of the code gets the logger through this module rather than\n``logging.getLogger`` to make sure that it is configured.\n\nLogging levels are used for specific purposes:\n\n- errors are used in ``LOGGER`` for unreachable or unusable external resources,\n  including unreachable stylesheets, unreachables images and unreadable images;\n- warnings are used in ``LOGGER`` for unknown or bad HTML/CSS syntaxes,\n  unreachable local fonts and various non-fatal problems;\n- infos are used in ``PROCESS_LOGGER`` to advertise rendering steps.\n\n\"\"\"\n\nimport contextlib\nimport logging\n\nLOGGER = logging.getLogger('weasyprint')\nif not LOGGER.handlers:  # pragma: no cover\n    LOGGER.setLevel(logging.WARNING)\n    LOGGER.addHandler(logging.NullHandler())\n\nPROGRESS_LOGGER = logging.getLogger('weasyprint.progress')\n\n\nclass CallbackHandler(logging.Handler):\n    \"\"\"A logging handler that calls a function for every message.\"\"\"\n    def __init__(self, callback):\n        logging.Handler.__init__(self)\n        self.emit = callback\n\n\n@contextlib.contextmanager\ndef capture_logs(logger='weasyprint', level=None):\n    \"\"\"Return a context manager that captures all logged messages.\"\"\"\n    if level is None:\n        level = logging.INFO\n    logger = logging.getLogger(logger)\n    messages = []\n\n    def emit(record):\n        if record.name == 'weasyprint.progress':\n            return\n        if record.levelno < level:\n            return\n        messages.append(f'{record.levelname.upper()}: {record.getMessage()}')\n\n    previous_handlers = logger.handlers\n    previous_level = logger.level\n    logger.handlers = []\n    logger.addHandler(CallbackHandler(emit))\n    logger.setLevel(logging.DEBUG)\n    try:\n        yield messages\n    finally:\n        logger.handlers = previous_handlers\n        logger.setLevel(previous_level)\n"
  },
  {
    "path": "weasyprint/matrix.py",
    "content": "\"\"\"Transformation matrix.\"\"\"\n\n\nclass Matrix(list):\n    def __init__(self, a=1, b=0, c=0, d=1, e=0, f=0, matrix=None):\n        if matrix is None:\n            matrix = [[a, b, 0], [c, d, 0], [e, f, 1]]\n        super().__init__(matrix)\n\n    def __matmul__(self, other):\n        assert len(self[0]) == len(other) == len(other[0]) == 3\n        return Matrix(matrix=[\n            [sum(self[i][k] * other[k][j] for k in range(3)) for j in range(3)]\n            for i in range(len(self))])\n\n    @property\n    def invert(self):\n        d = self.determinant\n        return Matrix(matrix=[\n            [\n                (self[1][1] * self[2][2] - self[1][2] * self[2][1]) / d,\n                (self[0][1] * self[2][2] - self[0][2] * self[2][1]) / -d,\n                (self[0][1] * self[1][2] - self[0][2] * self[1][1]) / d,\n            ],\n            [\n                (self[1][0] * self[2][2] - self[1][2] * self[2][0]) / -d,\n                (self[0][0] * self[2][2] - self[0][2] * self[2][0]) / d,\n                (self[0][0] * self[1][2] - self[0][2] * self[1][0]) / -d,\n            ],\n            [\n                (self[1][0] * self[2][1] - self[1][1] * self[2][0]) / d,\n                (self[0][0] * self[2][1] - self[0][1] * self[2][0]) / -d,\n                (self[0][0] * self[1][1] - self[0][1] * self[1][0]) / d,\n            ],\n        ])\n\n    @property\n    def determinant(self):\n        assert len(self) == len(self[0]) == 3\n        return (\n            self[0][0] * (self[1][1] * self[2][2] - self[1][2] * self[2][1]) -\n            self[1][0] * (self[0][1] * self[2][2] - self[0][2] * self[2][1]) +\n            self[2][0] * (self[0][1] * self[1][2] - self[0][2] * self[1][1]))\n\n    def transform_point(self, x, y):\n        return (Matrix(matrix=[[x, y, 1]]) @ self)[0][:2]\n\n    @property\n    def values(self):\n        (a, b), (c, d), (e, f) = [column[:2] for column in self]\n        return a, b, c, d, e, f\n"
  },
  {
    "path": "weasyprint/pdf/__init__.py",
    "content": "\"\"\"PDF generation management.\"\"\"\n\nfrom importlib.resources import files\n\nimport pydyf\nfrom tinycss2.color5 import D50, D65\n\nfrom .. import VERSION, Attachment\nfrom ..html import W3C_DATE_RE\nfrom ..logger import LOGGER, PROGRESS_LOGGER\nfrom ..matrix import Matrix\nfrom ..urls import select_source\nfrom . import debug, pdfa, pdfua, pdfx\nfrom .fonts import build_fonts_dictionary\nfrom .stream import Stream\nfrom .tags import add_tags\n\nfrom .anchors import (  # isort:skip\n    add_annotations, add_forms, add_links, add_outlines, resolve_links,\n    write_pdf_attachment)\n\nVARIANTS = {\n    name: data\n    for variants in (pdfa.VARIANTS, pdfua.VARIANTS, pdfx.VARIANTS, debug.VARIANTS)\n    for (name, data) in variants.items()}\n\n\ndef _w3c_date_to_pdf(string, attr_name):\n    \"\"\"Tranform W3C date to PDF format.\"\"\"\n    if string is None:\n        return None\n    match = W3C_DATE_RE.match(string)\n    if match is None:\n        LOGGER.warning(f'Invalid {attr_name} date: {string!r}')\n        return None\n    groups = match.groupdict()\n    pdf_date = ''\n    found = groups['hour']\n    for key in ('second', 'minute', 'hour', 'day', 'month', 'year'):\n        if groups[key]:\n            found = True\n            pdf_date = groups[key] + pdf_date\n        elif found:\n            pdf_date = f'{(key in (\"day\", \"month\")):02d}{pdf_date}'\n    if groups['hour']:\n        assert groups['minute']\n        if groups['tz_hour']:\n            assert groups['tz_hour'].startswith(('+', '-'))\n            assert groups['tz_minute']\n            tz_hour = int(groups['tz_hour'])\n            tz_minute = int(groups['tz_minute'])\n            pdf_date += f\"{tz_hour:+03d}'{tz_minute:02d}\"\n        else:\n            pdf_date += 'Z'\n    return f'D:{pdf_date}'\n\n\ndef _reference_resources(pdf, resources, images, fonts, color_profiles):\n    if 'Font' in resources:\n        assert resources['Font'] is None\n        resources['Font'] = fonts\n    _use_references(pdf, resources, images, color_profiles)\n    pdf.add_object(resources)\n    return resources.reference\n\n\ndef _use_references(pdf, resources, images, color_profiles):\n    # XObjects\n    for key, x_object in resources.get('XObject', {}).items():\n        # Images\n        if x_object is None:\n            image_data = images[key]\n            x_object = image_data['x_object']\n\n            if x_object is not None:\n                # Image already added to PDF\n                resources['XObject'][key] = x_object.reference\n                continue\n\n            image = image_data['image']\n            dpi_ratio = max(image_data['dpi_ratios'])\n            x_object = image.get_x_object(image_data['interpolate'], dpi_ratio)\n            image_data['x_object'] = x_object\n\n        pdf.add_object(x_object)\n        resources['XObject'][key] = x_object.reference\n\n        # Masks\n        if 'SMask' in x_object.extra:\n            pdf.add_object(x_object.extra['SMask'])\n            x_object.extra['SMask'] = x_object.extra['SMask'].reference\n\n        # Resources\n        if 'Resources' in x_object.extra:\n            x_object.extra['Resources'] = _reference_resources(\n                pdf, x_object.extra['Resources'], images, resources['Font'],\n                color_profiles)\n\n    # Patterns\n    for key, pattern in resources.get('Pattern', {}).items():\n        pdf.add_object(pattern)\n        resources['Pattern'][key] = pattern.reference\n        if 'Resources' in pattern.extra:\n            pattern.extra['Resources'] = _reference_resources(\n                pdf, pattern.extra['Resources'], images, resources['Font'],\n                color_profiles)\n\n    # Shadings\n    for key, shading in resources.get('Shading', {}).items():\n        pdf.add_object(shading)\n        resources['Shading'][key] = shading.reference\n\n    # Alpha states\n    for key, alpha in resources.get('ExtGState', {}).items():\n        if 'SMask' in alpha and 'G' in alpha['SMask']:\n            alpha['SMask']['G'] = alpha['SMask']['G'].reference\n\n\ndef generate_pdf(document, target, zoom, **options):\n    # 0.75 = 72 PDF point per inch / 96 CSS pixel per inch\n    scale = zoom * 0.75\n\n    PROGRESS_LOGGER.info('Step 6 - Creating PDF')\n\n    compress = not options['uncompressed_pdf']\n\n    # Set properties according to PDF variants\n    srgb = options['srgb']\n    pdf_tags = options['pdf_tags']\n    variant = options['pdf_variant']\n    if variant:\n        variant_function, properties = VARIANTS[variant]\n        if 'srgb' in properties:\n            srgb = properties['srgb']\n        if 'pdf_tags' in properties:\n            pdf_tags = properties['pdf_tags']\n\n    pdf = pydyf.PDF()\n    images = {}\n    color_space = pydyf.Dictionary({\n        'lab-d50': pydyf.Array(('/Lab', pydyf.Dictionary({\n            'WhitePoint': pydyf.Array(D50),\n            'Range': pydyf.Array((-125, 125, -125, 125)),\n        }))),\n        'lab-d65': pydyf.Array(('/Lab', pydyf.Dictionary({\n            'WhitePoint': pydyf.Array(D65),\n            'Range': pydyf.Array((-125, 125, -125, 125)),\n        }))),\n    })\n    # Custom color profiles\n    for key, color_profile in document.color_profiles.items():\n        if key == 'device-cmyk':\n            # Device CMYK profile is stored as OutputIntent.\n            continue\n        profile = pydyf.Stream(\n            [color_profile.content],\n            pydyf.Dictionary({'N': len(color_profile.components)}),\n            compress=compress)\n        pdf.add_object(profile)\n        color_space[key] = pydyf.Array(('/ICCBased', profile.reference))\n    pdf.add_object(color_space)\n    resources = pydyf.Dictionary({\n        'ExtGState': pydyf.Dictionary(),\n        'XObject': pydyf.Dictionary(),\n        'Pattern': pydyf.Dictionary(),\n        'Shading': pydyf.Dictionary(),\n        'ColorSpace': color_space.reference,\n    })\n    pdf.add_object(resources)\n    pdf_names = []\n\n    # Links and anchors\n    page_links_and_anchors = list(resolve_links(document.pages))\n\n    annot_files = {}\n    pdf_pages, page_streams = [], []\n    for page_number, (page, links_and_anchors) in enumerate(\n            zip(document.pages, page_links_and_anchors)):\n        tags = {} if pdf_tags else None\n\n        # Draw from the top-left corner\n        matrix = Matrix(scale, 0, 0, -scale, 0, page.height * scale)\n\n        page_width = scale * (\n            page.width + page.bleed['left'] + page.bleed['right'])\n        page_height = scale * (\n            page.height + page.bleed['top'] + page.bleed['bottom'])\n        left = -scale * page.bleed['left']\n        top = -scale * page.bleed['top']\n        right = left + page_width\n        bottom = top + page_height\n\n        page_rectangle = (\n            left / scale, top / scale,\n            (right - left) / scale, (bottom - top) / scale)\n        stream = Stream(\n            document.fonts, page_rectangle, resources, images, tags,\n            document.color_profiles, compress=compress)\n        stream.transform(d=-1, f=(page.height * scale))\n        pdf.add_object(stream)\n        page_streams.append(stream)\n\n        pdf_page = pydyf.Dictionary({\n            'Type': '/Page',\n            'Parent': pdf.pages.reference,\n            'MediaBox': pydyf.Array([left, top, right, bottom]),\n            'Contents': stream.reference,\n            'Resources': resources.reference,\n        })\n        if pdf_tags:\n            pdf_page['Tabs'] = '/S'\n            pdf_page['StructParents'] = page_number\n        pdf.add_page(pdf_page)\n        pdf_pages.append(pdf_page)\n\n        add_links(links_and_anchors, matrix, pdf, pdf_page, pdf_names, tags)\n        add_annotations(\n            links_and_anchors[0], matrix, document, pdf, pdf_page, annot_files,\n            compress)\n        add_forms(\n            page.forms, matrix, pdf, pdf_page, resources, stream,\n            document.font_config.font_map)\n        page.paint(stream, scale)\n\n        # Bleed\n        bleed = {key: value * 0.75 for key, value in page.bleed.items()}\n\n        trim_left = left + bleed['left']\n        trim_top = top + bleed['top']\n        trim_right = right - bleed['right']\n        trim_bottom = bottom - bleed['bottom']\n\n        # Arbitrarly set PDF BleedBox between CSS bleed box (MediaBox) and\n        # CSS page box (TrimBox) at most 10 points from the TrimBox.\n        bleed_left = trim_left - min(10, bleed['left'])\n        bleed_top = trim_top - min(10, bleed['top'])\n        bleed_right = trim_right + min(10, bleed['right'])\n        bleed_bottom = trim_bottom + min(10, bleed['bottom'])\n\n        pdf_page['TrimBox'] = pydyf.Array([\n            trim_left, trim_top, trim_right, trim_bottom])\n        pdf_page['BleedBox'] = pydyf.Array([\n            bleed_left, bleed_top, bleed_right, bleed_bottom])\n\n    # Outlines\n    add_outlines(pdf, document.make_bookmark_tree(scale, transform_pages=True))\n\n    PROGRESS_LOGGER.info('Step 7 - Adding PDF metadata')\n\n    # PDF information\n    pdf.info['Producer'] = pydyf.String(f'WeasyPrint {VERSION}')\n    metadata = document.metadata\n    if metadata.title:\n        pdf.info['Title'] = pydyf.String(metadata.title)\n    if metadata.authors:\n        pdf.info['Author'] = pydyf.String(', '.join(metadata.authors))\n    if metadata.description:\n        pdf.info['Subject'] = pydyf.String(metadata.description)\n    if metadata.keywords:\n        pdf.info['Keywords'] = pydyf.String(', '.join(metadata.keywords))\n    if metadata.generator:\n        pdf.info['Creator'] = pydyf.String(metadata.generator)\n    if metadata.created:\n        pdf.info['CreationDate'] = pydyf.String(\n            _w3c_date_to_pdf(metadata.created, 'created'))\n    if metadata.modified:\n        pdf.info['ModDate'] = pydyf.String(\n            _w3c_date_to_pdf(metadata.modified, 'modified'))\n    if metadata.lang:\n        pdf.catalog['Lang'] = pydyf.String(metadata.lang)\n    if options['custom_metadata']:\n        for key, value in metadata.custom.items():\n            key = ''.join(char for char in key if char.isalnum())\n            key = key.encode('ascii', errors='ignore').decode()\n            if key:\n                pdf.info[key] = pydyf.String(value)\n    if options['xmp_metadata']:\n        for url in options['xmp_metadata']:\n            result = select_source(url)\n            with result as (file_obj, base_url, charset, _):\n                xmp_metadata = file_obj.read()\n                if charset:\n                    xmp_metadata = xmp_metadata.decode(charset).encode()\n                metadata.xmp_metadata.append(xmp_metadata)\n\n    # Embedded files\n    attachments = metadata.attachments.copy()\n    if options['attachments']:\n        relationships = iter(options['attachment_relationships'] or [])\n        for attachment in options['attachments']:\n            if not isinstance(attachment, Attachment):\n                attachment = Attachment(\n                    attachment, url_fetcher=document.url_fetcher,\n                    relationship=next(relationships, 'Unspecified'))\n            attachments.append(attachment)\n    pdf_attachments = []\n    for attachment in attachments:\n        pdf_attachment = write_pdf_attachment(pdf, attachment, compress)\n        if pdf_attachment is not None:\n            pdf_attachments.append(pdf_attachment)\n    if pdf_attachments:\n        content = pydyf.Dictionary({'Names': pydyf.Array()})\n        for i, pdf_attachment in enumerate(pdf_attachments):\n            content['Names'].append(pdf_attachment['F'])\n            content['Names'].append(pdf_attachment.reference)\n        pdf.add_object(content)\n        if 'Names' not in pdf.catalog:\n            pdf.catalog['Names'] = pydyf.Dictionary()\n        pdf.catalog['Names']['EmbeddedFiles'] = content.reference\n\n    # Embedded fonts\n    subset = not options['full_fonts']\n    pdf_fonts = build_fonts_dictionary(\n        pdf, document.fonts, compress, subset, options)\n    pdf.add_object(pdf_fonts)\n    if 'AcroForm' in pdf.catalog:\n        # Include Dingbats for forms\n        dingbats = pydyf.Dictionary({\n            'Type': '/Font',\n            'Subtype': '/Type1',\n            'BaseFont': '/ZapfDingbats',\n        })\n        pdf.add_object(dingbats)\n        pdf_fonts['ZaDb'] = dingbats.reference\n    resources['Font'] = pdf_fonts.reference\n    _use_references(pdf, resources, images, document.color_profiles)\n\n    # Anchors\n    if pdf_names:\n        # Anchors are name trees that have to be sorted\n        name_array = pydyf.Array()\n        for anchor in sorted(pdf_names):\n            name_array.append(pydyf.String(anchor[0]))\n            name_array.append(anchor[1])\n        dests = pydyf.Dictionary({'Names': name_array})\n        if 'Names' not in pdf.catalog:\n            pdf.catalog['Names'] = pydyf.Dictionary()\n        pdf.catalog['Names']['Dests'] = dests\n\n    # Add output ICC profile.\n    # TODO: we should allow the user or the PDF variant code to set custom values in\n    # OutputIntents and remove the \"srgb\" option. See PDF 2.0 chapter 14.11.5, and\n    # https://www.color.org/chardata/drsection1.xalter for a list of \"standard\n    # production conditions\".\n    if 'device-cmyk' in document.color_profiles:\n        color_profile = document.color_profiles['device-cmyk']\n        profile = pydyf.Stream(\n            [color_profile.content], pydyf.Dictionary({'N': 4}), compress=compress)\n        pdf.add_object(profile)\n        pdf.catalog['OutputIntents'] = pydyf.Array([\n            pydyf.Dictionary({\n                'Type': '/OutputIntent',\n                'S': '/GTS_PDFX',\n                'OutputConditionIdentifier': pydyf.String(color_profile.name),\n                'DestOutputProfile': profile.reference,\n            }),\n        ])\n    elif srgb:\n        profile = pydyf.Stream(\n            [(files(__package__) / 'sRGB2014.icc').read_bytes()],\n            pydyf.Dictionary({'N': 3, 'Alternate': '/DeviceRGB'}),\n            compress=compress)\n        pdf.add_object(profile)\n        pdf.catalog['OutputIntents'] = pydyf.Array([\n            pydyf.Dictionary({\n                'Type': '/OutputIntent',\n                'S': '/GTS_PDFA1',\n                'OutputConditionIdentifier': pydyf.String('sRGB IEC61966-2.1'),\n                'DestOutputProfile': profile.reference,\n            }),\n        ])\n\n    # Add tags\n    if pdf_tags:\n        add_tags(pdf, document, page_streams)\n\n    # Apply PDF variants functions\n    if variant:\n        variant_function(\n            pdf, metadata, document, page_streams, attachments, compress)\n\n    return pdf\n"
  },
  {
    "path": "weasyprint/pdf/anchors.py",
    "content": "\"\"\"Insert anchors, links, bookmarks and inputs in PDFs.\"\"\"\n\nimport collections\nimport mimetypes\nfrom hashlib import md5\nfrom os.path import basename\nfrom urllib.parse import unquote, urlsplit\n\nimport pydyf\n\nfrom .. import Attachment\nfrom ..logger import LOGGER\nfrom ..text.ffi import ffi, gobject, pango\nfrom ..text.fonts import get_font_description\nfrom ..urls import URLFetchingError\n\n# Get mimetypes from Python code, not from various files. See #2707.\nMIMETYPES = mimetypes.MimeTypes()\n\n\ndef add_links(links_and_anchors, matrix, pdf, page, names, tags):\n    \"\"\"Include hyperlinks in given PDF page.\"\"\"\n    links, anchors = links_and_anchors\n\n    for link_type, link_target, rectangle, box in links:\n        x1, y1 = matrix.transform_point(*rectangle[:2])\n        x2, y2 = matrix.transform_point(*rectangle[2:])\n        if link_type in ('internal', 'external'):\n            box.link_annotation = pydyf.Dictionary({\n                'Type': '/Annot',\n                'Subtype': '/Link',\n                'Rect': pydyf.Array([x1, y1, x2, y2]),\n                'BS': pydyf.Dictionary({'W': 0}),\n            })\n            if tags is not None:\n                box.link_annotation['Contents'] = pydyf.String(link_target)\n            if link_type == 'internal':\n                box.link_annotation['Dest'] = pydyf.String(link_target)\n            else:\n                box.link_annotation['A'] = pydyf.Dictionary({\n                    'Type': '/Action',\n                    'S': '/URI',\n                    'URI': pydyf.String(link_target),\n                })\n            pdf.add_object(box.link_annotation)\n            if 'Annots' not in page:\n                page['Annots'] = pydyf.Array()\n            page['Annots'].append(box.link_annotation.reference)\n\n    for anchor in anchors:\n        anchor_name, x, y = anchor\n        x, y = matrix.transform_point(x, y)\n        names.append([\n            anchor_name, pydyf.Array([page.reference, '/XYZ', x, y, 0])])\n\n\ndef add_outlines(pdf, bookmarks, parent=None):\n    \"\"\"Include bookmark outlines in PDF.\"\"\"\n    count = len(bookmarks)\n    outlines = []\n    for title, (page, x, y), children, state in bookmarks:\n        destination = pydyf.Array((pdf.page_references[page], '/XYZ', x, y, 0))\n        outline = pydyf.Dictionary({\n            'Title': pydyf.String(title), 'Dest': destination})\n        pdf.add_object(outline)\n        children_outlines, children_count = add_outlines(\n            pdf, children, parent=outline)\n        outline['Count'] = children_count\n        if state == 'closed':\n            outline['Count'] *= -1\n        else:\n            count += children_count\n        if outlines:\n            outline['Prev'] = outlines[-1].reference\n            outlines[-1]['Next'] = outline.reference\n        if children_outlines:\n            outline['First'] = children_outlines[0].reference\n            outline['Last'] = children_outlines[-1].reference\n        if parent is not None:\n            outline['Parent'] = parent.reference\n        outlines.append(outline)\n\n    if parent is None and outlines:\n        outlines_dictionary = pydyf.Dictionary({\n            'Count': count,\n            'First': outlines[0].reference,\n            'Last': outlines[-1].reference,\n        })\n        pdf.add_object(outlines_dictionary)\n        for outline in outlines:\n            outline['Parent'] = outlines_dictionary.reference\n        pdf.catalog['Outlines'] = outlines_dictionary.reference\n\n    return outlines, count\n\n\ndef add_forms(forms, matrix, pdf, page, resources, stream, font_map):\n    \"\"\"Include form inputs in PDF.\"\"\"\n    if not forms or not any(forms.values()):\n        return\n\n    if 'Annots' not in page:\n        page['Annots'] = pydyf.Array()\n    if 'AcroForm' not in pdf.catalog:\n        pdf.catalog['AcroForm'] = pydyf.Dictionary({\n            'Fields': pydyf.Array(),\n            'DR': resources.reference,\n            'NeedAppearances': 'true',\n        })\n    page_reference = page['Contents'].split()[0]\n    context = ffi.gc(\n        pango.pango_font_map_create_context(font_map),\n        gobject.g_object_unref)\n    inputs_with_forms = [\n        (form, element, style, rectangle)\n        for form, inputs in forms.items()\n        for element, style, rectangle in inputs\n    ]\n    radio_groups = collections.defaultdict(dict)\n    forms = collections.defaultdict(dict)\n    for i, (form, element, style, rectangle) in enumerate(inputs_with_forms):\n        rectangle = (\n            *matrix.transform_point(*rectangle[:2]),\n            *matrix.transform_point(*rectangle[2:]))\n\n        input_type = element.attrib.get('type')\n        input_value = element.attrib.get('value', 'Yes')\n        default_name = f'unknown-{page_reference.decode()}-{i}'\n        input_name = element.attrib.get('name', default_name)\n        # TODO: where does this 0.75 scale come from?\n        font_size = style['font_size'] * 0.75\n        field_stream = stream.clone()\n        field_stream.set_color(style['color'])\n        field = pydyf.Dictionary({\n            'Type': '/Annot',\n            'Subtype': '/Widget',\n            'Rect': pydyf.Array(rectangle),\n            'P': page.reference,\n            'F': 1 << (3 - 1),  # Print flag\n            'T': pydyf.String(input_name),\n        })\n        if input_type in ('radio', 'checkbox'):\n            if input_type == 'radio':\n                if input_name not in radio_groups[form]:\n                    radio_groups[form][input_name] = group = pydyf.Dictionary({\n                        'FT': '/Btn',\n                        'Ff': (1 << (15 - 1)) + (1 << (16 - 1)),  # NoToggle & Radio\n                        'T': pydyf.String(input_name),\n                        'V': '/Off',\n                        'Kids': pydyf.Array(),\n                        'Opt': pydyf.Array(),\n                    })\n                    pdf.add_object(group)\n                    pdf.catalog['AcroForm']['Fields'].append(group.reference)\n                group = radio_groups[form][input_name]\n                font_size = style['font_size'] * 0.5\n                character = 'l'  # Disc character in Dingbats\n            else:\n                character = '4'  # Check character in Dingbats\n\n            # Create stream when input is checked.\n            width = rectangle[2] - rectangle[0]\n            height = rectangle[1] - rectangle[3]\n            checked_stream = stream.clone(extra={\n                'Resources': resources.reference,\n                'Type': '/XObject',\n                'Subtype': '/Form',\n                'BBox': pydyf.Array((0, 0, width, height)),\n            })\n            checked_stream.push_state()\n            checked_stream.begin_text()\n            checked_stream.set_color(style['color'])\n            checked_stream.set_font_size('ZaDb', font_size)\n            # Center (assuming that Dingbat’s characters have a 0.75em size).\n            x = (width - font_size * 0.75) / 2\n            y = (height - font_size * 0.75) / 2\n            checked_stream.move_text_to(x, y)\n            checked_stream.show_text_string(character)\n            checked_stream.end_text()\n            checked_stream.pop_state()\n            pdf.add_object(checked_stream)\n\n            field_stream.set_font_size('ZaDb', font_size)\n\n            checked = 'checked' in element.attrib\n            key = len(group['Kids']) if input_type == 'radio' else 'on'\n            appearance = pydyf.Dictionary({key: checked_stream.reference})\n            field['FT'] = '/Btn'\n            field['DA'] = pydyf.String(b' '.join(field_stream.stream))\n            field['AS'] = f'/{key}' if checked else '/Off'\n            field['AP'] = pydyf.Dictionary({'N': appearance})\n            field['MK'] = pydyf.Dictionary({'CA': pydyf.String(character)})\n            pdf.add_object(field)\n            if input_type == 'radio':\n                field['Parent'] = group.reference\n                if checked:\n                    group['V'] = f'/{key}'\n                group['Kids'].append(field.reference)\n                group['Opt'].append(pydyf.String(input_value))\n            else:\n                field['T'] = pydyf.String(input_name)\n                field['V'] = field['AS']\n\n        elif element.tag == 'select':\n            font_description = get_font_description(style)\n            font = pango.pango_font_map_load_font(\n                font_map, context, font_description)\n            font, _ = stream.add_font(font)\n            font.used_in_forms = True\n\n            field_stream.set_font_size(font.hash, font_size)\n            options = []\n            selected_values = []\n            for option in element:\n                value = pydyf.String(option.attrib.get('value', ''))\n                text = pydyf.String(option.text or '')\n                options.append(pydyf.Array([value, text]))\n                if 'selected' in option.attrib:\n                    selected_values.append(value)\n\n            field['FT'] = '/Ch'\n            field['DA'] = pydyf.String(b' '.join(field_stream.stream))\n            field['Opt'] = pydyf.Array(options)\n            if 'multiple' in element.attrib:\n                field['Ff'] = 1 << (22 - 1)\n                field['V'] = pydyf.Array(selected_values)\n            else:\n                field['Ff'] = 1 << (18 - 1)\n                field['V'] = (\n                    selected_values[-1] if selected_values\n                    else pydyf.String(''))\n            pdf.add_object(field)\n\n        elif input_type == 'submit' or element.tag == 'button':\n            flags = 1 << (3 - 1)  # HTML form format\n            if form.attrib.get('method', '').lower() != 'post':\n                flags += 1 << (4 - 1)  # GET method\n            fields = pydyf.Array(field.reference for field in forms[form].values())\n            field['FT'] = '/Btn'\n            field['DA'] = pydyf.String(b' '.join(field_stream.stream))\n            field['V'] = pydyf.String(form.attrib.get('value', ''))\n            field['Ff'] = 1 << (17 - 1)  # Push-button\n            field['A'] = pydyf.Dictionary({\n                'Type': '/Action',\n                'S': '/SubmitForm',\n                'F': pydyf.String(form.attrib.get('action')),\n                'Fields': fields,\n                'Flags': flags,\n            })\n            pdf.add_object(field)\n\n        else:\n            # Text, password, textarea, files, and other unknown fields.\n            font_description = get_font_description(style)\n            font = pango.pango_font_map_load_font(\n                font_map, context, font_description)\n            font, _ = stream.add_font(font)\n            font.used_in_forms = True\n\n            field_stream.set_font_size(font.hash, font_size)\n            field['FT'] = '/Tx'\n            field['DA'] = pydyf.String(b' '.join(field_stream.stream))\n            field['V'] = pydyf.String(element.attrib.get('value', ''))\n            if element.tag == 'textarea':\n                field['Ff'] = 1 << (13 - 1)\n                field['V'] = pydyf.String(element.text or '')\n            elif input_type == 'password':\n                field['Ff'] = 1 << (14 - 1)\n            elif input_type == 'file':\n                field['Ff'] = 1 << (21 - 1)\n            if (max_length := element.get('maxlength', '')).isdigit():\n                field['MaxLen'] = max_length\n            pdf.add_object(field)\n\n        page['Annots'].append(field.reference)\n        pdf.catalog['AcroForm']['Fields'].append(field.reference)\n        if input_name not in forms:\n            forms[form][input_name] = field\n\n\ndef add_annotations(links, matrix, document, pdf, page, annot_files, compress):\n    \"\"\"Include annotations in PDF.\"\"\"\n    # TODO: splitting a link into multiple independent rectangular\n    # annotations works well for pure links, but rather mediocre for\n    # other annotations and fails completely for transformed (CSS) or\n    # complex link shapes (area). It would be better to use /AP for all\n    # links and coalesce link shapes that originate from the same HTML\n    # link. This would give a feeling similiar to what browsers do with\n    # links that span multiple lines.\n    for link_type, annot_target, rectangle, _ in links:\n        if link_type != 'attachment':\n            continue\n        if annot_target not in annot_files:\n            # A single link can be split in multiple regions. We don't want\n            # to embed a file multiple times of course, so keep a reference\n            # to every embedded URL and reuse the object number.\n            # TODO: Use the title attribute as description. The comment\n            # above about multiple regions won't always be correct, because\n            # two links might have the same href, but different titles.\n            attachment = Attachment(\n                url=annot_target, url_fetcher=document.url_fetcher)\n            annot_files[annot_target] = write_pdf_attachment(\n                pdf, attachment, compress)\n        annot_file = annot_files[annot_target]\n        if annot_file is None:\n            continue\n        rectangle = (\n            *matrix.transform_point(*rectangle[:2]),\n            *matrix.transform_point(*rectangle[2:]))\n        stream = pydyf.Stream([], {\n            'Type': '/XObject',\n            'Subtype': '/Form',\n            'BBox': pydyf.Array(rectangle),\n        }, compress)\n        pdf.add_object(stream)\n        annot = pydyf.Dictionary({\n            'Type': '/Annot',\n            'Rect': pydyf.Array(rectangle),\n            'Subtype': '/FileAttachment',\n            'T': pydyf.String(),\n            'FS': annot_file.reference,\n            'AP': pydyf.Dictionary({'N': stream.reference}),\n            'AS': '/N',\n        })\n        pdf.add_object(annot)\n        if 'Annots' not in page:\n            page['Annots'] = pydyf.Array()\n        page['Annots'].append(annot.reference)\n\n\ndef write_pdf_attachment(pdf, attachment, compress):\n    \"\"\"Write an attachment to the PDF stream.\"\"\"\n    # Attachments from document links like <link> or <a> can only be URLs.\n    # They're passed in as tuples\n    url = mime_type = None\n    try:\n        with attachment.source as (file_obj, url, _, mime_type):\n            stream = file_obj.read()\n            if isinstance(stream, str):\n                stream = stream.encode()\n    except URLFetchingError as exception:\n        LOGGER.error('Failed to load attachment: %s', exception)\n        LOGGER.debug('Error while loading attachment:', exc_info=exception)\n        return\n    attachment.md5 = md5(stream, usedforsecurity=False).hexdigest()\n\n    # TODO: Use the result object from a URL fetch operation to provide more\n    # details on the possible filename and MIME type.\n    if attachment.name:\n        filename = attachment.name\n    elif url and urlsplit(url).path:\n        filename = basename(unquote(urlsplit(url).path))\n    else:\n        filename = 'attachment.bin'\n    mime_type = (\n        mime_type or\n        MIMETYPES.guess_type(filename, strict=False)[0] or\n        'application/octet-stream')\n\n    creation = pydyf.String(attachment.created.strftime('D:%Y%m%d%H%M%SZ'))\n    mod = pydyf.String(attachment.modified.strftime('D:%Y%m%d%H%M%SZ'))\n    file_extra = pydyf.Dictionary({\n        'Type': '/EmbeddedFile',\n        'Subtype': f'/{mime_type.replace(\"/\", \"#2f\")}',\n        'Params': pydyf.Dictionary({\n            'CheckSum': f'<{attachment.md5}>',\n            'Size': len(stream),\n            'CreationDate': creation,\n            'ModDate': mod,\n        })\n    })\n    file_stream = pydyf.Stream([stream], file_extra, compress=compress)\n    pdf.add_object(file_stream)\n\n    pdf_attachment = pydyf.Dictionary({\n        'Type': '/Filespec',\n        'F': pydyf.String(filename.encode(errors='ignore')),\n        'UF': pydyf.String(filename),\n        'EF': pydyf.Dictionary({'F': file_stream.reference}),\n        'Desc': pydyf.String(attachment.description or ''),\n    })\n    pdf.add_object(pdf_attachment)\n    return pdf_attachment\n\n\ndef resolve_links(pages):\n    \"\"\"Resolve internal hyperlinks.\n\n    Links to a missing anchor are removed with a warning.\n\n    If multiple anchors have the same name, the first one is used.\n\n    :returns:\n        A generator yielding lists (one per page) like :attr:`Page.links`,\n        except that ``target`` for internal hyperlinks is\n        ``(page_number, x, y)`` instead of an anchor name.\n        The page number is a 0-based index into the :attr:`pages` list,\n        and ``x, y`` are in CSS pixels from the top-left of the page.\n\n    \"\"\"\n    anchors = set()\n    paged_anchors = []\n    for i, page in enumerate(pages):\n        paged_anchors.append([])\n        for anchor_name, (point_x, point_y, _, _) in page.anchors.items():\n            if anchor_name not in anchors:\n                paged_anchors[-1].append((anchor_name, point_x, point_y))\n                anchors.add(anchor_name)\n    for page in pages:\n        page_links = []\n        for link in page.links:\n            link_type, anchor_name, _, _ = link\n            if link_type == 'internal':\n                if anchor_name not in anchors:\n                    LOGGER.error(\n                        'No anchor #%s for internal URI reference',\n                        anchor_name)\n                else:\n                    page_links.append(link)\n            else:\n                # External link\n                page_links.append(link)\n        yield page_links, paged_anchors.pop(0)\n"
  },
  {
    "path": "weasyprint/pdf/debug.py",
    "content": "\"\"\"PDF generation with debug information.\"\"\"\n\nimport pydyf\n\nfrom ..matrix import Matrix\n\n\ndef debug(pdf, metadata, document, page_streams, attachments, compress):\n    \"\"\"Set debug PDF metadata.\"\"\"\n\n    # Add links on ids.\n    pages = zip(pdf.pages['Kids'][::3], document.pages, page_streams)\n    for pdf_page_number, document_page, stream in pages:\n        if not document_page.anchors:\n            continue\n\n        page = pdf.objects[pdf_page_number]\n        if 'Annots' not in page:\n            page['Annots'] = pydyf.Array()\n\n        for id, (x1, y1, x2, y2) in document_page.anchors.items():\n            # TODO: handle zoom correctly.\n            matrix = Matrix(0.75, 0, 0, 0.75) @ stream.ctm\n            x1, y1 = matrix.transform_point(x1, y1)\n            x2, y2 = matrix.transform_point(x2, y2)\n            annotation = pydyf.Dictionary({\n                'Type': '/Annot',\n                'Subtype': '/Link',\n                'Rect': pydyf.Array([x1, y1, x2, y2]),\n                'BS': pydyf.Dictionary({'W': 0}),\n                'P': page.reference,\n                'T': pydyf.String(id),  # id added as metadata\n            })\n\n            # The next line makes all of this relevent to use\n            # with PDFjs\n            annotation['Dest'] = pydyf.String(id)\n\n            pdf.add_object(annotation)\n            page['Annots'].append(annotation.reference)\n\n\nVARIANTS = {'debug': (debug, {})}\n"
  },
  {
    "path": "weasyprint/pdf/fonts.py",
    "content": "\"\"\"Fonts integration in PDF.\"\"\"\n\nimport io\nimport re\nfrom hashlib import md5\nfrom logging import WARNING\nfrom math import ceil\n\nimport pydyf\nfrom fontTools import subset\nfrom fontTools.ttLib import TTFont, TTLibError, ttFont\nfrom fontTools.varLib.instancer import instantiateVariableFont\n\nfrom ..logger import LOGGER, capture_logs\nfrom ..text.constants import PANGO_STRETCH_PERCENT\nfrom ..text.ffi import FROM_UNITS, ffi, harfbuzz, harfbuzz_subset, pango\nfrom ..text.fonts import get_hb_object_data, get_pango_font_hb_face\n\n\nclass Font:\n    def __init__(self, pango_font, description, font_size):\n        self.hb_font = pango.pango_font_get_hb_font(pango_font)\n        self.hb_face = get_pango_font_hb_face(pango_font)\n        self.file_content = get_hb_object_data(self.hb_face)\n        self.index = harfbuzz.hb_face_get_index(self.hb_face)\n\n        self.font_size = font_size\n        self.style = pango.pango_font_description_get_style(description)\n        self.family = ffi.string(\n            pango.pango_font_description_get_family(description)).decode()\n\n        self.variations = {}\n        variations = pango.pango_font_description_get_variations(description)\n        if variations != ffi.NULL:\n            self.variations = {\n                part.split('=')[0]: float(part.split('=')[1])\n                for part in ffi.string(variations).decode().split(',')}\n        if weight := self.variations.get('weight'):\n            self.weight = round(weight)\n            pango.pango_font_description_set_weight(description, weight)\n        else:\n            self.weight = pango.pango_font_description_get_weight(description)\n        if self.variations.get('ital'):\n            pango.pango_font_description_set_style(\n                description, pango.PANGO_STYLE_ITALIC)\n        elif self.variations.get('slnt'):\n            pango.pango_font_description_set_style(\n                description, pango.PANGO_STYLE_OBLIQUE)\n        if (width := self.variations.get('wdth')) is not None:\n            stretch = min(\n                PANGO_STRETCH_PERCENT.items(),\n                key=lambda item: abs(item[0] - width))[1]\n            pango.pango_font_description_set_stretch(description, stretch)\n        description_string = ffi.string(\n            pango.pango_font_description_to_string(description))\n\n        # Never use the built-in hash function here: it’s not stable.\n        self.hash = ''.join(\n            chr(65 + letter % 26) for letter\n            in md5(description_string, usedforsecurity=False).digest()[:6])\n\n        # Set font name.\n        name = re.split(b' [#@]', description_string)[0]\n        self.name = b'/' + self.hash.encode() + b'+' + name.replace(b' ', b'-')\n\n        # Set ascent and descent.\n        if self.font_size:\n            pango_metrics = pango.pango_font_get_metrics(pango_font, ffi.NULL)\n            self.ascent = round(\n                pango.pango_font_metrics_get_ascent(pango_metrics) * FROM_UNITS /\n                self.font_size * 1000)\n            self.descent = -round(\n                pango.pango_font_metrics_get_descent(pango_metrics) * FROM_UNITS /\n                self.font_size * 1000)\n        else:\n            self.ascent = self.descent = 0\n\n        # Get font tables and set metadata.\n        table_count = ffi.new('unsigned int *', 100)\n        table_tags = ffi.new('hb_tag_t[100]')\n        table_name = ffi.new('char[4]')\n        harfbuzz.hb_face_get_table_tags(self.hb_face, 0, table_count, table_tags)\n        self.tables = []\n        for i in range(table_count[0]):\n            harfbuzz.hb_tag_to_string(table_tags[i], table_name)\n            self.tables.append(ffi.string(table_name).decode())\n        self.bitmap = False\n        if 'EBDT' in self.tables and 'EBLC' in self.tables:\n            if 'glyf' in self.tables:\n                tag = harfbuzz.hb_tag_from_string(b'glyf', -1)\n                blob = harfbuzz.hb_face_reference_table(self.hb_face, tag)\n                if harfbuzz.hb_blob_get_length(blob) == 0:\n                    self.bitmap = True\n                harfbuzz.hb_blob_destroy(blob)\n            else:\n                self.bitmap = True\n        self.italic_angle = 0  # TODO: this should be different\n        self.upem = harfbuzz.hb_face_get_upem(self.hb_face)\n        self.png = harfbuzz.hb_ot_color_has_png(self.hb_face)\n        self.svg = harfbuzz.hb_ot_color_has_svg(self.hb_face)\n        self.glyph_count = harfbuzz.hb_face_get_glyph_count(self.hb_face)\n        self.stemv = 80\n        self.stemh = 80\n        self.widths = {}\n        self.to_unicode = {}\n        self.missing = {}\n        self.used_in_forms = False\n\n        # Set font flags.\n        self.flags = 2 ** (3 - 1)  # Symbolic, custom character set\n        if self.style:\n            self.flags += 2 ** (7 - 1)  # Italic\n        if b'Serif' in name.split(b' '):\n            self.flags += 2 ** (2 - 1)  # Serif\n\n    def get_unused_glyph_id(self, codepoint):\n        \"\"\"Get a glyph id that’s not used in the font, for given Unicode codepoint.\"\"\"\n        if codepoint not in self.missing:\n            next_unused_glyph_id = self.glyph_count + len(self.missing)\n            if next_unused_glyph_id > 2 ** 16 - 1:\n                LOGGER.warning(\n                    f'Too many glyphs missing from \"{self.family}\", '\n                    'expect text selection problems')\n                next_unused_glyph_id = 2 ** 16 - 1\n            self.missing[codepoint] = next_unused_glyph_id\n        return self.missing[codepoint]\n\n    def clean(self, to_unicode, hinting):\n        \"\"\"Remove useless data from font.\"\"\"\n\n        # Subset font.\n        self.subset(to_unicode, hinting)\n\n        # Transform variable into static font.\n        if 'fvar' in self.tables:\n            full_font = io.BytesIO(self.file_content)\n            ttfont = TTFont(full_font, fontNumber=self.index)\n            axes = {axis.axisTag: axis for axis in ttfont['fvar'].axes}\n            if 'wght' in axes and 'wght' not in self.variations:\n                self.variations['wght'] = self.weight\n            if 'opsz' in axes and 'opsz' not in self.variations:\n                self.variations['opsz'] = self.font_size\n            if 'slnt' in axes and 'slnt' not in self.variations:\n                slnt = 0\n                if self.style == 1:\n                    if axes['slnt'].maxValue == 0:\n                        slnt = axes['slnt'].minValue\n                    else:\n                        slnt = axes['slnt'].maxValue\n                self.variations['slnt'] = slnt\n            if 'ital' in axes and 'ital' not in self.variations:\n                self.variations['ital'] = int(self.style == 2)\n            partial_font = io.BytesIO()\n            try:\n                ttfont = instantiateVariableFont(ttfont, self.variations, static=True)\n                ttfont.save(partial_font)\n            except Exception as exception:\n                LOGGER.warning(f'Unable to instantiate \"{self.family}\" variable font')\n                LOGGER.debug('Original exception:', exc_info=exception)\n            else:\n                self.file_content = partial_font.getvalue()\n\n        # Remove images.\n        if self.png or self.svg:\n            full_font = io.BytesIO(self.file_content)\n            ttfont = TTFont(full_font, fontNumber=self.index)\n            try:\n                # Add empty glyphs instead of PNG or SVG emojis.\n                if 'loca' not in self.tables or 'glyf' not in self.tables:\n                    ttfont['loca'] = ttFont.getTableClass('loca')()\n                    ttfont['glyf'] = ttFont.getTableClass('glyf')()\n                    ttfont['glyf'].glyphOrder = ttfont.getGlyphOrder()\n                    ttfont['glyf'].glyphs = {\n                        name: ttFont.getTableModule('glyf').Glyph()\n                        for name in ttfont['glyf'].glyphOrder}\n                else:\n                    for glyph in ttfont['glyf'].glyphs:\n                        ttfont['glyf'][glyph] = ttFont.getTableModule('glyf').Glyph()\n                for table_name in ('CBDT', 'CBLC', 'SVG '):\n                    if table_name in ttfont:\n                        del ttfont[table_name]\n                output_font = io.BytesIO()\n                ttfont.save(output_font)\n                self.file_content = output_font.getvalue()\n            except TTLibError as exception:\n                LOGGER.warning(f'Unable to save emoji font \"{self.family}\"')\n                LOGGER.debug('Original exception:', exc_info=exception)\n\n    @property\n    def type(self):\n        return 'otf' if self.file_content[:4] == b'OTTO' else 'ttf'\n\n    def subset(self, to_unicode, hinting):\n        \"\"\"Remove unused glyphs and tables from font.\"\"\"\n        if not to_unicode:\n            return\n\n        if harfbuzz_subset and harfbuzz.hb_version_atleast(4, 1, 0):\n            # 4.1.0 is required for hb_set_add_sorted_array.\n            self._harfbuzz_subset(to_unicode, hinting)\n        else:\n            self._fonttools_subset(to_unicode, hinting)\n\n    def _harfbuzz_subset(self, to_unicode, hinting):\n        \"\"\"Subset font using Harfbuzz.\"\"\"\n        hb_subset = ffi.gc(\n            harfbuzz_subset.hb_subset_input_create_or_fail(),\n            harfbuzz_subset.hb_subset_input_destroy)\n\n        # Only keep used glyphs.\n        gid_set = harfbuzz_subset.hb_subset_input_glyph_set(hb_subset)\n        gid_array = ffi.new(f'hb_codepoint_t[{len(to_unicode)}]', sorted(to_unicode))\n        harfbuzz.hb_set_add_sorted_array(gid_set, gid_array, len(to_unicode))\n\n        # Set flags.\n        flags = (\n            harfbuzz_subset.HB_SUBSET_FLAGS_RETAIN_GIDS |\n            harfbuzz_subset.HB_SUBSET_FLAGS_PASSTHROUGH_UNRECOGNIZED |\n            harfbuzz_subset.HB_SUBSET_FLAGS_DESUBROUTINIZE)\n        if self.missing:\n            flags |= harfbuzz_subset.HB_SUBSET_FLAGS_NOTDEF_OUTLINE\n        harfbuzz_subset.hb_subset_input_set_flags(hb_subset, flags)\n\n        # Drop useless tables.\n        drop_set = harfbuzz_subset.hb_subset_input_set(\n            hb_subset, harfbuzz_subset.HB_SUBSET_SETS_DROP_TABLE_TAG)\n        drop_tables = tuple(harfbuzz.hb_tag_from_string(name, -1) for name in (\n            b'BASE', b'DSIG', b'EBDT', b'EBLC', b'EBSC', b'GPOS', b'GSUB', b'JSTF',\n            b'LTSH', b'PCLT', b'SVG '))\n        drop_tables_array = ffi.new(f'hb_codepoint_t[{len(drop_tables)}]', drop_tables)\n        harfbuzz.hb_set_add_sorted_array(drop_set, drop_tables_array, len(drop_tables))\n\n        # Subset font.\n        hb_face = ffi.gc(\n            harfbuzz_subset.hb_subset_or_fail(self.hb_face, hb_subset),\n            harfbuzz.hb_face_destroy)\n\n        # Drop empty glyphs after last one used.\n        gid_set = harfbuzz_subset.hb_subset_input_glyph_set(hb_subset)\n        keep = tuple(range(max(to_unicode) + 1))\n        gid_array = ffi.new(f'hb_codepoint_t[{len(keep)}]', keep)\n        harfbuzz.hb_set_add_sorted_array(gid_set, gid_array, len(keep))\n\n        # Set flags.\n        flags = (\n            harfbuzz_subset.HB_SUBSET_FLAGS_PASSTHROUGH_UNRECOGNIZED |\n            harfbuzz_subset.HB_SUBSET_FLAGS_DESUBROUTINIZE)\n        if not hinting:\n            flags |= harfbuzz_subset.HB_SUBSET_FLAGS_NO_HINTING\n        if self.missing:\n            flags |= harfbuzz_subset.HB_SUBSET_FLAGS_NOTDEF_OUTLINE\n        harfbuzz_subset.hb_subset_input_set_flags(hb_subset, flags)\n\n        # Subset font.\n        hb_face = ffi.gc(\n            harfbuzz_subset.hb_subset_or_fail(hb_face, hb_subset),\n            harfbuzz.hb_face_destroy)\n\n        # Store new font.\n        if hb_face:\n            file_content = get_hb_object_data(hb_face)\n            if file_content:\n                self.file_content = file_content\n                return\n\n        LOGGER.warning(f'Unable to subset \"{self.family}\" with HarfBuzz')\n\n    def _fonttools_subset(self, to_unicode, hinting):\n        \"\"\"Subset font using Fonttools.\"\"\"\n        full_font = io.BytesIO(self.file_content)\n\n        # Set subset options.\n        options = subset.Options(\n            retain_gids=True, passthrough_tables=True, ignore_missing_glyphs=True,\n            hinting=hinting, desubroutinize=True, notdef_outline=bool(self.missing))\n        options.drop_tables += ['GSUB', 'GPOS', 'SVG']\n        subsetter = subset.Subsetter(options)\n        subsetter.populate(gids=to_unicode)\n\n        # Subset font.\n        try:\n            ttfont = TTFont(full_font, fontNumber=self.index)\n            with capture_logs('fontTools', level=WARNING) as logs:\n                subsetter.subset(ttfont)\n            for log in logs:\n                LOGGER.warning(\n                    'fontTools warning when subsetting '\n                    f'\"{self.family}\": {log}')\n        except TTLibError as exception:\n            LOGGER.warning(f'Unable to subset \"{self.family}\" with fontTools')\n            LOGGER.debug('Original exception:', exc_info=exception)\n        else:\n            optimized_font = io.BytesIO()\n            ttfont.save(optimized_font)\n            self.file_content = optimized_font.getvalue()\n\n\ndef build_fonts_dictionary(pdf, fonts, compress, subset, options):\n    \"\"\"Build PDF dictionary for fonts.\"\"\"\n    pdf_fonts = pydyf.Dictionary()\n    fonts_by_file_hash = {}\n    for font in fonts.values():\n        fonts_by_file_hash.setdefault(font.hash, []).append(font)\n    font_references_by_file_hash = {}\n    for file_hash, file_fonts in fonts_by_file_hash.items():\n        # TODO: Find why we can have multiple fonts for one font file.\n        font = file_fonts[0]\n        if font.bitmap:\n            continue\n\n        # Clean font, optimize and handle emojis.\n        to_unicode = {}\n        if subset and not font.used_in_forms:\n            for file_font in file_fonts:\n                to_unicode = {**to_unicode, **file_font.to_unicode}\n        font.clean(to_unicode, options['hinting'])\n\n        # Include font.\n        if font.type == 'otf':\n            font_extra = pydyf.Dictionary({'Subtype': '/OpenType'})\n        else:\n            font_extra = pydyf.Dictionary({'Length1': len(font.file_content)})\n        font_stream = pydyf.Stream([font.file_content], font_extra, compress=compress)\n        pdf.add_object(font_stream)\n        font_references_by_file_hash[file_hash] = font_stream.reference\n\n    for font in fonts.values():\n        if subset and not font.used_in_forms:\n            # Only store widths and map for used glyphs\n            font_widths = font.widths\n            to_unicode = font.to_unicode\n        else:\n            # Store width and Unicode map for all glyphs\n            full_font = io.BytesIO(font.file_content)\n            ttfont = TTFont(full_font, fontNumber=font.index)\n            font_widths, to_unicode = {}, {}\n            for i, glyph in enumerate(ttfont.getGlyphSet().values()):\n                font_widths[i] = glyph.width * 1000 / font.upem\n            for letter, key in ttfont.getBestCmap().items():\n                glyph_id = ttfont.getGlyphID(key)\n                if glyph_id not in to_unicode:\n                    to_unicode[glyph_id] = chr(letter)\n\n        to_unicode_object = pydyf.Stream([\n            b'/CIDInit /ProcSet findresource begin',\n            b'12 dict begin',\n            b'begincmap',\n            b'/CIDSystemInfo',\n            b'<< /Registry (Adobe)',\n            b'/Ordering (UCS)',\n            b'/Supplement 0',\n            b'>> def',\n            b'/CMapName /Adobe-Identity-UCS def',\n            b'/CMapType 2 def',\n            b'1 begincodespacerange',\n            b'<0000> <ffff>',\n            b'endcodespacerange'], compress=compress)\n        to_unicode_stream = to_unicode_object.stream\n        to_unicode_length = len(to_unicode)\n        to_unicode_items = tuple(to_unicode.items())\n        for i in range(ceil(to_unicode_length / 100)):\n            batch_length = min(100, to_unicode_length - i * 100)\n            to_unicode_stream.append(f'{batch_length} beginbfchar'.encode())\n            for glyph, text in to_unicode_items[i*100:(i+1)*100]:\n                unicode_codepoints = ''.join(\n                    f'{letter.encode(\"utf-16-be\").hex()}' for letter in text)\n                to_unicode_stream.append(\n                    f'<{glyph:04x}> <{unicode_codepoints}>'.encode())\n            to_unicode_stream.append(b'endbfchar')\n        to_unicode_stream.extend([\n            b'endcmap',\n            b'CMapName currentdict /CMap defineresource pop',\n            b'end',\n            b'end'])\n        pdf.add_object(to_unicode_object)\n        font_dictionary = pydyf.Dictionary({\n            'Type': '/Font',\n            'Subtype': f'/Type{3 if font.bitmap else 0}',\n            'BaseFont': font.name,\n            'ToUnicode': to_unicode_object.reference,\n        })\n\n        if font.bitmap:\n            _build_bitmap_font_dictionary(\n                font_dictionary, pdf, font, font_widths, compress, subset)\n        else:\n            _build_vector_font_dictionary(\n                font_dictionary, pdf, font, font_widths, compress,\n                font_references_by_file_hash[font.hash], options['pdf_version'])\n        pdf.add_object(font_dictionary)\n        pdf_fonts[font.hash] = font_dictionary.reference\n\n    return pdf_fonts\n\n\ndef _build_bitmap_font_dictionary(font_dictionary, pdf, font, widths, compress, subset):\n    # https://docs.microsoft.com/typography/opentype/spec/ebdt\n    font_dictionary['FontBBox'] = pydyf.Array([0, 0, 1, 1])\n    font_dictionary['FontMatrix'] = pydyf.Array([1, 0, 0, 1, 0, 0])\n    if subset:\n        chars = tuple(sorted(font.to_unicode))\n    else:\n        chars = tuple(range(256))\n    first, last = chars[0], chars[-1]\n    differences = []\n    for glyph in sorted(widths):\n        if glyph - 1 not in widths:\n            differences.append(glyph)\n        differences.append(f'/{glyph}')\n    font_dictionary['FirstChar'] = first\n    font_dictionary['LastChar'] = last\n    font_dictionary['Encoding'] = pydyf.Dictionary({\n        'Type': '/Encoding',\n        'Differences': pydyf.Array(differences),\n    })\n    char_procs = pydyf.Dictionary({})\n    full_font = io.BytesIO(font.file_content)\n    ttfont = TTFont(full_font, fontNumber=font.index)\n    font_glyphs = ttfont['EBDT'].strikeData[0]\n    widths = [0] * (last - first + 1)\n    glyphs_info = {}\n    for key, glyph in font_glyphs.items():\n        glyph_format = glyph.getFormat()\n        glyph_id = ttfont.getGlyphID(key)\n\n        # Get and store glyph metrics.\n        if glyph_format == 5:\n            data = glyph.data\n            subtables = ttfont['EBLC'].strikes[0].indexSubTables\n            for subtable in subtables:\n                first_index = subtable.firstGlyphIndex\n                last_index = subtable.lastGlyphIndex\n                if first_index <= glyph_id <= last_index:\n                    height = subtable.metrics.height\n                    advance = width = subtable.metrics.width\n                    bearing_x = subtable.metrics.horiBearingX\n                    bearing_y = subtable.metrics.horiBearingY\n                    break\n            else:\n                LOGGER.warning(\n                    f'Unknown bitmap metrics in \"{font.family}\" for glyph: {glyph_id}')\n                continue\n        else:\n            data_start = 5 if glyph_format in (1, 2, 8) else 8\n            data = glyph.data[data_start:]\n            height, width = glyph.data[0:2]\n            bearing_x = int.from_bytes(glyph.data[2:3], 'big', signed=True)\n            bearing_y = int.from_bytes(glyph.data[3:4], 'big', signed=True)\n            advance = glyph.data[4]\n        position_y = bearing_y - height\n        if glyph_id in chars:\n            widths[glyph_id - first] = advance\n        stride = ceil(width / 8)\n        glyph_info = glyphs_info[glyph_id] = {\n            'width': width,\n            'height': height,\n            'x': bearing_x,\n            'y': position_y,\n            'stride': stride,\n            'bitmap': None,\n            'subglyphs': None,\n        }\n\n        # Decode bitmaps.\n        if 0 in (width, height) or not data:\n            glyph_info['bitmap'] = b''\n        elif glyph_format in (1, 6):\n            glyph_info['bitmap'] = data\n        elif glyph_format in (2, 5, 7):\n            padding = (8 - (width % 8)) % 8\n            bits = bin(int(data.hex(), 16))[2:]\n            bits = bits.zfill(8 * len(data))\n            bitmap_bits = ''.join(\n                bits[i * width:(i + 1) * width] + padding * '0'\n                for i in range(height))\n            glyph_info['bitmap'] = int(bitmap_bits, 2).to_bytes(height * stride, 'big')\n        elif glyph_format in (8, 9):\n            subglyphs = glyph_info['subglyphs'] = []\n            i = 0 if glyph_format == 9 else 1\n            number_of_components = int.from_bytes(data[i:i+2], 'big')\n            for j in range(number_of_components):\n                index = (i + 2) + (j * 4)\n                subglyph_id = int.from_bytes(data[index:index+2], 'big')\n                x = int.from_bytes(data[index+2:index+3], 'big', signed=True)\n                y = int.from_bytes(data[index+3:index+4], 'big', signed=True)\n                subglyphs.append({'id': subglyph_id, 'x': x, 'y': y})\n        else:  # pragma: no cover\n            LOGGER.warning(\n                f'Unsupported bitmap glyph format in \"{font.family}\": {glyph_format}')\n            glyph_info['bitmap'] = bytes(height * stride)\n\n    for glyph_id, glyph_info in glyphs_info.items():\n        # Don’t store glyph not in to_unicode.\n        if glyph_id not in chars:\n            continue\n\n        # Draw glyph.\n        stride = glyph_info['stride']\n        width = glyph_info['width']\n        height = glyph_info['height']\n        x = glyph_info['x']\n        y = glyph_info['y']\n        if glyph_info['bitmap'] is None:\n            length = height * stride\n            bitmap_int = int.from_bytes(bytes(length), 'big')\n            for subglyph in glyph_info['subglyphs']:\n                sub_x = subglyph['x']\n                sub_y = subglyph['y']\n                sub_id = subglyph['id']\n                if sub_id not in glyphs_info:\n                    LOGGER.warning(f'Unknown subglyph in \"{font.family}\": {sub_id}')\n                    continue\n                subglyph = glyphs_info[sub_id]\n                if subglyph['bitmap'] is None:\n                    # TODO: Support subglyph in subglyph.\n                    LOGGER.warning(\n                        'Unsupported subglyph in subglyph in '\n                        f'\"{font.family}\": {sub_id}')\n                    continue\n                for row_y in range(subglyph['height']):\n                    row_slice = slice(\n                        row_y * subglyph['stride'],\n                        (row_y + 1) * subglyph['stride'])\n                    row = subglyph['bitmap'][row_slice]\n                    row_int = int.from_bytes(row, 'big')\n                    shift = stride * 8 * (height - sub_y - row_y - 1)\n                    stride_difference = stride - subglyph['stride']\n                    if stride_difference > 0:\n                        row_int <<= stride_difference * 8\n                    elif stride_difference < 0:\n                        row_int >>= -stride_difference * 8\n                    if sub_x > 0:\n                        row_int >>= sub_x\n                    elif sub_x < 0:\n                        row_int <<= -sub_x\n                    row_int %= 1 << stride * 8\n                    row_int <<= shift\n                    bitmap_int |= row_int\n            bitmap = bitmap_int.to_bytes(length, 'big')\n        else:\n            bitmap = glyph_info['bitmap']\n        bitmap_stream = pydyf.Stream([\n            b'0 0 d0',\n            f'{width} 0 0 {height} {x} {y} cm'.encode(),\n            b'BI',\n            b'/IM true',\n            b'/W', width,\n            b'/H', height,\n            b'/BPC 1',\n            b'/D [1 0]',\n            b'ID', bitmap, b'EI'\n        ], compress=compress)\n        pdf.add_object(bitmap_stream)\n        char_procs[glyph_id] = bitmap_stream.reference\n\n    pdf.add_object(char_procs)\n    font_dictionary['Widths'] = pydyf.Array(widths)\n    font_dictionary['CharProcs'] = char_procs.reference\n\n\ndef _build_vector_font_dictionary(font_dictionary, pdf, font, widths, compress,\n                                  reference, pdf_version):\n    font_file = f'FontFile{3 if font.type == \"otf\" else 2}'\n    max_x = max(widths.values()) if widths else 0\n    bbox = (0, font.descent, max_x, font.ascent)\n    flags = font.flags\n    if len(widths) > 1 and len(set(font.widths.values())) == 1:\n        flags += 2 ** (1 - 1)  # FixedPitch\n    font_descriptor = pydyf.Dictionary({\n        'Type': '/FontDescriptor',\n        'FontName': font.name,\n        'FontFamily': pydyf.String(font.family),\n        'Flags': flags,\n        'FontBBox': pydyf.Array(bbox),\n        'ItalicAngle': font.italic_angle,\n        'Ascent': font.ascent,\n        'Descent': font.descent,\n        'CapHeight': bbox[3],\n        'StemV': font.stemv,\n        'StemH': font.stemh,\n        font_file: reference,\n    })\n    if str(pdf_version) <= '1.4':  # Cast for bytes and None\n        cids = sorted(font.widths)\n        padded_width = ceil((cids[-1] + 1) / 8)\n        bits = ['0'] * padded_width * 8\n        for cid in cids:\n            bits[cid] = '1'\n        stream = pydyf.Stream(\n            (int(''.join(bits), 2).to_bytes(padded_width, 'big'),),\n            compress=compress)\n        pdf.add_object(stream)\n        font_descriptor['CIDSet'] = stream.reference\n    pdf.add_object(font_descriptor)\n\n    pdf_widths = pydyf.Array()\n    for i in sorted(widths):\n        if i - 1 not in widths:\n            pdf_widths.append(i)\n            current_widths = pydyf.Array()\n            pdf_widths.append(current_widths)\n        current_widths.append(widths[i])\n\n    subfont_dictionary = pydyf.Dictionary({\n        'Type': '/Font',\n        'Subtype': f'/CIDFontType{0 if font.type == \"otf\" else 2}',\n        'BaseFont': font.name,\n        'CIDSystemInfo': pydyf.Dictionary({\n            'Registry': pydyf.String('Adobe'),\n            'Ordering': pydyf.String('Identity'),\n            'Supplement': 0,\n        }),\n        'CIDToGIDMap': '/Identity',\n        'W': pdf_widths,\n        'FontDescriptor': font_descriptor.reference,\n    })\n    pdf.add_object(subfont_dictionary)\n    if font.missing:\n        # Add CMap that doesn’t include missing glyphs, so that they can be replaced by\n        # .notdef.\n        cmap_extra = pydyf.Dictionary({\n            'Type': '/CMap',\n            'CMapName': '/WP-Encod-0',\n            'CIDSystemInfo': pydyf.Dictionary({\n                'Registry': pydyf.String('Adobe'),\n                'Ordering': pydyf.String('Identity'),\n                'Supplement': 0,\n            }),\n        })\n        encoding = pydyf.Stream([\n            b'/CIDInit /ProcSet findresource begin',\n            b'12 dict begin',\n            b'begincmap',\n            b'/CIDSystemInfo',\n            b'3 dict dup begin',\n            b'/Registry (Adobe) def',\n            b'/Ordering (Identity) def',\n            b'/Supplement 0 def',\n            b'end def',\n            b'/CMapName /WP-Encod-0 def',\n            b'/CMapType 1 def',\n            b'1 begincodespacerange',\n            b'<0000> <ffff>',\n            b'endcodespacerange',\n        ], cmap_extra, compress=compress)\n        available = tuple(font.to_unicode)\n        available_length = len(available)\n        for i in range(ceil(available_length / 100)):\n            batch_length = min(100, available_length - i * 100)\n            encoding.stream.append(f'{batch_length} begincidchar'.encode())\n            for glyph_id in available[i*100:(i+1)*100]:\n                font_glyph_id = 0 if glyph_id in font.missing.values() else glyph_id\n                encoding.stream.append(f'<{glyph_id:04x}> {font_glyph_id}'.encode())\n            encoding.stream.append(b'endcidchar')\n        encoding.stream.extend([\n            b'endcmap',\n            b'CMapName currentdict /CMap defineresource pop',\n            b'end',\n            b'end'])\n        pdf.add_object(encoding)\n        font_dictionary['Encoding'] = encoding.reference\n    else:\n        # No missing glyph in this font, use the identity mapping to map all glyphs.\n        font_dictionary['Encoding'] = '/Identity-H'\n    font_dictionary['DescendantFonts'] = pydyf.Array([subfont_dictionary.reference])\n"
  },
  {
    "path": "weasyprint/pdf/metadata.py",
    "content": "\"\"\"PDF metadata stream generation.\"\"\"\n\nfrom uuid import uuid4\nfrom xml.etree.ElementTree import Element, SubElement, register_namespace, tostring\n\nimport pydyf\n\nfrom .. import __version__\n\n# XML namespaces used for metadata\nNS = {\n    'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',\n    'dc': 'http://purl.org/dc/elements/1.1/',\n    '': '',\n    'xmp': 'http://ns.adobe.com/xap/1.0/',\n    'xmpMM': 'http://ns.adobe.com/xap/1.0/mm/',\n    'pdf': 'http://ns.adobe.com/pdf/1.3/',\n    'pdfaid': 'http://www.aiim.org/pdfa/ns/id/',\n    'pdfuaid': 'http://www.aiim.org/pdfua/ns/id/',\n    'pdfxid': 'http://www.npes.org/pdfx/ns/id/',\n    'pdfx': 'http://ns.adobe.com/pdfx/1.3/',\n}\nfor key, value in NS.items():\n    register_namespace(key, value)\n\n\nclass DocumentMetadata:\n    \"\"\"Meta-information belonging to a whole :class:`Document`.\n\n    New attributes may be added in future versions of WeasyPrint.\n    \"\"\"\n    def __init__(self, title=None, authors=None, description=None, keywords=None,\n                 generator=None, created=None, modified=None, attachments=None,\n                 lang=None, custom=None, xmp_metadata=None):\n        #: The title of the document, as a string or :obj:`None`.\n        #: Extracted from the ``<title>`` element in HTML\n        #: and written to the ``/Title`` info field in PDF.\n        self.title = title\n        #: The authors of the document, as a list of strings.\n        #: (Defaults to the empty list.)\n        #: Extracted from the ``<meta name=author>`` elements in HTML\n        #: and written to the ``/Author`` info field in PDF.\n        self.authors = authors or []\n        #: The description of the document, as a string or :obj:`None`.\n        #: Extracted from the ``<meta name=description>`` element in HTML\n        #: and written to the ``/Subject`` info field in PDF.\n        self.description = description\n        #: Keywords associated with the document, as a list of strings.\n        #: (Defaults to the empty list.)\n        #: Extracted from ``<meta name=keywords>`` elements in HTML\n        #: and written to the ``/Keywords`` info field in PDF.\n        self.keywords = keywords or []\n        #: The name of one of the software packages\n        #: used to generate the document, as a string or :obj:`None`.\n        #: Extracted from the ``<meta name=generator>`` element in HTML\n        #: and written to the ``/Creator`` info field in PDF.\n        self.generator = generator\n        #: The creation date of the document, as a string or :obj:`None`.\n        #: Dates are in one of the six formats specified in\n        #: `W3C’s profile of ISO 8601 <https://www.w3.org/TR/NOTE-datetime>`_.\n        #: Extracted from the ``<meta name=dcterms.created>`` element in HTML\n        #: and written to the ``/CreationDate`` info field in PDF.\n        self.created = created\n        #: The modification date of the document, as a string or :obj:`None`.\n        #: Dates are in one of the six formats specified in\n        #: `W3C’s profile of ISO 8601 <https://www.w3.org/TR/NOTE-datetime>`_.\n        #: Extracted from the ``<meta name=dcterms.modified>`` element in HTML\n        #: and written to the ``/ModDate`` info field in PDF.\n        self.modified = modified\n        #: A list of :class:`attachments <weasyprint.Attachment>`, empty by default.\n        #: Extracted from the ``<link rel=attachment>`` elements in HTML\n        #: and written to the ``/EmbeddedFiles`` dictionary in PDF.\n        self.attachments = attachments or []\n        #: Document language as BCP 47 language tags.\n        #: Extracted from ``<html lang=lang>`` in HTML.\n        self.lang = lang\n        #: Custom metadata, as a dict whose keys are the metadata names and\n        #: values are the metadata values.\n        self.custom = custom or {}\n        #: A list of XML bytestrings to add into the XMP metadata.\n        self.xmp_metadata = xmp_metadata or []\n\n\n    def include_in_pdf(self, pdf, variant, version, conformance, compress):\n        \"\"\"Add PDF stream of metadata.\n\n        Described in ISO-32000-1:2008, 14.3.2.\n\n        \"\"\"\n        header = b'<?xpacket begin=\"\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\\n'\n        header += b'<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">'\n        footer = b'</x:xmpmeta>\\n<?xpacket end=\"r\"?>'\n        xml_data = self.generate_rdf_metadata(variant, version, conformance)\n        stream_content = b'\\n'.join((header, xml_data, *self.xmp_metadata, footer))\n        extra = {'Type': '/Metadata', 'Subtype': '/XML'}\n        metadata = pydyf.Stream([stream_content], extra, compress)\n        pdf.add_object(metadata)\n        pdf.catalog['Metadata'] = metadata.reference\n\n\n    def generate_rdf_metadata(self, variant, version, conformance):\n        \"\"\"Generate RDF metadata as a bytestring.\"\"\"\n        namespace = f'pdf{variant}id'\n        rdf = Element(f'{{{NS[\"rdf\"]}}}RDF')\n\n        if version:\n            element = SubElement(rdf, f'{{{NS[\"rdf\"]}}}Description')\n            element.attrib[f'{{{NS[\"rdf\"]}}}about'] = ''\n            element.attrib[f'{{{NS[namespace]}}}part'] = str(version)\n        if conformance:\n            assert version\n            if variant == 'x':\n                for key in (\n                    f'{{{NS[\"pdfxid\"]}}}GTS_PDFXVersion',\n                    f'{{{NS[\"pdfx\"]}}}GTS_PDFXVersion',\n                    f'{{{NS[\"pdfx\"]}}}GTS_PDFXConformance',\n                ):\n                    subelement = SubElement(element, key)\n                    subelement.text = conformance\n                subelement = SubElement(element, f'{{{NS[\"pdf\"]}}}Trapped')\n                subelement.text = 'False'\n                if version >= 4:\n                    # TODO: these values could be useful instead of using random values.\n                    assert self.modified\n                    subelement = SubElement(element, f'{{{NS[\"xmp\"]}}}MetadataDate')\n                    subelement.text = self.modified\n                    subelement = SubElement(element, f'{{{NS[\"xmpMM\"]}}}DocumentID')\n                    subelement.text = f'xmp.did:{uuid4()}'\n                    subelement = SubElement(element, f'{{{NS[\"xmpMM\"]}}}RenditionClass')\n                    subelement.text = 'proof:pdf'\n                    subelement = SubElement(element, f'{{{NS[\"xmpMM\"]}}}VersionID')\n                    subelement.text = '1'\n            else:\n                element.attrib[f'{{{NS[namespace]}}}conformance'] = conformance\n                if variant == 'a' and version == 4:\n                    subelement = SubElement(element, f'{{{NS[\"pdfaid\"]}}}rev')\n                    subelement.text = '2020'\n\n        element = SubElement(rdf, f'{{{NS[\"rdf\"]}}}Description')\n        element.attrib[f'{{{NS[\"rdf\"]}}}about'] = ''\n        element.attrib[f'{{{NS[\"pdf\"]}}}Producer'] = f'WeasyPrint {__version__}'\n\n        if self.title:\n            element = SubElement(rdf, f'{{{NS[\"rdf\"]}}}Description')\n            element.attrib[f'{{{NS[\"rdf\"]}}}about'] = ''\n            element = SubElement(element, f'{{{NS[\"dc\"]}}}title')\n            element = SubElement(element, f'{{{NS[\"rdf\"]}}}Alt')\n            element = SubElement(element, f'{{{NS[\"rdf\"]}}}li')\n            element.attrib['xml:lang'] = 'x-default'\n            element.text = self.title\n        if self.authors:\n            element = SubElement(rdf, f'{{{NS[\"rdf\"]}}}Description')\n            element.attrib[f'{{{NS[\"rdf\"]}}}about'] = ''\n            element = SubElement(element, f'{{{NS[\"dc\"]}}}creator')\n            element = SubElement(element, f'{{{NS[\"rdf\"]}}}Seq')\n            for author in self.authors:\n                author_element = SubElement(element, f'{{{NS[\"rdf\"]}}}li')\n                author_element.text = author\n        if self.description:\n            element = SubElement(rdf, f'{{{NS[\"rdf\"]}}}Description')\n            element.attrib[f'{{{NS[\"rdf\"]}}}about'] = ''\n            element = SubElement(element, f'{{{NS[\"dc\"]}}}subject')\n            element = SubElement(element, f'{{{NS[\"rdf\"]}}}Bag')\n            element = SubElement(element, f'{{{NS[\"rdf\"]}}}li')\n            element.attrib['xml:lang'] = 'x-default'\n            element.text = self.description\n        if self.keywords:\n            element = SubElement(rdf, f'{{{NS[\"rdf\"]}}}Description')\n            element.attrib[f'{{{NS[\"rdf\"]}}}about'] = ''\n            element = SubElement(element, f'{{{NS[\"pdf\"]}}}Keywords')\n            element.text = ', '.join(self.keywords)\n        if self.generator:\n            element = SubElement(rdf, f'{{{NS[\"rdf\"]}}}Description')\n            element.attrib[f'{{{NS[\"rdf\"]}}}about'] = ''\n            element = SubElement(element, f'{{{NS[\"xmp\"]}}}CreatorTool')\n            element.text = self.generator\n        if self.created:\n            element = SubElement(rdf, f'{{{NS[\"rdf\"]}}}Description')\n            element.attrib[f'{{{NS[\"rdf\"]}}}about'] = ''\n            element = SubElement(element, f'{{{NS[\"xmp\"]}}}CreateDate')\n            element.text = self.created\n        if self.modified:\n            element = SubElement(rdf, f'{{{NS[\"rdf\"]}}}Description')\n            element.attrib[f'{{{NS[\"rdf\"]}}}about'] = ''\n            element = SubElement(element, f'{{{NS[\"xmp\"]}}}ModifyDate')\n            element.text = self.modified\n        return tostring(rdf, encoding='utf-8')\n"
  },
  {
    "path": "weasyprint/pdf/pdfa.py",
    "content": "\"\"\"PDF/A generation.\"\"\"\n\nfrom functools import partial\n\nimport pydyf\n\n\ndef pdfa(pdf, metadata, document, page_streams, attachments, compress,\n         version, variant):\n    \"\"\"Set metadata for PDF/A documents.\"\"\"\n\n    # Handle attachments.\n    if version == 1:\n        # Remove embedded files dictionary.\n        if 'Names' in pdf.catalog and 'EmbeddedFiles' in pdf.catalog['Names']:\n            del pdf.catalog['Names']['EmbeddedFiles']\n    if version <= 2:\n        # Remove attachments.\n        for pdf_object in pdf.objects:\n            if not isinstance(pdf_object, dict):\n                continue\n            if pdf_object.get('Type') != '/Filespec':\n                continue\n            reference = int(pdf_object['EF']['F'].split()[0])\n            stream = pdf.objects[reference]\n            # Remove all attachments for version 1.\n            # Remove non-PDF attachments for version 2.\n            # TODO: check that PDFs are actually PDF/A-2+ files.\n            if version == 1 or stream.extra['Subtype'] != '/application#2fpdf':\n                del pdf_object['EF']\n    if version >= 3:\n        # Add AF for attachments.\n        relationships = {\n            f'<{attachment.md5}>': attachment.relationship\n            for attachment in attachments if attachment.md5}\n        pdf_attachments = []\n        if 'Names' in pdf.catalog and 'EmbeddedFiles' in pdf.catalog['Names']:\n            reference = int(pdf.catalog['Names']['EmbeddedFiles'].split()[0])\n            names = pdf.objects[reference]\n            for name in names['Names'][1::2]:\n                pdf_attachments.append(name)\n        for pdf_object in pdf.objects:\n            if not isinstance(pdf_object, dict):\n                continue\n            if pdf_object.get('Type') != '/Filespec':\n                continue\n            reference = int(pdf_object['EF']['F'].split()[0])\n            checksum = pdf.objects[reference].extra['Params']['CheckSum']\n            relationship = relationships.get(checksum, 'Unspecified')\n            pdf_object['AFRelationship'] = f'/{relationship}'\n            pdf_attachments.append(pdf_object.reference)\n        if pdf_attachments:\n            if 'AF' not in pdf.catalog:\n                pdf.catalog['AF'] = pydyf.Array()\n            pdf.catalog['AF'].extend(pdf_attachments)\n\n    # Print annotations.\n    for pdf_object in pdf.objects:\n        if isinstance(pdf_object, dict) and pdf_object.get('Type') == '/Annot':\n            pdf_object['F'] = 2 ** (3 - 1)\n\n    # Common PDF metadata stream.\n    if version == 1:\n        # Metadata compression is forbidden for version 1.\n        compress = False\n    metadata.include_in_pdf(pdf, 'a', version, variant, compress)\n\n    # Remove document information.\n    if version >= 4:\n        pdf.info.clear()\n\n\nVARIANTS = {\n    'pdf/a-1b': (\n        partial(pdfa, version=1, variant='B'),\n        {'version': '1.4', 'identifier': True, 'srgb': True}),\n    'pdf/a-2b': (\n        partial(pdfa, version=2, variant='B'),\n        {'version': '1.7', 'identifier': True, 'srgb': True}),\n    'pdf/a-3b': (\n        partial(pdfa, version=3, variant='B'),\n        {'version': '1.7', 'identifier': True, 'srgb': True}),\n    'pdf/a-2u': (\n        partial(pdfa, version=2, variant='U'),\n        {'version': '1.7', 'identifier': True, 'srgb': True}),\n    'pdf/a-3u': (\n        partial(pdfa, version=3, variant='U'),\n        {'version': '1.7', 'identifier': True, 'srgb': True}),\n    'pdf/a-4u': (\n        partial(pdfa, version=4, variant='U'),\n        {'version': '2.0', 'identifier': True, 'srgb': True}),\n    'pdf/a-1a': (\n        partial(pdfa, version=1, variant='A'),\n        {'version': '1.4', 'identifier': True, 'srgb': True, 'pdf_tags': True}),\n    'pdf/a-2a': (\n        partial(pdfa, version=2, variant='A'),\n        {'version': '1.7', 'identifier': True, 'srgb': True, 'pdf_tags': True}),\n    'pdf/a-3a': (\n        partial(pdfa, version=3, variant='A'),\n        {'version': '1.7', 'identifier': True, 'srgb': True, 'pdf_tags': True}),\n    'pdf/a-4e': (\n        partial(pdfa, version=4, variant='E'),\n        {'version': '2.0', 'identifier': True, 'srgb': True}),\n    'pdf/a-4f': (\n        partial(pdfa, version=4, variant='F'),\n        {'version': '2.0', 'identifier': True, 'srgb': True}),\n}\n"
  },
  {
    "path": "weasyprint/pdf/pdfua.py",
    "content": "\"\"\"PDF/UA generation.\"\"\"\n\nfrom functools import partial\n\n\ndef pdfua(pdf, metadata, document, page_streams, attachments, compress, version):\n    \"\"\"Set metadata for PDF/UA documents.\"\"\"\n    # Common PDF metadata stream\n    metadata.include_in_pdf(pdf, 'ua', version, conformance=None, compress=compress)\n\n\nVARIANTS = {\n    'pdf/ua-1': (partial(pdfua, version=1), {'version': '1.7', 'pdf_tags': True}),\n    'pdf/ua-2': (partial(pdfua, version=2), {'version': '2.0', 'pdf_tags': True}),\n}\n"
  },
  {
    "path": "weasyprint/pdf/pdfx.py",
    "content": "\"\"\"PDF/X generation.\"\"\"\n\nfrom functools import partial\nfrom time import localtime\n\nimport pydyf\n\n\ndef pdfx(pdf, metadata, document, page_streams, attachments, compress, version,\n         variant):\n    \"\"\"Set metadata for PDF/X documents.\"\"\"\n\n    # Add conformance metadata.\n    conformance = f'PDF/X-{version}{variant}'\n    if version < 4:\n        pdf.info['GTS_PDFXVersion'] = pydyf.String(conformance)\n        pdf.info['GTS_PDFXConformance'] = pydyf.String(conformance)\n    pdf.info['Trapped'] = '/False'\n    now = localtime()\n    year, month, day, hour, minute, second = now[:6]\n    tz_hour, tz_minute = divmod(now.tm_gmtoff, 3600)\n    now_iso = (\n        f'{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}'\n        f'{tz_hour:+03}:{tz_minute:02}')\n    now_pdf = (\n        f'(D:{year:04}{month:02}{day:02}{hour:02}{minute:02}{second:02}'\n        f\"{tz_hour:+03}'{tz_minute:02}')\")\n    if not metadata.modified:\n        metadata.modified = now_iso\n        pdf.info['ModDate'] = now_pdf\n    if not metadata.created:\n        metadata.created = now_iso\n        pdf.info['CreationDate'] = now_pdf\n\n    # Add output intents.\n    if 'device-cmyk' not in document.color_profiles:\n        # Add standard CMYK profile.\n        pdf.catalog['OutputIntents'] = pydyf.Array([\n            pydyf.Dictionary({\n                'Type': '/OutputIntent',\n                'S': '/GTS_PDFX',\n                'OutputConditionIdentifier': pydyf.String('CGATS TR 001'),\n                'RegistryName': pydyf.String('http://www.color.org'),\n            }),\n        ])\n\n    # Common PDF metadata stream.\n    metadata.include_in_pdf(pdf, 'x', version, conformance, compress=compress)\n\n\nVARIANTS = {\n    'pdf/x-1a': (\n        partial(pdfx, version=1, variant='a:2003'),\n        {'version': '1.4', 'identifier': True},\n    ),\n    'pdf/x-3': (\n        partial(pdfx, version=3, variant=':2003'),\n        {'version': '1.4', 'identifier': True},\n    ),\n    'pdf/x-4': (\n        partial(pdfx, version=4, variant=''),\n        {'version': '1.6', 'identifier': True},\n    ),\n    'pdf/x-5g': (\n        partial(pdfx, version=5, variant='g'),\n        {'version': '1.6', 'identifier': True},\n    ),\n    # TODO: these variants forbid OutputIntent to include ICC file.\n    # 'pdf/x-4p': (\n    #     partial(pdfx, version=4, variant='p'),\n    #     {'version': '1.6', 'identifier': True},\n    # ),\n    # 'pdf/x-5pg': (\n    #     partial(pdfx, version=5, variant='pg'),\n    #     {'version': '1.6', 'identifier': True},\n    # ),\n    # 'pdf/x-5n': (\n    #     partial(pdfx, version=5, variant='n'),\n    #     {'version': '1.6', 'identifier': True},\n    # ),\n}\n"
  },
  {
    "path": "weasyprint/pdf/stream.py",
    "content": "\"\"\"PDF stream.\"\"\"\n\nfrom contextlib import contextmanager\n\nimport pydyf\n\nfrom ..logger import LOGGER\nfrom ..matrix import Matrix\nfrom ..text.ffi import ffi\nfrom ..text.fonts import get_pango_font_key\nfrom .fonts import Font\n\n\nclass Stream(pydyf.Stream):\n    \"\"\"PDF stream object with extra features.\"\"\"\n    def __init__(self, fonts, page_rectangle, resources, images, tags, color_profiles,\n                 *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.page_rectangle = page_rectangle\n        self._fonts = fonts\n        self._resources = resources\n        self._images = images\n        self._tags = tags\n        self._color_profiles = color_profiles\n        self._current_color = self._current_color_stroke = None\n        self._current_alpha = self._current_alpha_stroke = None\n        self._current_font = self._current_font_size = None\n        self._old_font = self._old_font_size = None\n        self._ctm_stack = [Matrix()]\n\n        # These objects are used in text.show_first_line\n        self.length = ffi.new('unsigned int *')\n        self.ink_rect = ffi.new('PangoRectangle *')\n        self.logical_rect = ffi.new('PangoRectangle *')\n\n    def clone(self, **kwargs):\n        if 'fonts' not in kwargs:\n            kwargs['fonts'] = self._fonts\n        if 'page_rectangle' not in kwargs:\n            kwargs['page_rectangle'] = self.page_rectangle\n        if 'resources' not in kwargs:\n            kwargs['resources'] = self._resources\n        if 'images' not in kwargs:\n            kwargs['images'] = self._images\n        if 'tags' not in kwargs:\n            kwargs['tags'] = self._tags\n        if 'color_profiles' not in kwargs:\n            kwargs['color_profiles'] = self._color_profiles\n        if 'compress' not in kwargs:\n            kwargs['compress'] = self.compress\n        return Stream(**kwargs)\n\n    @property\n    def ctm(self):\n        return self._ctm_stack[-1]\n\n    def push_state(self):\n        super().push_state()\n        self._ctm_stack.append(self.ctm)\n\n    def pop_state(self):\n        if self.stream and self.stream[-1] == b'q':\n            self.stream.pop()\n        else:\n            super().pop_state()\n        self._current_color = self._current_color_stroke = None\n        self._current_alpha = self._current_alpha_stroke = None\n        self._current_font = None\n        self._ctm_stack.pop()\n        assert self._ctm_stack\n\n    def transform(self, a=1, b=0, c=0, d=1, e=0, f=0):\n        super().set_matrix(a, b, c, d, e, f)\n        self._ctm_stack[-1] = Matrix(a, b, c, d, e, f) @ self.ctm\n\n    def begin_text(self):\n        if self.stream and self.stream[-1] == b'ET':\n            self._current_font = self._old_font\n            self.stream.pop()\n        else:\n            super().begin_text()\n\n    def end_text(self):\n        self._old_font, self._current_font = self._current_font, None\n        super().end_text()\n\n    def set_color(self, color, stroke=False):\n        *channels, alpha = color\n        self.set_alpha(alpha, stroke)\n\n        if stroke:\n            if (color.space, *channels) == self._current_color_stroke:\n                return\n            else:\n                self._current_color_stroke = (color.space, *channels)\n        else:\n            if (color.space, *channels) == self._current_color:\n                return\n            else:\n                self._current_color = (color.space, *channels)\n\n        if color.space in ('srgb', 'hsl', 'hwb'):\n            self.set_color_rgb(*color.to('srgb').coordinates, stroke)\n        elif color.space in ('xyz-d65', 'oklab', 'oklch'):\n            self.set_color_space('lab-d65', stroke)\n            lightness, a, b = color.to('lab').coordinates\n            self.set_color_special(None, stroke, lightness, a, b)\n        elif color.space in ('xyz-d50', 'lab', 'lch'):\n            self.set_color_space('lab-d50', stroke)\n            lightness, a, b = color.to('lab').coordinates\n            self.set_color_special(None, stroke, lightness, a, b)\n        elif color.space == 'device-cmyk':\n            self.set_color_space('DeviceCMYK', stroke)\n            c, m, y, k = color.coordinates\n            self.set_color_special(None, stroke, c, m, y, k)\n        elif color.space.startswith('--') and color.space in self._color_profiles:\n            self.set_color_space(color.space, stroke)\n            self.set_color_special(None, stroke, *color.coordinates)\n        else:\n            LOGGER.warning('Unsupported color space %s, use sRGB instead', color.space)\n            if len(channels) > 3:\n                channels = channels[:3]\n            elif len(channels) == 2:\n                channels = *channels, 0\n            elif len(channels) == 1:\n                channels = *channels, 0, 0\n            self.set_color_rgb(*channels, stroke)\n\n    def set_font_size(self, font, size):\n        if (font, size) == self._current_font:\n            return\n        self._current_font = (font, size)\n        super().set_font_size(font, size)\n\n    def set_state(self, state):\n        key = f's{len(self._resources[\"ExtGState\"])}'\n        self._resources['ExtGState'][key] = state\n        super().set_state(key)\n\n    def set_alpha(self, alpha, stroke=False, fill=None):\n        if fill is None:\n            fill = not stroke\n\n        if stroke:\n            key = f'A{alpha}'\n            if key != self._current_alpha_stroke:\n                self._current_alpha_stroke = key\n                if key not in self._resources['ExtGState']:\n                    self._resources['ExtGState'][key] = pydyf.Dictionary({'CA': alpha})\n                super().set_state(key)\n\n        if fill:\n            key = f'a{alpha}'\n            if key != self._current_alpha:\n                self._current_alpha = key\n                if key not in self._resources['ExtGState']:\n                    self._resources['ExtGState'][key] = pydyf.Dictionary({'ca': alpha})\n                super().set_state(key)\n\n    def set_alpha_state(self, x, y, width, height, mode='luminosity'):\n        alpha_stream = self.add_group(x, y, width, height)\n        alpha_state = pydyf.Dictionary({\n            'Type': '/ExtGState',\n            'SMask': pydyf.Dictionary({\n                'Type': '/Mask',\n                'S': f'/{mode.capitalize()}',\n                'G': alpha_stream,\n            }),\n            'ca': 1,\n            'AIS': 'false',\n        })\n        self.set_state(alpha_state)\n        return alpha_stream\n\n    def set_blend_mode(self, mode):\n        self.set_state(pydyf.Dictionary({\n            'Type': '/ExtGState',\n            'BM': f'/{mode}',\n        }))\n\n    def add_font(self, pango_font):\n        key, description, font_size = get_pango_font_key(pango_font)\n        if key not in self._fonts:\n            self._fonts[key] = Font(pango_font, description, font_size)\n        return self._fonts[key], font_size\n\n    def add_group(self, x, y, width, height):\n        resources = pydyf.Dictionary({\n            'ExtGState': pydyf.Dictionary(),\n            'XObject': pydyf.Dictionary(),\n            'Pattern': pydyf.Dictionary(),\n            'Shading': pydyf.Dictionary(),\n            'ColorSpace': self._resources['ColorSpace'],\n            'Font': None,  # Will be set by _use_references\n        })\n        extra = pydyf.Dictionary({\n            'Type': '/XObject',\n            'Subtype': '/Form',\n            'BBox': pydyf.Array((x, y, x + width, y + height)),\n            'Resources': resources,\n            'Group': pydyf.Dictionary({\n                'Type': '/Group',\n                'S': '/Transparency',\n                'I': 'true',\n                'CS': '/DeviceRGB',\n            }),\n        })\n        group = self.clone(resources=resources, extra=extra)\n        group.id = f'x{len(self._resources[\"XObject\"])}'\n        self._resources['XObject'][group.id] = group\n        return group\n\n    def add_image(self, image, interpolate, ratio):\n        image_name = f'i{image.id}{int(interpolate)}'\n        self._resources['XObject'][image_name] = None  # Set by write_pdf\n        if image_name in self._images:\n            # Reuse image already stored in document\n            self._images[image_name]['dpi_ratios'].add(ratio)\n            return image_name\n\n        self._images[image_name] = {\n            'image': image,\n            'interpolate': interpolate,\n            'dpi_ratios': {ratio},\n            'x_object': None,  # Set by write_pdf\n        }\n        return image_name\n\n    def add_pattern(self, x, y, width, height, repeat_width, repeat_height, matrix):\n        resources = pydyf.Dictionary({\n            'ExtGState': pydyf.Dictionary(),\n            'XObject': pydyf.Dictionary(),\n            'Pattern': pydyf.Dictionary(),\n            'Shading': pydyf.Dictionary(),\n            'ColorSpace': self._resources['ColorSpace'],\n            'Font': None,  # Will be set by _use_references\n        })\n        extra = pydyf.Dictionary({\n            'Type': '/Pattern',\n            'PatternType': 1,\n            'BBox': pydyf.Array([x, y, x + width, y + height]),\n            'XStep': repeat_width,\n            'YStep': repeat_height,\n            'TilingType': 1,\n            'PaintType': 1,\n            'Matrix': pydyf.Array(matrix.values),\n            'Resources': resources,\n        })\n        pattern = self.clone(resources=resources, extra=extra)\n        pattern.id = f'p{len(self._resources[\"Pattern\"])}'\n        self._resources['Pattern'][pattern.id] = pattern\n        return pattern\n\n    def add_shading(self, shading_type, color_space, domain, coords, extend,\n                    function):\n        shading = pydyf.Dictionary({\n            'ShadingType': shading_type,\n            'ColorSpace': f'/Device{color_space}',\n            'Domain': pydyf.Array(domain),\n            'Coords': pydyf.Array(coords),\n            'Function': function,\n        })\n        if extend:\n            shading['Extend'] = pydyf.Array((b'true', b'true'))\n        shading.id = f's{len(self._resources[\"Shading\"])}'\n        self._resources['Shading'][shading.id] = shading\n        return shading\n\n    @contextmanager\n    def stacked(self):\n        \"\"\"Save and restore stream context when used with the ``with`` keyword.\"\"\"\n        self.push_state()\n        try:\n            yield\n        finally:\n            self.pop_state()\n\n    @contextmanager\n    def marked(self, box, tag):\n        if self._tags is not None:\n            property_list = None\n            mcid = len(self._tags)\n            assert box not in self._tags\n            self._tags[box] = {'tag': tag, 'mcid': mcid}\n            property_list = pydyf.Dictionary({'MCID': mcid})\n            super().begin_marked_content(tag, property_list)\n        try:\n            yield\n        finally:\n            if self._tags is not None:\n                super().end_marked_content()\n\n    @contextmanager\n    def artifact(self):\n        if self._tags is not None:\n            super().begin_marked_content('Artifact')\n        try:\n            yield\n        finally:\n            if self._tags is not None:\n                super().end_marked_content()\n\n    @staticmethod\n    def create_interpolation_function(domain, c0, c1, n):\n        return pydyf.Dictionary({\n            'FunctionType': 2,\n            'Domain': pydyf.Array(domain),\n            'C0': pydyf.Array(c0),\n            'C1': pydyf.Array(c1),\n            'N': n,\n        })\n\n    @staticmethod\n    def create_stitching_function(domain, encode, bounds, sub_functions):\n        return pydyf.Dictionary({\n            'FunctionType': 3,\n            'Domain': pydyf.Array(domain),\n            'Encode': pydyf.Array(encode),\n            'Bounds': pydyf.Array(bounds),\n            'Functions': pydyf.Array(sub_functions),\n        })\n"
  },
  {
    "path": "weasyprint/pdf/tags.py",
    "content": "\"\"\"PDF tagging.\"\"\"\n\nfrom collections import defaultdict\n\nimport pydyf\n\nfrom ..formatting_structure import boxes\nfrom ..layout.absolute import AbsolutePlaceholder\nfrom ..logger import LOGGER\n\n\ndef add_tags(pdf, document, page_streams):\n    \"\"\"Add tag tree to the document.\"\"\"\n\n    # Add root structure.\n    content_mapping = pydyf.Dictionary({})\n    pdf.add_object(content_mapping)\n    structure_root = pydyf.Dictionary({\n        'Type': '/StructTreeRoot',\n        'ParentTree': content_mapping.reference,\n    })\n    pdf.add_object(structure_root)\n    structure_document = pydyf.Dictionary({\n        'Type': '/StructElem',\n        'S': '/Document',\n        'K': pydyf.Array(),\n        'P': structure_root.reference,\n    })\n    pdf.add_object(structure_document)\n    structure_root['K'] = pydyf.Array([structure_document.reference])\n    pdf.catalog['StructTreeRoot'] = structure_root.reference\n\n    # Map content.\n    content_mapping['Nums'] = pydyf.Array()\n    links = []\n    for page_number, (page, stream) in enumerate(zip(document.pages, page_streams)):\n        tags = stream._tags\n        page_box = page._page_box\n\n        # Prepare array for this page’s MCID-to-StructElem mapping.\n        content_mapping['Nums'].append(page_number)\n        content_mapping['Nums'].append(pydyf.Array())\n        page_nums = {}\n\n        # Map page box content.\n        elements = _build_box_tree(\n            page_box, structure_document, pdf, page_number, page_nums, links, tags)\n        for element in elements:\n            structure_document['K'].append(element.reference)\n        assert not tags\n\n        # Flatten page-local nums into global mapping.\n        sorted_refs = [ref for _, ref in sorted(page_nums.items())]\n        content_mapping['Nums'][-1].extend(sorted_refs)\n\n    # Add annotations for links.\n    for i, (link_reference, annotation) in enumerate(links, start=len(document.pages)):\n        content_mapping['Nums'].append(i)\n        content_mapping['Nums'].append(link_reference)\n        annotation['StructParent'] = i\n\n    # Add required metadata.\n    pdf.catalog['ViewerPreferences'] = pydyf.Dictionary({'DisplayDocTitle': 'true'})\n    pdf.catalog['MarkInfo'] = pydyf.Dictionary({'Marked': 'true'})\n    if 'Lang' not in pdf.catalog:\n        LOGGER.error('Missing required \"lang\" attribute at the root of the document')\n        pdf.catalog['Lang'] = pydyf.String()\n\n\ndef _get_pdf_tag(tag):\n    \"\"\"Get PDF tag corresponding to HTML tag.\"\"\"\n    if tag is None:\n        return 'NonStruct'\n    elif tag == 'div':\n        return 'Div'\n    elif tag.split(':')[0] == 'a':\n        # Links and link pseudo elements create link annotations.\n        return 'Link'\n    elif tag == 'span':\n        return 'Span'\n    elif tag == 'main':\n        return 'Part'\n    elif tag == 'article':\n        return 'Art'\n    elif tag == 'section':\n        return 'Sect'\n    elif tag == 'blockquote':\n        return 'BlockQuote'\n    elif tag == 'p':\n        return 'P'\n    elif tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):\n        return tag.upper()\n    elif tag in ('dl', 'ul', 'ol'):\n        return 'L'\n    elif tag in ('li', 'dt', 'dd'):\n        # TODO: dt should be different.\n        return 'LI'\n    elif tag == 'li::marker':\n        return 'Lbl'\n    elif tag == 'table':\n        return 'Table'\n    elif tag in ('tr', 'th', 'td'):\n        return tag.upper()\n    elif tag in ('thead', 'tbody', 'tfoot'):\n        return tag[:2].upper() + tag[2:]\n    elif tag == 'img':\n        return 'Figure'\n    elif tag in ('caption', 'figcaption'):\n        return 'Caption'\n    else:\n        return 'NonStruct'\n\n\ndef _build_box_tree(box, parent, pdf, page_number, nums, links, tags):\n    \"\"\"Recursively build tag tree for given box and yield children.\"\"\"\n\n    # Special case for absolute elements.\n    if isinstance(box, AbsolutePlaceholder):\n        box = box._box\n\n    element_tag = None if box.element is None else box.element_tag\n    tag = _get_pdf_tag(element_tag)\n\n    # Special case for html, body, page boxes and margin boxes.\n    if element_tag in ('html', 'body') or isinstance(box, boxes.PageBox):\n        # Avoid generate page, html and body boxes as a semantic node, yield children.\n        if isinstance(box, boxes.ParentBox) and not isinstance(box, boxes.LineBox):\n            for child in box.children:\n                yield from _build_box_tree(\n                    child, parent, pdf, page_number, nums, links, tags)\n            return\n    elif isinstance(box, boxes.MarginBox):\n        # Build tree for margin boxes but don’t link it to main tree. It ensures that\n        # marked content is mapped in document and removed from list. It could be\n        # included in tree as Artifact, but that’s only allowed in PDF 2.0.\n        for child in box.children:\n            tuple(_build_box_tree(child, parent, pdf, page_number, nums, links, tags))\n        return\n\n    # Create box element.\n    if tag == 'LI':\n        anonymous_list_element = parent['S'] == '/LI'\n        anonymous_li_child = parent['S'] == '/LBody'\n        dl_item = box.element_tag in ('dt', 'dd')\n        no_bullet_li = box.element_tag == 'li' and (\n            'list-item' not in box.style['display'] or\n            box.style['list_style_type'] == 'none')\n        if anonymous_list_element:\n            # Store as list item body.\n            tag = 'LBody'\n        elif anonymous_li_child:\n            # Store as non struct list item body child.\n            tag = 'NonStruct'\n        elif dl_item or no_bullet_li:\n            # Wrap in list item.\n            tag = 'LBody'\n            parent = pydyf.Dictionary({\n                'Type': '/StructElem',\n                'S': '/LI',\n                'K': pydyf.Array([]),\n                'Pg': pdf.page_references[page_number],\n                'P': parent.reference,\n            })\n            pdf.add_object(parent)\n            children = _build_box_tree(box, parent, pdf, page_number, nums, links, tags)\n            for child in children:\n                parent['K'].append(child.reference)\n            yield parent\n            return\n\n    element = pydyf.Dictionary({\n        'Type': '/StructElem',\n        'S': f'/{tag}',\n        'K': pydyf.Array([]),\n        'Pg': pdf.page_references[page_number],\n        'P': parent.reference,\n    })\n    pdf.add_object(element)\n\n    # Handle special cases.\n    if tag == 'Figure':\n        # Add extra data for images.\n        x1, y1 = box.content_box_x(), box.content_box_y()\n        x2, y2 = x1 + box.width, y1 + box.height\n        element['A'] = pydyf.Dictionary({\n            'O': '/Layout',\n            'BBox': pydyf.Array((x1, y1, x2, y2)),\n        })\n        if alt := box.element.attrib.get('alt'):\n            element['Alt'] = pydyf.String(alt)\n        else:\n            source = box.element.attrib.get('src', 'unknown')\n            LOGGER.error(f'Image \"{source}\" has no required alt description')\n    elif tag == 'Table':\n        # Use wrapped table as tagged box, and put captions in it.\n        if box.is_table_wrapper:\n            # Can be false if table has another display type.\n            wrapper, table = box, box.get_wrapped_table()\n            box = table.copy_with_children([])\n            for child in wrapper.children:\n                box.children.extend(child.children if child is table else [child])\n    elif tag == 'TH':\n        # Set identifier for table headers to reference them in cells.\n        element['ID'] = pydyf.String(id(box))\n    elif tag == 'TD':\n        # Store table cell element to map it to headers later.\n        # TODO: don’t use the box to store this.\n        box.mark = element\n\n    # Include link annotations.\n    if box.link_annotation:\n        annotation = box.link_annotation\n        object_reference = pydyf.Dictionary({\n            'Type': '/OBJR',\n            'Obj': annotation.reference,\n            'Pg': pdf.page_references[page_number],\n        })\n        pdf.add_object(object_reference)\n        links.append((element.reference, annotation))\n        element['K'].append(object_reference.reference)\n\n    if isinstance(box, boxes.ParentBox):\n        # Build tree for box children.\n        for child in box.children:\n            children = child.children if isinstance(child, boxes.LineBox) else [child]\n            for child in children:\n                if isinstance(child, boxes.TextBox):\n                    # Add marked element from the stream.\n                    kid = tags.pop(child)\n                    assert kid['mcid'] not in nums\n                    if tag == 'Link':\n                        # Associate MCID directly with link reference.\n                        element['K'].append(kid['mcid'])\n                        nums[kid['mcid']] = element.reference\n                    else:\n                        kid_element = pydyf.Dictionary({\n                            'Type': '/StructElem',\n                            'S': f'/{kid[\"tag\"]}',\n                            'K': pydyf.Array([kid['mcid']]),\n                            'Pg': pdf.page_references[page_number],\n                            'P': element.reference,\n                        })\n                        pdf.add_object(kid_element)\n                        element['K'].append(kid_element.reference)\n                        nums[kid['mcid']] = kid_element.reference\n                else:\n                    # Recursively build tree for child.\n                    if child.element_tag in ('ul', 'ol') and element['S'] == '/LI':\n                        # In PDFs, nested lists are linked to the parent list, but in\n                        # HTML, nested lists are linked to a parent’s list item.\n                        child_parent = parent\n                    else:\n                        child_parent = element\n                    child_elements = _build_box_tree(\n                        child, child_parent, pdf, page_number, nums, links, tags)\n\n                    # Check if it is already been referenced before.\n                    for child_element in child_elements:\n                        child_parent['K'].append(child_element.reference)\n\n    else:\n        # Add replaced box.\n        assert isinstance(box, boxes.ReplacedBox)\n        kid = tags.pop(box)\n        element['K'].append(kid['mcid'])\n        assert kid['mcid'] not in nums\n        nums[kid['mcid']] = element.reference\n\n    # Link table cells to related headers.\n    if tag == 'Table':\n        def _get_rows(table_box):\n            for child in table_box.children:\n                if child.element_tag == 'tr':\n                    yield child\n                else:\n                    yield from _get_rows(child)\n\n        # Get headers and rows.\n        column_headers = defaultdict(list)\n        row_headers = defaultdict(list)\n        rows = tuple(_get_rows(box))\n\n        # Find column and row headers.\n        # TODO: handle rowspan and colspan values.\n        for i, row in enumerate(rows):\n            for j, cell in enumerate(row.children):\n                if cell.element is None:\n                    continue\n                if cell.element_tag == 'th':\n                    # TODO: handle rowgroup and colgroup values.\n                    if cell.element.attrib.get('scope') == 'row':\n                        row_headers[i].append(pydyf.String(id(cell)))\n                    else:\n                        column_headers[j].append(pydyf.String(id(cell)))\n\n        # Map headers to cells.\n        for i, row in enumerate(rows):\n            for j, cell in enumerate(row.children):\n                if cell.element is None:\n                    continue\n                if cell.element_tag == 'td':\n                    cell.mark['A'] = pydyf.Dictionary({\n                        'O': '/Table',\n                        'Headers': pydyf.Array(row_headers[i] + column_headers[j]),\n                    })\n\n    yield element\n"
  },
  {
    "path": "weasyprint/stacking.py",
    "content": "\"\"\"Stacking contexts management.\"\"\"\n\nfrom .formatting_structure import boxes\nfrom .layout.absolute import AbsolutePlaceholder\n\n\nclass StackingContext:\n    \"\"\"Stacking contexts define the paint order of all pieces of a document.\n\n    https://www.w3.org/TR/CSS21/visuren.html#x43\n    https://www.w3.org/TR/CSS21/zindex.html\n\n    \"\"\"\n    def __init__(self, box, child_contexts, blocks, floats, blocks_and_cells,\n                 page):\n        self.box = box\n        self.page = page\n        self.block_level_boxes = blocks  # 4: In flow, non positioned\n        self.float_contexts = floats  # 5: Non positioned\n        self.negative_z_contexts = []  # 3: Child contexts, z-index < 0\n        self.zero_z_contexts = []  # 8: Child contexts, z-index = 0\n        self.positive_z_contexts = []  # 9: Child contexts, z-index > 0\n        self.blocks_and_cells = blocks_and_cells  # 7: Non positioned\n\n        for context in child_contexts:\n            if context.z_index < 0:\n                self.negative_z_contexts.append(context)\n            elif context.z_index == 0:\n                self.zero_z_contexts.append(context)\n            else:  # context.z_index > 0\n                self.positive_z_contexts.append(context)\n        self.negative_z_contexts.sort(key=lambda context: context.z_index)\n        self.positive_z_contexts.sort(key=lambda context: context.z_index)\n        # sort() is stable, so the lists are now storted\n        # by z-index, then tree order.\n\n        self.z_index = box.style['z_index']\n        if self.z_index == 'auto':\n            self.z_index = 0\n\n    @classmethod\n    def from_page(cls, page):\n        # Page children (the box for the root element and margin boxes)\n        # as well as the page box itself are unconditionally stacking contexts.\n        child_contexts = [cls.from_box(child, page) for child in page.children]\n        # Children are sub-contexts, remove them from the \"normal\" tree.\n        page = page.copy_with_children([])\n        return cls(page, child_contexts, [], [], {}, page)\n\n    @classmethod\n    def from_box(cls, box, page, child_contexts=None):\n        children = []  # What will be passed to this box\n        if child_contexts is None:\n            child_contexts = children\n        # child_contexts: where to put sub-contexts that we find here.\n        # May not be the same as children for:\n        #   \"treat the element as if it created a new stacking context, but any\n        #    positioned descendants and descendants which actually create a new\n        #    stacking context should be considered part of the parent stacking\n        #    context, not this new one.\"\n        blocks = []\n        floats = []\n        blocks_and_cells = {}\n        box = _dispatch_children(\n            box, page, child_contexts, blocks, floats, blocks_and_cells)\n        return cls(box, children, blocks, floats, blocks_and_cells, page)\n\n\ndef _dispatch(box, page, child_contexts, blocks, floats, blocks_and_cells):\n    if isinstance(box, AbsolutePlaceholder):\n        box = box._box\n    style = box.style\n\n    # Remove boxes defining a new stacking context from the children list.\n    defines_stacking_context = (\n        (style['position'] != 'static' and style['z_index'] != 'auto') or\n        (box.is_grid_item and style['z_index'] != 'auto') or\n        style['opacity'] < 1 or\n        style['transform'] or  # 'transform: none' gives a \"falsy\" empty list\n        style['overflow'] != 'visible')\n    if defines_stacking_context:\n        child_contexts.append(StackingContext.from_box(box, page))\n        return\n\n    stacking_classes = (boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)\n    if style['position'] != 'static':\n        assert style['z_index'] == 'auto'\n        # \"Fake\" context: sub-contexts will go in this `child_contexts` list.\n        # Insert at the position before creating the sub-context.\n        index = len(child_contexts)\n        stacking_context = StackingContext.from_box(box, page, child_contexts)\n        child_contexts.insert(index, stacking_context)\n    elif box.is_floated():\n        floats.append(StackingContext.from_box(box, page, child_contexts))\n    elif isinstance(box, stacking_classes):\n        # Have this fake stacking context be part of the \"normal\" box tree,\n        # because we need its position in the middle of a tree of inline boxes.\n        return StackingContext.from_box(box, page, child_contexts)\n    else:\n        if isinstance(box, boxes.BlockLevelBox):\n            blocks_index = len(blocks)\n            box_blocks_and_cells = {}\n            box = _dispatch_children(\n                box, page, child_contexts, blocks, floats, box_blocks_and_cells)\n            blocks.insert(blocks_index, box)\n            blocks_and_cells[box] = box_blocks_and_cells\n        elif isinstance(box, boxes.TableCellBox):\n            box_blocks_and_cells = {}\n            box = _dispatch_children(\n                box, page, child_contexts, blocks, floats, box_blocks_and_cells)\n            blocks_and_cells[box] = box_blocks_and_cells\n        else:\n            blocks_index = None\n            box_blocks_and_cells = None\n            box = _dispatch_children(\n                box, page, child_contexts, blocks, floats, blocks_and_cells)\n\n        return box\n\n\ndef _dispatch_children(box, page, child_contexts, blocks, floats,\n                       blocks_and_cells):\n    if not isinstance(box, boxes.ParentBox):\n        return box\n\n    new_children = []\n    for child in box.children:\n        result = _dispatch(\n            child, page, child_contexts, blocks, floats, blocks_and_cells)\n        if result is not None:\n            new_children.append(result)\n    return box.copy_with_children(new_children)\n"
  },
  {
    "path": "weasyprint/svg/__init__.py",
    "content": "\"\"\"Render SVG images.\"\"\"\n\nimport re\nfrom contextlib import suppress\nfrom math import cos, hypot, pi, radians, sin, sqrt\nfrom xml.etree import ElementTree\n\nfrom cssselect2 import ElementWrapper\n\nfrom ..urls import get_url_attribute\nfrom .css import parse_declarations, parse_stylesheets\nfrom .defs import apply_filters, draw_gradient_or_pattern, paint_mask, use\nfrom .images import image, svg\nfrom .path import path\nfrom .shapes import circle, ellipse, line, polygon, polyline, rect\nfrom .text import text\n\nfrom .bounding_box import (  # isort:skip\n    EMPTY_BOUNDING_BOX, bounding_box, extend_bounding_box, is_valid_bounding_box)\nfrom .utils import (  # isort:skip\n    PointError, alpha_value, color, normalize, parse_url, preserve_ratio, size,\n    transform)\n\nTAGS = {\n    'a': text,\n    'circle': circle,\n    'ellipse': ellipse,\n    'image': image,\n    'line': line,\n    'path': path,\n    'polyline': polyline,\n    'polygon': polygon,\n    'rect': rect,\n    'svg': svg,\n    'text': text,\n    'textPath': text,\n    'tspan': text,\n    'use': use,\n}\n\nNOT_INHERITED_ATTRIBUTES = frozenset((\n    'clip',\n    'clip-path',\n    'filter',\n    'height',\n    'id',\n    'mask',\n    'opacity',\n    'overflow',\n    'rotate',\n    'stop-color',\n    'stop-opacity',\n    'style',\n    'transform',\n    'transform-origin',\n    'viewBox',\n    'width',\n    'x',\n    'y',\n    'dx',\n    'dy',\n    '{http://www.w3.org/1999/xlink}href',\n    'href',\n))\n\nCOLOR_ATTRIBUTES = frozenset((\n    'fill',\n    'flood-color',\n    'lighting-color',\n    'stop-color',\n    'stroke',\n))\n\nDEF_TYPES = frozenset((\n    'clipPath',\n    'filter',\n    'gradient',\n    'image',\n    'marker',\n    'mask',\n    'path',\n    'pattern',\n    'symbol',\n))\n\n\nclass Node:\n    \"\"\"An SVG document node.\"\"\"\n\n    def __init__(self, wrapper, style):\n        self._wrapper = wrapper\n        self._etree_node = wrapper.etree_element\n        self._style = style\n        self._children = None\n\n        self.attrib = wrapper.etree_element.attrib.copy()\n\n        self.vertices = []\n        self.bounding_box = None\n\n    def copy(self):\n        \"\"\"Create a deep copy of the node as it was when first created.\"\"\"\n        return Node(self._wrapper, self._style)\n\n    def get(self, key, default=None):\n        \"\"\"Get attribute.\"\"\"\n        return self.attrib.get(key, default)\n\n    @property\n    def tag(self):\n        \"\"\"XML tag name with no namespace.\"\"\"\n        return self._etree_node.tag.split('}', 1)[-1]\n\n    @property\n    def text(self):\n        \"\"\"XML node text.\"\"\"\n        return self._etree_node.text\n\n    @property\n    def tail(self):\n        \"\"\"Text after the XML node.\"\"\"\n        return self._etree_node.tail\n\n    @property\n    def display(self):\n        \"\"\"Whether node should be displayed.\"\"\"\n        return self.get('display') != 'none'\n\n    @property\n    def visible(self):\n        \"\"\"Whether node is visible.\"\"\"\n        return self.display and self.get('visibility') != 'hidden'\n\n    def cascade(self, child):\n        \"\"\"Apply CSS cascade and other related operations to given child.\"\"\"\n        wrapper = child._wrapper\n\n        # Cascade\n        for key, value in self.attrib.items():\n            if key not in NOT_INHERITED_ATTRIBUTES:\n                if key not in child.attrib:\n                    child.attrib[key] = value\n\n        # Apply style attribute\n        if style_attr := child.get('style'):\n            normal_attr, important_attr = parse_declarations(style_attr)\n        else:\n            normal_attr, important_attr = [], []\n        normal_matcher, important_matcher = self._style\n        normal = [rule[-1] for rule in normal_matcher.match(wrapper)]\n        important = [rule[-1] for rule in important_matcher.match(wrapper)]\n        for declarations_list in (normal, [normal_attr], important, [important_attr]):\n            for declarations in declarations_list:\n                for name, value in declarations:\n                    child.attrib[name] = value.strip()\n\n        # Expand\n        # TODO: simplified expanders, use CSS expander code instead.\n        if font := child.attrib.pop('font', None):\n            parts = font.strip().split(maxsplit=1)\n            if len(parts) == 2:\n                child.attrib['font-size'] = parts[0]\n                child.attrib['font-family'] = parts[1]\n\n        # Replace 'currentColor' value\n        for key in COLOR_ATTRIBUTES:\n            if child.get(key) == 'currentColor':\n                child.attrib[key] = child.get('color', 'black')\n\n        # Handle 'inherit' values\n        for key, value in child.attrib.copy().items():\n            if value == 'inherit':\n                value = self.get(key)\n                if value is None:\n                    del child.attrib[key]\n                else:\n                    child.attrib[key] = value\n\n        # Fix text in text tags\n        if child.tag in ('text', 'textPath', 'a'):\n            children, _ = child.text_children(\n                wrapper, trailing_space=True, text_root=True)\n            child._wrapper.etree_children = [\n                child._etree_node for child in children]\n\n    def __iter__(self):\n        \"\"\"Yield node children, handling cascade.\"\"\"\n        if self._children is None:\n            children = []\n            for wrapper in self._wrapper:\n                child = Node(wrapper, self._style)\n                self.cascade(child)\n                children.append(child)\n            self._children = children\n        return iter(self._children)\n\n    def get_viewbox(self):\n        \"\"\"Get node viewBox as a tuple of floats.\"\"\"\n        viewbox = self.get('viewBox')\n        if viewbox:\n            return tuple(float(number) for number in normalize(viewbox).split())\n\n    def get_href(self, base_url):\n        \"\"\"Get the href attribute, with or without a namespace.\"\"\"\n        for attr_name in ('{http://www.w3.org/1999/xlink}href', 'href'):\n            if url := get_url_attribute(self, attr_name, base_url, allow_relative=True):\n                return url\n\n    def del_href(self):\n        \"\"\"Remove the href attributes, with or without a namespace.\"\"\"\n        for attr_name in ('{http://www.w3.org/1999/xlink}href', 'href'):\n            self.attrib.pop(attr_name, None)\n\n    @staticmethod\n    def process_whitespace(string, preserve):\n        \"\"\"Replace newlines by spaces, and merge spaces if not preserved.\"\"\"\n        # TODO: should be merged with build.process_whitespace\n        if not string:\n            return ''\n        if preserve:\n            return re.sub('[\\n\\r\\t]', ' ', string)\n        else:\n            string = re.sub('[\\n\\r]', '', string)\n            string = string.replace('\\t', ' ')\n            return re.sub(' +', ' ', string)\n\n    def get_child(self, id_):\n        \"\"\"Get a child with given id in the whole child tree.\"\"\"\n        if self._etree_node.find(f'.//*[@id=\"{id_}\"]') is None:\n            return\n        for child in self:\n            if child.get('id') == id_:\n                return child\n            grandchild = child.get_child(id_)\n            if grandchild:\n                return grandchild\n\n    def text_children(self, element, trailing_space, text_root=False):\n        \"\"\"Handle text node by fixing whitespaces and flattening tails.\"\"\"\n        children = []\n        space = '{http://www.w3.org/XML/1998/namespace}space'\n        preserve = self.get(space) == 'preserve'\n        self._etree_node.text = self.process_whitespace(\n            element.etree_element.text, preserve)\n        if trailing_space and not preserve:\n            self._etree_node.text = self.text.lstrip(' ')\n\n        original_rotate = [\n            float(i) for i in\n            normalize(self.get('rotate')).strip().split(' ') if i]\n        rotate = original_rotate.copy()\n        if original_rotate:\n            self.pop_rotation(original_rotate, rotate)\n        if self.text:\n            trailing_space = self.text.endswith(' ')\n        element_children = tuple(element.iter_children())\n        for child_element in element_children:\n            child = child_element.etree_element\n            if child.tag in ('{http://www.w3.org/2000/svg}tref', 'tref'):\n                child_node = Node(child_element, self._style)\n                child_node._etree_node.tag = 'tspan'\n                # Retrieve the referenced node and get its flattened text\n                # and remove the node children.\n                child = child_node._etree_node\n                child._etree_node.text = child.flatten()\n                child_element = ElementWrapper.from_xml_root(child)\n            else:\n                child_node = Node(child_element, self._style)\n            child_preserve = child_node.get(space) == 'preserve'\n            child_node._etree_node.text = self.process_whitespace(\n                child.text, child_preserve)\n            child_node.children, trailing_space = child_node.text_children(\n                child_element, trailing_space)\n            trailing_space = child_node.text.endswith(' ')\n            if original_rotate and 'rotate' not in child_node:\n                child_node.pop_rotation(original_rotate, rotate)\n            children.append(child_node)\n            tail = self.process_whitespace(child.tail, preserve)\n            if text_root and child_element is element_children[-1]:\n                if not preserve:\n                    tail = tail.rstrip(' ')\n            if tail:\n                anonymous_etree = ElementTree.Element(\n                    '{http://www.w3.org/2000/svg}tspan')\n                anonymous = Node(\n                    ElementWrapper.from_xml_root(anonymous_etree), self._style)\n                anonymous._etree_node.text = tail\n                if original_rotate:\n                    anonymous.pop_rotation(original_rotate, rotate)\n                if trailing_space and not preserve:\n                    anonymous._etree_node.text = anonymous.text.lstrip(' ')\n                if anonymous.text:\n                    trailing_space = anonymous.text.endswith(' ')\n                children.append(anonymous)\n\n        if text_root and not children and not preserve:\n            self._etree_node.text = self.text.rstrip(' ')\n\n        return children, trailing_space\n\n    def flatten(self):\n        \"\"\"Flatten text in node and in its children.\"\"\"\n        flattened_text = [self.text or '']\n        for child in list(self):\n            flattened_text.append(child.flatten())\n            flattened_text.append(child.tail or '')\n            self.remove(child)\n        return ''.join(flattened_text)\n\n    def pop_rotation(self, original_rotate, rotate):\n        \"\"\"Merge nested letter rotations.\"\"\"\n        self.attrib['rotate'] = ' '.join(\n            str(rotate.pop(0) if rotate else original_rotate[-1])\n            for i in range(len(self.text)))\n\n    def override_iter(self, iterator):\n        \"\"\"Override node’s children iterator.\"\"\"\n        # As special methods are bound to classes and not instances, we have to\n        # create and assign a new type.\n        self.__class__ = type(\n            'Node', (Node,), {'__iter__': lambda _: iterator})\n\n    def set_svg_size(self, svg, concrete_width, concrete_height):\n        \"\"\"\"Set SVG concrete and inner widths and heights from svg node.\"\"\"\n        svg.concrete_width = concrete_width\n        svg.concrete_height = concrete_height\n        svg.normalized_diagonal = hypot(concrete_width, concrete_height) / sqrt(2)\n\n        if viewbox := self.get_viewbox():\n            svg.inner_width, svg.inner_height = viewbox[2], viewbox[3]\n        else:\n            svg.inner_width, svg.inner_height = svg.concrete_width, svg.concrete_height\n        svg.inner_diagonal = hypot(svg.inner_width, svg.inner_height) / sqrt(2)\n\n\nclass LazyDefs:\n    def __init__(self, name, svg):\n        self._name = name\n        self._svg = svg\n        self._data = {}\n\n    def __getitem__(self, name):\n        return self.get(name)\n\n    def get(self, name):\n        if not name:\n            return\n        if name in self._data:\n            return self._data[name]\n        node = self._svg.tree.get_child(name)\n        if node is not None and self._name in node.tag.lower():\n            self._data[name] = node\n            if self._name in ('gradient', 'pattern'):\n                self._svg.inherit_element(node, self)\n        else:\n            self._data[name] = None\n        return self._data[name]\n\n    def __contains__(self, name):\n        return self.get(name)\n\n\nclass SVG:\n    \"\"\"An SVG document.\"\"\"\n\n    def __init__(self, tree, url, font_config, url_fetcher=None):\n        wrapper = ElementWrapper.from_xml_root(tree)\n        style = parse_stylesheets(wrapper, url, font_config, url_fetcher)\n        self.tree = Node(wrapper, style)\n        self.font_config = font_config\n        self.url_fetcher = url_fetcher\n        self.url = url\n\n        self.filters = LazyDefs('filter', self)\n        self.gradients = LazyDefs('gradient', self)\n        self.images = LazyDefs('image', self)\n        self.markers = LazyDefs('marker', self)\n        self.masks = LazyDefs('mask', self)\n        self.patterns = LazyDefs('pattern', self)\n        self.paths = LazyDefs('path', self)\n        self.symbols = LazyDefs('symbol', self)\n\n        self.use_cache = {}\n\n        self.cursor_position = [0, 0]\n        self.cursor_d_position = [0, 0]\n        self.text_path_width = 0\n\n        self.tree.cascade(self.tree)\n\n    def get_intrinsic_size(self, font_size):\n        \"\"\"Get intrinsic size of the image.\"\"\"\n        intrinsic_width = self.tree.get('width', '100%')\n        if '%' in intrinsic_width:\n            intrinsic_width = None\n        else:\n            intrinsic_width = size(intrinsic_width, font_size)\n\n        intrinsic_height = self.tree.get('height', '100%')\n        if '%' in intrinsic_height:\n            intrinsic_height = None\n        else:\n            intrinsic_height = size(intrinsic_height, font_size)\n\n        return intrinsic_width, intrinsic_height\n\n    def get_viewbox(self):\n        \"\"\"Get document viewBox as a tuple of floats.\"\"\"\n        return self.tree.get_viewbox()\n\n    def point(self, x, y, font_size):\n        \"\"\"Compute size of an x/y or width/height couple.\"\"\"\n        return (\n            size(x, font_size, self.inner_width),\n            size(y, font_size, self.inner_height))\n\n    def length(self, length, font_size):\n        \"\"\"Compute size of an arbirtary attribute.\"\"\"\n        return size(length, font_size, self.inner_diagonal)\n\n    def draw(self, stream, concrete_width, concrete_height, base_url, context):\n        \"\"\"Draw image on a stream.\"\"\"\n        self.stream = stream\n\n        self.tree.set_svg_size(self, concrete_width, concrete_height)\n\n        self.base_url = base_url\n        self.context = context\n\n        self.draw_node(self.tree, size('12pt'))\n\n    def draw_node(self, node, font_size, fill_stroke=True):\n        \"\"\"Draw a node.\"\"\"\n        if node.tag == 'defs':\n            return\n\n        # Update font size\n        font_size = size(node.get('font-size', '1em'), font_size, font_size)\n\n        original_streams = []\n\n        call_fill_stroke = fill_stroke and node.tag in (\n            'circle', 'ellipse', 'line', 'path', 'polyline', 'polygon', 'rect')\n\n        if fill_stroke:\n            self.stream.push_state()\n\n        # Apply filters\n        filter_ = self.filters.get(parse_url(node.get('filter')).fragment)\n        if filter_:\n            apply_filters(self, node, filter_, font_size)\n\n        # Apply transform attribute\n        self.transform(node, font_size)\n\n        # Create substream for opacity\n        opacity = alpha_value(node.get('opacity', 1))\n        if fill_stroke and 0 <= opacity < 1:\n            original_streams.append(self.stream)\n            self.stream = self.stream.add_group(0, 0, 0, 0)  # BBox set after drawing\n\n        # Set graphical state\n        if call_fill_stroke:\n            self.set_graphical_state(node, font_size)\n\n        # Clip\n        clip_path = parse_url(node.get('clip-path')).fragment\n        if clip_path and clip_path in self.paths:\n            old_ctm = self.stream.ctm\n            clip_path = self.paths[clip_path]\n            if clip_path.get('clipPathUnits') == 'objectBoundingBox':\n                x, y = self.point(node.get('x'), node.get('y'), font_size)\n                width, height = self.point(\n                    node.get('width'), node.get('height'), font_size)\n                self.stream.transform(a=width, d=height, e=x, f=y)\n            original_tag = clip_path._etree_node.tag\n            clip_path._etree_node.tag = 'g'\n            self.draw_node(clip_path, font_size, fill_stroke=False)\n            clip_path._etree_node.tag = original_tag\n            # At least set the clipping area to an empty path, so that it’s\n            # totally clipped when the clipping path is empty.\n            self.stream.rectangle(0, 0, 0, 0)\n            self.stream.clip()\n            self.stream.end()\n            new_ctm = self.stream.ctm\n            if new_ctm.determinant:\n                self.stream.transform(*(old_ctm @ new_ctm.invert).values)\n\n        # Handle text anchor and set text bounding box\n        text_anchor_shift = False\n        if node.display and TAGS.get(node.tag) == text:\n            if (text_anchor := node.get('text-anchor')) in ('middle', 'end'):\n                text_anchor_shift = True\n                group = self.stream.add_group(0, 0, 0, 0)  # BBox set after drawing\n                original_streams.append(self.stream)\n                self.stream = group\n            node.text_bounding_box = EMPTY_BOUNDING_BOX\n\n        # Save concrete size of root svg tag\n        if node.tag == 'svg':\n            concrete_width = self.concrete_width\n            concrete_height = self.concrete_height\n\n        # Draw node\n        if node.visible and node.tag in TAGS:\n            with suppress(PointError):\n                TAGS[node.tag](self, node, font_size)\n\n        # Draw node children\n        if node.display and node.tag not in DEF_TYPES:\n            for child in node:\n                new_chunk = text_anchor_shift and (\n                    child.tag == 'text' or 'x' in child.attrib or 'y' in child.attrib)\n                if new_chunk:\n                    new_stream = self.stream\n                    self.stream = original_streams[-1]\n                self.draw_node(child, font_size, fill_stroke)\n                if new_chunk:\n                    self.stream = new_stream\n                visible_text_child = (\n                    TAGS.get(node.tag) == text and\n                    TAGS.get(child.tag) == text and\n                    child.visible)\n                if visible_text_child:\n                    if not is_valid_bounding_box(child.text_bounding_box):\n                        continue\n                    x1, y1 = child.text_bounding_box[:2]\n                    x2 = x1 + child.text_bounding_box[2]\n                    y2 = y1 + child.text_bounding_box[3]\n                    node.text_bounding_box = extend_bounding_box(\n                        node.text_bounding_box, ((x1, y1), (x2, y2)))\n\n        # Restore concrete and inner size of root svg tag\n        if node.tag == 'svg':\n            self.tree.set_svg_size(svg, concrete_width, concrete_height)\n\n        # Handle text anchor\n        if text_anchor_shift:\n            group_id = self.stream.id\n            self.stream = original_streams.pop()\n            self.stream.push_state()\n            if is_valid_bounding_box(node.text_bounding_box):\n                x, y, width, height = node.text_bounding_box\n                # Add extra space to include ink extents\n                group.extra['BBox'][:] = (\n                    x - font_size, y - font_size,\n                    x + width + font_size, y + height + font_size)\n                x_align = width / 2 if text_anchor == 'middle' else width\n                if node.tag == 'text' or 'x' in node.attrib or 'y' in node.attrib:\n                    self.stream.transform(e=-x_align)\n            self.stream.draw_x_object(group_id)\n            self.stream.pop_state()\n\n        # Apply mask\n        mask = self.masks.get(parse_url(node.get('mask')).fragment)\n        if mask:\n            paint_mask(self, node, mask, opacity)\n\n        # Fill and stroke\n        if call_fill_stroke:\n            self.fill_stroke(node, font_size)\n\n        # Draw markers\n        self.draw_markers(node, font_size, fill_stroke)\n\n        # Apply opacity stream and restore original stream\n        if fill_stroke and 0 <= opacity < 1:\n            box = self.calculate_bounding_box(node, font_size)\n            if not is_valid_bounding_box(box):\n                box = (0, 0, self.inner_width, self.inner_height)\n            x, y, width, height = box\n            self.stream.extra['BBox'][:] = x, y, x + width, y + height\n\n            group_id = self.stream.id\n            self.stream = original_streams.pop()\n            self.stream.set_alpha(opacity, stroke=True, fill=True)\n            self.stream.draw_x_object(group_id)\n\n        # Clean text tag\n        if node.tag == 'text':\n            self.cursor_position = [0, 0]\n            self.cursor_d_position = [0, 0]\n            self.text_path_width = 0\n\n        if fill_stroke:\n            self.stream.pop_state()\n\n    def draw_markers(self, node, font_size, fill_stroke):\n        \"\"\"Draw markers defined in a node.\"\"\"\n        if not node.vertices:\n            return\n\n        markers = {}\n        common_marker = parse_url(node.get('marker')).fragment\n        for position in ('start', 'mid', 'end'):\n            attribute = f'marker-{position}'\n            if attribute in node.attrib:\n                markers[position] = parse_url(node.attrib[attribute]).fragment\n            else:\n                markers[position] = common_marker\n\n        angle1, angle2 = None, None\n        position = 'start'\n\n        while node.vertices:\n            # Calculate position and angle\n            point = node.vertices.pop(0)\n            angles = node.vertices.pop(0) if node.vertices else None\n            if angles:\n                if position == 'start':\n                    angle = pi - angles[0]\n                else:\n                    angle = (angle2 + pi - angles[0]) / 2\n                angle1, angle2 = angles\n            else:\n                angle = angle2\n                position = 'end'\n\n            # Draw marker\n            if not (marker_node := self.markers.get(markers[position])):\n                position = 'mid' if angles else 'start'\n                continue\n\n            # Calculate position, scale and clipping\n            translate_x, translate_y = self.point(\n                marker_node.get('refX'), marker_node.get('refY'),\n                font_size)\n            marker_width, marker_height = self.point(\n                marker_node.get('markerWidth', 3),\n                marker_node.get('markerHeight', 3),\n                font_size)\n            if 'viewBox' in marker_node.attrib:\n                scale_x, scale_y, _, _ = preserve_ratio(\n                    self, marker_node, font_size, marker_width, marker_height)\n\n                clip_x, clip_y, viewbox_width, viewbox_height = (\n                    marker_node.get_viewbox())\n\n                align = marker_node.get(\n                    'preserveAspectRatio', 'xMidYMid').split(' ')[0]\n                if align == 'none':\n                    x_position = y_position = 'min'\n                else:\n                    x_position = align[1:4].lower()\n                    y_position = align[5:].lower()\n\n                if x_position == 'mid':\n                    clip_x += (viewbox_width - marker_width / scale_x) / 2\n                elif x_position == 'max':\n                    clip_x += viewbox_width - marker_width / scale_x\n\n                if y_position == 'mid':\n                    clip_y += (\n                        viewbox_height - marker_height / scale_y) / 2\n                elif y_position == 'max':\n                    clip_y += viewbox_height - marker_height / scale_y\n\n                clip_box = (\n                    clip_x, clip_y,\n                    marker_width / scale_x, marker_height / scale_y)\n            else:\n                scale_x = scale_y = 1\n                clip_box = (0, 0, marker_width, marker_height)\n\n            # Scale\n            if marker_node.get('markerUnits') != 'userSpaceOnUse':\n                scale = self.length(node.get('stroke-width', 1), font_size)\n                scale_x *= scale\n                scale_y *= scale\n\n            # Override angle\n            node_angle = marker_node.get('orient', 0)\n            if node_angle not in ('auto', 'auto-start-reverse'):\n                angle = radians(float(node_angle))\n            elif node_angle == 'auto-start-reverse' and position == 'start':\n                angle += radians(180)\n\n            # Draw marker path\n            for child in marker_node:\n                self.stream.push_state()\n\n                self.stream.transform(\n                    scale_x * cos(angle), scale_x * sin(angle),\n                    -scale_y * sin(angle), scale_y * cos(angle),\n                    *point)\n                self.stream.transform(e=-translate_x, f=-translate_y)\n\n                overflow = marker_node.get('overflow', 'hidden')\n                if overflow in ('hidden', 'scroll'):\n                    self.stream.rectangle(*clip_box)\n                    self.stream.clip()\n                    self.stream.end()\n\n                self.draw_node(child, font_size, fill_stroke)\n                self.stream.pop_state()\n\n            position = 'mid' if angles else 'start'\n\n    @staticmethod\n    def get_paint(value):\n        \"\"\"Get paint fill or stroke attribute with a color or a URL.\"\"\"\n        if not value or value == 'none':\n            return None, None\n\n        value = value.strip()\n        match = re.compile(r'(url\\(.+\\)) *(.*)').search(value)\n        if match:\n            source = parse_url(match.group(1)).fragment\n            color = match.group(2) or None\n        else:\n            source = None\n            color = value or None\n\n        return source, color\n\n    def set_graphical_state(self, node, font_size, text=False):\n        \"\"\"Set stroke and fill colors, and line options.\"\"\"\n        # Get fill data\n        fill_source, fill_color = self.get_paint(node.get('fill', 'black'))\n        fill_opacity = alpha_value(node.get('fill-opacity', 1))\n        fill_in_gradient = fill_source in self.gradients\n        fill_in_pattern = fill_source in self.patterns\n        if fill_color and not (fill_in_gradient or fill_in_pattern):\n            stream_color = color(fill_color)\n            stream_color.alpha *= fill_opacity\n            self.stream.set_color(stream_color)\n\n        # Get stroke data\n        stroke_source, stroke_color = self.get_paint(node.get('stroke'))\n        stroke_opacity = alpha_value(node.get('stroke-opacity', 1))\n        stroke_in_gradient = stroke_source in self.gradients\n        stroke_in_pattern = stroke_source in self.patterns\n        if stroke_color and not (stroke_in_gradient or stroke_in_pattern):\n            stream_color = color(stroke_color)\n            stream_color.alpha *= stroke_opacity\n            self.stream.set_color(stream_color, stroke=True)\n        stroke_width = self.length(node.get('stroke-width', '1px'), font_size)\n        if stroke_width:\n            self.stream.set_line_width(stroke_width)\n\n        # Apply dash array\n        dash_array = tuple(\n            self.length(value, font_size) for value in\n            normalize(node.get('stroke-dasharray')).split() if value != 'none')\n        dash_condition = (\n            dash_array and\n            not all(value == 0 for value in dash_array) and\n            not any(value < 0 for value in dash_array))\n        if dash_condition:\n            offset = self.length(node.get('stroke-dashoffset'), font_size)\n            if offset < 0:\n                sum_dashes = sum(float(value) for value in dash_array)\n                offset = sum_dashes - abs(offset) % sum_dashes\n            self.stream.set_dash(dash_array, offset)\n\n        # Apply line cap\n        line_cap = node.get('stroke-linecap', 'butt')\n        if line_cap == 'round':\n            line_cap = 1\n        elif line_cap == 'square':\n            line_cap = 2\n        else:\n            line_cap = 0\n        self.stream.set_line_cap(line_cap)\n\n        # Apply line join\n        line_join = node.get('stroke-linejoin', 'miter')\n        if line_join == 'round':\n            line_join = 1\n        elif line_join == 'bevel':\n            line_join = 2\n        else:\n            line_join = 0\n        self.stream.set_line_join(line_join)\n\n        # Apply miter limit\n        miter_limit = float(node.get('stroke-miterlimit', 4))\n        if miter_limit < 0:\n            miter_limit = 4\n        self.stream.set_miter_limit(miter_limit)\n\n    def fill_stroke(self, node, font_size, text=False):\n        \"\"\"Paint fill and stroke for a node.\"\"\"\n        # Get fill data\n        fill_source, fill_color = self.get_paint(node.get('fill', 'black'))\n        fill_opacity = alpha_value(node.get('fill-opacity', 1))\n        fill_drawn = draw_gradient_or_pattern(\n            self, node, fill_source, font_size, fill_opacity, stroke=False)\n        fill = fill_color or fill_drawn\n\n        # Get stroke data\n        stroke_source, stroke_color = self.get_paint(node.get('stroke'))\n        stroke_opacity = alpha_value(node.get('stroke-opacity', 1))\n        stroke_drawn = draw_gradient_or_pattern(\n            self, node, stroke_source, font_size, stroke_opacity, stroke=True)\n        stroke_width = self.length(node.get('stroke-width', '1px'), font_size)\n        stroke = (stroke_color or stroke_drawn) and stroke_width\n\n        # Fill and stroke\n        even_odd = node.get('fill-rule') == 'evenodd'\n        if text:\n            if stroke and fill:\n                text_rendering = 2\n            elif stroke:\n                text_rendering = 1\n            elif fill:\n                text_rendering = 0\n            else:\n                text_rendering = 3\n            self.stream.set_text_rendering(text_rendering)\n        else:\n            if fill and stroke:\n                self.stream.fill_and_stroke(even_odd)\n            elif stroke:\n                self.stream.stroke()\n            elif fill:\n                self.stream.fill(even_odd)\n            else:\n                self.stream.end()\n\n    def transform(self, node, font_size):\n        \"\"\"Apply a transformation string to the node.\"\"\"\n        transform_origin = node.get('transform-origin')\n        transform_string = node.get('transform')\n        if not transform_string:\n            return\n\n        matrix = transform(\n            transform_string, transform_origin, font_size, self.inner_diagonal)\n        if matrix.determinant:\n            self.stream.transform(*matrix.values)\n\n    def inherit_element(self, element, defs):\n        \"\"\"Recursively handle inheritance of defined element.\"\"\"\n        href = element.get_href(self.url)\n        if not href:\n            return\n        element.del_href()\n        parent = defs.get(parse_url(href).fragment)\n        if not parent:\n            return\n        self.inherit_element(parent, defs)\n        for key, value in parent.attrib.items():\n            if key not in element.attrib:\n                element.attrib[key] = value\n        if next(iter(element), None) is None:\n            element.override_iter(parent.__iter__())\n\n    def calculate_bounding_box(self, node, font_size, stroke=True):\n        \"\"\"Calculate the bounding box of a node.\"\"\"\n        if stroke or node.bounding_box is None:\n            box = bounding_box(self, node, font_size, stroke)\n            if is_valid_bounding_box(box) and 0 not in box[2:]:\n                if stroke:\n                    return box\n                node.bounding_box = box\n        return node.bounding_box\n\n\nclass Pattern(SVG):\n    \"\"\"SVG node applied as a pattern.\"\"\"\n    def __init__(self, tree, svg):\n        super().__init__(tree._etree_node, svg.url, svg.font_config, svg.url_fetcher)\n        self.svg = svg\n        self.tree = tree\n\n    def draw_node(self, node, font_size, fill_stroke=True):\n        # Store the original tree in self.tree when calling draw(), so that we\n        # can reach defs outside the pattern\n        if node == self.tree:\n            self.tree = self.svg.tree\n        super().draw_node(node, font_size, fill_stroke=True)\n"
  },
  {
    "path": "weasyprint/svg/bounding_box.py",
    "content": "\"\"\"Calculate bounding boxes of SVG tags.\"\"\"\n\nfrom math import atan, atan2, cos, inf, isinf, pi, radians, sin, sqrt, tan\n\nfrom .path import PATH_LETTERS\nfrom .utils import normalize, point\n\nEMPTY_BOUNDING_BOX = inf, inf, 0, 0\n\n\ndef bounding_box(svg, node, font_size, stroke):\n    \"\"\"Bounding box for any node.\"\"\"\n    if node.tag not in BOUNDING_BOX_METHODS:\n        return EMPTY_BOUNDING_BOX\n    box = BOUNDING_BOX_METHODS[node.tag](svg, node, font_size)\n    if not is_valid_bounding_box(box):\n        return EMPTY_BOUNDING_BOX\n    if stroke and node.tag != 'g' and any(svg.get_paint(node.get('stroke'))):\n        stroke_width = svg.length(node.get('stroke-width', '1px'), font_size)\n        box = (\n            box[0] - stroke_width / 2, box[1] - stroke_width / 2,\n            box[2] + stroke_width, box[3] + stroke_width)\n    return box\n\n\ndef bounding_box_rect(svg, node, font_size):\n    \"\"\"Bounding box for rect node.\"\"\"\n    x, y = svg.point(node.get('x'), node.get('y'), font_size)\n    width, height = svg.point(\n        node.get('width'), node.get('height'), font_size)\n    return x, y, width, height\n\n\ndef bounding_box_circle(svg, node, font_size):\n    \"\"\"Bounding box for circle node.\"\"\"\n    cx, cy = svg.point(node.get('cx'), node.get('cy'), font_size)\n    r = svg.length(node.get('r'), font_size)\n    return cx - r, cy - r, 2 * r, 2 * r\n\n\ndef bounding_box_ellipse(svg, node, font_size):\n    \"\"\"Bounding box for ellipse node.\"\"\"\n    rx, ry = svg.point(node.get('rx'), node.get('ry'), font_size)\n    cx, cy = svg.point(node.get('cx'), node.get('cy'), font_size)\n    return cx - rx, cy - ry, 2 * rx, 2 * ry\n\n\ndef bounding_box_line(svg, node, font_size):\n    \"\"\"Bounding box for line node.\"\"\"\n    x1, y1 = svg.point(node.get('x1'), node.get('y1'), font_size)\n    x2, y2 = svg.point(node.get('x2'), node.get('y2'), font_size)\n    x, y = min(x1, x2), min(y1, y2)\n    width, height = max(x1, x2) - x, max(y1, y2) - y\n    return x, y, width, height\n\n\ndef bounding_box_polyline(svg, node, font_size):\n    \"\"\"Bounding box for polyline node.\"\"\"\n    bounding_box = EMPTY_BOUNDING_BOX\n    points = []\n    normalized_points = normalize(node.get('points', ''))\n    while normalized_points:\n        x, y, normalized_points = point(svg, normalized_points, font_size)\n        points.append((x, y))\n    return extend_bounding_box(bounding_box, points)\n\n\ndef bounding_box_path(svg, node, font_size):\n    \"\"\"Bounding box for path node.\"\"\"\n    path_data = node.get('d', '')\n\n    # Normalize path data for correct parsing\n    for letter in PATH_LETTERS:\n        path_data = path_data.replace(letter, f' {letter} ')\n    path_data = normalize(path_data)\n\n    bounding_box = EMPTY_BOUNDING_BOX\n    previous_x = 0\n    previous_y = 0\n    letter = 'M'    # Move as default\n    while path_data:\n        path_data = path_data.strip()\n        if path_data.split(' ', 1)[0] in PATH_LETTERS:\n            letter, path_data = (f'{path_data} ').split(' ', 1)\n\n        if letter in 'aA':\n            # Elliptical arc curve\n            rx, ry, path_data = point(svg, path_data, font_size)\n            rotation, path_data = path_data.split(' ', 1)\n            rotation = radians(float(rotation))\n\n            # The large and sweep values are not always separated from the\n            # following values, here is the crazy parser\n            large, path_data = path_data[0], path_data[1:].strip()\n            while not large[-1].isdigit():\n                large, path_data = large + path_data[0], path_data[1:].strip()\n            sweep, path_data = path_data[0], path_data[1:].strip()\n            while not sweep[-1].isdigit():\n                sweep, path_data = sweep + path_data[0], path_data[1:].strip()\n\n            large, sweep = bool(int(large)), bool(int(sweep))\n\n            x, y, path_data = point(svg, path_data, font_size)\n\n            # Relative coordinate, convert to absolute\n            if letter == 'a':\n                x += previous_x\n                y += previous_y\n\n            # Extend bounding box with start and end coordinates\n            arc_bounding_box = _bounding_box_elliptical_arc(\n                previous_x, previous_y, rx, ry, rotation, large, sweep, x, y)\n            x1, y1, width, height = arc_bounding_box\n            x2 = x1 + width\n            y2 = y1 + height\n            points = (x1, y1), (x2, y2)\n            bounding_box = extend_bounding_box(bounding_box, points)\n            previous_x = x\n            previous_y = y\n\n        elif letter in 'cC':\n            # Curve\n            x1, y1, path_data = point(svg, path_data, font_size)\n            x2, y2, path_data = point(svg, path_data, font_size)\n            x, y, path_data = point(svg, path_data, font_size)\n\n            # Relative coordinates, convert to absolute\n            if letter == 'c':\n                x1 += previous_x\n                y1 += previous_y\n                x2 += previous_x\n                y2 += previous_y\n                x += previous_x\n                y += previous_y\n\n            # Extend bounding box with all coordinates\n            bounding_box = extend_bounding_box(\n                bounding_box, ((x1, y1), (x2, y2), (x, y)))\n            previous_x = x\n            previous_y = y\n\n        elif letter in 'hH':\n            # Horizontal line\n            x, path_data = (f'{path_data} ').split(' ', 1)\n            x, _ = svg.point(x, 0, font_size)\n\n            # Relative coordinate, convert to absolute\n            if letter == 'h':\n                x += previous_x\n\n            # Extend bounding box with coordinate\n            bounding_box = extend_bounding_box(\n                bounding_box, ((x, previous_y),))\n            previous_x = x\n\n        elif letter in 'lLmMtT':\n            # Line/Move/Smooth quadratic curve\n            x, y, path_data = point(svg, path_data, font_size)\n\n            # Relative coordinate, convert to absolute\n            if letter in 'lmt':\n                x += previous_x\n                y += previous_y\n\n            # Extend bounding box with coordinate\n            bounding_box = extend_bounding_box(bounding_box, ((x, y),))\n            previous_x = x\n            previous_y = y\n\n        elif letter in 'qQsS':\n            # Quadratic curve/Smooth curve\n            x1, y1, path_data = point(svg, path_data, font_size)\n            x, y, path_data = point(svg, path_data, font_size)\n\n            # Relative coordinates, convert to absolute\n            if letter in 'qs':\n                x1 += previous_x\n                y1 += previous_y\n                x += previous_x\n                y += previous_y\n\n            # Extend bounding box with coordinates\n            bounding_box = extend_bounding_box(\n                bounding_box, ((x1, y1), (x, y)))\n            previous_x = x\n            previous_y = y\n\n        elif letter in 'vV':\n            # Vertical line\n            y, path_data = (f'{path_data} ').split(' ', 1)\n            _, y = svg.point(0, y, font_size)\n\n            # Relative coordinate, convert to absolute\n            if letter == 'v':\n                y += previous_y\n\n            # Extend bounding box with coordinate\n            bounding_box = extend_bounding_box(\n                bounding_box, ((previous_x, y),))\n            previous_y = y\n\n        path_data = path_data.strip()\n\n    return bounding_box\n\n\ndef bounding_box_text(svg, node, font_size):\n    \"\"\"Bounding box for text node.\"\"\"\n    return getattr(node, 'text_bounding_box', None)\n\n\ndef bounding_box_g(svg, node, font_size):\n    \"\"\"Bounding box for g node.\"\"\"\n    bounding_box = EMPTY_BOUNDING_BOX\n    for child in node:\n        child_bounding_box = svg.calculate_bounding_box(child, font_size)\n        if is_valid_bounding_box(child_bounding_box):\n            minx, miny, width, height = child_bounding_box\n            maxx, maxy = minx + width, miny + height\n            bounding_box = extend_bounding_box(\n                bounding_box, ((minx, miny), (maxx, maxy)))\n    return bounding_box\n\n\ndef bounding_box_use(svg, node, font_size):\n    \"\"\"Bounding box for use node.\"\"\"\n    from .defs import get_use_tree\n\n    if (tree := get_use_tree(svg, node, font_size)) is None:\n        return EMPTY_BOUNDING_BOX\n    else:\n        x, y = svg.point(node.get('x'), node.get('y'), font_size)\n        box = bounding_box(svg, tree, font_size, True)\n        return box[0] + x, box[1] + y, box[2], box[3]\n\n\ndef _bounding_box_elliptical_arc(x1, y1, rx, ry, phi, large, sweep, x, y):\n    \"\"\"Bounding box of an elliptical arc in path node.\"\"\"\n    rx, ry = abs(rx), abs(ry)\n    if 0 in (rx, ry):\n        return min(x, x1), min(y, y1), abs(x - x1), abs(y - y1)\n\n    x1prime = cos(phi) * (x1 - x) / 2 + sin(phi) * (y1 - y) / 2\n    y1prime = -sin(phi) * (x1 - x) / 2 + cos(phi) * (y1 - y) / 2\n\n    radicant = (\n        rx ** 2 * ry ** 2 - rx ** 2 * y1prime ** 2 - ry ** 2 * x1prime ** 2)\n    radicant /= rx ** 2 * y1prime ** 2 + ry ** 2 * x1prime ** 2\n    cxprime = cyprime = 0\n\n    if radicant < 0:\n        ratio = rx / ry\n        radicant = y1prime ** 2 + x1prime ** 2 / ratio ** 2\n        if radicant < 0:\n            return min(x, x1), min(y, y1), abs(x - x1), abs(y - y1)\n        ry = sqrt(radicant)\n        rx = ratio * ry\n    else:\n        factor = (-1 if large == sweep else 1) * sqrt(radicant)\n\n        cxprime = factor * rx * y1prime / ry\n        cyprime = -factor * ry * x1prime / rx\n\n    cx = cxprime * cos(phi) - cyprime * sin(phi) + (x1 + x) / 2\n    cy = cxprime * sin(phi) + cyprime * cos(phi) + (y1 + y) / 2\n\n    if phi in (0, pi):\n        minx = cx - rx\n        tminx = atan2(0, -rx)\n        maxx = cx + rx\n        tmaxx = atan2(0, rx)\n        miny = cy - ry\n        tminy = atan2(-ry, 0)\n        maxy = cy + ry\n        tmaxy = atan2(ry, 0)\n    elif phi in (pi / 2, 3 * pi / 2):\n        minx = cx - ry\n        tminx = atan2(0, -ry)\n        maxx = cx + ry\n        tmaxx = atan2(0, ry)\n        miny = cy - rx\n        tminy = atan2(-rx, 0)\n        maxy = cy + rx\n        tmaxy = atan2(rx, 0)\n    else:\n        tminx = -atan(ry * tan(phi) / rx)\n        tmaxx = pi - atan(ry * tan(phi) / rx)\n        minx = cx + rx * cos(tminx) * cos(phi) - ry * sin(tminx) * sin(phi)\n        maxx = cx + rx * cos(tmaxx) * cos(phi) - ry * sin(tmaxx) * sin(phi)\n        if minx > maxx:\n            minx, maxx = maxx, minx\n            tminx, tmaxx = tmaxx, tminx\n        tmp_y = cy + rx * cos(tminx) * sin(phi) + ry * sin(tminx) * cos(phi)\n        tminx = atan2(minx - cx, tmp_y - cy)\n        tmp_y = cy + rx * cos(tmaxx) * sin(phi) + ry * sin(tmaxx) * cos(phi)\n        tmaxx = atan2(maxx - cx, tmp_y - cy)\n\n        tminy = atan(ry / (tan(phi) * rx))\n        tmaxy = atan(ry / (tan(phi) * rx)) + pi\n        miny = cy + rx * cos(tminy) * sin(phi) + ry * sin(tminy) * cos(phi)\n        maxy = cy + rx * cos(tmaxy) * sin(phi) + ry * sin(tmaxy) * cos(phi)\n        if miny > maxy:\n            miny, maxy = maxy, miny\n            tminy, tmaxy = tmaxy, tminy\n        tmp_x = cx + rx * cos(tminy) * cos(phi) - ry * sin(tminy) * sin(phi)\n        tminy = atan2(tmp_x - cx, miny - cy)\n        tmp_x = cx + rx * cos(tmaxy) * cos(phi) - ry * sin(tmaxy) * sin(phi)\n        tmaxy = atan2(maxy - cy, tmp_x - cx)\n\n    angle1 = atan2(y1 - cy, x1 - cx)\n    angle2 = atan2(y - cy, x - cx)\n\n    if not sweep:\n        angle1, angle2 = angle2, angle1\n\n    other_arc = False\n    if angle1 > angle2:\n        angle1, angle2 = angle2, angle1\n        other_arc = True\n\n    if ((not other_arc and (angle1 > tminx or angle2 < tminx)) or\n            (other_arc and not (angle1 > tminx or angle2 < tminx))):\n        minx = min(x, x1)\n    if ((not other_arc and (angle1 > tmaxx or angle2 < tmaxx)) or\n            (other_arc and not (angle1 > tmaxx or angle2 < tmaxx))):\n        maxx = max(x, x1)\n    if ((not other_arc and (angle1 > tminy or angle2 < tminy)) or\n            (other_arc and not (angle1 > tminy or angle2 < tminy))):\n        miny = min(y, y1)\n    if ((not other_arc and (angle1 > tmaxy or angle2 < tmaxy)) or\n            (other_arc and not (angle1 > tmaxy or angle2 < tmaxy))):\n        maxy = max(y, y1)\n\n    return minx, miny, maxx - minx, maxy - miny\n\n\ndef extend_bounding_box(bounding_box, points):\n    \"\"\"Extend a bounding box to include given points.\"\"\"\n    minx, miny, width, height = bounding_box\n    maxx, maxy = (\n        -inf if isinf(minx) else minx + width,\n        -inf if isinf(miny) else miny + height)\n    x_list, y_list = zip(*points)\n    minx, miny, maxx, maxy = (\n        min(minx, *x_list), min(miny, *y_list),\n        max(maxx, *x_list), max(maxy, *y_list))\n    return minx, miny, maxx - minx, maxy - miny\n\n\ndef is_valid_bounding_box(bounding_box):\n    \"\"\"Check that a bounding box doesn’t have infinite boundaries.\"\"\"\n    return bounding_box and not isinf(bounding_box[0] + bounding_box[1])\n\n\nBOUNDING_BOX_METHODS = {\n    'rect': bounding_box_rect,\n    'circle': bounding_box_circle,\n    'ellipse': bounding_box_ellipse,\n    'line': bounding_box_line,\n    'polyline': bounding_box_polyline,\n    'polygon': bounding_box_polyline,\n    'path': bounding_box_path,\n    'g': bounding_box_g,\n    'use': bounding_box_use,\n    'marker': bounding_box_g,\n    'text': bounding_box_text,\n    'tspan': bounding_box_text,\n    'textPath': bounding_box_text,\n}\n"
  },
  {
    "path": "weasyprint/svg/css.py",
    "content": "\"\"\"Apply CSS to SVG documents.\"\"\"\n\nfrom urllib.parse import urljoin\n\nimport cssselect2\nimport tinycss2\n\nfrom ..css.validation.descriptors import preprocess_descriptors\nfrom ..logger import LOGGER\nfrom .utils import parse_url\n\n\ndef find_stylesheets_rules(tree, stylesheet_rules, url, font_config, url_fetcher):\n    \"\"\"Find rules among stylesheet rules and imports.\"\"\"\n    for rule in stylesheet_rules:\n        if rule.type == 'at-rule':\n            if rule.lower_at_keyword == 'import' and rule.content is None:\n                # TODO: support media types in @import\n                url_token = tinycss2.parse_one_component_value(rule.prelude)\n                if url_token.type not in ('string', 'url'):\n                    continue\n                css_url = parse_url(urljoin(url, url_token.value))\n                stylesheet = tinycss2.parse_stylesheet(\n                    tree.fetch_url(css_url, 'text/css').decode())\n                url = css_url.geturl()\n                yield from find_stylesheets_rules(\n                    tree, stylesheet, url, font_config, url_fetcher)\n            elif rule.lower_at_keyword == 'font-face':\n                if font_config is not None and url_fetcher is not None:\n                    content = tinycss2.parse_blocks_contents(rule.content)\n                    rule_descriptors = dict(\n                        preprocess_descriptors('font-face', url, content))\n                    for key in ('src', 'font_family'):\n                        if key not in rule_descriptors:\n                            LOGGER.warning(\n                                \"Missing %s descriptor in '@font-face' rule at \"\n                                \"%d:%d\", key.replace('_', '-'),\n                                rule.source_line, rule.source_column)\n                            break\n                    else:\n                        font_config.add_font_face(rule_descriptors, url_fetcher)\n            # TODO: support media types\n            # if rule.lower_at_keyword == 'media':\n        elif rule.type == 'qualified-rule':\n            yield rule\n        # TODO: warn on error\n        # if rule.type == 'error':\n\n\ndef parse_declarations(input):\n    \"\"\"Parse declarations in a given rule content.\"\"\"\n    normal_declarations = []\n    important_declarations = []\n    for declaration in tinycss2.parse_blocks_contents(input):\n        # TODO: warn on error\n        # if declaration.type == 'error':\n        if (declaration.type == 'declaration' and\n                not declaration.name.startswith('-')):\n            # Serializing perfectly good tokens just to re-parse them later :(\n            value = tinycss2.serialize(declaration.value).strip()\n            declarations = (\n                important_declarations if declaration.important\n                else normal_declarations)\n            declarations.append((declaration.lower_name, value))\n    return normal_declarations, important_declarations\n\n\ndef parse_stylesheets(tree, url, font_config, url_fetcher):\n    \"\"\"Find stylesheets and return rule matchers in given tree.\"\"\"\n    normal_matcher = cssselect2.Matcher()\n    important_matcher = cssselect2.Matcher()\n\n    # Find stylesheets\n    # TODO: support contentStyleType on <svg>\n    stylesheets = []\n    for element in tree.etree_element.iter():\n        # https://www.w3.org/TR/SVG/styling.html#StyleElement\n        if (element.tag == '{http://www.w3.org/2000/svg}style' and\n                element.get('type', 'text/css') == 'text/css' and\n                element.text):\n            # TODO: pass href for relative URLs\n            # TODO: support media types\n            # TODO: what if <style> has children elements?\n            stylesheets.append(tinycss2.parse_stylesheet(\n                element.text, skip_comments=True, skip_whitespace=True))\n\n    # Parse rules and fill matchers\n    for stylesheet in stylesheets:\n        for rule in find_stylesheets_rules(\n                tree, stylesheet, url, font_config, url_fetcher):\n            normal_declarations, important_declarations = parse_declarations(\n                rule.content)\n            try:\n                selectors = cssselect2.compile_selector_list(rule.prelude)\n            except cssselect2.parser.SelectorError as exception:\n                LOGGER.warning(\n                    'Failed to apply CSS rule in SVG rule: %s', exception)\n                break\n            for selector in selectors:\n                if (selector.pseudo_element is None and\n                        not selector.never_matches):\n                    if normal_declarations:\n                        normal_matcher.add_selector(\n                            selector, normal_declarations)\n                    if important_declarations:\n                        important_matcher.add_selector(\n                            selector, important_declarations)\n\n    return normal_matcher, important_matcher\n"
  },
  {
    "path": "weasyprint/svg/defs.py",
    "content": "\"\"\"Parse and draw definitions: gradients, patterns, masks, uses…\"\"\"\n\nfrom itertools import cycle\nfrom math import ceil, hypot\n\nfrom ..matrix import Matrix\nfrom .bounding_box import bounding_box, is_valid_bounding_box\nfrom .utils import alpha_value, color, parse_url, size, transform\n\n\ndef get_use_tree(svg, node, font_size):\n    from . import SVG\n\n    parsed_url = parse_url(node.get_href(svg.url))\n    svg_url = parse_url(svg.url)\n    if svg_url.scheme == 'data':\n        svg_url = parse_url('')\n    same_origin = (\n        parsed_url[:3] == ('', '', '') or\n        parsed_url[:3] == svg_url[:3])\n    if parsed_url.fragment and same_origin:\n        if parsed_url.fragment in svg.use_cache:\n            tree = svg.use_cache[parsed_url.fragment].copy()\n        else:\n            try:\n                tree = svg.tree.get_child(parsed_url.fragment).copy()\n            except Exception:\n                return\n            else:\n                svg.use_cache[parsed_url.fragment] = tree\n    else:\n        url = parsed_url.geturl()\n        try:\n            bytestring_svg = svg.url_fetcher(url)\n            use_svg = SVG(bytestring_svg, url)\n        except Exception:\n            return\n        else:\n            use_svg.get_intrinsic_size(font_size)\n            tree = use_svg.tree\n\n    return tree\n\n\ndef use(svg, node, font_size):\n    \"\"\"Draw use tags.\"\"\"\n    if (tree := get_use_tree(svg, node, font_size)) is None:\n        return\n\n    if tree.tag in ('svg', 'symbol'):\n        # Explicitely specified\n        # https://www.w3.org/TR/SVG11/struct.html#UseElement\n        if 'width' in node.attrib and 'height' in node.attrib:\n            tree.attrib['width'] = node.attrib['width']\n            tree.attrib['height'] = node.attrib['height']\n        else:\n            tree._etree_node.tag = 'g'\n            box = bounding_box(svg, tree, font_size, stroke=True)\n            if is_valid_bounding_box(box):\n                tree.attrib['width'] = box[0] + box[2]\n                tree.attrib['height'] = box[1] + box[3]\n        tree._etree_node.tag = 'svg'\n\n    tree._children = None  # Force cascade to go through children again\n    node.cascade(tree)\n    node.override_iter(iter((tree,)))\n    x, y = svg.point(node.get('x'), node.get('y'), font_size)\n    svg.stream.transform(e=x, f=y)\n\n\ndef draw_gradient_or_pattern(svg, node, name, font_size, opacity, stroke):\n    \"\"\"Draw given gradient or pattern.\"\"\"\n    if name in svg.gradients:\n        return draw_gradient(\n            svg, node, svg.gradients[name], font_size, opacity, stroke)\n    elif name in svg.patterns:\n        return draw_pattern(\n            svg, node, svg.patterns[name], font_size, opacity, stroke)\n\n\ndef draw_gradient(svg, node, gradient, font_size, opacity, stroke):\n    \"\"\"Draw given gradient node.\"\"\"\n    # TODO: merge with Gradient.draw\n    positions = []\n    colors = []\n    for child in gradient:\n        positions.append(max(\n            positions[-1] if positions else 0,\n            size(child.get('offset'), font_size, 1)))\n        stop_opacity = alpha_value(child.get('stop-opacity', 1)) * opacity\n        stop_color = color(child.get('stop-color', 'black'))\n        stop_color.alpha *= stop_opacity\n        colors.append(stop_color)\n\n    if not colors:\n        return False\n    elif len(colors) == 1:\n        svg.stream.set_color(colors[0])\n        return True\n\n    bounding_box = svg.calculate_bounding_box(node, font_size, stroke)\n    if not is_valid_bounding_box(bounding_box):\n        return False\n    if gradient.get('gradientUnits') == 'userSpaceOnUse':\n        width, height = svg.inner_width, svg.inner_height\n        bx1, by1 = bounding_box[:2]\n        matrix = Matrix()\n    else:\n        width, height = 1, 1\n        e, f, a, d = bounding_box\n        bx1, by1 = 0, 0\n        matrix = Matrix(a=a, d=d, e=e, f=f)\n\n    spread = gradient.get('spreadMethod', 'pad')\n    if spread in ('repeat', 'reflect'):\n        if positions[0] > 0:\n            positions.insert(0, 0)\n            colors.insert(0, colors[0])\n        if positions[-1] < 1:\n            positions.append(1)\n            colors.append(colors[-1])\n    else:\n        # Add explicit colors at boundaries if needed, because PDF doesn’t\n        # extend color stops that are not displayed\n        if positions[0] == positions[1]:\n            if gradient.tag == 'radialGradient':\n                # Avoid negative radius for radial gradients\n                positions.insert(0, 0)\n            else:\n                positions.insert(0, positions[0] - 1)\n            colors.insert(0, colors[0])\n        if positions[-2] == positions[-1]:\n            positions.append(positions[-1] + 1)\n            colors.append(colors[-1])\n\n    if 'gradientTransform' in gradient.attrib:\n        transform_matrix = transform(\n            gradient.get('gradientTransform'), '0 0', font_size,\n            svg.normalized_diagonal)\n        matrix = transform_matrix @ matrix\n\n    if gradient.tag == 'linearGradient':\n        shading_type = 2\n        x1, y1 = (\n            size(gradient.get('x1', 0), font_size, width),\n            size(gradient.get('y1', 0), font_size, height))\n        x2, y2 = (\n            size(gradient.get('x2', '100%'), font_size, width),\n            size(gradient.get('y2', 0), font_size, height))\n        positions, colors, coords = spread_linear_gradient(\n            spread, positions, colors, x1, y1, x2, y2, bounding_box, matrix)\n    else:\n        assert gradient.tag == 'radialGradient'\n        shading_type = 3\n        cx, cy = (\n            size(gradient.get('cx', '50%'), font_size, width),\n            size(gradient.get('cy', '50%'), font_size, height))\n        r = size(gradient.get('r', '50%'), font_size, hypot(width, height))\n        fx, fy = (\n            size(gradient.get('fx', cx), font_size, width),\n            size(gradient.get('fy', cy), font_size, height))\n        fr = size(gradient.get('fr', 0), font_size, hypot(width, height))\n        positions, colors, coords = spread_radial_gradient(\n            spread, positions, colors, fx, fy, fr, cx, cy, r, width, height,\n            matrix)\n\n    alphas = [color[3] for color in colors]\n    alpha_couples = [\n        (alphas[i], alphas[i + 1])\n        for i in range(len(alphas) - 1)]\n    color_couples = [\n        [colors[i][:3], colors[i + 1][:3], 1]\n        for i in range(len(colors) - 1)]\n\n    # Premultiply colors\n    for i, alpha in enumerate(alphas):\n        if alpha == 0:\n            if i > 0:\n                color_couples[i - 1][1] = color_couples[i - 1][0]\n            if i < len(colors) - 1:\n                color_couples[i][0] = color_couples[i][1]\n    for i, (a0, a1) in enumerate(alpha_couples):\n        if 0 not in (a0, a1) and (a0, a1) != (1, 1):\n            color_couples[i][2] = a0 / a1\n\n    if 'gradientTransform' in gradient.attrib:\n        bx2, by2 = bx1 + width, by1 + height\n        bx1, by1 = transform_matrix.invert.transform_point(bx1, by1)\n        bx2, by2 = transform_matrix.invert.transform_point(bx2, by2)\n        width, height = bx2 - bx1, by2 - by1\n\n        # Ensure that width and height are positive to please some PDF readers\n        if bx1 > bx2:\n            width = -width\n            bx1, bx2 = bx2, bx1\n        if by1 > by2:\n            height = -height\n            by1, by2 = by2, by1\n\n    pattern = svg.stream.add_pattern(\n        bx1, by1, width, height, width, height, matrix @ svg.stream.ctm)\n    group = pattern.add_group(bx1, by1, width, height)\n\n    domain = (positions[0], positions[-1])\n    extend = spread not in ('repeat', 'reflect')\n    encode = (len(colors) - 1) * (0, 1)\n    bounds = positions[1:-1]\n    sub_functions = (\n        group.create_interpolation_function(domain, c0, c1, n)\n        for c0, c1, n in color_couples)\n    function = group.create_stitching_function(\n        domain, encode, bounds, sub_functions)\n    shading = group.add_shading(\n        shading_type, 'RGB', domain, coords, extend, function)\n\n    if any(alpha != 1 for alpha in alphas):\n        alpha_stream = group.set_alpha_state(bx1, by1, width, height)\n        domain = (positions[0], positions[-1])\n        extend = spread not in ('repeat', 'reflect')\n        encode = (len(colors) - 1) * (0, 1)\n        bounds = positions[1:-1]\n        sub_functions = (\n            group.create_interpolation_function((0, 1), [c0], [c1], 1)\n            for c0, c1 in alpha_couples)\n        function = group.create_stitching_function(\n            domain, encode, bounds, sub_functions)\n        alpha_shading = alpha_stream.add_shading(\n            shading_type, 'Gray', domain, coords, extend, function)\n        alpha_stream.stream = [f'/{alpha_shading.id} sh']\n\n    group.paint_shading(shading.id)\n    pattern.set_alpha(1)\n    pattern.draw_x_object(group.id)\n    svg.stream.set_color_space('Pattern', stroke=stroke)\n    svg.stream.set_color_special(pattern.id, stroke=stroke)\n    return True\n\n\ndef spread_linear_gradient(spread, positions, colors, x1, y1, x2, y2,\n                           bounding_box, matrix):\n    \"\"\"Repeat linear gradient.\"\"\"\n    # TODO: merge with LinearGradient.layout\n    from ..images import gradient_average_color, normalize_stop_positions\n\n    first, last, positions = normalize_stop_positions(positions)\n    if spread in ('repeat', 'reflect'):\n        # Render as a solid color if the first and last positions are equal\n        # See https://drafts.csswg.org/css-images-3/#repeating-gradients\n        if first == last:\n            average_color = gradient_average_color(colors, positions)\n            return 1, 'solid', None, [], [average_color]\n\n        # Define defined gradient length and steps between positions\n        stop_length = last - first\n        position_steps = [\n            positions[i + 1] - positions[i]\n            for i in range(len(positions) - 1)]\n\n        # Create cycles used to add colors\n        if spread == 'repeat':\n            next_steps = cycle((0, *position_steps))\n            next_colors = cycle(colors)\n            previous_steps = cycle((0, *position_steps[::-1]))\n            previous_colors = cycle(colors[::-1])\n        else:\n            assert spread == 'reflect'\n            next_steps = cycle((0, *position_steps[::-1], 0, *position_steps))\n            next_colors = cycle(colors[::-1] + colors)\n            previous_steps = cycle((0, *position_steps, 0, *position_steps[::-1]))\n            previous_colors = cycle(colors + colors[::-1])\n\n        # Normalize bounding box\n        bx1, by1, bw, bh = bounding_box\n        bx1, bx2 = (bx1, bx1 + bw) if bw > 0 else (bx1 + bw, bx1)\n        by1, by2 = (by1, by1 + bh) if bh > 0 else (by1 + bh, by1)\n\n        # Transform gradient vector coordinates\n        tx1, ty1 = matrix.transform_point(x1, y1)\n        tx2, ty2 = matrix.transform_point(x2, y2)\n\n        # Find the extremities of the repeating vector, by projecting the\n        # bounding box corners on the gradient vector\n        xb, yb = tx1, ty1\n        xv, yv = tx2 - tx1, ty2 - ty1\n        xa1, xa2 = (bx1, bx2) if tx1 < tx2 else (bx2, bx1)\n        ya1, ya2 = (by1, by2) if ty1 < ty2 else (by2, by1)\n        min_vector = ((xa1 - xb) * xv + (ya1 - yb) * yv) / hypot(xv, yv) ** 2\n        max_vector = ((xa2 - xb) * xv + (ya2 - yb) * yv) / hypot(xv, yv) ** 2\n\n        # Add colors after last step\n        while last < max_vector:\n            step = next(next_steps)\n            colors.append(next(next_colors))\n            positions.append(positions[-1] + step)\n            last += step * stop_length\n\n        # Add colors before first step\n        while first > min_vector:\n            step = next(previous_steps)\n            colors.insert(0, next(previous_colors))\n            positions.insert(0, positions[0] - step)\n            first -= step * stop_length\n\n    x1, x2 = x1 + (x2 - x1) * first, x1 + (x2 - x1) * last\n    y1, y2 = y1 + (y2 - y1) * first, y1 + (y2 - y1) * last\n    coords = (x1, y1, x2, y2)\n    return positions, colors, coords\n\n\ndef spread_radial_gradient(spread, positions, colors, fx, fy, fr, cx, cy, r,\n                           width, height, matrix):\n    \"\"\"Repeat radial gradient.\"\"\"\n    # TODO: merge with RadialGradient._repeat\n    from ..images import gradient_average_color, normalize_stop_positions\n\n    first, last, positions = normalize_stop_positions(positions)\n    fr, r = fr + (r - fr) * first, fr + (r - fr) * last\n\n    if spread in ('repeat', 'reflect'):\n        # Keep original lists and values, they’re useful\n        original_colors = colors.copy()\n        original_positions = positions.copy()\n\n        # Get the maximum distance between the center and the corners, to find\n        # how many times we have to repeat the colors outside\n        tw, th = matrix.invert.transform_point(width, height)\n        max_distance = hypot(\n            max(abs(fx), abs(tw - fx)), max(abs(fy), abs(th - fy)))\n        gradient_length = r - fr\n        repeat_after = ceil((max_distance - r) / gradient_length)\n        if repeat_after > 0:\n            # Repeat colors and extrapolate positions\n            repeat = 1 + repeat_after\n            if spread == 'repeat':\n                colors *= repeat\n            else:\n                assert spread == 'reflect'\n                colors = []\n                for i in range(repeat):\n                    colors += original_colors[::-1 if i % 2 else 1]\n            positions = [\n                i + position for i in range(repeat) for position in positions]\n            r += gradient_length * repeat_after\n\n        if fr == 0:\n            # Inner circle has 0 radius, no need to repeat inside, return\n            coords = (fx, fy, fr, cx, cy, r)\n            return positions, colors, coords\n\n        # Find how many times we have to repeat the colors inside\n        repeat_before = fr / gradient_length\n\n        # Set the inner circle size to 0\n        fr = 0\n\n        # Find how many times the whole gradient can be repeated\n        full_repeat = int(repeat_before)\n        if full_repeat:\n            # Repeat colors and extrapolate positions\n            if spread == 'repeat':\n                colors += original_colors * full_repeat\n            else:\n                assert spread == 'reflect'\n                for i in range(full_repeat):\n                    colors += original_colors[\n                        ::-1 if (i + repeat_after) % 2 else 1]\n            positions = [\n                i - full_repeat + position for i in range(full_repeat)\n                for position in original_positions] + positions\n\n        # Find the ratio of gradient that must be added to reach the center\n        partial_repeat = repeat_before - full_repeat\n        if partial_repeat == 0:\n            # No partial repeat, return\n            coords = (fx, fy, fr, cx, cy, r)\n            return positions, colors, coords\n\n        # Iterate through positions in reverse order, from the outer\n        # circle to the original inner circle, to find positions from\n        # the inner circle (including full repeats) to the center\n        assert (original_positions[0], original_positions[-1]) == (0, 1)\n        assert 0 < partial_repeat < 1\n        reverse = original_positions[::-1]\n        ratio = 1 - partial_repeat\n        if spread == 'reflect':\n            original_colors = original_colors[::-1]\n        for i, position in enumerate(reverse, start=1):\n            if position == ratio:\n                # The center is a color of the gradient, truncate original\n                # colors and positions and prepend them\n                colors = original_colors[-i:] + colors\n                new_positions = [\n                    position - full_repeat - 1\n                    for position in original_positions[-i:]]\n                positions = new_positions + positions\n                break\n            if position < ratio:\n                # The center is between two colors of the gradient,\n                # define the center color as the average of these two\n                # gradient colors\n                color = original_colors[-i]\n                next_color = original_colors[-(i - 1)]\n                next_position = original_positions[-(i - 1)]\n                average_colors = [color, color, next_color, next_color]\n                average_positions = [position, ratio, ratio, next_position]\n                zero_color = gradient_average_color(\n                    average_colors, average_positions)\n                colors = [zero_color, *original_colors[-(i - 1):], *colors]\n                new_positions = [\n                    position - 1 - full_repeat for position\n                    in original_positions[-(i - 1):]]\n                positions = [ratio - 1 - full_repeat, *new_positions, *positions]\n                break\n\n    coords = (fx, fy, fr, cx, cy, r)\n    return positions, colors, coords\n\n\ndef draw_pattern(svg, node, pattern, font_size, opacity, stroke):\n    \"\"\"Draw given gradient node.\"\"\"\n    from . import Pattern\n\n    pattern._etree_node.tag = 'svg'\n\n    bounding_box = svg.calculate_bounding_box(node, font_size, stroke)\n    if not is_valid_bounding_box(bounding_box):\n        return False\n    x, y = bounding_box[0], bounding_box[1]\n    matrix = Matrix(e=x, f=y)\n    if pattern.get('patternUnits') == 'userSpaceOnUse':\n        pattern_width = size(pattern.get('width', 0), font_size, 1)\n        pattern_height = size(pattern.get('height', 0), font_size, 1)\n    else:\n        width, height = bounding_box[2], bounding_box[3]\n        pattern_width = (\n            size(pattern.attrib.pop('width', '1'), font_size, 1) * width)\n        pattern_height = (\n            size(pattern.attrib.pop('height', '1'), font_size, 1) * height)\n        if 'viewBox' not in pattern:\n            pattern.attrib['width'] = pattern_width\n            pattern.attrib['height'] = pattern_height\n            if pattern.get('patternContentUnits') == 'objectBoundingBox':\n                pattern.attrib['transform'] = f'scale({width}, {height})'\n\n    # Fail if pattern has an invalid size\n    if pattern_width == 0 or pattern_height == 0:\n        return False\n\n    if 'patternTransform' in pattern.attrib:\n        transform_matrix = transform(\n            pattern.get('patternTransform'), '0 0', font_size, svg.inner_diagonal)\n        matrix = transform_matrix @ matrix\n\n    matrix = matrix @ svg.stream.ctm\n    stream_pattern = svg.stream.add_pattern(\n        0, 0, pattern_width, pattern_height, pattern_width, pattern_height,\n        matrix)\n    stream_pattern.set_alpha(opacity)\n\n    group = stream_pattern.add_group(0, 0, pattern_width, pattern_height)\n    Pattern(pattern, svg).draw(\n        group, pattern_width, pattern_height, svg.base_url,\n        svg.context)\n    stream_pattern.draw_x_object(group.id)\n    svg.stream.set_color_space('Pattern', stroke=stroke)\n    svg.stream.set_color_special(stream_pattern.id, stroke=stroke)\n    return True\n\n\ndef apply_filters(svg, node, filter_node, font_size):\n    \"\"\"Apply filters defined in given filter node.\"\"\"\n    for child in filter_node:\n        if child.tag == 'feOffset':\n            if filter_node.get('primitiveUnits') == 'objectBoundingBox':\n                bounding_box = svg.calculate_bounding_box(node, font_size)\n                if is_valid_bounding_box(bounding_box):\n                    _, _, width, height = bounding_box\n                    dx = size(child.get('dx', 0), font_size, 1) * width\n                    dy = size(child.get('dy', 0), font_size, 1) * height\n                else:\n                    dx = dy = 0\n            else:\n                dx, dy = svg.point(\n                    child.get('dx', 0), child.get('dy', 0), font_size)\n            svg.stream.transform(e=dx, f=dy)\n        elif child.tag == 'feBlend':\n            mode = child.get('mode', 'normal')\n            mode = mode.replace('-', ' ').title().replace(' ', '')\n            svg.stream.set_blend_mode(mode)\n\n\ndef paint_mask(svg, node, mask, font_size):\n    \"\"\"Apply given mask node.\"\"\"\n    mask._etree_node.tag = 'g'\n\n    if mask.get('maskUnits') == 'userSpaceOnUse':\n        width_ref, height_ref = svg.inner_width, svg.inner_height\n    else:\n        width_ref, height_ref = svg.point(\n            node.get('width'), node.get('height'), font_size)\n\n    mask.attrib['x'] = size(mask.get('x', '-10%'), font_size, width_ref)\n    mask.attrib['y'] = size(mask.get('y', '-10%'), font_size, height_ref)\n    mask.attrib['height'] = size(\n        mask.get('height', '120%'), font_size, height_ref)\n    mask.attrib['width'] = size(\n        mask.get('width', '120%'), font_size, width_ref)\n\n    if mask.get('maskUnits') == 'userSpaceOnUse':\n        x, y = mask.get('x'), mask.get('y')\n        width, height = mask.get('width'), mask.get('height')\n        mask.attrib['viewBox'] = f'{x} {y} {width} {height}'\n    else:\n        x, y = 0, 0\n        width, height = width_ref, height_ref\n\n    svg_stream = svg.stream\n    svg.stream = svg.stream.set_alpha_state(x, y, width, height)\n    svg.draw_node(mask, font_size)\n    svg.stream = svg_stream\n"
  },
  {
    "path": "weasyprint/svg/images.py",
    "content": "\"\"\"Draw image and svg tags.\"\"\"\n\nfrom .bounding_box import bounding_box, is_valid_bounding_box\nfrom .utils import preserve_ratio\n\n\ndef svg(svg, node, font_size):\n    \"\"\"Draw svg tags.\"\"\"\n    x, y = svg.point(node.get('x'), node.get('y'), font_size)\n    svg.stream.transform(e=x, f=y)\n    if svg.tree == node:\n        width, height = svg.concrete_width, svg.concrete_height\n    else:\n        width, height = node.get('width'), node.get('height')\n        if None in (width, height):\n            node._etree_node.tag = 'g'\n            box = bounding_box(svg, node, font_size, stroke=True)\n            if is_valid_bounding_box(box):\n                width = box[0] + box[2]\n                height = box[1] + box[3]\n            else:\n                width = height = 0\n            node._etree_node.tag = 'svg'\n        else:\n            width, height = svg.point(width, height, font_size)\n    node.set_svg_size(svg, width, height)\n    scale_x, scale_y, translate_x, translate_y = preserve_ratio(\n        svg, node, font_size, width, height)\n    if svg.tree != node and node.get('overflow', 'hidden') == 'hidden':\n        svg.stream.rectangle(0, 0, width, height)\n        svg.stream.clip()\n        svg.stream.end()\n    svg.stream.transform(a=scale_x, d=scale_y, e=translate_x, f=translate_y)\n\n\ndef image(svg, node, font_size):\n    \"\"\"Draw image tags.\"\"\"\n    x, y = svg.point(node.get('x'), node.get('y'), font_size)\n    svg.stream.transform(e=x, f=y)\n    base_url = node.get('{http://www.w3.org/XML/1998/namespace}base')\n    url = node.get_href(base_url or svg.url)\n    image = svg.context.get_image_from_uri(url=url, forced_mime_type='image/*')\n    if image is None:\n        return\n\n    width, height = svg.point(node.get('width'), node.get('height'), font_size)\n    intrinsic_width, intrinsic_height, intrinsic_ratio = (\n        image.get_intrinsic_size(1, font_size))\n    if intrinsic_width is None and intrinsic_height is None:\n        if intrinsic_ratio is None or (not width and not height):\n            intrinsic_width, intrinsic_height = 300, 150\n        elif not width:\n            intrinsic_width, intrinsic_height = (\n                intrinsic_ratio * height, height)\n        else:\n            intrinsic_width, intrinsic_height = width, width / intrinsic_ratio\n    elif intrinsic_width is None:\n        intrinsic_width = intrinsic_ratio * intrinsic_height\n    elif intrinsic_height is None:\n        intrinsic_height = intrinsic_width / intrinsic_ratio\n\n    # Calculate final dimensions while preserving aspect ratio\n    if width and not height:\n        height = width / intrinsic_ratio\n    elif height and not width:\n        width = height * intrinsic_ratio\n    else:\n        width = width or intrinsic_width\n        height = height or intrinsic_height\n\n    scale_x, scale_y, translate_x, translate_y = preserve_ratio(\n        svg, node, font_size, width, height,\n        (0, 0, intrinsic_width, intrinsic_height))\n    svg.stream.rectangle(0, 0, width, height)\n    svg.stream.clip()\n    svg.stream.end()\n    svg.stream.push_state()\n    svg.stream.transform(a=scale_x, d=scale_y, e=translate_x, f=translate_y)\n    # TODO: pass real style instead of dict.\n    image.draw(\n        svg.stream, intrinsic_width, intrinsic_height,\n        {'image_rendering': node.attrib.get('image-rendering', 'auto')},\n    )\n    svg.stream.pop_state()\n"
  },
  {
    "path": "weasyprint/svg/path.py",
    "content": "\"\"\"Draw paths.\"\"\"\n\nfrom math import atan2, cos, isclose, pi, radians, sin, tan\n\nfrom ..matrix import Matrix\nfrom .utils import normalize, point\n\nPATH_LETTERS = 'achlmqstvzACHLMQSTVZ'\n\n\ndef _rotate(x, y, angle):\n    \"\"\"Rotate (x, y) point of given angle around (0, 0).\"\"\"\n    return x * cos(angle) - y * sin(angle), y * cos(angle) + x * sin(angle)\n\n\ndef path(svg, node, font_size):\n    \"\"\"Draw path node.\"\"\"\n    string = node.get('d', '')\n\n    for letter in PATH_LETTERS:\n        string = string.replace(letter, f' {letter} ')\n    string = normalize(string)\n\n    # TODO: get current point\n    current_point = 0, 0\n    svg.stream.move_to(*current_point)\n    last_letter = None\n\n    while string:\n        string = string.strip()\n        if string.split(' ', 1)[0] in PATH_LETTERS:\n            letter, string = (f'{string} ').split(' ', 1)\n            if last_letter in (None, 'z', 'Z') and letter not in 'mM':\n                node.vertices.append(current_point)\n                first_path_point = current_point\n        elif letter == 'M':\n            letter = 'L'\n        elif letter == 'm':\n            letter = 'l'\n\n        if last_letter in (None, 'm', 'M', 'z', 'Z'):\n            first_path_point = None\n        if letter not in (None, 'm', 'M', 'z', 'Z') and (\n                first_path_point is None):\n            first_path_point = current_point\n\n        if letter in 'aA':\n            # Elliptic curve\n            # Drawn as an approximation using Bézier curves\n            x1, y1 = current_point\n            rx, ry, string = point(svg, string, font_size)\n            rotation, string = string.split(' ', 1)\n            rotation = radians(float(rotation))\n\n            # The large and sweep values are not always separated from the\n            # following values. These flags can only be 0 or 1, so reading a\n            # single digit suffices.\n            large, string = string[0], string[1:].strip()\n            sweep, string = string[0], string[1:].strip()\n\n            # Retrieve end point and set remainder (before checking flags)\n            x3, y3, string = point(svg, string, font_size)\n            if letter == 'a':\n                x3 += x1\n                y3 += y1\n\n            # Only allow 0 or 1 for flags\n            large, sweep = int(large), int(sweep)\n            if large not in (0, 1) or sweep not in (0, 1):\n                continue\n            large, sweep = bool(large), bool(sweep)\n\n            # rx=0 or ry=0 means straight line\n            if not rx or not ry:\n                if string and string[0] not in PATH_LETTERS:\n                    # As we replace the current operation by l, we must be sure\n                    # that the next letter is set to the real current letter (a\n                    # or A) in case it’s omitted\n                    next_letter = f'{letter} '\n                else:\n                    next_letter = ''\n                string = f'L {x3} {y3} {next_letter}{string}'\n                continue\n\n            # Cancel the rotation of the second point\n            xe, ye = _rotate(x3 - x1, y3 - y1, -rotation)\n            y_scale = ry / rx\n            ye /= y_scale\n\n            # Find the angle between the second point and the x axis\n            angle = atan2(ye, xe)\n\n            # Put the second point onto the x axis\n            xe = (xe ** 2 + ye ** 2) ** .5\n            ye = 0\n\n            # Update the x radius if it is too small\n            rx = max(rx, xe / 2)\n\n            # Find one circle centre\n            xc = xe / 2\n            yc = (rx ** 2 - xc ** 2) ** .5\n\n            # Choose between the two circles according to flags\n            if large == sweep:\n                yc = -yc\n\n            # Put the second point and the center back to their positions\n            xe, ye = _rotate(xe, ye, angle)\n            xc, yc = _rotate(xc, yc, angle)\n\n            # Find the drawing angles\n            angle1 = atan2(-yc, -xc)\n            angle2 = atan2(ye - yc, xe - xc)\n            while angle1 < 0 or angle2 < 0:\n                angle1 += 2 * pi\n                angle2 += 2 * pi\n\n            # Store the tangent angles\n            node.vertices.append((-angle1, -angle2))\n\n            # Fix angles to follow large arc flag\n            if isclose(abs(angle2 - angle1), pi):\n                if sweep and (angle2 < angle1):\n                    angle1 -= 2 * pi\n                elif not sweep and (angle2 > angle1):\n                    angle2 -= 2 * pi\n            elif large == (abs(angle2 - angle1) < pi):\n                if angle1 > angle2:\n                    angle1 -= 2 * pi\n                else:\n                    angle2 -= 2 * pi\n\n            # Split arc into 3 Bézier curves when larger than pi\n            if large:\n                step = (angle2 - angle1) / 3\n                angles = (\n                    (angle1, angle1 + step),\n                    (angle1 + step, angle1 + 2 * step),\n                    (angle1 + 2 * step, angle2))\n            else:\n                angles = ((angle1, angle2),)\n\n            # Draw Bézier curves\n            matrix = Matrix(\n                cos(rotation), sin(rotation),\n                -sin(rotation) * y_scale, cos(rotation) * y_scale,\n                x1, y1)\n            h = 4 / 3 * tan((angles[0][1] - angles[0][0]) / 4)\n            for angle1, angle2 in angles:\n                point1 = matrix.transform_point(\n                    xc + rx * cos(angle1) - h * rx * sin(angle1),\n                    yc + rx * sin(angle1) + h * rx * cos(angle1))\n                point2 = matrix.transform_point(\n                    xc + rx * cos(angle2) + h * rx * sin(angle2),\n                    yc + rx * sin(angle2) - h * rx * cos(angle2))\n                point3 = matrix.transform_point(\n                    xc + rx * cos(angle2),\n                    yc + rx * sin(angle2))\n                svg.stream.curve_to(*point1, *point2, *point3)\n\n            current_point = x3, y3\n\n        elif letter in 'cC':\n            # Curve\n            x1, y1, string = point(svg, string, font_size)\n            x2, y2, string = point(svg, string, font_size)\n            x3, y3, string = point(svg, string, font_size)\n            if letter == 'c':\n                x, y = current_point\n                x1 += x\n                x2 += x\n                x3 += x\n                y1 += y\n                y2 += y\n                y3 += y\n            node.vertices.append((\n                atan2(y1 - y2, x1 - x2), atan2(y3 - y2, x3 - x2)))\n            svg.stream.curve_to(x1, y1, x2, y2, x3, y3)\n            current_point = x3, y3\n\n        elif letter in 'hH':\n            # Horizontal line\n            x, string = (f'{string} ').split(' ', 1)\n            old_x, old_y = current_point\n            x, _ = svg.point(x, 0, font_size)\n            if letter == 'h':\n                x += old_x\n            angle = 0 if x > old_x else pi\n            node.vertices.append((pi - angle, angle))\n            svg.stream.line_to(x, old_y)\n            current_point = x, old_y\n\n        elif letter in 'lL':\n            # Straight line\n            x, y, string = point(svg, string, font_size)\n            old_x, old_y = current_point\n            if letter == 'l':\n                x += old_x\n                y += old_y\n            angle = atan2(y - old_y, x - old_x)\n            node.vertices.append((pi - angle, angle))\n            svg.stream.line_to(x, y)\n            current_point = x, y\n\n        elif letter in 'mM':\n            # Current point move\n            x, y, string = point(svg, string, font_size)\n            if last_letter and last_letter not in 'zZ':\n                node.vertices.append(None)\n            if letter == 'm':\n                x += current_point[0]\n                y += current_point[1]\n            svg.stream.move_to(x, y)\n            current_point = x, y\n\n        elif letter in 'qQtT':\n            # Quadratic curve\n            x1, y1 = current_point\n            if letter in 'qQ':\n                x2, y2, string = point(svg, string, font_size)\n            else:\n                if last_letter not in 'QqTt':\n                    x2, y2, x3, y3 = x, y, x, y\n                x2 = x1 + x3 - x2\n                y2 = y1 + y3 - y2\n            x3, y3, string = point(svg, string, font_size)\n            if letter == 'q':\n                x2 += x1\n                y2 += y1\n            if letter in 'qt':\n                x3 += x1\n                y3 += y1\n            xq1 = x2 * 2 / 3 + x1 / 3\n            yq1 = y2 * 2 / 3 + y1 / 3\n            xq2 = x2 * 2 / 3 + x3 / 3\n            yq2 = y2 * 2 / 3 + y3 / 3\n            svg.stream.curve_to(xq1, yq1, xq2, yq2, x3, y3)\n            node.vertices.append((0, 0))\n            current_point = x3, y3\n\n        elif letter in 'sS':\n            # Smooth curve\n            x, y = current_point\n            x1 = x3 + (x3 - x2) if last_letter in 'csCS' else x\n            y1 = y3 + (y3 - y2) if last_letter in 'csCS' else y\n            x2, y2, string = point(svg, string, font_size)\n            x3, y3, string = point(svg, string, font_size)\n            if letter == 's':\n                x2 += x\n                x3 += x\n                y2 += y\n                y3 += y\n            node.vertices.append((\n                atan2(y1 - y2, x1 - x2), atan2(y3 - y2, x3 - x2)))\n            svg.stream.curve_to(x1, y1, x2, y2, x3, y3)\n            current_point = x3, y3\n\n        elif letter in 'vV':\n            # Vertical line\n            y, string = (f'{string} ').split(' ', 1)\n            old_x, old_y = current_point\n            _, y = svg.point(0, y, font_size)\n            if letter == 'v':\n                y += old_y\n            angle = pi / 2 if y > old_y else -pi / 2\n            node.vertices.append((pi - angle, angle))\n            svg.stream.line_to(old_x, y)\n            current_point = old_x, y\n\n        elif letter in 'zZ' and first_path_point:\n            # End of path\n            node.vertices.append(None)\n            svg.stream.close()\n            current_point = first_path_point\n\n        if letter not in 'zZ':\n            node.vertices.append(current_point)\n\n        string = string.strip()\n        last_letter = letter\n"
  },
  {
    "path": "weasyprint/svg/shapes.py",
    "content": "\"\"\"Draw simple shapes.\"\"\"\n\nfrom math import atan2, pi, sqrt\n\nfrom .utils import normalize, point\n\n\ndef circle(svg, node, font_size):\n    \"\"\"Draw circle tag.\"\"\"\n    r = svg.length(node.get('r'), font_size)\n    if not r:\n        return\n    ratio = r / sqrt(pi)\n    cx, cy = svg.point(node.get('cx'), node.get('cy'), font_size)\n\n    svg.stream.move_to(cx + r, cy)\n    svg.stream.curve_to(cx + r, cy + ratio, cx + ratio, cy + r, cx, cy + r)\n    svg.stream.curve_to(cx - ratio, cy + r, cx - r, cy + ratio, cx - r, cy)\n    svg.stream.curve_to(cx - r, cy - ratio, cx - ratio, cy - r, cx, cy - r)\n    svg.stream.curve_to(cx + ratio, cy - r, cx + r, cy - ratio, cx + r, cy)\n    svg.stream.close()\n\n\ndef ellipse(svg, node, font_size):\n    \"\"\"Draw ellipse tag.\"\"\"\n    rx, ry = svg.point(node.get('rx'), node.get('ry'), font_size)\n    if not rx or not ry:\n        return\n    ratio_x = rx / sqrt(pi)\n    ratio_y = ry / sqrt(pi)\n    cx, cy = svg.point(node.get('cx'), node.get('cy'), font_size)\n\n    svg.stream.move_to(cx + rx, cy)\n    svg.stream.curve_to(\n        cx + rx, cy + ratio_y, cx + ratio_x, cy + ry, cx, cy + ry)\n    svg.stream.curve_to(\n        cx - ratio_x, cy + ry, cx - rx, cy + ratio_y, cx - rx, cy)\n    svg.stream.curve_to(\n        cx - rx, cy - ratio_y, cx - ratio_x, cy - ry, cx, cy - ry)\n    svg.stream.curve_to(\n        cx + ratio_x, cy - ry, cx + rx, cy - ratio_y, cx + rx, cy)\n    svg.stream.close()\n\n\ndef rect(svg, node, font_size):\n    \"\"\"Draw rect tag.\"\"\"\n    width, height = svg.point(node.get('width'), node.get('height'), font_size)\n    if width <= 0 or height <= 0:\n        return\n\n    x, y = svg.point(node.get('x'), node.get('y'), font_size)\n\n    rx = node.get('rx')\n    ry = node.get('ry')\n    if rx and ry is None:\n        ry = rx\n    elif ry and rx is None:\n        rx = ry\n    rx, ry = svg.point(rx, ry, font_size)\n\n    if rx == 0 or ry == 0:\n        svg.stream.rectangle(x, y, width, height)\n        return\n\n    if rx > width / 2:\n        rx = width / 2\n    if ry > height / 2:\n        ry = height / 2\n\n    # Inspired by Cairo Cookbook\n    # https://cairographics.org/cookbook/roundedrectangles/\n    arc_to_bezier = 4 * (2 ** .5 - 1) / 3\n    c1, c2 = arc_to_bezier * rx, arc_to_bezier * ry\n\n    svg.stream.move_to(x + rx, y)\n    svg.stream.line_to(x + width - rx, y)\n    svg.stream.curve_to(\n        x + width - rx + c1, y, x + width, y + c2, x + width, y + ry)\n    svg.stream.line_to(x + width, y + height - ry)\n    svg.stream.curve_to(\n        x + width, y + height - ry + c2, x + width + c1 - rx, y + height,\n        x + width - rx, y + height)\n    svg.stream.line_to(x + rx, y + height)\n    svg.stream.curve_to(\n        x + rx - c1, y + height, x, y + height - c2, x, y + height - ry)\n    svg.stream.line_to(x, y + ry)\n    svg.stream.curve_to(x, y + ry - c2, x + rx - c1, y, x + rx, y)\n    svg.stream.close()\n\n\ndef line(svg, node, font_size):\n    \"\"\"Draw line tag.\"\"\"\n    x1, y1 = svg.point(node.get('x1'), node.get('y1'), font_size)\n    x2, y2 = svg.point(node.get('x2'), node.get('y2'), font_size)\n    svg.stream.move_to(x1, y1)\n    svg.stream.line_to(x2, y2)\n    angle = atan2(y2 - y1, x2 - x1)\n    node.vertices = [(x1, y1), (pi - angle, angle), (x2, y2)]\n\n\ndef polygon(svg, node, font_size):\n    \"\"\"Draw polygon tag.\"\"\"\n    polyline(svg, node, font_size)\n    svg.stream.close()\n\n\ndef polyline(svg, node, font_size):\n    \"\"\"Draw polyline tag.\"\"\"\n    points = normalize(node.get('points'))\n    if points:\n        x, y, points = point(svg, points, font_size)\n        svg.stream.move_to(x, y)\n        node.vertices = [(x, y)]\n        while points:\n            x_old, y_old = x, y\n            x, y, points = point(svg, points, font_size)\n            angle = atan2(x - x_old, y - y_old)\n            node.vertices.append((pi - angle, angle))\n            svg.stream.line_to(x, y)\n            node.vertices.append((x, y))\n"
  },
  {
    "path": "weasyprint/svg/text.py",
    "content": "\"\"\"Draw text.\"\"\"\n\nfrom math import cos, inf, radians, sin\n\nfrom ..matrix import Matrix\nfrom .bounding_box import extend_bounding_box\nfrom .utils import normalize, size\n\n\nclass TextBox:\n    \"\"\"Dummy text box used to draw text.\"\"\"\n    def __init__(self, pango_layout, style):\n        self.pango_layout = pango_layout\n        self.style = style\n\n    @property\n    def text(self):\n        return self.pango_layout.text\n\n\nclass Style(dict):\n    \"\"\"Dummy class to store dict.\"\"\"\n\n\ndef text(svg, node, font_size):\n    \"\"\"Draw text node.\"\"\"\n    from ..css.properties import INITIAL_VALUES\n    from ..draw.text import draw_emojis, draw_first_line\n    from ..text.line_break import split_first_line\n\n    # TODO: use real computed values\n    style = Style()\n    style.update(INITIAL_VALUES)\n    style.font_config = svg.font_config\n    style['font_family'] = [\n        font.strip('\"\\'') for font in\n        node.get('font-family', 'sans-serif').split(',')]\n    style['font_style'] = node.get('font-style', 'normal')\n    style['font_weight'] = node.get('font-weight', 400)\n    style['font_size'] = font_size\n    if style['font_weight'] == 'normal':\n        style['font_weight'] = 400\n    elif style['font_weight'] == 'bold':\n        style['font_weight'] = 700\n    else:\n        try:\n            style['font_weight'] = int(style['font_weight'])\n        except ValueError:\n            style['font_weight'] = 400\n\n    layout, _, _, width, height, _ = split_first_line(\n        node.text, style, svg.context, inf, 0)\n\n    # Get rotations and translations\n    x, y, dx, dy, rotate = [], [], [], [], [0]\n    if 'x' in node.attrib:\n        x = [size(i, font_size, svg.inner_width)\n             for i in normalize(node.attrib['x']).strip().split(' ')]\n    if 'y' in node.attrib:\n        y = [size(i, font_size, svg.inner_height)\n             for i in normalize(node.attrib['y']).strip().split(' ')]\n    if 'dx' in node.attrib:\n        dx = [size(i, font_size, svg.inner_width)\n              for i in normalize(node.attrib['dx']).strip().split(' ')]\n    if 'dy' in node.attrib:\n        dy = [size(i, font_size, svg.inner_height)\n              for i in normalize(node.attrib['dy']).strip().split(' ')]\n    if 'rotate' in node.attrib:\n        rotate = [radians(float(i)) if i else 0\n                  for i in normalize(node.attrib['rotate']).strip().split(' ')]\n    last_r = rotate[-1]\n    letters_positions = [\n        ([pl.pop(0) if pl else None for pl in (x, y, dx, dy, rotate)], char)\n        for char in node.text]\n\n    letter_spacing = svg.length(node.get('letter-spacing'), font_size)\n    text_length = svg.length(node.get('textLength'), font_size)\n    scale_x = 1\n    if text_length and node.text:\n        # calculate the number of spaces to be considered for the text\n        spaces_count = len(node.text) - 1\n        if normalize(node.attrib.get('lengthAdjust')) == 'spacingAndGlyphs':\n            # scale letter_spacing up/down to textLength\n            width_with_spacing = width + spaces_count * letter_spacing\n            letter_spacing *= text_length / width_with_spacing\n            # calculate the glyphs scaling factor by:\n            # - deducting the scaled letter_spacing from textLength\n            # - dividing the calculated value by the original width\n            spaceless_text_length = text_length - spaces_count * letter_spacing\n            scale_x = spaceless_text_length / width\n        elif spaces_count:\n            # adjust letter spacing to fit textLength\n            letter_spacing = (text_length - width) / spaces_count\n        width = text_length\n\n    # TODO: use real values\n    ascent, descent = font_size * .8, font_size * .2\n\n    # Align text box vertically\n    # TODO: This is a hack. Other baseline alignment tags are not supported.\n    # See https://www.w3.org/TR/SVG2/text.html#TextPropertiesSVG\n    y_align = 0\n    display_anchor = node.get('display-anchor')\n    alignment_baseline = node.get(\n        'dominant-baseline', node.get('alignment-baseline'))\n    if display_anchor == 'middle':\n        y_align = -height / 2\n    elif display_anchor == 'top':\n        pass\n    elif display_anchor == 'bottom':\n        y_align = -height\n    elif alignment_baseline in ('central', 'middle'):\n        # TODO: This is wrong, we use font top-to-bottom\n        y_align = (ascent + descent) / 2 - descent\n    elif alignment_baseline in (\n            'text-before-edge', 'before_edge', 'top', 'hanging', 'text-top'):\n        y_align = ascent\n    elif alignment_baseline in (\n            'text-after-edge', 'after_edge', 'bottom', 'text-bottom'):\n        y_align = -descent\n\n    # Return early when there’s no text\n    if not node.text:\n        x = x[0] if x else svg.cursor_position[0]\n        y = y[0] if y else svg.cursor_position[1]\n        dx = dx[0] if dx else 0\n        dy = dy[0] if dy else 0\n        svg.cursor_position = (x + dx, y + dy)\n        return\n\n    svg.stream.push_state()\n    svg.set_graphical_state(node, font_size, text=True)\n    svg.stream.begin_text()\n    emoji_lines = []\n\n    # Draw letters\n    for i, ((x, y, dx, dy, r), letter) in enumerate(letters_positions):\n        if x:\n            svg.cursor_d_position[0] = 0\n        if y:\n            svg.cursor_d_position[1] = 0\n        svg.cursor_d_position[0] += dx or 0\n        svg.cursor_d_position[1] += dy or 0\n        layout, _, _, width, height, baseline = split_first_line(\n            letter, style, svg.context, inf, 0)\n        x = svg.cursor_position[0] if x is None else x\n        y = svg.cursor_position[1] if y is None else y\n        width *= scale_x\n        if i:\n            x += letter_spacing\n        svg.cursor_position = x + width, y\n\n        x_position = x + svg.cursor_d_position[0]\n        y_position = y + svg.cursor_d_position[1] + y_align\n        angle = last_r if r is None else r\n        points = (\n            (x_position, y_position - baseline),\n            (x_position + width, y_position - baseline + height))\n        # TODO: Use ink extents instead of logical from line_break.line_size().\n        node.text_bounding_box = extend_bounding_box(\n            node.text_bounding_box, points)\n\n        layout.reactivate(style)\n        svg.fill_stroke(node, font_size, text=True)\n        matrix = Matrix(a=scale_x, d=-1, e=x_position, f=y_position)\n        if angle:\n            a, c = cos(angle), sin(angle)\n            matrix = Matrix(a, -c, c, a) @ matrix\n        emojis = draw_first_line(\n            svg.stream, TextBox(layout, style), 'none', 'none', matrix)\n        emoji_lines.append((x, y, emojis))\n\n    svg.stream.end_text()\n    svg.stream.pop_state()\n\n    for x, y, emojis in emoji_lines:\n        draw_emojis(svg.stream, style, x, y, emojis)\n"
  },
  {
    "path": "weasyprint/svg/utils.py",
    "content": "\"\"\"Util functions for SVG rendering.\"\"\"\n\nimport re\nfrom contextlib import suppress\nfrom math import cos, radians, sin, tan\nfrom urllib.parse import urlparse\n\nfrom tinycss2.color5 import parse_color\n\nfrom ..matrix import Matrix\n\n\nclass PointError(Exception):\n    \"\"\"Exception raised when parsing a point fails.\"\"\"\n\n\ndef normalize(string):\n    \"\"\"Give a canonical version of a given value string.\"\"\"\n    string = (string or '').replace('E', 'e')\n    string = re.sub('(?<!e)-', ' -', string)\n    string = re.sub('[ \\n\\r\\t,]+', ' ', string)\n    string = re.sub(r'(\\.[0-9-]+)(?=\\.)', r'\\1 ', string)\n    return string.strip()\n\n\ndef size(string, font_size=None, percentage_reference=None):\n    \"\"\"Compute size from string, resolving units and percentages.\"\"\"\n    from ..css.units import LENGTHS_TO_PIXELS\n\n    if not string:\n        return 0\n\n    with suppress(ValueError):\n        return float(string)\n\n    # Not a float, try something else\n    string = normalize(string).split(' ', 1)[0]\n    if string.endswith('%'):\n        assert percentage_reference is not None\n        return float(string[:-1]) * percentage_reference / 100\n    elif string.endswith('rem'):\n        assert font_size is not None\n        return font_size * float(string[:-3])\n    elif string.endswith('em'):\n        assert font_size is not None\n        return font_size * float(string[:-2])\n    elif string.endswith('ex'):\n        # Assume that 1em == 2ex\n        assert font_size is not None\n        return font_size * float(string[:-2]) / 2\n\n    for unit, coefficient in LENGTHS_TO_PIXELS.items():\n        if string.endswith(unit):\n            return float(string[:-len(unit)]) * coefficient\n\n    # Unknown size\n    return 0\n\n\ndef alpha_value(value):\n    \"\"\"Return opacity between 0 and 1 from str, number or percentage.\"\"\"\n    ratio = 1\n    if isinstance(value, str):\n        value = value.strip()\n        if value.endswith('%'):\n            ratio = 100\n            value = value[:-1].strip()\n    return min(1, max(0, float(value) / ratio))\n\n\ndef point(svg, string, font_size):\n    \"\"\"Pop first two size values from a string.\"\"\"\n    match = re.match('(.*?) (.*?)(?: |$)', string)\n    if match:\n        x, y = match.group(1, 2)\n        string = string[match.end():]\n        return (*svg.point(x, y, font_size), string)\n    else:\n        raise PointError\n\n\ndef preserve_ratio(svg, node, font_size, width, height, viewbox=None):\n    \"\"\"Compute scale and translation needed to preserve ratio.\"\"\"\n    viewbox = viewbox or node.get_viewbox()\n    if viewbox:\n        viewbox_width, viewbox_height = viewbox[2:]\n    elif svg.tree == node:\n        viewbox_width, viewbox_height = svg.get_intrinsic_size(font_size)\n        if None in (viewbox_width, viewbox_height):\n            return 1, 1, 0, 0\n    else:\n        return 1, 1, 0, 0\n\n    scale_x = width / viewbox_width if viewbox_width else 1\n    scale_y = height / viewbox_height if viewbox_height else 1\n\n    if viewbox:\n        aspect_ratio = node.get('preserveAspectRatio', 'xMidYMid').split()\n    else:\n        aspect_ratio = ('none',)\n    align = aspect_ratio[0]\n    if align == 'none':\n        x_position = 'min'\n        y_position = 'min'\n    else:\n        meet_or_slice = aspect_ratio[1] if len(aspect_ratio) > 1 else None\n        if meet_or_slice == 'slice':\n            scale_value = max(scale_x, scale_y)\n        else:\n            scale_value = min(scale_x, scale_y)\n        scale_x = scale_y = scale_value\n        x_position = align[1:4].lower()\n        y_position = align[5:].lower()\n\n    if node.tag == 'marker':\n        translate_x, translate_y = svg.point(\n            node.get('refX'), node.get('refY', '0'), font_size)\n    else:\n        translate_x = 0\n        if x_position == 'mid':\n            translate_x = (width - viewbox_width * scale_x) / 2\n        elif x_position == 'max':\n            translate_x = width - viewbox_width * scale_x\n\n        translate_y = 0\n        if y_position == 'mid':\n            translate_y += (height - viewbox_height * scale_y) / 2\n        elif y_position == 'max':\n            translate_y += height - viewbox_height * scale_y\n\n    if viewbox:\n        translate_x -= viewbox[0] * scale_x\n        translate_y -= viewbox[1] * scale_y\n\n    return scale_x, scale_y, translate_x, translate_y\n\n\ndef parse_url(url):\n    \"\"\"Parse a URL, possibly in a \"url(…)\" string.\"\"\"\n    if url and url.startswith('url(') and url.endswith(')'):\n        url = url[4:-1]\n        if len(url) >= 2:\n            for quote in (\"'\", '\"'):\n                if url[0] == url[-1] == quote:\n                    url = url[1:-1]\n                    break\n    return urlparse(url or '')\n\n\ndef color(string):\n    \"\"\"Safely parse a color string and return a RGBA tuple.\"\"\"\n    return parse_color(string or '') or parse_color('black')\n\n\ndef transform(transform_string, transform_origin, font_size, normalized_diagonal):\n    \"\"\"Get a matrix corresponding to the transform string.\"\"\"\n    # TODO: merge with gather_anchors and css.validation.properties.transform\n\n    origin_x, origin_y = 0, 0\n    size_strings = normalize(transform_origin).split()\n    if len(size_strings) == 2:\n        origin_x, origin_y = size(size_strings[0]), size(size_strings[1])\n    matrix = Matrix(e=origin_x, f=origin_y)\n\n    transformations = re.findall(r'(\\w+) ?\\( ?(.*?) ?\\)', normalize(transform_string))\n    for transformation_type, transformation in transformations:\n        values = [\n            size(value, font_size, normalized_diagonal)\n            for value in transformation.split(' ')]\n        if transformation_type == 'matrix':\n            matrix = Matrix(*values) @ matrix\n        elif transformation_type == 'rotate':\n            if len(values) == 3:\n                matrix = Matrix(e=values[1], f=values[2]) @ matrix\n            matrix = Matrix(\n                cos(radians(float(values[0]))),\n                sin(radians(float(values[0]))),\n                -sin(radians(float(values[0]))),\n                cos(radians(float(values[0])))) @ matrix\n            if len(values) == 3:\n                matrix = Matrix(e=-values[1], f=-values[2]) @ matrix\n        elif transformation_type.startswith('skew'):\n            if len(values) == 1:\n                values.append(0)\n            if transformation_type in ('skewX', 'skew'):\n                matrix = Matrix(\n                    c=tan(radians(float(values.pop(0))))) @ matrix\n            if transformation_type in ('skewY', 'skew'):\n                matrix = Matrix(\n                    b=tan(radians(float(values.pop(0))))) @ matrix\n        elif transformation_type.startswith('translate'):\n            if len(values) == 1:\n                values.append(0)\n            if transformation_type in ('translateX', 'translate'):\n                matrix = Matrix(e=values.pop(0)) @ matrix\n            if transformation_type in ('translateY', 'translate'):\n                matrix = Matrix(f=values.pop(0)) @ matrix\n        elif transformation_type.startswith('scale'):\n            if len(values) == 1:\n                values.append(values[0])\n            if transformation_type in ('scaleX', 'scale'):\n                matrix = Matrix(a=values.pop(0)) @ matrix\n            if transformation_type in ('scaleY', 'scale'):\n                matrix = Matrix(d=values.pop(0)) @ matrix\n\n    return Matrix(e=-origin_x, f=-origin_y) @ matrix\n"
  },
  {
    "path": "weasyprint/text/constants.py",
    "content": "\"\"\"Constants used for text layout.\"\"\"\n\nfrom functools import lru_cache\n\nfrom .ffi import pango\n\n# Pango features\nPANGO_STYLE = {\n    'normal': pango.PANGO_STYLE_NORMAL,\n    'oblique': pango.PANGO_STYLE_OBLIQUE,\n    'italic': pango.PANGO_STYLE_ITALIC,\n}\nPANGO_STRETCH = {\n    'ultra-condensed': pango.PANGO_STRETCH_ULTRA_CONDENSED,\n    'extra-condensed': pango.PANGO_STRETCH_EXTRA_CONDENSED,\n    'condensed': pango.PANGO_STRETCH_CONDENSED,\n    'semi-condensed': pango.PANGO_STRETCH_SEMI_CONDENSED,\n    'normal': pango.PANGO_STRETCH_NORMAL,\n    'semi-expanded': pango.PANGO_STRETCH_SEMI_EXPANDED,\n    'expanded': pango.PANGO_STRETCH_EXPANDED,\n    'extra-expanded': pango.PANGO_STRETCH_EXTRA_EXPANDED,\n    'ultra-expanded': pango.PANGO_STRETCH_ULTRA_EXPANDED,\n}\n# From https://drafts.csswg.org/css-fonts/#font-stretch-prop\nPANGO_STRETCH_PERCENT = {\n    50: pango.PANGO_STRETCH_ULTRA_CONDENSED,\n    62.5: pango.PANGO_STRETCH_EXTRA_CONDENSED,\n    75: pango.PANGO_STRETCH_CONDENSED,\n    87.5: pango.PANGO_STRETCH_SEMI_CONDENSED,\n    100: pango.PANGO_STRETCH_NORMAL,\n    112.5: pango.PANGO_STRETCH_SEMI_EXPANDED,\n    125: pango.PANGO_STRETCH_EXPANDED,\n    150: pango.PANGO_STRETCH_EXTRA_EXPANDED,\n    200: pango.PANGO_STRETCH_ULTRA_EXPANDED,\n}\nPANGO_WRAP_MODE = {\n    'WRAP_WORD': pango.PANGO_WRAP_WORD,\n    'WRAP_CHAR': pango.PANGO_WRAP_CHAR,\n    'WRAP_WORD_CHAR': pango.PANGO_WRAP_WORD_CHAR\n}\n# Some variants have been added in Pango 1.50 and are ignored when used.\nPANGO_VARIANT = {\n    'normal': pango.PANGO_VARIANT_NORMAL,\n    'small-caps': pango.PANGO_VARIANT_SMALL_CAPS,\n    'all-small-caps': pango.PANGO_VARIANT_ALL_SMALL_CAPS,\n    'petite-caps': pango.PANGO_VARIANT_PETITE_CAPS,\n    'all-petite-caps': pango.PANGO_VARIANT_ALL_PETITE_CAPS,\n    'unicase': pango.PANGO_VARIANT_UNICASE,\n    'titling-caps': pango.PANGO_VARIANT_TITLE_CAPS,\n}\n\nPANGO_DIRECTION = {\n    'ltr': pango.PANGO_DIRECTION_LTR,\n    'rtl': pango.PANGO_DIRECTION_RTL,\n}\n\n# Language system tags\n# From https://docs.microsoft.com/typography/opentype/spec/languagetags\nLST_TO_ISO = {\n    'aba': 'abq',\n    'afk': 'afr',\n    'afr': 'aar',\n    'agw': 'ahg',\n    'als': 'gsw',\n    'alt': 'atv',\n    'ari': 'aiw',\n    'ark': 'mhv',\n    'ath': 'apk',\n    'avr': 'ava',\n    'bad': 'bfq',\n    'bad0': 'bad',\n    'bag': 'bfy',\n    'bal': 'krc',\n    'bau': 'bci',\n    'bch': 'bcq',\n    'bgr': 'bul',\n    'bil': 'byn',\n    'bkf': 'bla',\n    'bli': 'bal',\n    'bln': 'bjt',\n    'blt': 'bft',\n    'bmb': 'bam',\n    'bri': 'bra',\n    'brm': 'mya',\n    'bsh': 'bak',\n    'bti': 'btb',\n    'chg': 'sgw',\n    'chh': 'hne',\n    'chi': 'nya',\n    'chk': 'ckt',\n    'chk0': 'chk',\n    'chu': 'chv',\n    'chy': 'chy',\n    'cmr': 'swb',\n    'crr': 'crx',\n    'crt': 'crh',\n    'csl': 'chu',\n    'csy': 'ces',\n    'dcr': 'cwd',\n    'dgr': 'doi',\n    'djr': 'dje',\n    'djr0': 'djr',\n    'dng': 'ada',\n    'dnk': 'din',\n    'dri': 'prs',\n    'dun': 'dng',\n    'dzn': 'dzo',\n    'ebi': 'igb',\n    'ecr': 'crj',\n    'edo': 'bin',\n    'erz': 'myv',\n    'esp': 'spa',\n    'eti': 'est',\n    'euq': 'eus',\n    'evk': 'evn',\n    'evn': 'eve',\n    'fan': 'acf',\n    'fan0': 'fan',\n    'far': 'fas',\n    'fji': 'fij',\n    'fle': 'vls',\n    'fne': 'enf',\n    'fos': 'fao',\n    'fri': 'fry',\n    'frl': 'fur',\n    'frp': 'frp',\n    'fta': 'fuf',\n    'gad': 'gaa',\n    'gae': 'gla',\n    'gal': 'glg',\n    'gaw': 'gbm',\n    'gil': 'niv',\n    'gil0': 'gil',\n    'gmz': 'guk',\n    'grn': 'kal',\n    'gro': 'grt',\n    'gua': 'grn',\n    'hai': 'hat',\n    'hal': 'flm',\n    'har': 'hoj',\n    'hbn': 'amf',\n    'hma': 'mrj',\n    'hnd': 'hno',\n    'ho': 'hoc',\n    'hri': 'har',\n    'hye0': 'hye',\n    'ijo': 'ijc',\n    'ing': 'inh',\n    'inu': 'iku',\n    'iri': 'gle',\n    'irt': 'gle',\n    'ism': 'smn',\n    'iwr': 'heb',\n    'jan': 'jpn',\n    'jii': 'yid',\n    'jud': 'lad',\n    'jul': 'dyu',\n    'kab': 'kbd',\n    'kab0': 'kab',\n    'kac': 'kfr',\n    'kal': 'kln',\n    'kar': 'krc',\n    'keb': 'ktb',\n    'kge': 'kat',\n    'kha': 'kjh',\n    'khk': 'kca',\n    'khs': 'kca',\n    'khv': 'kca',\n    'kis': 'kqs',\n    'kkn': 'kex',\n    'klm': 'xal',\n    'kmb': 'kam',\n    'kmn': 'kfy',\n    'kmo': 'kmw',\n    'kms': 'kxc',\n    'knr': 'kau',\n    'kod': 'kfa',\n    'koh': 'okm',\n    'kon': 'ktu',\n    'kon0': 'kon',\n    'kop': 'koi',\n    'koz': 'kpv',\n    'kpl': 'kpe',\n    'krk': 'kaa',\n    'krm': 'kdr',\n    'krn': 'kar',\n    'krt': 'kqy',\n    'ksh': 'kas',\n    'ksh0': 'ksh',\n    'ksi': 'kha',\n    'ksm': 'sjd',\n    'kui': 'kxu',\n    'kul': 'kfx',\n    'kuu': 'kru',\n    'kuy': 'kdt',\n    'kyk': 'kpy',\n    'lad': 'lld',\n    'lah': 'bfu',\n    'lak': 'lbe',\n    'lam': 'lmn',\n    'laz': 'lzz',\n    'lcr': 'crm',\n    'ldk': 'lbj',\n    'lma': 'mhr',\n    'lmb': 'lif',\n    'lmw': 'ngl',\n    'lsb': 'dsb',\n    'lsm': 'smj',\n    'lth': 'lit',\n    'luh': 'luy',\n    'lvi': 'lav',\n    'maj': 'mpe',\n    'mak': 'vmw',\n    'man': 'mns',\n    'map': 'arn',\n    'maw': 'mwr',\n    'mbn': 'kmb',\n    'mch': 'mnc',\n    'mcr': 'crm',\n    'mde': 'men',\n    'men': 'mym',\n    'miz': 'lus',\n    'mkr': 'mak',\n    'mle': 'mdy',\n    'mln': 'mlq',\n    'mlr': 'mal',\n    'mly': 'msa',\n    'mnd': 'mnk',\n    'mng': 'mon',\n    'mnk': 'man',\n    'mnx': 'glv',\n    'mok': 'mdf',\n    'mon': 'mnw',\n    'mth': 'mai',\n    'mts': 'mlt',\n    'mun': 'unr',\n    'nan': 'gld',\n    'nas': 'nsk',\n    'ncr': 'csw',\n    'ndg': 'ndo',\n    'nhc': 'csw',\n    'nis': 'dap',\n    'nkl': 'nyn',\n    'nko': 'nqo',\n    'nor': 'nob',\n    'nsm': 'sme',\n    'nta': 'nod',\n    'nto': 'epo',\n    'nyn': 'nno',\n    'ocr': 'ojs',\n    'ojb': 'oji',\n    'oro': 'orm',\n    'paa': 'sam',\n    'pal': 'pli',\n    'pap': 'plp',\n    'pap0': 'pap',\n    'pas': 'pus',\n    'pgr': 'ell',\n    'pil': 'fil',\n    'plg': 'pce',\n    'plk': 'pol',\n    'ptg': 'por',\n    'qin': 'bgr',\n    'rbu': 'bxr',\n    'rcr': 'atj',\n    'rms': 'roh',\n    'rom': 'ron',\n    'roy': 'rom',\n    'rsy': 'rue',\n    'rua': 'kin',\n    'sad': 'sck',\n    'say': 'chp',\n    'sek': 'xan',\n    'sel': 'sel',\n    'sgo': 'sag',\n    'sgs': 'sgs',\n    'sib': 'sjo',\n    'sig': 'xst',\n    'sks': 'sms',\n    'sky': 'slk',\n    'sla': 'scs',\n    'sml': 'som',\n    'sna': 'seh',\n    'sna0': 'sna',\n    'snh': 'sin',\n    'sog': 'gru',\n    'srb': 'srp',\n    'ssl': 'xsl',\n    'ssm': 'sma',\n    'sur': 'suq',\n    'sve': 'swe',\n    'swa': 'aii',\n    'swk': 'swa',\n    'swz': 'ssw',\n    'sxt': 'ngo',\n    'taj': 'tgk',\n    'tcr': 'cwd',\n    'tgn': 'ton',\n    'tgr': 'tig',\n    'tgy': 'tir',\n    'tht': 'tah',\n    'tib': 'bod',\n    'tkm': 'tuk',\n    'tmn': 'tem',\n    'tna': 'tsn',\n    'tne': 'enh',\n    'tng': 'toi',\n    'tod': 'xal',\n    'tod0': 'tod',\n    'trk': 'tur',\n    'tsg': 'tso',\n    'tua': 'tru',\n    'tul': 'tcy',\n    'tuv': 'tyv',\n    'twi': 'aka',\n    'usb': 'hsb',\n    'uyg': 'uig',\n    'vit': 'vie',\n    'vro': 'vro',\n    'wa': 'wbm',\n    'wag': 'wbr',\n    'wcr': 'crk',\n    'wel': 'cym',\n    'wlf': 'wol',\n    'xbd': 'khb',\n    'xhs': 'xho',\n    'yak': 'sah',\n    'yba': 'yor',\n    'ycr': 'cre',\n    'yim': 'iii',\n    'zhh': 'zho',\n    'zhp': 'zho',\n    'zhs': 'zho',\n    'zht': 'zho',\n    'znd': 'zne',\n}\n\n# Quotes, from https://github.com/unicode-org/cldr/tree/main/common/main\nLANG_QUOTES = {\n    None: (('“', '‘'), ('”', '’')),  # Default, chosen by user agent\n    'ab': (('«', '„'), ('»', '“')),\n    'agq': (('„', '‚'), ('”', '’')),\n    'am': (('«', '‹'), ('»', '›')),\n    'an': (('«', '”'), ('»', '”')),\n    'ar': (('”', '’'), ('“', '‘')),\n    'ast': (('«', '“'), ('»', '”')),\n    'az_Arab': (('«', '‹'), ('»', '›')),\n    'az_Cyrl': (('«', '‹'), ('»', '›')),\n    'bas': (('«', '„'), ('»', '“')),\n    'be': (('«', '„'), ('»', '“')),\n    'bg': (('„',), ('“',)),\n    'blo': (('«', '“'), ('»', '”')),\n    'bm': (('«', '“'), ('»', '”')),\n    'br': (('«', '“'), ('»', '”')),\n    'bs': (('„', '‘'), ('”', '’')),\n    'bs_Cyrl': (('„', '‚'), ('“', '‘')),\n    'ca': (('«', '“'), ('»', '”')),\n    'co': (('«',), ('»',)),\n    'cs': (('„', '‚'), ('“', '‘')),\n    'cu': (('«', '„'), ('»', '“')),\n    'cv': (('«', '„'), ('»', '“')),\n    'de': (('„', '‚'), ('“', '‘')),\n    'dsb': (('„', '‚'), ('“', '‘')),\n    'dua': (('«', '‘'), ('»', '’')),\n    'dyo': (('«', '“'), ('»', '”')),\n    'el': (('«', '“'), ('»', '”')),\n    'el_POLYTON': (('«', '‘'), ('»', '’')),\n    'es_US': (('«', '“'), ('»', '”')),\n    'et': (('„', '‚'), ('“', '‘')),\n    'eu': (('«', '“'), ('»', '”')),\n    'ewo': (('«', '“'), ('»', '”')),\n    'fa': (('«', '‹'), ('»', '›')),\n    'ff': (('„', '‚'), ('”', '’')),\n    'fi': (('”', '’'), ('”', '’')),\n    'fr': (('«',), ('»',)),\n    'fr_CA': (('«', '”'), ('»', '“')),\n    'fr_CH': (('«', '‹'), ('»', '›')),\n    'fur': (('‘', '“'), ('’', '”')),\n    'gsw': (('«', '‹'), ('»', '›')),\n    'he': (('”', '’'), ('”', '’')),\n    'hr': (('„', '‚'), ('“', '‘')),\n    'hsb': (('„', '‚'), ('“', '‘')),\n    'hu': (('„', '»'), ('”', '«')),\n    'hy': (('«',), ('»',)),\n    'ia': (('‘', '“'), ('’', '”')),\n    'ie': (('«', '“'), ('»', '”')),\n    'is': (('„', '‚'), ('“', '‘')),\n    'it': (('«', '“'), ('»', '”')),\n    'it_CH': (('«', '‹'), ('»', '›')),\n    'ja': (('「', '『'), ('」', '』')),\n    'jgo': (('«', '‹'), ('»', '›')),\n    'ka': (('„', '«'), ('“', '»')),\n    'kab': (('«', '“'), ('»', '”')),\n    'kk': (('«', '“'), ('»', '”')),\n    'kkj': (('«', '‹'), ('»', '›')),\n    'kl': (('»', '›'), ('«', '‹')),\n    'ksf': (('«', '‘'), ('»', '’')),\n    'ksh': (('„', '‚'), ('“', '‘')),\n    'ky': (('«', '„'), ('»', '“')),\n    'lag': (('”', '’'), ('”', '’')),\n    'lb': (('„', '‚'), ('“', '‘')),\n    'lij': (('«', '“'), ('»', '”')),\n    'lt': (('„',), ('“',)),\n    'luy': (('„', '‚'), ('“', '‘')),\n    'mg': (('«', '“'), ('»', '”')),\n    'mk': (('„', '‚'), ('“', '‘')),\n    'ms_Arab': (('”', '’'), ('“', '‘')),\n    'mua': (('«', '“'), ('»', '”')),\n    'mzn': (('«', '‹'), ('»', '›')),\n    'nds': (('„', '‚'), ('“', '‘')),\n    'nl': (('‘',), ('’',)),\n    'nmg': (('„', '«'), ('”', '»')),\n    'nnh': (('«', '“'), ('»', '”')),\n    'no': (('«', '‘'), ('»', '’')),\n    'nr': (('‘', '“'), ('’', '”')),\n    'nso': (('‘', '“'), ('’', '”')),\n    'oc': (('«',), ('»',)),\n    'oc_ES': (('«', '“'), ('»', '”')),\n    'os': (('«', '„'), ('»', '“')),\n    'pl': (('„', '«'), ('”', '»')),\n    'prg': (('„',), ('“',)),\n    'pt_PT': (('«', '“'), ('»', '”')),\n    'rm': (('«', '‹'), ('»', '›')),\n    'rn': (('”', '’'), ('”', '’')),\n    'ro': (('„', '«'), ('”', '»')),\n    'ru': (('«', '„'), ('»', '“')),\n    'rw': (('«', '‘'), ('»', '’')),\n    'sah': (('«', '„'), ('»', '“')),\n    'sc': (('«', '“'), ('»', '”')),\n    'sdh': (('«', '‹'), ('»', '›')),\n    'se': (('”', '’'), ('”', '’')),\n    'sg': (('«', '“'), ('»', '”')),\n    'shi': (('«', '„'), ('»', '”')),\n    'sk': (('„', '‚'), ('“', '‘')),\n    'sl': (('„', '‚'), ('“', '‘')),\n    'sn': (('”', '’'), ('”', '’')),\n    'sq': (('«', '“'), ('»', '”')),\n    'sr': (('„', '‘'), ('“', '‘')),\n    'sr_Latn': (('„', '‘'), ('“', '‘')),\n    'ss': (('‘', '“'), ('’', '”')),\n    'st': (('‘', '“'), ('’', '”')),\n    'sv': (('”', '’'), ('”', '’')),\n    'syr': (('”', '’'), ('“', '‘')),\n    'szl': (('„', '»'), ('”', '«')),\n    'tg': (('»', '‘'), ('«', '’')),\n    'ti': (('«', '“'), ('»', '”')),\n    'ti_ER': (('‘', '“'), ('’', '”')),\n    'tk': (('“',), ('”',)),\n    'tn': (('‘', '“'), ('’', '”')),\n    'ts': (('‘', '“'), ('’', '”')),\n    'ug': (('»', '›'), ('«', '‹')),\n    'uk': (('«', '„'), ('»', '“')),\n    'ur': (('”', '’'), ('“', '‘')),\n    'uz': (('“', '’'), ('”', '‘')),\n    've': (('‘', '“'), ('’', '”')),\n    'wae': (('«', '‹'), ('»', '›')),\n    'yav': (('«',), ('»',)),\n    'yi': (('”', '’'), ('”', '’')),\n    'yue': (('「', '『'), ('」', '』')),\n    'zgh': (('«', '„'), ('»', '”')),\n    'zh_Hant': (('「', '『'), ('」', '』')),\n}\n\n\n@lru_cache\ndef get_lang_quotes(lang):\n    if lang in LANG_QUOTES:\n        return LANG_QUOTES[lang]\n    # Revert to find long names before short ones\n    for key, value in tuple(LANG_QUOTES.items())[::-1]:\n        if key and lang.startswith(key):\n            return value\n    return LANG_QUOTES[None]\n\n\n# Font features\nLIGATURE_KEYS = {\n    'common-ligatures': ['liga', 'clig'],\n    'historical-ligatures': ['hlig'],\n    'discretionary-ligatures': ['dlig'],\n    'contextual': ['calt'],\n}\nCAPS_KEYS = {\n    'small-caps': ['smcp'],\n    'all-small-caps': ['c2sc', 'smcp'],\n    'petite-caps': ['pcap'],\n    'all-petite-caps': ['c2pc', 'pcap'],\n    'unicase': ['unic'],\n    'titling-caps': ['titl'],\n}\nNUMERIC_KEYS = {\n    'lining-nums': 'lnum',\n    'oldstyle-nums': 'onum',\n    'proportional-nums': 'pnum',\n    'tabular-nums': 'tnum',\n    'diagonal-fractions': 'frac',\n    'stacked-fractions': 'afrc',\n    'ordinal': 'ordn',\n    'slashed-zero': 'zero',\n}\nEAST_ASIAN_KEYS = {\n    'jis78': 'jp78',\n    'jis83': 'jp83',\n    'jis90': 'jp90',\n    'jis04': 'jp04',\n    'simplified': 'smpl',\n    'traditional': 'trad',\n    'full-width': 'fwid',\n    'proportional-width': 'pwid',\n    'ruby': 'ruby',\n}\n\n# Fontconfig features\nFONTCONFIG_WEIGHT = {\n    'normal': 80,\n    'bold': 200,\n    100: 0,\n    200: 40,\n    300: 50,\n    400: 80,\n    500: 100,\n    600: 180,\n    700: 200,\n    800: 205,\n    900: 210,\n}\nFONTCONFIG_STYLE = {\n    'normal': 'roman',\n    'italic': 'italic',\n    'oblique': 'oblique',\n}\nFONTCONFIG_STRETCH = {\n    'normal': 'normal',\n    'ultra-condensed': 'ultracondensed',\n    'extra-condensed': 'extracondensed',\n    'condensed': 'condensed',\n    'semi-condensed': 'semicondensed',\n    'semi-expanded': 'semiexpanded',\n    'expanded': 'expanded',\n    'extra-expanded': 'extraexpanded',\n    'ultra-expanded': 'ultraexpanded',\n}\n"
  },
  {
    "path": "weasyprint/text/ffi.py",
    "content": "\"\"\"Imports of dynamic libraries used for text layout.\"\"\"\n\nimport os\nimport sys\nfrom contextlib import suppress\n\nimport cffi\n\nffi = cffi.FFI()\nffi.cdef('''\n    // HarfBuzz\n\n    typedef ... hb_font_t;\n    typedef ... hb_face_t;\n    typedef ... hb_blob_t;\n    typedef int hb_bool_t;\n    typedef uint32_t hb_tag_t;\n    typedef uint32_t hb_codepoint_t;\n    hb_tag_t hb_tag_from_string (const char *str, int len);\n    void hb_tag_to_string (hb_tag_t tag, char *buf);\n    void hb_face_destroy (hb_face_t *face);\n    hb_blob_t * hb_face_reference_blob (hb_face_t *face);\n    unsigned int hb_face_get_index (const hb_face_t *face);\n    unsigned int hb_face_get_upem (const hb_face_t *face);\n    unsigned int hb_face_get_glyph_count (const hb_face_t *face);\n    hb_blob_t * hb_face_reference_table (const hb_face_t *face, hb_tag_t tag);\n    const char * hb_blob_get_data (hb_blob_t *blob, unsigned int *length);\n    unsigned int hb_blob_get_length (hb_blob_t *blob);\n    bool hb_ot_color_has_png (hb_face_t *face);\n    hb_blob_t * hb_ot_color_glyph_reference_png (hb_font_t *font, hb_codepoint_t glyph);\n    bool hb_ot_color_has_svg (hb_face_t *face);\n    hb_blob_t * hb_ot_color_glyph_reference_svg (hb_face_t *face, hb_codepoint_t glyph);\n    void hb_blob_destroy (hb_blob_t *blob);\n    unsigned int hb_face_get_table_tags (\n        const hb_face_t *face, unsigned int start_offset, unsigned int *table_count,\n        hb_tag_t *table_tags);\n    hb_bool_t hb_version_atleast (\n        unsigned int major, unsigned int minor, unsigned int micro);\n\n    // HarfBuzz Subset\n\n    typedef ... hb_subset_input_t;\n    typedef ... hb_set_t;\n\n    typedef enum {\n        HB_SUBSET_FLAGS_DEFAULT = 0x00000000u,\n        HB_SUBSET_FLAGS_NO_HINTING = 0x00000001u,\n        HB_SUBSET_FLAGS_RETAIN_GIDS = 0x00000002u,\n        HB_SUBSET_FLAGS_DESUBROUTINIZE = 0x00000004u,\n        HB_SUBSET_FLAGS_NAME_LEGACY = 0x00000008u,\n        HB_SUBSET_FLAGS_SET_OVERLAPS_FLAG = 0x00000010u,\n        HB_SUBSET_FLAGS_PASSTHROUGH_UNRECOGNIZED = 0x00000020u,\n        HB_SUBSET_FLAGS_NOTDEF_OUTLINE = 0x00000040u,\n        HB_SUBSET_FLAGS_GLYPH_NAMES = 0x00000080u,\n        HB_SUBSET_FLAGS_NO_PRUNE_UNICODE_RANGES = 0x00000100u,\n        HB_SUBSET_FLAGS_NO_LAYOUT_CLOSURE = 0x00000200u,\n    } hb_subset_flags_t;\n\n    typedef enum {\n        HB_SUBSET_SETS_GLYPH_INDEX = 0,\n        HB_SUBSET_SETS_UNICODE,\n        HB_SUBSET_SETS_NO_SUBSET_TABLE_TAG,\n        HB_SUBSET_SETS_DROP_TABLE_TAG,\n        HB_SUBSET_SETS_NAME_ID,\n        HB_SUBSET_SETS_NAME_LANG_ID,\n        HB_SUBSET_SETS_LAYOUT_FEATURE_TAG,\n        HB_SUBSET_SETS_LAYOUT_SCRIPT_TAG,\n    } hb_subset_sets_t;\n\n    hb_subset_input_t * hb_subset_input_create_or_fail (void);\n    void hb_subset_input_destroy (hb_subset_input_t *input);\n    hb_set_t * hb_subset_input_glyph_set (hb_subset_input_t *input);\n    void hb_set_add_sorted_array (\n        hb_set_t *set, const hb_codepoint_t *sorted_codepoints,\n        unsigned int num_codepoints);\n    hb_face_t * hb_subset_or_fail (hb_face_t *source, const hb_subset_input_t *input);\n    void hb_subset_input_set_flags (hb_subset_input_t *input, unsigned  value);\n    hb_set_t * hb_subset_input_set (\n        hb_subset_input_t *input, hb_subset_sets_t set_type);\n\n    // Pango\n\n    typedef unsigned int guint;\n    typedef int gint;\n    typedef char gchar;\n    typedef gint gboolean;\n    typedef void* gpointer;\n    typedef ... PangoLayout;\n    typedef ... PangoContext;\n    typedef ... PangoFontMap;\n    typedef ... PangoFontMetrics;\n    typedef ... PangoLanguage;\n    typedef ... PangoTabArray;\n    typedef ... PangoFontDescription;\n    typedef ... PangoLayoutIter;\n    typedef ... PangoAttrList;\n    typedef ... PangoAttrClass;\n    typedef ... PangoFont;\n    typedef guint PangoGlyph;\n    typedef gint PangoGlyphUnit;\n\n    const guint PANGO_GLYPH_EMPTY = 0x0FFFFFFF;\n    const guint PANGO_GLYPH_UNKNOWN_FLAG = 0x10000000;\n\n    typedef enum {\n        PANGO_STYLE_NORMAL,\n        PANGO_STYLE_OBLIQUE,\n        PANGO_STYLE_ITALIC\n    } PangoStyle;\n\n    typedef enum {\n        PANGO_WEIGHT_THIN = 100,\n        PANGO_WEIGHT_ULTRALIGHT = 200,\n        PANGO_WEIGHT_LIGHT = 300,\n        PANGO_WEIGHT_BOOK = 380,\n        PANGO_WEIGHT_NORMAL = 400,\n        PANGO_WEIGHT_MEDIUM = 500,\n        PANGO_WEIGHT_SEMIBOLD = 600,\n        PANGO_WEIGHT_BOLD = 700,\n        PANGO_WEIGHT_ULTRABOLD = 800,\n        PANGO_WEIGHT_HEAVY = 900,\n        PANGO_WEIGHT_ULTRAHEAVY = 1000\n    } PangoWeight;\n\n    typedef enum {\n        PANGO_FONT_MASK_SIZE = 1 << 5,\n        PANGO_FONT_MASK_GRAVITY = 1 << 6,\n        PANGO_FONT_MASK_VARIATIONS = 1 << 7\n    } PangoFontMask;\n\n    typedef enum {\n        PANGO_STRETCH_ULTRA_CONDENSED,\n        PANGO_STRETCH_EXTRA_CONDENSED,\n        PANGO_STRETCH_CONDENSED,\n        PANGO_STRETCH_SEMI_CONDENSED,\n        PANGO_STRETCH_NORMAL,\n        PANGO_STRETCH_SEMI_EXPANDED,\n        PANGO_STRETCH_EXPANDED,\n        PANGO_STRETCH_EXTRA_EXPANDED,\n        PANGO_STRETCH_ULTRA_EXPANDED\n    } PangoStretch;\n\n    typedef enum {\n        PANGO_WRAP_WORD,\n        PANGO_WRAP_CHAR,\n        PANGO_WRAP_WORD_CHAR\n    } PangoWrapMode;\n\n    typedef enum {\n        PANGO_VARIANT_NORMAL,\n        PANGO_VARIANT_SMALL_CAPS,\n        PANGO_VARIANT_ALL_SMALL_CAPS,\n        PANGO_VARIANT_PETITE_CAPS,\n        PANGO_VARIANT_ALL_PETITE_CAPS,\n        PANGO_VARIANT_UNICASE,\n        PANGO_VARIANT_TITLE_CAPS,\n    } PangoVariant;\n\n    typedef enum {\n        PANGO_TAB_LEFT\n    } PangoTabAlign;\n\n    typedef enum {\n        PANGO_ELLIPSIZE_NONE,\n        PANGO_ELLIPSIZE_START,\n        PANGO_ELLIPSIZE_MIDDLE,\n        PANGO_ELLIPSIZE_END\n    } PangoEllipsizeMode;\n\n    typedef enum {\n        PANGO_DIRECTION_LTR,\n        PANGO_DIRECTION_RTL,\n        PANGO_DIRECTION_TTB_LTR,\n        PANGO_DIRECTION_TTB_RTL,\n        PANGO_DIRECTION_WEAK_LTR,\n        PANGO_DIRECTION_WEAK_RTL,\n        PANGO_DIRECTION_NEUTRAL\n    } PangoDirection;\n\n    typedef struct GSList {\n       gpointer data;\n       struct GSList *next;\n    } GSList;\n\n    typedef struct {\n        void *shape_engine;\n        void *lang_engine;\n        PangoFont *font;\n        guint level;\n        guint gravity;\n        guint flags;\n        guint script;\n        PangoLanguage *language;\n        GSList *extra_attrs;\n    } PangoAnalysis;\n\n    typedef struct {\n        gint offset;\n        gint length;\n        gint num_chars;\n        PangoAnalysis analysis;\n    } PangoItem;\n\n    typedef struct {\n        PangoGlyphUnit width;\n        PangoGlyphUnit x_offset;\n        PangoGlyphUnit y_offset;\n    } PangoGlyphGeometry;\n\n    typedef struct {\n        guint is_cluster_start : 1;\n    } PangoGlyphVisAttr;\n\n    typedef struct {\n        PangoGlyph         glyph;\n        PangoGlyphGeometry geometry;\n        PangoGlyphVisAttr  attr;\n    } PangoGlyphInfo;\n\n    typedef struct {\n        gint num_glyphs;\n        PangoGlyphInfo *glyphs;\n        gint *log_clusters;\n    } PangoGlyphString;\n\n    typedef struct {\n        PangoItem        *item;\n        PangoGlyphString *glyphs;\n    } PangoGlyphItem;\n\n    typedef struct GSListRuns {\n       PangoGlyphItem    *data;\n       struct GSListRuns *next;\n    } GSListRuns;\n\n    typedef struct {\n        const PangoAttrClass *klass;\n        guint start_index;\n        guint end_index;\n    } PangoAttribute;\n\n    typedef struct {\n        PangoLayout *layout;\n        gint         start_index;\n        gint         length;\n        GSListRuns  *runs;\n        guint        is_paragraph_start : 1;\n        guint        resolved_dir : 3;\n    } PangoLayoutLine;\n\n    typedef struct  {\n        int x;\n        int y;\n        int width;\n        int height;\n    } PangoRectangle;\n\n    typedef struct {\n        guint is_line_break: 1;\n        guint is_mandatory_break : 1;\n        guint is_char_break : 1;\n        guint is_white : 1;\n        guint is_cursor_position : 1;\n        guint is_word_start : 1;\n        guint is_word_end : 1;\n        guint is_sentence_boundary : 1;\n        guint is_sentence_start : 1;\n        guint is_sentence_end : 1;\n        guint backspace_deletes_character : 1;\n        guint is_expandable_space : 1;\n        guint is_word_boundary : 1;\n    } PangoLogAttr;\n\n    int pango_version (void);\n\n    double pango_units_to_double (int i);\n    int pango_units_from_double (double d);\n    void g_object_unref (gpointer object);\n    void g_type_init (void);\n\n    PangoLayout * pango_layout_new (PangoContext *context);\n    void pango_layout_set_width (PangoLayout *layout, int width);\n    PangoAttrList * pango_layout_get_attributes (PangoLayout *layout);\n    void pango_layout_set_attributes (PangoLayout *layout, PangoAttrList *attrs);\n    void pango_layout_set_text (PangoLayout *layout, const char *text, int length);\n    void pango_layout_set_tabs (PangoLayout *layout, PangoTabArray *tabs);\n    void pango_layout_set_font_description (\n        PangoLayout *layout, const PangoFontDescription *desc);\n    void pango_layout_set_wrap (PangoLayout *layout, PangoWrapMode wrap);\n    void pango_layout_set_single_paragraph_mode (PangoLayout *layout, gboolean setting);\n    void pango_layout_set_ellipsize (PangoLayout *layout, PangoEllipsizeMode ellipsize);\n    void pango_layout_set_auto_dir (PangoLayout *layout, gboolean auto_dir);\n    int pango_layout_get_baseline (PangoLayout *layout);\n    void pango_layout_line_get_extents (\n        PangoLayoutLine *line, PangoRectangle *ink_rect, PangoRectangle *logical_rect);\n    PangoLayoutLine * pango_layout_get_line_readonly (PangoLayout *layout, int line);\n    const PangoLogAttr* pango_layout_get_log_attrs_readonly (\n        PangoLayout* layout, gint* n_attrs);\n\n    hb_font_t * pango_font_get_hb_font (PangoFont *font);\n\n    PangoFontDescription * pango_font_description_new (void);\n    void pango_font_description_free (PangoFontDescription *desc);\n    PangoFontMap* pango_font_get_font_map (PangoFont* font);\n\n    void pango_font_description_set_family (\n        PangoFontDescription *desc, const char *family);\n    void pango_font_description_set_style (\n        PangoFontDescription *desc, PangoStyle style);\n    void pango_font_description_set_stretch (\n        PangoFontDescription *desc, PangoStretch stretch);\n    void pango_font_description_set_weight (\n        PangoFontDescription *desc, PangoWeight weight);\n    void pango_font_description_set_absolute_size (\n        PangoFontDescription *desc, double size);\n    void pango_font_description_set_variations (\n        PangoFontDescription* desc, const char* variations);\n    void pango_font_description_set_variant (\n        PangoFontDescription* desc, PangoVariant variant);\n\n    PangoStyle pango_font_description_get_style (const PangoFontDescription *desc);\n    const char* pango_font_description_get_variations (\n        const PangoFontDescription* desc);\n    PangoWeight pango_font_description_get_weight (const PangoFontDescription* desc);\n    int pango_font_description_get_size (PangoFontDescription *desc);\n\n    void pango_font_description_unset_fields (\n        PangoFontDescription* desc, PangoFontMask to_unset);\n\n    char * pango_font_description_to_string (const PangoFontDescription *desc);\n\n    PangoFontDescription * pango_font_describe_with_absolute_size (PangoFont *font);\n    const char * pango_font_description_get_family (const PangoFontDescription *desc);\n    guint pango_font_description_hash (const PangoFontDescription *desc);\n\n    PangoContext * pango_font_map_create_context (PangoFontMap *fontmap);\n    PangoFont* pango_font_map_load_font (\n        PangoFontMap* fontmap, PangoContext* context, const PangoFontDescription* desc);\n\n    PangoFontMetrics * pango_context_get_metrics (\n        PangoContext *context, const PangoFontDescription *desc,\n        PangoLanguage *language);\n    PangoFontMetrics * pango_font_get_metrics (\n        PangoFont *font, PangoLanguage *language);\n    void pango_font_metrics_unref (PangoFontMetrics *metrics);\n    int pango_font_metrics_get_ascent (PangoFontMetrics *metrics);\n    int pango_font_metrics_get_descent (PangoFontMetrics *metrics);\n    int pango_font_metrics_get_underline_thickness (PangoFontMetrics *metrics);\n    int pango_font_metrics_get_underline_position (PangoFontMetrics *metrics);\n    int pango_font_metrics_get_strikethrough_thickness (PangoFontMetrics *metrics);\n    int pango_font_metrics_get_strikethrough_position (PangoFontMetrics *metrics);\n    void pango_font_get_glyph_extents (\n        PangoFont *font, PangoGlyph glyph, PangoRectangle *ink_rect,\n        PangoRectangle *logical_rect);\n\n    void pango_context_set_round_glyph_positions (\n        PangoContext *context, gboolean round_positions);\n\n    PangoAttrList * pango_attr_list_new (void);\n    void pango_attr_list_unref (PangoAttrList *list);\n    void pango_attr_list_insert (PangoAttrList *list, PangoAttribute *attr);\n    void pango_attr_list_change (PangoAttrList *list, PangoAttribute *attr);\n    PangoAttribute * pango_attr_font_features_new (const gchar *features);\n    PangoAttribute * pango_attr_letter_spacing_new (int letter_spacing);\n    PangoAttribute * pango_attr_insert_hyphens_new (gboolean insert_hyphens);\n\n    PangoTabArray * pango_tab_array_new_with_positions (\n        gint size, gboolean positions_in_pixels, PangoTabAlign first_alignment,\n        gint first_position, ...);\n    void pango_tab_array_free (PangoTabArray *tab_array);\n\n    PangoLanguage * pango_language_from_string (const char *language);\n    PangoLanguage * pango_language_get_default (void);\n    void pango_context_set_language (PangoContext *context, PangoLanguage *language);\n    void pango_context_set_base_dir (PangoContext *context, PangoDirection direction);\n\n    void pango_get_log_attrs (\n        const char *text, int length, int level, PangoLanguage *language,\n        PangoLogAttr *log_attrs, int attrs_len);\n\n\n    // FontConfig\n\n    typedef int FcBool;\n    typedef struct _FcConfig FcConfig;\n    typedef struct _FcPattern FcPattern;\n    typedef struct _FcStrList FcStrList;\n    typedef unsigned char FcChar8;\n\n    typedef enum {\n        FcResultMatch, FcResultNoMatch, FcResultTypeMismatch, FcResultNoId,\n        FcResultOutOfMemory\n    } FcResult;\n\n    typedef enum {\n        FcMatchPattern, FcMatchFont, FcMatchScan\n    } FcMatchKind;\n\n    typedef struct _FcFontSet {\n        int nfont;\n        int sfont;\n        FcPattern **fonts;\n    } FcFontSet;\n\n    typedef enum _FcSetName {\n        FcSetSystem = 0,\n        FcSetApplication = 1\n    } FcSetName;\n\n    FcConfig * FcInitLoadConfigAndFonts (void);\n    void FcConfigDestroy (FcConfig *config);\n    FcBool FcConfigAppFontAddFile (FcConfig *config, const FcChar8 *file);\n    FcBool FcConfigParseAndLoadFromMemory (\n        FcConfig *config, const FcChar8 *buffer, FcBool complain);\n\n    FcFontSet * FcConfigGetFonts (FcConfig *config, FcSetName set);\n    FcStrList * FcConfigGetConfigFiles (FcConfig *config);\n    FcChar8 * FcStrListNext (FcStrList *list);\n\n    void FcDefaultSubstitute (FcPattern *pattern);\n    FcBool FcConfigSubstitute (FcConfig *config, FcPattern *p, FcMatchKind kind);\n\n    FcPattern * FcPatternCreate (void);\n    FcPattern * FcPatternDestroy (FcPattern *p);\n    FcBool FcPatternAddString (FcPattern *p, const char *object, const FcChar8 *s);\n    FcResult FcPatternGetString (FcPattern *p, const char *object, int n, FcChar8 **s);\n    FcPattern * FcFontMatch (FcConfig *config, FcPattern *p, FcResult *result);\n\n\n    // PangoFT2\n\n    typedef ... PangoFcFont;\n    typedef ... PangoFcFontMap;\n\n    PangoFontMap * pango_ft2_font_map_new (void);\n    void pango_fc_font_map_set_config (PangoFcFontMap *fcfontmap, FcConfig *fcconfig);\n    void pango_fc_font_map_config_changed (PangoFcFontMap *fcfontmap);\n    hb_face_t* pango_fc_font_map_get_hb_face (\n         PangoFcFontMap* fcfontmap, PangoFcFont* fcfont);\n''')\n\n\ndef _dlopen(ffi, *names, allow_fail=False):\n    \"\"\"Try various names for the same library, for different platforms.\"\"\"\n    if os.name == 'nt':\n        flags = 0x00001000  # LOAD_LIBRARY_SEARCH_DEFAULT_DIRS\n    else:\n        flags = ffi.RTLD_NOW  # default\n    for name in names:\n        with suppress(OSError):\n            return ffi.dlopen(name, flags)\n    if allow_fail:\n        return\n    # Print error message and re-raise the exception.\n    print(  # noqa: T201, logger is not configured yet\n        '\\n-----\\n\\n'\n        'WeasyPrint could not import some external libraries. Please '\n        'carefully follow the installation steps before reporting an issue:\\n'\n        'https://doc.courtbouillon.org/weasyprint/stable/'\n        'first_steps.html#installation\\n'\n        'https://doc.courtbouillon.org/weasyprint/stable/'\n        'first_steps.html#troubleshooting',\n        '\\n\\n-----\\n')  # pragma: no cover\n    return ffi.dlopen(names[0], flags)  # pragma: no cover\n\n\nif hasattr(os, 'add_dll_directory') and not hasattr(sys, 'frozen'):  # pragma: no cover\n    dll_directories = os.getenv(\n        'WEASYPRINT_DLL_DIRECTORIES',\n        'C:\\\\msys64\\\\mingw64\\\\bin;'\n        'C:\\\\Program Files\\\\GTK3-Runtime Win64\\\\bin').split(';')\n    for dll_directory in dll_directories:\n        with suppress((OSError, FileNotFoundError)):\n            os.add_dll_directory(dll_directory)\n\ngobject = _dlopen(\n    ffi, 'libgobject-2.0-0', 'gobject-2.0-0', 'gobject-2.0',\n    'libgobject-2.0.so.0', 'libgobject-2.0.0.dylib', 'libgobject-2.0-0.dll')\npango = _dlopen(\n    ffi, 'libpango-1.0-0', 'pango-1.0-0', 'pango-1.0', 'libpango-1.0.so.0',\n    'libpango-1.0.dylib', 'libpango-1.0-0.dll')\nharfbuzz = _dlopen(\n    ffi, 'libharfbuzz-0', 'harfbuzz', 'harfbuzz-0.0',\n    'libharfbuzz.so.0', 'libharfbuzz.0.dylib', 'libharfbuzz-0.dll')\nharfbuzz_subset = _dlopen(\n    ffi, 'libharfbuzz-subset-0', 'harfbuzz-subset', 'harfbuzz-subset-0.0',\n    'libharfbuzz-subset.so.0', 'libharfbuzz-subset.0.dylib', 'libharfbuzz-subset-0.dll',\n    allow_fail=True)\nfontconfig = _dlopen(\n    ffi, 'libfontconfig-1', 'fontconfig-1', 'fontconfig',\n    'libfontconfig.so.1', 'libfontconfig.1.dylib', 'libfontconfig-1.dll')\npangoft2 = _dlopen(\n    ffi, 'libpangoft2-1.0-0', 'pangoft2-1.0-0', 'pangoft2-1.0',\n    'libpangoft2-1.0.so.0', 'libpangoft2-1.0.dylib', 'libpangoft2-1.0-0.dll')\n\ngobject.g_type_init()\n\n# Call once to avoid int overflows.\nTO_UNITS = pango.pango_units_from_double(1)\nFROM_UNITS = pango.pango_units_to_double(1)\n\n\ndef unicode_to_char_p(string):\n    \"\"\"Return ``(pointer, bytestring)``.\n\n    The byte string must live at least as long as the pointer is used.\n\n    \"\"\"\n    bytestring = string.encode().replace(b'\\x00', b'')\n    return ffi.new('char[]', bytestring), bytestring\n"
  },
  {
    "path": "weasyprint/text/fonts.py",
    "content": "\"\"\"Interface with external libraries managing fonts installed on the system.\"\"\"\n\nfrom hashlib import md5\nfrom io import BytesIO\nfrom locale import getpreferredencoding\nfrom pathlib import Path\nfrom shutil import rmtree\nfrom tempfile import mkdtemp\nfrom warnings import warn\nfrom xml.etree.ElementTree import Element, SubElement, tostring\n\nfrom fontTools.ttLib import TTFont, woff2\n\nfrom ..logger import LOGGER\nfrom ..urls import fetch\n\nfrom .constants import (  # isort:skip\n    CAPS_KEYS, EAST_ASIAN_KEYS, FONTCONFIG_STRETCH, FONTCONFIG_STYLE, FONTCONFIG_WEIGHT,\n    LIGATURE_KEYS, NUMERIC_KEYS, PANGO_STRETCH, PANGO_STYLE, PANGO_VARIANT)\nfrom .ffi import (  # isort:skip\n    FROM_UNITS, TO_UNITS, ffi, fontconfig, gobject, harfbuzz, pango, pangoft2,\n    unicode_to_char_p)\n\nPREFERRED_ENCODING = getpreferredencoding(False)\n\n\ndef _check_font_configuration(font_config):  # pragma: no cover\n    \"\"\"Check whether the given font_config has fonts.\n\n    The default fontconfig configuration file may be missing (particularly\n    on Windows or macOS, where installation of fontconfig isn't as\n    standardized as on Linux), resulting in \"Fontconfig error: Cannot load\n    default config file\".\n\n    Fontconfig tries to retrieve the system fonts as fallback, which may or\n    may not work, especially on macOS, where fonts can be installed at\n    various loactions. On Windows (at least since fontconfig 2.13) the\n    fallback seems to work.\n\n    If there’s no default configuration and the system fonts fallback\n    fails, or if the configuration file exists but doesn’t provide fonts,\n    output will be ugly.\n\n    If you happen to have no fonts and an HTML document without a valid\n    @font-face, all letters turn into rectangles.\n\n    If you happen to have an HTML document with at least one valid\n    @font-face, all text is styled with that font.\n\n    On Windows and macOS we can cause Pango to use native font rendering\n    instead of rendering fonts with FreeType. But then we must do without\n    @font-face. Expect other missing features and ugly output.\n\n    \"\"\"\n    # Having fonts means: fontconfig's config file returns fonts or\n    # fontconfig managed to retrieve system fallback-fonts. On Windows the\n    # fallback stragegy seems to work since fontconfig >= 2.13.\n    fonts = fontconfig.FcConfigGetFonts(font_config, fontconfig.FcSetSystem)\n    # Of course, with nfont == 1 the user wont be happy, too…\n    if fonts.nfont > 0:\n        return\n\n    # Find the reason why we have no fonts.\n    config_files = fontconfig.FcConfigGetConfigFiles(font_config)\n    config_file = fontconfig.FcStrListNext(config_files)\n    if config_file == ffi.NULL:\n        warn('FontConfig cannot load default config file. Expect ugly output.')\n    else:\n        # Useless config file, or indeed no fonts.\n        warn('No fonts configured in FontConfig. Expect ugly output.')\n\n\n_check_font_configuration(ffi.gc(\n    fontconfig.FcInitLoadConfigAndFonts(), fontconfig.FcConfigDestroy))\n\n\nclass FontConfiguration:\n    \"\"\"A Fontconfig font configuration.\n\n    Keep a list of fonts, including fonts installed on the system, fonts\n    installed for the current user, and fonts referenced by cascading\n    stylesheets.\n\n    When created, an instance of this class gathers available fonts. It can\n    then be given to :class:`weasyprint.HTML` methods or to\n    :class:`weasyprint.CSS` to find fonts in ``@font-face`` rules.\n\n    \"\"\"\n    _folder = None  # required by __del__ when code stops before __init__ finishes\n\n    def __init__(self):\n        \"\"\"Create a Fontconfig font configuration.\n\n        See Behdad's blog:\n        https://mces.blogspot.fr/2015/05/how-to-use-custom-application-fonts.html\n\n        \"\"\"\n        # Load the main config file and the fonts.\n        self._config = ffi.gc(\n            fontconfig.FcInitLoadConfigAndFonts(), fontconfig.FcConfigDestroy)\n        self.font_map = ffi.gc(\n            pangoft2.pango_ft2_font_map_new(), gobject.g_object_unref)\n        pangoft2.pango_fc_font_map_set_config(\n            ffi.cast('PangoFcFontMap *', self.font_map), self._config)\n        # pango_fc_font_map_set_config keeps a reference to config.\n        fontconfig.FcConfigDestroy(self._config)\n\n        # Temporary folder storing fonts.\n        self._folder = None\n\n        # Cache.\n        self.strut_layouts = {}\n        self.font_features = {}\n\n    def add_font_face(self, rule_descriptors, url_fetcher):\n        \"\"\"Add a font face to the Fontconfig configuration.\"\"\"\n\n        # Define path where to save font, depending on the rule descriptors.\n        config_key = str(rule_descriptors)\n        config_digest = md5(config_key.encode(), usedforsecurity=False).hexdigest()\n        if self._folder is None:\n            self._folder = Path(mkdtemp(prefix='weasyprint-'))\n        font_path = self._folder / config_digest\n        if font_path.exists():\n            # Font already exists, we have nothing more to do.\n            return\n\n        # Try values in \"src\" descriptor until one works.\n        string = ffi.new('FcChar8 **')\n        for font_type, url in rule_descriptors['src']:\n            # Abort if font URL is broken.\n            if url is None or font_type == 'internal':\n                continue\n\n            # Try to find a font installed on the system that matches descriptors.\n            if font_type == 'local':\n                # Create a pattern that matches font name.\n                font_name = url.encode()\n                pattern = ffi.gc(\n                    fontconfig.FcPatternCreate(), fontconfig.FcPatternDestroy)\n                fontconfig.FcConfigSubstitute(\n                    self._config, pattern, fontconfig.FcMatchFont)\n                fontconfig.FcDefaultSubstitute(pattern)\n                fontconfig.FcPatternAddString(pattern, b'fullname', font_name)\n                fontconfig.FcPatternAddString(pattern, b'postscriptname', font_name)\n                result = ffi.new('FcResult *')\n                matching_pattern = fontconfig.FcFontMatch(self._config, pattern, result)\n                if matching_pattern == ffi.NULL:\n                    # No font has been found, abort.\n                    LOGGER.debug('Failed to get matching local font for %r', url)\n                    continue\n\n                # Check that the font name in descriptor matches name in font.\n                for tag in b'fullname', b'postscriptname':\n                    fontconfig.FcPatternGetString(matching_pattern, tag, 0, string)\n                    name = ffi.string(string[0])\n                    if font_name.lower() == name.lower():\n                        fontconfig.FcPatternGetString(\n                            matching_pattern, b'file', 0, string)\n                        path = ffi.string(string[0]).decode(PREFERRED_ENCODING)\n                        url = Path(path).as_uri()\n                        break\n                else:\n                    # Names don’t match, abort.\n                    LOGGER.debug('Failed to load local font %r', font_name.decode())\n                    continue\n\n            # Get font content.\n            try:\n                with fetch(url_fetcher, url) as response:\n                    font = response.read()\n            except Exception as exception:\n                LOGGER.debug('Failed to load font at %r (%s)', url, exception)\n                continue\n\n            # Store font content.\n            try:\n                # Decode woff and woff2 fonts.\n                if font[:3] == b'wOF':\n                    out = BytesIO()\n                    woff_version_byte = font[3:4]\n                    if woff_version_byte == b'F':  # woff font\n                        ttfont = TTFont(BytesIO(font))\n                        ttfont.flavor = ttfont.flavorData = None\n                        ttfont.save(out)\n                    elif woff_version_byte == b'2':  # woff2 font\n                        woff2.decompress(BytesIO(font), out)\n                    font = out.getvalue()\n            except Exception as exc:\n                LOGGER.debug('Failed to handle woff font at %r (%s)', url, exc)\n                continue\n            font_path.write_bytes(font)\n\n            # Create Fontconfig XML config file.\n            mode = 'assign_replace'\n            root = Element('fontconfig')\n            match = SubElement(root, 'match', target='scan')\n            test = SubElement(match, 'test', name='file', compare='eq')\n            SubElement(test, 'string').text = str(font_path)\n            # Prepend, as replacing the font family breaks Pango, see #2510.\n            edit = SubElement(match, 'edit', name='family', mode='prepend')\n            SubElement(edit, 'string').text = rule_descriptors['font_family']\n            if 'font_style' in rule_descriptors:\n                edit = SubElement(match, 'edit', name='slant', mode=mode)\n                text = FONTCONFIG_STYLE[rule_descriptors['font_style']]\n                SubElement(edit, 'const').text = text\n            if 'font_weight' in rule_descriptors:\n                edit = SubElement(match, 'edit', name='weight', mode=mode)\n                integer = FONTCONFIG_WEIGHT[rule_descriptors['font_weight']]\n                SubElement(edit, 'int').text = str(integer)\n            if 'font_stretch' in rule_descriptors:\n                edit = SubElement(match, 'edit', name='width', mode=mode)\n                text = FONTCONFIG_STRETCH[rule_descriptors['font_stretch']]\n                SubElement(edit, 'const').text = text\n            match = SubElement(root, 'match', target='font')\n            test = SubElement(match, 'test', name='file', compare='eq')\n            SubElement(test, 'string').text = str(font_path)\n            descriptors = {\n                rules[0][0].replace('-', '_'): rules[0][1] for rules in\n                rule_descriptors.get('font_variant', [])}\n            settings = rule_descriptors.get('font_feature_settings', 'normal')\n            features = font_features(font_feature_settings=settings, **descriptors)\n            if features:\n                edit = SubElement(match, 'edit', name='fontfeatures', mode=mode)\n                for key, value in features.items():\n                    SubElement(edit, 'string').text = f'{key} {value}'\n            if unicode_ranges := rule_descriptors.get('unicode_range'):\n                edit = SubElement(match, 'edit', name='charset', mode=mode)\n                plus = SubElement(edit, 'plus')\n                for unicode_range in unicode_ranges:\n                    charset = SubElement(plus, 'charset')\n                    range_ = SubElement(charset, 'range')\n                    for value in (unicode_range.start, unicode_range.end):\n                        SubElement(range_, 'int').text = f'0x{value:x}'\n            header = (\n                b'<?xml version=\"1.0\"?>',\n                b'<!DOCTYPE fontconfig SYSTEM \"urn:fontconfig:fonts.dtd\">')\n            xml = b'\\n'.join((*header, tostring(root, encoding='utf-8')))\n\n            # Register font and configuration in Fontconfig.\n            # TODO: We should mask local fonts with the same name\n            # too as explained in Behdad's blog entry.\n            fontconfig.FcConfigParseAndLoadFromMemory(self._config, xml, True)\n            font_added = fontconfig.FcConfigAppFontAddFile(\n                self._config, str(font_path).encode(PREFERRED_ENCODING))\n            if font_added:\n                return pangoft2.pango_fc_font_map_config_changed(\n                    ffi.cast('PangoFcFontMap *', self.font_map))\n            LOGGER.debug('Failed to load font at %r', url)\n        LOGGER.warning('Font-face %r cannot be loaded', rule_descriptors['font_family'])\n\n    def __del__(self):\n        \"\"\"Clean a font configuration for a document.\"\"\"\n        if self._folder:\n            rmtree(self._folder, ignore_errors=True)\n\n\ndef font_features(font_kerning='normal', font_variant_ligatures='normal',\n                  font_variant_position='normal', font_variant_caps='normal',\n                  font_variant_numeric='normal', font_variant_alternates='normal',\n                  font_variant_east_asian='normal', font_feature_settings='normal'):\n    \"\"\"Get the font features from the different properties in style.\n\n    See https://www.w3.org/TR/css-fonts-3/#feature-precedence\n\n    \"\"\"\n    features = {}\n\n    # Step 1: getting the default, we rely on Pango for this.\n    # Step 2: @font-face font-variant, done in fonts.add_font_face.\n    # Step 3: @font-face font-feature-settings, done in fonts.add_font_face.\n\n    # Step 4: font-variant and OpenType features.\n\n    if font_kerning != 'auto':\n        features['kern'] = int(font_kerning == 'normal')\n\n    if font_variant_ligatures == 'none':\n        for keys in LIGATURE_KEYS.values():\n            for key in keys:\n                features[key] = 0\n    elif font_variant_ligatures != 'normal':\n        for ligature_type in font_variant_ligatures:\n            value = 1\n            if ligature_type.startswith('no-'):\n                value = 0\n                ligature_type = ligature_type[3:]\n            for key in LIGATURE_KEYS[ligature_type]:\n                features[key] = value\n\n    if font_variant_position == 'sub':\n        # TODO: the specification asks for additional checks\n        # https://www.w3.org/TR/css-fonts-3/#font-variant-position-prop\n        features['subs'] = 1\n    elif font_variant_position == 'super':\n        features['sups'] = 1\n\n    if font_variant_caps != 'normal':\n        # TODO: the specification asks for additional checks\n        # https://www.w3.org/TR/css-fonts-3/#font-variant-caps-prop\n        for key in CAPS_KEYS[font_variant_caps]:\n            features[key] = 1\n\n    if font_variant_numeric != 'normal':\n        for key in font_variant_numeric:\n            features[NUMERIC_KEYS[key]] = 1\n\n    if font_variant_alternates != 'normal':\n        # TODO: support other values\n        # See https://drafts.csswg.org/css-fonts/#font-variant-alternates-prop\n        if font_variant_alternates == 'historical-forms':\n            features['hist'] = 1\n\n    if font_variant_east_asian != 'normal':\n        for key in font_variant_east_asian:\n            features[EAST_ASIAN_KEYS[key]] = 1\n\n    # Step 5: incompatible non-OpenType features, already handled by Pango.\n\n    # Step 6: font-feature-settings.\n\n    if font_feature_settings != 'normal':\n        features.update(dict(font_feature_settings))\n\n    return features\n\n\ndef get_font_description(style):\n    \"\"\"Get font description string out of given style.\"\"\"\n    font_description = ffi.gc(\n        pango.pango_font_description_new(), pango.pango_font_description_free)\n    family_p, family = unicode_to_char_p(','.join(style['font_family']))\n    pango.pango_font_description_set_family(font_description, family_p)\n    font_style = PANGO_STYLE[style['font_style']]\n    pango.pango_font_description_set_style(font_description, font_style)\n    font_stretch = PANGO_STRETCH[style['font_stretch']]\n    pango.pango_font_description_set_stretch(font_description, font_stretch)\n    font_weight = style['font_weight']\n    pango.pango_font_description_set_weight(font_description, font_weight)\n    font_size = int(style['font_size'] * TO_UNITS)\n    pango.pango_font_description_set_absolute_size(font_description, font_size)\n    font_variant = PANGO_VARIANT[style['font_variant_caps']]\n    pango.pango_font_description_set_variant(font_description, font_variant)\n    if style['font_variation_settings'] != 'normal':\n        string = ','.join(\n            f'{key}={value}' for key, value in\n            style['font_variation_settings']).encode()\n        pango.pango_font_description_set_variations(font_description, string)\n    return font_description\n\n\ndef get_pango_font_hb_face(pango_font):\n    \"\"\"Get Harfbuzz face out of given Pango font.\"\"\"\n    fc_font = ffi.cast('PangoFcFont *', pango_font)\n    fontmap = ffi.cast('PangoFcFontMap *', pango.pango_font_get_font_map(pango_font))\n    return pangoft2.pango_fc_font_map_get_hb_face(fontmap, fc_font)\n\n\ndef get_hb_object_data(hb_object, ot_color=None, glyph=None):\n    \"\"\"Get binary data out of given Harfbuzz font or face.\n\n    If ``ot_color`` is 'svg', return the SVG color glyph reference. If it’s 'png',\n    return the PNG color glyph reference. Otherwise, return the whole face blob.\n\n    \"\"\"\n    if ot_color == 'png':\n        hb_blob = harfbuzz.hb_ot_color_glyph_reference_png(hb_object, glyph)\n    elif ot_color == 'svg':\n        hb_blob = harfbuzz.hb_ot_color_glyph_reference_svg(hb_object, glyph)\n    else:\n        hb_blob = harfbuzz.hb_face_reference_blob(hb_object)\n    with ffi.new('unsigned int *') as length:\n        hb_data = harfbuzz.hb_blob_get_data(hb_blob, length)\n        data = None if hb_data == ffi.NULL else ffi.unpack(hb_data, int(length[0]))\n        harfbuzz.hb_blob_destroy(hb_blob)\n        return data\n\n\ndef get_pango_font_key(pango_font):\n    \"\"\"Get key corresponding to given Pango font.\"\"\"\n    # TODO: This value is stable for a given Pango font in a given Pango map, but can’t\n    # be cached with just the Pango font as a key because two Pango fonts could point to\n    # the same address for two different Pango maps. We should cache it in the\n    # FontConfiguration object. See issue #2144.\n    description = ffi.gc(\n        pango.pango_font_describe_with_absolute_size(pango_font),\n        pango.pango_font_description_free)\n    font_size = pango.pango_font_description_get_size(description) * FROM_UNITS\n    mask = pango.PANGO_FONT_MASK_SIZE + pango.PANGO_FONT_MASK_GRAVITY\n    pango.pango_font_description_unset_fields(description, mask)\n    return pango.pango_font_description_hash(description), description, font_size\n"
  },
  {
    "path": "weasyprint/text/line_break.py",
    "content": "\"\"\"Decide where to break text lines.\"\"\"\n\nimport re\nfrom math import inf\n\nimport pyphen\n\nfrom .constants import LST_TO_ISO, PANGO_DIRECTION, PANGO_WRAP_MODE\nfrom .ffi import FROM_UNITS, TO_UNITS, ffi, gobject, pango, unicode_to_char_p\nfrom .fonts import font_features, get_font_description\n\n\ndef line_size(line, style):\n    \"\"\"Get logical width and height of the given ``line``.\n\n    ``style`` is used to add letter spacing (if needed).\n\n    \"\"\"\n    logical_extents = ffi.new('PangoRectangle *')\n    pango.pango_layout_line_get_extents(line, ffi.NULL, logical_extents)\n    width = logical_extents.width * FROM_UNITS\n    height = logical_extents.height * FROM_UNITS\n    ffi.release(logical_extents)\n    if style['letter_spacing'] != 'normal':\n        width += style['letter_spacing']\n    return width, height\n\n\ndef first_line_metrics(first_line, text, layout, resume_at, space_collapse,\n                       style, hyphenated=False, hyphenation_character=None):\n    length = first_line.length\n    if hyphenated:\n        length -= len(hyphenation_character.encode())\n    elif resume_at:\n        # Set an infinite width as we don't want to break lines when drawing,\n        # the lines have already been split and the size may differ. Rendering\n        # is also much faster when no width is set.\n        pango.pango_layout_set_width(layout.layout, -1)\n\n        # Create layout with final text\n        first_line_text = text.encode()[:length].decode()\n\n        # Remove trailing spaces if spaces collapse\n        if space_collapse:\n            first_line_text = first_line_text.rstrip(' ')\n\n        layout.set_text(first_line_text)\n        first_line, _ = layout.get_first_line()\n        length = first_line.length if first_line is not None else 0\n\n    width, height = line_size(first_line, style)\n    baseline = pango.pango_layout_get_baseline(layout.layout) * FROM_UNITS\n    layout.deactivate()\n    return layout, length, resume_at, width, height, baseline\n\n\nclass Layout:\n    \"\"\"Object holding PangoLayout-related cdata pointers.\"\"\"\n    def __init__(self, style, justification_spacing=0, max_width=None):\n        self.justification_spacing = justification_spacing\n        self.setup(style)\n        self.max_width = max_width\n\n    def setup(self, style):\n        self.style = style\n        self.first_line_direction = 0\n\n        font_map = style.font_config.font_map\n        pango_context = ffi.gc(\n            pango.pango_font_map_create_context(font_map),\n            gobject.g_object_unref)\n        pango.pango_context_set_round_glyph_positions(pango_context, False)\n        pango.pango_context_set_base_dir(\n            pango_context, PANGO_DIRECTION[style['direction']])\n\n        if style['font_language_override'] != 'normal':\n            lang_p, lang = unicode_to_char_p(LST_TO_ISO.get(\n                style['font_language_override'].lower(),\n                style['font_language_override']))\n        elif style['lang']:\n            lang_p, lang = unicode_to_char_p(style['lang'])\n        else:\n            lang = None\n            self.language = pango.pango_language_get_default()\n        if lang:\n            self.language = pango.pango_language_from_string(lang_p)\n            pango.pango_context_set_language(pango_context, self.language)\n\n        assert not isinstance(style['font_family'], str), (\n            'font_family should be a list')\n        font_description = get_font_description(style)\n        self.layout = ffi.gc(\n            pango.pango_layout_new(pango_context),\n            gobject.g_object_unref)\n        pango.pango_layout_set_auto_dir(self.layout, False)\n        pango.pango_layout_set_font_description(self.layout, font_description)\n\n        text_decoration = style['text_decoration_line']\n        if text_decoration != 'none':\n            metrics = ffi.gc(\n                pango.pango_context_get_metrics(\n                    pango_context, font_description, self.language),\n                pango.pango_font_metrics_unref)\n            self.ascent = FROM_UNITS * (\n                pango.pango_font_metrics_get_ascent(metrics))\n            self.underline_position = FROM_UNITS * (\n                pango.pango_font_metrics_get_underline_position(metrics))\n            self.strikethrough_position = FROM_UNITS * (\n                pango.pango_font_metrics_get_strikethrough_position(metrics))\n            self.underline_thickness = FROM_UNITS * (\n                pango.pango_font_metrics_get_underline_thickness(metrics))\n            self.strikethrough_thickness = FROM_UNITS * (\n                pango.pango_font_metrics_get_strikethrough_thickness(metrics))\n        else:\n            self.ascent = None\n            self.underline_position = None\n            self.strikethrough_position = None\n\n        features = font_features(\n            style['font_kerning'], style['font_variant_ligatures'],\n            style['font_variant_position'], style['font_variant_caps'],\n            style['font_variant_numeric'], style['font_variant_alternates'],\n            style['font_variant_east_asian'], style['font_feature_settings'])\n        if features:\n            features = ','.join(\n                f'{key} {value}' for key, value in features.items()).encode()\n            # In the meantime, keep a cache to avoid leaking too many of them.\n            attr = style.font_config.font_features.setdefault(\n                features, pango.pango_attr_font_features_new(features))\n            attr_list = pango.pango_attr_list_new()\n            pango.pango_attr_list_insert(attr_list, attr)\n            pango.pango_layout_set_attributes(self.layout, attr_list)\n\n    def get_first_line(self):\n        first_line = pango.pango_layout_get_line_readonly(self.layout, 0)\n        second_line = pango.pango_layout_get_line_readonly(self.layout, 1)\n        index = None if second_line == ffi.NULL else second_line.start_index\n        self.first_line_direction = first_line.resolved_dir\n        return first_line, index\n\n    def set_text(self, text, justify=False):\n        index = text.find('\\n')\n        if index != -1:\n            # Keep only the first line plus one character, we don't need more\n            text = text[:index+2]\n        self.text = text\n        text, bytestring = unicode_to_char_p(text)\n        pango.pango_layout_set_text(self.layout, text, -1)\n\n        word_spacing = self.style['word_spacing']\n        if justify:\n            # Justification is needed when drawing text but is useless during\n            # layout, when it can be ignored.\n            word_spacing += self.justification_spacing\n\n        letter_spacing = self.style['letter_spacing']\n        if letter_spacing == 'normal':\n            letter_spacing = 0\n\n        word_breaking = (\n            self.style['overflow_wrap'] in ('anywhere', 'break-word'))\n\n        if self.text and (word_spacing or letter_spacing or word_breaking):\n            attr_list = pango.pango_layout_get_attributes(self.layout)\n            if attr_list == ffi.NULL:\n                attr_list = ffi.gc(\n                    pango.pango_attr_list_new(),\n                    pango.pango_attr_list_unref)\n\n            def add_attr(start, end, spacing):\n                attr = pango.pango_attr_letter_spacing_new(spacing)\n                attr.start_index, attr.end_index = start, end\n                pango.pango_attr_list_change(attr_list, attr)\n\n            if letter_spacing:\n                letter_spacing = int(letter_spacing * TO_UNITS)\n                add_attr(0, len(bytestring), letter_spacing)\n\n            if word_spacing:\n                if bytestring == b' ':\n                    # We need more than one space to set word spacing\n                    self.text = ' \\u200b'  # Space + zero-width space\n                    text, bytestring = unicode_to_char_p(self.text)\n                    pango.pango_layout_set_text(self.layout, text, -1)\n\n                space_spacing = int(word_spacing * TO_UNITS + letter_spacing)\n                # Pango gives only half of word-spacing on boundaries\n                boundary_positions = (0, len(bytestring) - 1)\n                for match in re.finditer(' |\\u00a0'.encode(), bytestring):\n                    factor = 1 + (match.start() in boundary_positions)\n                    add_attr(match.start(), match.end(), factor * space_spacing)\n\n            if word_breaking:\n                attr = pango.pango_attr_insert_hyphens_new(False)\n                attr.start_index, attr.end_index = 0, len(bytestring)\n                pango.pango_attr_list_change(attr_list, attr)\n\n            pango.pango_layout_set_attributes(self.layout, attr_list)\n\n        # Tabs width\n        if b'\\t' in bytestring:\n            self.set_tabs()\n\n    def set_tabs(self):\n        if isinstance(self.style['tab_size'], int):\n            layout = Layout(self.style, self.justification_spacing)\n            layout.set_text(' ' * self.style['tab_size'])\n            line, _ = layout.get_first_line()\n            width, _ = line_size(line, self.style)\n            width = round(width)\n        else:\n            width = int(self.style['tab_size'].value)\n        # 0 is not handled correctly by Pango\n        array = ffi.gc(\n            pango.pango_tab_array_new_with_positions(\n                1, True, pango.PANGO_TAB_LEFT, width or 1),\n            pango.pango_tab_array_free)\n        pango.pango_layout_set_tabs(self.layout, array)\n\n    def deactivate(self):\n        del self.layout, self.language, self.style\n\n    def reactivate(self, style):\n        self.setup(style)\n        self.set_text(self.text, justify=True)\n\n\ndef create_layout(text, style, context, max_width, justification_spacing):\n    \"\"\"Return an opaque Pango layout with default Pango line-breaks.\"\"\"\n    layout = Layout(style, justification_spacing, max_width)\n\n    # Make sure that max_width * Pango.SCALE == max_width * 1024 fits in a\n    # signed integer. Treat bigger values same as None: unconstrained width.\n    text_wrap = style['white_space'] in ('normal', 'pre-wrap', 'pre-line')\n    if max_width is not None and text_wrap and max_width < 2 ** 21:\n        pango.pango_layout_set_width(layout.layout, int(max(0, max_width) * TO_UNITS))\n\n    layout.set_text(text)\n    return layout\n\n\ndef split_first_line(text, style, context, max_width, justification_spacing,\n                     is_line_start=True, minimum=False):\n    \"\"\"Fit as much as possible in the available width for one line of text.\n\n    Return ``(layout, length, resume_index, width, height, baseline)``.\n\n    ``layout``: a pango Layout with the first line\n    ``length``: length in UTF-8 bytes of the first line\n    ``resume_index``: The number of UTF-8 bytes to skip for the next line.\n                      May be ``None`` if the whole text fits in one line.\n                      This may be greater than ``length`` in case of preserved\n                      newline characters.\n    ``width``: width in pixels of the first line\n    ``height``: height in pixels of the first line\n    ``baseline``: baseline in pixels of the first line\n\n    \"\"\"\n    from ..layout.percent import percentage\n\n    # See https://www.w3.org/TR/css-text-3/#white-space-property\n    text_wrap = style['white_space'] in ('normal', 'pre-wrap', 'pre-line')\n    space_collapse = style['white_space'] in ('normal', 'nowrap', 'pre-line')\n\n    original_max_width = max_width\n    if not text_wrap:\n        max_width = None\n\n    # Step #1: Get a draft layout with the first line.\n    ratio = 4  # number that almost always respects char_height / char_width > ratio\n    short_text = text\n    if max_width is not None and max_width != inf and style['font_size']:\n        # Try to use a small amount of text to avoid the whole layout. We need\n        # at least one line, and one possible line break point on the second line.\n        if style['font_size'] * ratio > max_width:\n            # Trying to find minimum or very small size, let's naively split on\n            # spaces and keep one word + one letter.\n            space_index = text.find(' ')\n            if space_index != -1:\n                short_text = text[:space_index+2]  # index + space + one letter\n        else:\n            # Use the magic ration and hope that we’ll get the right amount of text.\n            short_text = text[:int(max_width / style['font_size'] * ratio)]\n        layout = create_layout(\n            short_text, style, context, max_width, justification_spacing)\n        first_line, resume_index = layout.get_first_line()\n        if resume_index is None and short_text != text:\n            # The small amount of text fits in one line, give up and use the\n            # whole text.\n            short_text = text\n            layout.set_text(text)\n            first_line, resume_index = layout.get_first_line()\n        else:\n            # If the second line of the short text can break, we have the next\n            # line break point required for step #3 in it, drop the end of the text.\n            first_line_text = short_text.encode()[:resume_index].decode()\n            if first_line_text != short_text:\n                start, end = len(first_line_text) + 1, len(short_text)\n                text_end_log_attrs = pango.pango_layout_get_log_attrs_readonly(\n                    layout.layout, ffi.NULL)[start:end]\n                if get_next_break_point(text_end_log_attrs) is not None:\n                    text = short_text\n    else:\n        layout = create_layout(\n            text, style, context, original_max_width, justification_spacing)\n        first_line, resume_index = layout.get_first_line()\n\n    # Step #2: Don't split lines when it's not needed.\n    if max_width is None:\n        # The first line can take all the place needed.\n        return first_line_metrics(\n            first_line, text, layout, resume_index, space_collapse, style)\n    first_line_width, _ = line_size(first_line, style)\n    if resume_index is None and first_line_width <= max_width:\n        # The first line fits in the available width.\n        return first_line_metrics(\n            first_line, text, layout, resume_index, space_collapse, style)\n\n    # Step #3: Try to put the first word of the second line on the first line\n    # https://mail.gnome.org/archives/gtk-i18n-list/2013-September/msg00006\n    # is a good thread related to this problem.\n    if first_line_width <= max_width:\n        # The first line fits but may have been cut too early by Pango.\n        encoded_text = text.encode()\n        first_line_text = encoded_text[:resume_index].decode()\n        second_line_text = encoded_text[resume_index:].decode()\n    else:\n        # The line can't be split earlier, try to hyphenate the first word.\n        first_line_text = ''\n        second_line_text = text\n    if first_line_text == short_text:\n        # There’s no second line, don’t try to find a next word.\n        break_point = None\n    else:\n        # Find then second line’s first break point.\n        log_attrs = pango.pango_layout_get_log_attrs_readonly(layout.layout, ffi.NULL)\n        start, end = len(first_line_text) + 1, len(short_text)\n        second_line_log_attrs = log_attrs[start:end]\n        break_point = get_next_break_point(second_line_log_attrs)\n        if break_point is not None:\n            break_point -= len(first_line_text) + 1\n    next_word = second_line_text[:break_point].rstrip(' ')\n    if next_word:\n        if space_collapse and second_line_text[break_point or -1] == ' ':\n            # Next word might fit without a space afterwards only try when\n            # space collapsing is allowed.\n            new_first_line_text = first_line_text + next_word\n            layout.set_text(new_first_line_text)\n            first_line, resume_index = layout.get_first_line()\n            if resume_index is None:\n                if first_line_text:\n                    # The next word fits in the first line, keep the layout.\n                    resume_index = len(new_first_line_text.encode()) + 1\n                    return first_line_metrics(\n                        first_line, text, layout, resume_index, space_collapse, style)\n                else:\n                    # Second line is None.\n                    resume_index = first_line.length + 1\n                    if resume_index >= len(text.encode()):\n                        resume_index = None\n    elif first_line_text:\n        # We found something on the first line but we did not find a word on\n        # the next line, no need to hyphenate, we can keep the current layout.\n        return first_line_metrics(\n            first_line, text, layout, resume_index, space_collapse, style)\n\n    # Step #4: Try to hyphenate\n    hyphens = style['hyphens']\n    lang = style['lang'] and pyphen.language_fallback(style['lang'])\n    total, left, right = style['hyphenate_limit_chars']\n    hyphenated = False\n    soft_hyphen = '\\xad'\n\n    auto_hyphenation = manual_hyphenation = False\n\n    if hyphens != 'none':\n        manual_hyphenation = soft_hyphen in first_line_text + second_line_text\n\n    if hyphens == 'auto' and lang:\n        # Get text until next line break opportunity.\n        next_text = second_line_text\n        if (next_break_point := get_next_break_point_from_text(second_line_text, lang)):\n            next_text = next_text[:next_break_point]\n\n        # Try all words included in this text.\n        next_text_index = 0\n        while next_text:\n            next_word_boundaries = get_next_word_boundaries(next_text, lang)\n            if next_word_boundaries:\n                # We have a word to hyphenate.\n                start_word, stop_word = next_word_boundaries\n                next_word = next_text[start_word:stop_word]\n                if stop_word - start_word >= total:\n                    # This word is long enough.\n                    first_line_width, _ = line_size(first_line, style)\n                    space = max_width - first_line_width\n                    limit_zone = percentage(\n                        style['hyphenate_limit_zone'], style, max_width)\n                    if space > limit_zone or space < 0:\n                        # Available space is worth the try, or the line is even too long\n                        # to fit: try to hyphenate.\n                        auto_hyphenation = True\n                        next_text_index += start_word\n                        break\n\n                # This word doesn’t work, try next one.\n                next_text = next_text[stop_word:]\n                next_text_index += stop_word\n            else:\n                break\n\n    # Automatic hyphenation opportunities within a word must be ignored if the\n    # word contains a conditional hyphen, in favor of the conditional\n    # hyphen(s).\n    # See https://drafts.csswg.org/css-text-3/#valdef-hyphens-auto\n    if manual_hyphenation:\n        # Manual hyphenation: check that the line ends with a soft\n        # hyphen and add the missing hyphen\n        if first_line_text.endswith(soft_hyphen):\n            # The first line has been split on a soft hyphen\n            first_line_text, second_line_text = '', first_line_text\n        soft_hyphen_indexes = [\n            match.start() for match in re.finditer(soft_hyphen, second_line_text)]\n        soft_hyphen_indexes.reverse()\n        dictionary_iterations = [second_line_text[:i+1] for i in soft_hyphen_indexes]\n    elif auto_hyphenation:\n        dictionary_key = (lang, left, right, total)\n        dictionary = context.dictionaries.get(dictionary_key)\n        if dictionary is None:\n            dictionary = pyphen.Pyphen(lang=lang, left=left, right=right)\n            context.dictionaries[dictionary_key] = dictionary\n        previous_words = second_line_text[:next_text_index]\n        dictionary_iterations = [\n            previous_words + start for start, end in dictionary.iterate(next_word)]\n    else:\n        dictionary_iterations = []\n\n    if dictionary_iterations:\n        for first_word_part in dictionary_iterations:\n            new_first_line_text = first_line_text + first_word_part\n            hyphenated_first_line_text = (\n                new_first_line_text + style['hyphenate_character'])\n            new_layout = create_layout(\n                hyphenated_first_line_text, style, context, max_width,\n                justification_spacing)\n            new_first_line, index = new_layout.get_first_line()\n            new_first_line_width, _ = line_size(new_first_line, style)\n            new_space = max_width - new_first_line_width\n            hyphenated = index is None and (\n                new_space >= 0 or first_word_part == dictionary_iterations[-1])\n            if hyphenated:\n                layout = new_layout\n                first_line = new_first_line\n                resume_index = len(new_first_line_text.encode())\n                break\n\n        if not hyphenated and not first_line_text:\n            # Recreate the layout with no max_width to be sure that\n            # we don't break before or inside the hyphenate character\n            hyphenated = True\n            layout.set_text(hyphenated_first_line_text)\n            pango.pango_layout_set_width(layout.layout, -1)\n            first_line, _ = layout.get_first_line()\n            resume_index = len(new_first_line_text.encode())\n            if text[len(first_line_text)] == soft_hyphen:\n                resume_index += len(soft_hyphen.encode())\n\n    if not hyphenated and first_line_text.endswith(soft_hyphen):\n        # Recreate the layout with no max_width to be sure that\n        # we don't break inside the hyphenate-character string\n        hyphenated = True\n        hyphenated_first_line_text = (\n            first_line_text + style['hyphenate_character'])\n        layout.set_text(hyphenated_first_line_text)\n        pango.pango_layout_set_width(layout.layout, -1)\n        first_line, _ = layout.get_first_line()\n        resume_index = len(first_line_text.encode())\n\n    # Step 5: Try to break word if it's too long for the line\n    overflow_wrap = style['overflow_wrap']\n    first_line_width, _ = line_size(first_line, style)\n    space = max_width - first_line_width\n    # If we can break words and the first line is too long\n    can_break = (\n        style['word_break'] == 'break-all' or (\n            is_line_start and (\n                overflow_wrap == 'anywhere' or\n                (overflow_wrap == 'break-word' and not minimum))))\n    if space < 0 and can_break:\n        # Is it really OK to remove hyphenation for word-break ?\n        hyphenated = False\n        # TODO: Modify code to preserve W3C condition:\n        # \"Shaping characters are still shaped as if the word were not broken\"\n        # The way new lines are processed in this function (one by one with no\n        # memory of the last) prevents shaping characters (arabic, for\n        # instance) from keeping their shape when wrapped on the next line with\n        # pango layout. Maybe insert Unicode shaping characters in text?\n        layout.set_text(text)\n        pango.pango_layout_set_width(layout.layout, int(max_width * TO_UNITS))\n        pango.pango_layout_set_wrap(layout.layout, PANGO_WRAP_MODE['WRAP_CHAR'])\n        first_line, index = layout.get_first_line()\n        resume_index = index or first_line.length\n        if resume_index >= len(text.encode()):\n            resume_index = None\n\n    return first_line_metrics(\n        first_line, text, layout, resume_index, space_collapse, style,\n        hyphenated, style['hyphenate_character'])\n\n\ndef _font_style_cache_key(style, include_size=False):\n    key = str((\n        style['font_family'],\n        style['font_style'],\n        style['font_stretch'],\n        style['font_weight'],\n        style['font_variant_ligatures'],\n        style['font_variant_position'],\n        style['font_variant_caps'],\n        style['font_variant_numeric'],\n        style['font_variant_alternates'],\n        style['font_variant_east_asian'],\n        style['font_feature_settings'],\n        style['font_variation_settings'],\n        style['font_language_override'],\n        style['lang'],\n    ))\n    if include_size:\n        key += str(style['font_size']) + str(style['line_height'])\n    return key\n\n\ndef strut(style):\n    \"\"\"Return a tuple of the used value of ``line-height`` and the baseline.\n\n    The baseline is given from the top edge of line height.\n\n    \"\"\"\n    if style['font_size'] == 0:\n        return 0, 0\n\n    key = _font_style_cache_key(style, include_size=True)\n    if key in style.font_config.strut_layouts:\n        return style.font_config.strut_layouts[key]\n\n    layout = Layout(style)\n    layout.set_text(' ')\n    line, _ = layout.get_first_line()\n    _, _, _, _, text_height, baseline = first_line_metrics(\n        line, '', layout, resume_at=None, space_collapse=False, style=style)\n    if style['line_height'] == 'normal':\n        result = text_height, baseline\n        style.font_config.strut_layouts[key] = result\n        return result\n    type_, line_height = style['line_height']\n    if type_ == 'NUMBER':\n        line_height *= style['font_size']\n    result = line_height, baseline + (line_height - text_height) / 2\n    style.font_config.strut_layouts[key] = result\n    return result\n\n\ndef character_ratio(style, unit):\n    \"\"\"Return the font size ratio used by given unit.\"\"\"\n    character = {'ex': 'x', 'cap': 'O', 'ic': '水', 'ch': '0'}.get(unit)\n    assert character\n\n    cache = style.cache.setdefault(unit, {})\n    cache_key = _font_style_cache_key(style)\n    if cache_key in cache:\n        return cache[cache_key]\n\n    # Avoid recursion for letter-spacing and word-spacing properties\n    style = style.copy()\n    style['letter_spacing'] = 'normal'\n    style['word_spacing'] = 0\n    # Random big value\n    style['font_size'] = 1000\n\n    layout = Layout(style)\n    layout.set_text(character)\n    line, _ = layout.get_first_line()\n\n    ink_extents = ffi.new('PangoRectangle *')\n    logical_extents = ffi.new('PangoRectangle *')\n    pango.pango_layout_line_get_extents(line, ink_extents, logical_extents)\n    if unit == 'ex':\n        measure = -ink_extents.y * FROM_UNITS\n    elif character == 'cap':\n        measure = logical_extents.height * FROM_UNITS\n    else:\n        measure = logical_extents.width * FROM_UNITS\n    ffi.release(ink_extents)\n    ffi.release(logical_extents)\n\n    # Zero means some kind of failure, fallback is 0.5.\n    # We round to try keeping exact values that were altered by Pango.\n    cache[cache_key] = round(measure / style['font_size'], 5) or 0.5\n    return cache[cache_key]\n\n\ndef get_log_attrs(text, lang):\n    if lang:\n        lang_p, lang = unicode_to_char_p(lang)\n    else:\n        lang = None\n        language = pango.pango_language_get_default()\n    if lang:\n        language = pango.pango_language_from_string(lang_p)\n    # TODO: this should be removed when bidi is supported\n    for char in ('\\u202a', '\\u202b', '\\u202c', '\\u202d', '\\u202e'):\n        text = text.replace(char, '\\u200b')\n    text_p, bytestring = unicode_to_char_p(text)\n    length = len(text) + 1\n    log_attrs = ffi.new('PangoLogAttr[]', length)\n    pango.pango_get_log_attrs(\n        text_p, len(bytestring), -1, language, log_attrs, length)\n    return log_attrs\n\n\ndef get_next_break_point(log_attrs):\n    for i, attr in enumerate(log_attrs):\n        if attr.is_line_break:\n            return i\n\n\ndef get_next_break_point_from_text(text, lang):\n    if not text or len(text) < 2:\n        return None\n    log_attrs = get_log_attrs(text, lang)\n    length = len(text) + 1\n    return get_next_break_point(log_attrs[1:length-1])\n\n\ndef can_break_text(text, lang):\n    return get_next_break_point_from_text(text, lang) is not None\n\n\ndef get_next_word_boundaries(text, lang):\n    if not text or len(text) < 2:\n        return None\n    log_attrs = get_log_attrs(text, lang)\n    for i, attr in enumerate(log_attrs):\n        if attr.is_word_end:\n            word_end = i\n            break\n        if attr.is_word_boundary:\n            word_start = i\n    else:\n        return None\n    return word_start, word_end\n\n\ndef get_last_word_end(text, lang):\n    if not text or len(text) < 2:\n        return None\n    log_attrs = get_log_attrs(text, lang)\n    for i, attr in enumerate(list(log_attrs)[::-1]):\n        if i and attr.is_word_end:\n            return len(text) - i\n"
  },
  {
    "path": "weasyprint/urls.py",
    "content": "\"\"\"Various utility functions and classes for URL management.\"\"\"\n\nimport contextlib\nimport os.path\nimport re\nimport sys\nimport traceback\nimport warnings\nimport zlib\nfrom email.message import EmailMessage\nfrom gzip import GzipFile\nfrom io import BytesIO, StringIO\nfrom pathlib import Path\nfrom urllib import request\nfrom urllib.parse import quote, unquote, urljoin, urlsplit\n\nfrom . import __version__\nfrom .logger import LOGGER\n\n# See https://stackoverflow.com/a/11687993/1162888\n# Both are needed in Python 3 as the re module does not like to mix\n# https://datatracker.ietf.org/doc/html/rfc3986#section-3.1\nUNICODE_SCHEME_RE = re.compile('^([a-zA-Z][a-zA-Z0-9.+-]+):')\nBYTES_SCHEME_RE = re.compile(b'^([a-zA-Z][a-zA-Z0-9.+-]+):')\n\nFILESYSTEM_ENCODING = sys.getfilesystemencoding()\n\nHTTP_HEADERS = {\n    'User-Agent': f'WeasyPrint {__version__}',\n    'Accept': '*/*',\n    'Accept-Encoding': 'gzip, deflate',\n}\n\n\nclass StreamingGzipFile(GzipFile):\n    def __init__(self, fileobj):\n        GzipFile.__init__(self, fileobj=fileobj)\n        self.fileobj_to_close = fileobj\n\n    def close(self):\n        GzipFile.close(self)\n        self.fileobj_to_close.close()\n\n    def seekable(self):\n        return False\n\n\ndef iri_to_uri(url):\n    \"\"\"Turn a Unicode IRI into an ASCII-only URI that conforms to RFC 3986.\"\"\"\n    if url.startswith('data:'):\n        # Data URIs can be huge, but don’t need this anyway.\n        return url\n    # Use UTF-8 as per RFC 3987 (IRI), except for file://\n    url = url.encode(FILESYSTEM_ENCODING if url.startswith('file:') else 'utf-8')\n    # This is a full URI, not just a component. Only %-encode characters\n    # that are not allowed at all in URIs. Everthing else is \"safe\":\n    # * Reserved characters: /:?#[]@!$&'()*+,;=\n    # * Unreserved characters: ASCII letters, digits and -._~\n    #   Of these, only '~' is not in urllib’s \"always safe\" list.\n    # * '%' to avoid double-encoding\n    return quote(url, safe=b\"/:?#[]@!$&'()*+,;=~%\")\n\n\ndef path2url(path):\n    \"\"\"Return file URL of `path`.\n\n    Accepts 'str', 'bytes' or 'Path', returns 'str'.\n\n    \"\"\"\n    # Ensure 'str'\n    if isinstance(path, Path):\n        path = str(path)\n    elif isinstance(path, bytes):\n        path = path.decode(FILESYSTEM_ENCODING)\n    # If a trailing path.sep is given, keep it\n    wants_trailing_slash = path.endswith((os.path.sep, '/'))\n    path = os.path.abspath(path)\n    if wants_trailing_slash or os.path.isdir(path):\n        # Make sure directory names have a trailing slash.\n        # Otherwise relative URIs are resolved from the parent directory.\n        path += os.path.sep\n        wants_trailing_slash = True\n    path = request.pathname2url(path)\n    # On Windows pathname2url cuts off trailing slash\n    if wants_trailing_slash and not path.endswith('/'):\n        path += '/'  # pragma: no cover\n    if path.startswith('///'):\n        # On Windows pathname2url(r'C:\\foo') is apparently '///C:/foo'\n        # That enough slashes already.\n        return f'file:{path}'  # pragma: no cover\n    else:\n        return f'file://{path}'\n\n\ndef url_is_absolute(url):\n    \"\"\"Return whether an URL (bytes or string) is absolute.\"\"\"\n    scheme = UNICODE_SCHEME_RE if isinstance(url, str) else BYTES_SCHEME_RE\n    return bool(scheme.match(url))\n\n\ndef get_url_attribute(element, attr_name, base_url, allow_relative=False):\n    \"\"\"Get the URI corresponding to the ``attr_name`` attribute.\n\n    Return ``None`` if:\n\n    * the attribute is empty or missing or,\n    * the value is a relative URI but the document has no base URI and\n      ``allow_relative`` is ``False``.\n\n    Otherwise return an URI, absolute if possible.\n\n    \"\"\"\n    value = element.get(attr_name, '').strip()\n    if value:\n        return url_join(\n            base_url or '', value, allow_relative, '<%s %s=\"%s\">',\n            (element.tag, attr_name, value))\n\n\ndef get_url_tuple(url, base_url):\n    \"\"\"Get tuple describing internal or external URI.\"\"\"\n    if url.startswith('#'):\n        return ('internal', unquote(url[1:]))\n    elif url_is_absolute(url):\n        return ('external', iri_to_uri(url))\n    elif base_url:\n        return ('external', iri_to_uri(urljoin(base_url, url)))\n\n\ndef url_join(base_url, url, allow_relative, context, context_args):\n    \"\"\"Like urllib.urljoin, but warn if base_url is required but missing.\"\"\"\n    if url_is_absolute(url):\n        return iri_to_uri(url)\n    elif base_url:\n        return iri_to_uri(urljoin(base_url, url))\n    elif allow_relative:\n        return iri_to_uri(url)\n    else:\n        LOGGER.error(\n            f'Relative URI reference without a base URI: {context}',\n            *context_args)\n        return None\n\n\ndef get_link_attribute(element, attr_name, base_url):\n    \"\"\"Get the URL value of an element attribute.\n\n    Return ``('external', absolute_uri)``, or ``('internal',\n    unquoted_fragment_id)``, or ``None``.\n\n    \"\"\"\n    attr_value = element.get(attr_name, '').strip()\n    if attr_value.startswith('#') and len(attr_value) > 1:\n        # Do not require a base_url when the value is just a fragment.\n        return ('url', ('internal', unquote(attr_value[1:])))\n    uri = get_url_attribute(element, attr_name, base_url, allow_relative=True)\n    if uri:\n        if base_url:\n            try:\n                parsed = urlsplit(uri)\n            except ValueError:\n                LOGGER.warning('Malformed URL: %s', uri)\n            else:\n                try:\n                    parsed_base = urlsplit(base_url)\n                except ValueError:\n                    LOGGER.warning('Malformed base URL: %s', base_url)\n                else:\n                    # Compare with fragments removed\n                    if parsed.fragment and parsed[:-1] == parsed_base[:-1]:\n                        return ('url', ('internal', unquote(parsed.fragment)))\n        return ('url', ('external', uri))\n\n\ndef ensure_url(string):\n    \"\"\"Get a ``scheme://path`` URL from ``string``.\n\n    If ``string`` looks like an URL, return it unchanged. Otherwise assume a\n    filename and convert it to a ``file://`` URL.\n\n    \"\"\"\n    return string if url_is_absolute(string) else path2url(string)\n\n\ndef default_url_fetcher(url, timeout=10, ssl_context=None, http_headers=None,\n                        allowed_protocols=None):\n    \"\"\"Fetch an external resource such as an image or stylesheet.\n\n    This function is deprecated, use ``URLFetcher`` instead.\n\n    \"\"\"\n    warnings.warn(\n        'default_url_fetcher is deprecated and will be removed in WeasyPrint 69.0, '\n        'please use URLFetcher instead. For security reasons, HTTP redirects are not '\n        'supported anymore with default_url_fetcher, but are with URLFetcher.\\n\\nSee '\n        'https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#url-fetchers',\n        category=DeprecationWarning)\n    fetcher = URLFetcher(\n        timeout, ssl_context, http_headers, allowed_protocols, allow_redirects=False)\n    return fetcher.fetch(url)\n\n\n@contextlib.contextmanager\ndef select_source(guess=None, filename=None, url=None, file_obj=None, string=None,\n                  base_url=None, url_fetcher=None, check_css_mime_type=False):\n    \"\"\"If only one input is given, return it.\n\n    Yield a file object, the base url, the protocol encoding and the protocol mime-type.\n\n    \"\"\"\n    if base_url is not None:\n        base_url = ensure_url(base_url)\n    if url_fetcher is None:\n        url_fetcher = URLFetcher()\n\n    selected_params = [\n        param for param in (guess, filename, url, file_obj, string) if\n        param is not None]\n    if len(selected_params) != 1:\n        source = ', '.join(selected_params) or 'nothing'\n        raise TypeError(f'Expected exactly one source, got {source}')\n    elif guess is not None:\n        kwargs = {\n            'base_url': base_url,\n            'url_fetcher': url_fetcher,\n            'check_css_mime_type': check_css_mime_type,\n        }\n        if hasattr(guess, 'read'):\n            kwargs['file_obj'] = guess\n        elif isinstance(guess, Path):\n            kwargs['filename'] = guess\n        elif url_is_absolute(guess):\n            kwargs['url'] = guess\n        else:\n            kwargs['filename'] = guess\n        result = select_source(**kwargs)\n        with result as result:\n            yield result\n    elif filename is not None:\n        if base_url is None:\n            base_url = path2url(filename)\n        with open(filename, 'rb') as file_obj:\n            yield file_obj, base_url, None, None\n    elif url is not None:\n        with fetch(url_fetcher, url) as response:\n            if check_css_mime_type and response.content_type != 'text/css':\n                LOGGER.error(\n                    f'Unsupported stylesheet type {response.content_type} '\n                    f'for {response.url}')\n                yield StringIO(''), base_url, None, None\n            else:\n                if base_url is None:\n                    base_url = response.url\n                yield response, base_url, response.charset, response.content_type\n    elif file_obj is not None:\n        if base_url is None:\n            # filesystem file-like objects have a 'name' attribute.\n            name = getattr(file_obj, 'name', None)\n            # Some streams have a .name like '<stdin>', not a filename.\n            if name and not name.startswith('<'):\n                base_url = ensure_url(name)\n        yield file_obj, base_url, None, None\n    else:\n        if isinstance(string, str):\n            yield StringIO(string), base_url, None, None\n        else:\n            yield BytesIO(string), base_url, None, None\n\n\nclass URLFetchingError(IOError):\n    \"\"\"Some error happened when fetching an URL.\"\"\"\n\n\nclass FatalURLFetchingError(BaseException):\n    \"\"\"Some error happened when fetching an URL and must stop the rendering.\"\"\"\n\n\nclass URLFetcher(request.OpenerDirector):\n    \"\"\"Fetcher of external resources such as images or stylesheets.\n\n    :param int timeout: The number of seconds before HTTP requests are dropped.\n    :param ssl.SSLContext ssl_context: An SSL context used for HTTPS requests.\n    :param dict http_headers: Additional HTTP headers used for HTTP requests.\n    :type allowed_protocols: :term:`sequence`\n    :param allowed_protocols: A set of authorized protocols, :obj:`None` means all.\n    :param bool allow_redirects: Whether HTTP redirects must be followed.\n    :param bool fail_on_errors: Whether HTTP errors should stop the rendering.\n\n    Another class inheriting from this class, with a ``fetch`` method that has a\n    compatible signature, can be given as the ``url_fetcher`` argument to\n    :class:`weasyprint.HTML` or :class:`weasyprint.CSS`.\n\n    See :ref:`URL Fetchers` for more information and examples.\n\n    \"\"\"\n\n    def __init__(self, timeout=10, ssl_context=None, http_headers=None,\n                 allowed_protocols=None, allow_redirects=True, fail_on_errors=False,\n                 **kwargs):\n        super().__init__()\n        handlers = [\n            request.ProxyHandler(), request.UnknownHandler(), request.HTTPHandler(),\n            request.HTTPDefaultErrorHandler(), request.FTPHandler(),\n            request.FileHandler(), request.HTTPErrorProcessor(), request.DataHandler(),\n            request.HTTPSHandler(context=ssl_context)]\n        if allow_redirects:\n            handlers.append(request.HTTPRedirectHandler())\n        for handler in handlers:\n            self.add_handler(handler)\n\n        self._timeout = timeout\n        self._http_headers = {**HTTP_HEADERS, **(http_headers or {})}\n        self._allowed_protocols = allowed_protocols\n        self._fail_on_errors = fail_on_errors\n        self._request = None\n\n    def fetch(self, url, headers=None):\n        \"\"\"Fetch a given URL.\n\n        :returns: A :obj:`URLFetcherResponse` instance.\n        :raises: An exception indicating failure, e.g. :obj:`ValueError` on\n            syntactically invalid URL. All exceptions are catched internally by\n            WeasyPrint, except when they inherit from :obj:`FatalURLFetchingError`.\n\n        \"\"\"\n        # Discard URLs with no or invalid protocol.\n        if not (match := UNICODE_SCHEME_RE.match(url)):  # pragma: no cover\n            raise ValueError(f'Not an absolute URI: {url}')\n        scheme = match[1].lower()\n\n        # Discard URLs with forbidden protocol.\n        if self._allowed_protocols is not None:\n            if scheme not in self._allowed_protocols:\n                raise ValueError(f'URI uses disallowed protocol: {url}')\n\n        # Remove query and fragment parts from file URLs.\n        # See https://bugs.python.org/issue34702.\n        if scheme == 'file':\n            url = url.split('?')[0]\n\n        # Transform Unicode IRI to ASCII URI.\n        url = iri_to_uri(url)\n\n        # Open URL.\n        headers = {**self._http_headers, **(headers or {})}\n        http_request = self._request or request.Request(url, headers=headers)\n        self._request = None\n        response = super().open(http_request, timeout=self._timeout)\n\n        # Decompress response.\n        body = response\n        if 'Content-Encoding' in response.headers:\n            content_encoding = response.headers['Content-Encoding']\n            del response.headers['Content-Encoding']\n            if content_encoding == 'gzip':\n                body = StreamingGzipFile(fileobj=response)\n            elif content_encoding == 'deflate':\n                data = response.read()\n                try:\n                    body = zlib.decompress(data)\n                except zlib.error:\n                    # Try without zlib header or checksum.\n                    body = zlib.decompress(data, -15)\n\n        return URLFetcherResponse(response.url, body, response.headers, response.status)\n\n    def open(self, url, data=None, timeout=None):\n        if isinstance(url, request.Request):\n            self._request = url\n            return self.fetch(url.full_url, url.headers)\n        return self.fetch(url)\n\n    def __call__(self, url):\n        return self.fetch(url)\n\n\nclass URLFetcherResponse:\n    \"\"\"The HTTP response of an URL fetcher.\n\n    :param str url: The URL of the HTTP response.\n    :type body: :class:`str`, :class:`bytes` or :term:`file object`\n    :param body: The body of the HTTP response.\n    :type headers: dict or email.message.EmailMessage\n    :param headers: The headers of the HTTP response.\n    :param int status: The status of the HTTP response.\n\n    Has the same interface as :class:`urllib.response.addinfourl`.\n\n    If a :term:`file object` is given for the body, it is the caller’s responsibility to\n    call ``close()`` on it. The default function used internally to fetch data in\n    WeasyPrint tries to close the file object after retreiving; but if this URL fetcher\n    is used elsewhere, the file object has to be closed manually.\n\n    \"\"\"\n    def __init__(self, url, body=None, headers=None, status=200, **kwargs):\n        self.url = url\n        self.status = status\n\n        if isinstance(headers, EmailMessage):\n            self.headers = headers\n        else:\n            self.headers = EmailMessage()\n            for key, value in (headers or {}).items():\n                try:\n                    self.headers[key] = value\n                except ValueError:\n                    pass  # Ignore forbidden duplicated headers.\n\n        if hasattr(body, 'read'):\n            self._file_obj = body\n        elif isinstance(body, str):\n            self.headers.set_param('charset', 'utf-8')\n            self._file_obj = BytesIO(body.encode('utf-8'))\n        else:\n            self._file_obj = BytesIO(body)\n\n    def read(self, *args, **kwargs):\n        return self._file_obj.read(*args, **kwargs)\n\n    def close(self):\n        try:\n            self._file_obj.close()\n        except Exception:  # pragma: no cover\n            # May already be closed or something.\n            # This is just cleanup anyway: log but make it non-fatal.\n            LOGGER.warning(\n                'Error when closing stream for %s:\\n%s',\n                self.url, traceback.format_exc())\n\n    @property\n    def path(self):\n        if self.url.startswith('file:'):\n            return request.url2pathname(self.url.split('?')[0].removeprefix('file:'))\n\n    @property\n    def content_type(self):\n        return self.headers.get_content_type()\n\n    @property\n    def charset(self):\n        return self.headers.get_param('charset')\n\n    def geturl(self):\n        return self.url\n\n    def info(self):\n        return self.headers\n\n    @property\n    def code(self):\n        return self.status\n\n    def getcode(self):\n        return self.status\n\n\n@contextlib.contextmanager\ndef fetch(url_fetcher, url):\n    \"\"\"Fetch an ``url`` with ```url_fetcher``, fill in optional data, and clean up.\n\n    Fatal errors must raise a ``FatalURLFetchingError`` that stops the rendering. All\n    other exceptions are catched and raise an ``URLFetchingError``, that is usually\n    catched by the code that fetches the resource and emits a warning.\n\n    \"\"\"\n    try:\n        resource = url_fetcher(url)\n    except Exception as exception:\n        if getattr(url_fetcher, '_fail_on_errors', False):\n            raise FatalURLFetchingError(f'Error fetching \"{url}\"') from exception\n        raise URLFetchingError(f'{type(exception).__name__}: {exception}')\n\n    if isinstance(resource, dict):\n        warnings.warn(\n            'Returning dicts in URL fetchers is deprecated and will be removed '\n            'in WeasyPrint 69.0, please return URLFetcherResponse instead.',\n            category=DeprecationWarning)\n        if 'url' not in resource:\n            resource['url'] = resource.get('redirected_url', url)\n        resource['body'] = resource.get('file_obj', resource.get('string'))\n        content_type = resource.get('mime_type', 'application/octet-stream')\n        if charset := resource.get('encoding'):\n            content_type += f'; charset={charset}'\n        resource['headers'] = {'Content-Type': content_type}\n        resource = URLFetcherResponse(**resource)\n\n    assert isinstance(resource, URLFetcherResponse), (\n        'URL fetcher must return either a dict or a URLFetcherResponse instance')\n\n    try:\n        yield resource\n    finally:\n        resource.close()\n"
  }
]