././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.015308 weasyprint-54.1/LICENSE0000644000000000000000000000277600000000000013020 0ustar0000000000000000BSD 3-Clause License Copyright (c) 2011-2021, Simon Sapin and contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.015308 weasyprint-54.1/README.rst0000644000000000000000000000314400000000000013470 0ustar0000000000000000**The Awesome Document Factory** WeasyPrint is a smart solution helping web developers to create PDF documents. It turns simple HTML pages into gorgeous statistical reports, invoices, tickets… From a technical point of view, WeasyPrint is a visual rendering engine for HTML and CSS that can export to PDF. It aims to support web standards for printing. WeasyPrint is free software made available under a BSD license. It is based on various libraries but *not* on a full rendering engine like WebKit or Gecko. The CSS layout engine is written in Python, designed for pagination, and meant to be easy to hack on. * Free software: BSD license * For Python 3.6+, tested on CPython and PyPy * Documentation: https://doc.courtbouillon.org/weasyprint * Examples: https://weasyprint.org/samples/ * Changelog: https://github.com/Kozea/WeasyPrint/releases * Code, issues, tests: https://github.com/Kozea/WeasyPrint * Code of conduct: https://www.courtbouillon.org/code-of-conduct * Professional support: https://www.courtbouillon.org * Donation: https://opencollective.com/courtbouillon WeasyPrint has been created and developed by Kozea (https://kozea.fr/). Professional support, maintenance and community management is provided by CourtBouillon (https://www.courtbouillon.org/). Copyrights are retained by their contributors, no copyright assignment is required to contribute to WeasyPrint. Unless explicitly stated otherwise, any contribution intentionally submitted for inclusion is licensed under the BSD 3-clause license, without any additional terms or conditions. For full authorship information, see the version control history. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1643655079.3575528 weasyprint-54.1/docs/api_reference.rst0000644000000000000000000006730100000000000016257 0ustar0000000000000000API Reference ============= .. currentmodule:: weasyprint This page is for WeasyPrint |version|. See :doc:`changelog ` for older versions. API Stability ------------- Everything described here is considered “public”: this is what you can rely on. We will try to maintain backward-compatibility, and we really often do, but there is no hard promise. Anything else should not be used outside of WeasyPrint itself. We reserve the right to change it or remove it at any point. Use it at your own risk, or have dependency to a specific WeasyPrint version. Versioning ---------- WeasyPrint provides frequent major releases, and minor releases with only bug fixes. Versioning is close to what many browsers do, including Firefox and Chrome: big major numbers, small minor numbers. Even if each version does not break the API, each version does break the way documents are rendered, which is what really matters at the end. Providing minor versions would give the illusion that developers can just update WeasyPrint without checking that everything works. Unfortunately, we have the same problem as the other browsers: when a new version is released, most of the user's websites are rendered exactly the same, but a small part is not. And the only ways to know that, for web developers, are to read the changelog and to check that their pages are correctly rendered. More about this choice can be found in issue `#900`_. .. _#900: https://github.com/Kozea/WeasyPrint/issues/900 Command-line API ---------------- .. autofunction:: weasyprint.__main__.main(argv=sys.argv) Python API ---------- .. autoclass:: HTML(input, **kwargs) :members: .. autoclass:: CSS(input, **kwargs) .. autoclass:: Attachment(input, **kwargs) .. autofunction:: default_url_fetcher .. module:: weasyprint.document .. autoclass:: Document :members: .. autoclass:: DocumentMetadata() :members: .. autoclass:: Page() :members: .. module:: weasyprint.text.fonts .. autoclass:: FontConfiguration() .. module:: weasyprint.css.counters .. autoclass:: CounterStyle() Supported Features ------------------ URLs ~~~~ WeasyPrint can read normal files, HTTP, FTP and `data URIs`_. It will follow HTTP redirects but more advanced features like cookies and authentication are currently not supported, although a custom :ref:`URL fetcher ` can help. .. _data URIs: http://en.wikipedia.org/wiki/Data_URI_scheme HTML ~~~~ Supported HTML Tags +++++++++++++++++++ Many HTML elements are implemented in CSS through the HTML5 `User-Agent stylesheet`_. Some elements need special treatment: * The ```` element, if present, determines the base for relative URLs. * CSS stylesheets can be embedded in `` … Automatic hyphenation can be disabled again with the ``manual`` value: .. code-block:: css html { hyphens: auto } a[href]::after { content: ' [' attr(href) ']'; hyphens: manual } The other features provided by `CSS Text Module Level 3`_ are **not** supported: - the ``line-break`` and ``word-break`` properties; - the ``start``, ``end``, ``match-parent`` and ``start end`` values of the ``text-align`` property; - the ``text-align-last`` and ``text-justify`` properties; and - the ``text-indent`` and ``hanging-punctuation`` properties. The other features provided by `CSS Text Module Level 4`_ are **not** supported: - the ``text-space-collapse`` and ``text-space-trim`` properties; - the ``text-wrap``, ``wrap-before``, ``wrap-after`` and ``wrap-inside`` properties; - the ``text-align`` property with an alignment character; - the ``pre-wrap-auto`` value of the ``white-space`` property; and - the ``text-spacing`` property. .. _supported by Pyphen: https://github.com/Kozea/Pyphen/tree/master/pyphen/dictionaries .. _hyphenation: http://www.w3.org/TR/css3-text/#hyphenation .. _CSS Text Module Level 3: https://www.w3.org/TR/css-text-3/ .. _CSS Text Module Level 4: https://www.w3.org/TR/css-text-4/ CSS Fonts Module Level 3 ++++++++++++++++++++++++ The `CSS Fonts Module Level 3`_ is a candidate recommendation describing "how font properties are specified and how font resources are loaded dynamically". WeasyPrint supports the ``font-size``, ``font-stretch``, ``font-style`` and ``font-weight`` properties, coming from CSS 2.1. WeasyPrint also supports the following font features added in Level 3: - ``font-kerning``, - ``font-variant-ligatures``, - ``font-variant-position``, - ``font-variant-caps``, - ``font-variant-numeric``, - ``font-variant-east-asian``, - ``font-feature-settings``, and - ``font-language-override``. ``font-family`` is supported. The string is given to Pango that tries to find a matching font in a way different from what is defined in the recommendation, but that should not be a problem for common use. The shorthand ``font`` and ``font-variant`` properties are supported. WeasyPrint supports the ``@font-face`` rule, provided that Pango >= 1.38 is installed. WeasyPrint does **not** support the ``@font-feature-values`` rule and the values of ``font-variant-alternates`` other than ``normal`` and ``historical-forms``. The ``font-variant-caps`` property is supported but needs the small-caps variant of the font to be installed. WeasyPrint does **not** simulate missing small-caps fonts. CSS Paged Media Module Level 3 ++++++++++++++++++++++++++++++ The `CSS Paged Media Module Level 3`_ is a working draft including features for paged media "describing how: - page breaks are created and avoided; - the page properties such as size, orientation, margins, border, and padding are specified; - headers and footers are established within the page margins; - content such as page counters are placed in the headers and footers; and - orphans and widows can be controlled." All the features of this draft are available, including: - the ``@page`` rule and the ``:left``, ``:right``, ``:first`` and ``:blank`` selectors; - the page margin boxes; - the page-based counters (with known limitations `#93`_); - the page ``size``, ``bleed`` and ``marks`` properties; - the named pages. .. _CSS Paged Media Module Level 3: http://dev.w3.org/csswg/css3-page/ .. _#93: https://github.com/Kozea/WeasyPrint/issues/93 CSS Generated Content for Paged Media Module ++++++++++++++++++++++++++++++++++++++++++++ The `CSS Generated Content for Paged Media Module`_ (GCPM) is a working draft defining "new properties and values, so that authors may bring new techniques (running headers and footers, footnotes, page selection) to paged media". `Page selectors`_ are supported by WeasyPrint. You can select pages according to their position in the document: .. code-block:: css @page :nth(3) { background: red } /* Third page */ @page :nth(2n+1) { background: green } /* Odd pages */ You can also use `running elements`_ to put HTML boxes into the page margins (but the ``start`` parameter of ``element()`` is not supported). Footnotes_ are supported. You can put a box in the footnote area using the ``float: footnote`` property. Footnote markers and footnote calls can be defined using the ``::footnote-marker`` and ``::footnote-call`` pseudo-elements. You can also change the way footnotes are displayed using the ``footnote-display`` property (``compact`` is not supported), and influence over the rendering of difficult pages with ``footnote-policy``. Page groups (``:nth(X of pagename)`` pseudo-class) are not supported. .. _CSS Generated Content for Paged Media Module: http://www.w3.org/TR/css-gcpm-3/ .. _Page selectors: https://www.w3.org/TR/css-gcpm-3/#document-page-selectors .. _running elements: https://www.w3.org/TR/css-gcpm-3/#running-elements .. _Footnotes: https://www.w3.org/TR/css-gcpm-3/#footnotes CSS Generated Content Module Level 3 ++++++++++++++++++++++++++++++++++++ The `CSS Generated Content Module Level 3`_ is a working draft helping "authors [who] sometimes want user agents to render content that does not come from the document tree. One familiar example of this is numbered headings […]. Similarly, authors may want the user agent to insert the word "Figure" before the caption of a figure […], or replacing elements with images or other multimedia content." `Named strings`_ are supported by WeasyPrint. You can define strings related to the first or last element of a type present on a page, and display these strings in page borders. This feature is really useful to add the title of the current chapter at the top of the pages of a book for example. The named strings can embed static strings, counters, cross-references, tag contents and tag attributes. .. code-block:: css @top-center { content: string(chapter) } h2 { string-set: chapter "Current chapter: " content() } `Cross-references`_ retrieve counter or content values from targets (anchors or identifiers) in the current document: .. code-block:: css a::after { content: ", on page " target-counter(attr(href), page) } a::after { content: ", see " target-text(attr(href)) } In particular, ``target-counter()`` and ``target-text()`` are useful when it comes to tables of contents (see `an example`_). You can also control `PDF bookmarks`_ with WeasyPrint. Using the experimental_ ``bookmark-level``, ``bookmark-label`` and ``bookmark-state`` properties, you can add bookmarks that will be available in your PDF reader. Bookmarks have already been added in the WeasyPrint's `user agent stylesheet`_, so your generated documents will automatically have bookmarks on headers (from ``

`` to ``

``). But for example, if you have only one top-level ``

`` and do not wish to include it in the bookmarks, add this in your stylesheet: .. code-block:: css h1 { bookmark-level: none } The other features of this module are **not** implemented: - quotes (``content: *-quote``); - leaders (``content: leader()``). .. _CSS Generated Content Module Level 3: http://www.w3.org/TR/css-content-3/ .. _Quotes: https://www.w3.org/TR/css-content-3/#quotes .. _Named strings: https://www.w3.org/TR/css-content-3/#named-strings .. _Cross-references: https://www.w3.org/TR/css-content-3/#cross-references .. _an example: https://github.com/Kozea/WeasyPrint/pull/652#issuecomment-403276559 .. _PDF bookmarks: https://www.w3.org/TR/css-content-3/#bookmark-generation .. _experimental: http://www.w3.org/TR/css-2010/#experimental .. _user agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/master/weasyprint/css/html5_ua.css CSS Color Module Level 3 ++++++++++++++++++++++++ The `CSS Color Module Level 3`_ is a recommendation defining "CSS properties which allow authors to specify the foreground color and opacity of an element". Its main goal is to specify how colors are defined, including color keywords and the ``#rgb``, ``#rrggbb``, ``rgb()``, ``rgba()``, ``hsl()``, ``hsla()`` syntaxes. Opacity and alpha compositing are also defined in this document. This recommendation is fully implemented in WeasyPrint, except the deprecated System Colors. .. _CSS Color Module Level 3: http://www.w3.org/TR/css3-color/ CSS Transforms Module Level 1 +++++++++++++++++++++++++++++ The `CSS Transforms Module Level 1`_ working draft "describes a coordinate system within each element is positioned. This coordinate space can be modified with the transform property. Using transform, elements can be translated, rotated and scaled in two or three dimensional space." WeasyPrint supports the ``transform`` and ``transform-origin`` properties, and all the 2D transformations (``matrix``, ``rotate``, ``translate(X|Y)?``, ``scale(X|Y)?``, ``skew(X|Y)?``). WeasyPrint does **not** support the ``transform-style``, ``perspective``, ``perspective-origin`` and ``backface-visibility`` properties, and all the 3D transformations (``matrix3d``, ``rotate(3d|X|Y|Z)``, ``translate(3d|Z)``, ``scale(3d|Z)``). .. _CSS Transforms Module Level 1: http://dev.w3.org/csswg/css3-transforms/ CSS Backgrounds and Borders Module Level 3 ++++++++++++++++++++++++++++++++++++++++++ The `CSS Backgrounds and Borders Module Level 3`_ is a candidate recommendation defining properties dealing "with the decoration of the border area and with the background of the content, padding and border areas". The `border part`_ of this module is supported, as it is already included in the the CSS 2.1 specification. WeasyPrint supports the `background part`_ of this module (allowing multiple background layers per box), including the ``background``, ``background-color``, ``background-image``, ``background-repeat``, ``background-attachment``, ``background-position``, ``background-clip``, ``background-origin`` and ``background-size`` properties. WeasyPrint also supports the `rounded corners part`_ of this module, including the ``border-radius`` property. WeasyPrint does **not** support the `border images part`_ of this module, including the ``border-image``, ``border-image-source``, ``border-image-slice``, ``border-image-width``, ``border-image-outset`` and ``border-image-repeat`` properties. WeasyPrint does **not** support the `box shadow part`_ of this module, including the ``box-shadow`` property. This feature has been implemented in a `git branch`_ that is not released, as it relies on raster implementation of shadows. .. _CSS Backgrounds and Borders Level 3: http://www.w3.org/TR/css3-background/ .. _border part: http://www.w3.org/TR/css3-background/#borders .. _background part: http://www.w3.org/TR/css3-background/#backgrounds .. _rounded corners part: http://www.w3.org/TR/css3-background/#corners .. _border images part: http://www.w3.org/TR/css3-background/#border-images .. _box shadow part: http://www.w3.org/TR/css3-background/#misc .. _git branch: https://github.com/Kozea/WeasyPrint/pull/149 CSS Image Values and Replaced Content Module Level 3 / 4 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The `Image Values and Replaced Content Module Level 3`_ is a candidate recommendation introducing "additional ways of representing 2D images, for example as a list of URIs denoting fallbacks, or as a gradient", defining "several properties for manipulating raster images and for sizing or positioning replaced elements" and "generic sizing algorithm for replaced elements". The `Image Values and Replaced Content Module Level 4`_ is a working draft on the same subject. The ``linear-gradient()``, ``radial-gradient()`` and ``repeating-radial-gradient()`` properties are supported as background images. The the ``url()`` notation is supported, but the ``image()`` notation is **not** supported for background images. The ``object-fit`` and ``object-position`` properties are supported. The ``from-image`` and ``snap`` values of the ``image-resolution`` property are **not** supported, but the ``resolution`` value is supported. The ``image-rendering`` property is supported. The ``image-orientation`` property is **not** supported. .. _Image Values and Replaced Content Module Level 3: http://www.w3.org/TR/css3-images/ .. _Image Values and Replaced Content Module Level 4: http://www.w3.org/TR/css4-images/ CSS Box Sizing Module Level 3 +++++++++++++++++++++++++++++ The `CSS Box Sizing Module Level 3`_ is a candidate recommendation extending "the CSS sizing properties with keywords that represent content-based 'intrinsic' sizes and context-based 'extrinsic' sizes." The new property defined in this document is implemented in WeasyPrint: ``box-sizing``. The ``min-content``, ``max-content`` and ``fit-content()`` sizing values are **not** supported. .. _CSS Box Sizing Module Level 3: https://www.w3.org/TR/css-sizing-3/ CSS Overflow Module Level 3 +++++++++++++++++++++++++++ The `CSS Overflow Module Level 3`_ is a working draft containing "the features of CSS relating to scrollable overflow handling in visual media." The ``overflow`` property is supported, as defined in CSS2. ``overflow-x``, ``overflow-y``, ``overflow-clip-margin``, ``overflow-inline`` and ``overflow-block`` are **not** supported. The ``text-overflow``, ``block-ellipsis``, ``line-clamp``, ``max-lines`` and ``continue`` properties are supported. .. _CSS Overflow Module Level 3: https://www.w3.org/TR/2020/WD-css-overflow-3-20200603/ CSS Values and Units Module Level 3 +++++++++++++++++++++++++++++++++++ The `CSS Values and Units Module Level 3`_ defines various units and keywords used in "value definition field of each CSS property". The ``initial`` and ``inherit`` CSS-wide keywords are supported, but the ``unset`` keyword is **not** supported. Quoted strings, URLs and numeric data types are supported. Font-related lengths (``em``, ``ex``, ``ch``, ``rem``), absolute lengths (``cm``, ``mm``, ``q``, ``in``, ``pt``, ``pc``, ``px``), angles (``rad``, ``grad``, ``turn``, ``deg``), resolutions (``dpi``, ``dpcm``, ``dppx``) are supported. The ``attr()`` functional notation is allowed in the ``content`` and ``string-set`` properties. Viewport-percentage lengths (``vw``, ``vh``, ``vmin``, ``vmax``) are **not** supported. .. _CSS Values and Units Module Level 3: https://www.w3.org/TR/css3-values/ CSS Multi-column Layout Module ++++++++++++++++++++++++++++++ The `CSS Multi-column Layout Module`_ "describes multi-column layouts in CSS, a style sheet language for the web. Using functionality described in the specification, content can be flowed into multiple columns with a gap and a rule between them." Simple multi-column layouts are supported in WeasyPrint. Features such as constrained height, spanning columns or column breaks are **not** supported. Pagination and overflow are not seriously tested. The ``column-width`` and ``column-count`` properties, and the ``columns`` shorthand property are supported. The ``column-gap``, ``column-rule-color``, ``column-rule-style`` and ``column-rule-width`` properties, and the ``column-rule`` shorthand property are supported. The ``break-before``, ``break-after`` and ``break-inside`` properties are **not** supported. The ``column-span`` property is supported for direct children of columns. The ``column-fill`` property is supported, with a column balancing algorithm that should be efficient with simple cases. .. _CSS Multi-column Layout Module: https://www.w3.org/TR/css3-multicol/ CSS Fragmentation Module Level 3 / 4 ++++++++++++++++++++++++++++++++++++ The `CSS Fragmentation Module Level 3`_ "describes the fragmentation model that partitions a flow into pages, columns, or regions. It builds on the Page model module and introduces and defines the fragmentation model. It adds functionality for pagination, breaking variable fragment size and orientation, widows and orphans." The `CSS Fragmentation Module Level 4`_ is a working draft on the same subject. The ``break-before``, ``break-after`` and ``break-inside`` properties are supported for pages, but **not** for columns and regions. ``page-break-*`` aliases as defined in CSS2 are supported too. The ``orphans`` and ``widows`` properties are supported. The ``box-decoration-break`` property is supported, but backgrounds are always repeated and not extended through the whole box as it should be with 'slice' value. The ``margin-break`` property is supported. .. _CSS Fragmentation Module Level 3: https://www.w3.org/TR/css-break-3/ .. _CSS Fragmentation Module Level 4: https://www.w3.org/TR/css-break-4/ CSS Custom Properties for Cascading Variables Module Level 1 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The `CSS Custom Properties for Cascading Variables Module Level 1`_ "introduces cascading variables as a new primitive value type that is accepted by all CSS properties, and custom properties for defining them." The custom properties and the ``var()`` notation are supported. .. _CSS Custom Properties for Cascading Variables Module Level 1: https://www.w3.org/TR/css-variables/ CSS Text Decoration Module Level 3 ++++++++++++++++++++++++++++++++++ The `CSS Text Decoration Module Level 3`_ "contains the features of CSS relating to text decoration, such as underlines, text shadows, and emphasis marks." The ``text-decoration-line``, ``text-decoration-style`` and ``text-decoration-color`` properties are supported, except from the ``wavy`` value of ``text-decoration-style``. The ``text-decoration`` shorthand is also supported. The other properties (``text-underline-position``, ``text-emphasis-*``, ``text-shadow``) are not supported. .. _CSS Text Decoration Module Level 3: https://www.w3.org/TR/css-text-decor-3/ CSS Flexible Box Layout Module Level 1 ++++++++++++++++++++++++++++++++++++++ The `CSS Flexible Box Layout Module Level 1`_ "describes a CSS box model optimized for user interface design", also known as "flexbox". This module works for simple use cases but is not deeply tested. All the ``flex-*``, ``align-*``, ``justify-*`` and ``order`` properties are supported. The ``flex`` and ``flex-flow`` shorthands are supported too. .. _CSS Flexible Box Layout Module Level 1: https://www.w3.org/TR/css-flexbox-1/ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1643654890.8982391 weasyprint-54.1/docs/changelog.rst0000644000000000000000000021105400000000000015413 0ustar0000000000000000Changelog ========= Version 54.1 ------------ Released on 2022-01-31. Features: * `#1547 `_: Handle break-inside: avoid on tr tags Bug fixes: * `#1540 `_, `#1239 `_: Handle absolute children in running elements * `#1538 `_: Handle invalid values in text-align * `#1536 `_: Handle absolute flex boxes Contirbutors: * Guillaume Ayoub * Lucie Anglade Backers and sponsors: * H-Net: Humanities and Social Sciences Online * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * Crisp BV * Maykin Media * René Fritz * Simon Sapin * NCC Group * Nathalie Gutton * Andreas Zettl * Tom Pohl * Spacinov * Des images et des mots * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * Gábor * Piotr Horzycki Version 54.0 ------------ Released on 2022-01-08. This version also includes the changes from unstable b1 version listed below. Bug fixes: * `#1531 `_: Always use absolute paths to get hrefs in SVG * `#1523 `_: Fix many rendering problems of broken tables * `e1aee70 `_: Fix support of fonts with SVG emojis Contirbutors: * Guillaume Ayoub Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * Crisp BV * Maykin Media * René Fritz * Simon Sapin * NCC Group * Nathalie Gutton * Andreas Zettl * Tom Pohl * Des images et des mots * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * Gábor * Piotr Horzycki Version 54.0b1 -------------- Released on 2021-12-13. **This version is experimental, don't use it in production. If you find bugs, please report them!** Dependencies: * html5lib 1.1+ is now needed. New features: * `#1509 `_: Support footnotes, with financial support from Code & Co. * `#36 `_: Handle parallel flows for floats, absolutes, table-cells * `#1389 `_: Support ``text-align-last`` and ``text-align-all`` properties * `#1434 `_: Draw SVG and PNG emojis * `#1520 `_: Support ``overflow-wrap: anywhere`` * `#1435 `_: Add environment variable to set DLL folder on Windows Performance: * `#1439 `_: Cache SVG ``use`` tags * `#1481 `_: Encode non-JPEG images as PNGs instead of JPEG2000s Bug fixes: * `#137 `_: Don’t use ``text-transform`` text for content-based uses * `#1443 `_: Don’t serialize and parse again inline SVG files * `#607 `_: Correctly handle whitespaces in bookmark labels * `#1094 `_: Fix column height with ``column-span`` content * `#1473 `_: Fix absolutely positioned boxes in duplicated pages * `#1491 `_: Fix ``target-counter`` attribute in flex items * `#1515 `_, `#1508 `_: Don’t draw empty glyphs * `#1499 `_: Don’t crash when font size is really small Documentation: * `#1519 `_: Fix typo Packaging: * The source package does not include a ``setup.py`` file anymore. You can find more information about this in `issue #1410 `_. Contirbutors: * Guillaume Ayoub * Lucie Anglade * Colin Kinloch * aschmitz * Pablo González * Rian McGuire Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * Crisp BV * Maykin Media * René Fritz * Simon Sapin * NCC Group * Nathalie Gutton * Andreas Zettl * Tom Pohl * Des images et des mots * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * Gábor * Piotr Horzycki Version 53.4 ------------ Released on 2021-11-14. Bug fixes: * `#1446 `_: Fix background on pages with a bleed property * `#1455 `_: Use SVG width/height as inner size when no viewBox is given * `#1469 `_: Only enable letter- and word-spacing when needed * `#1471 `_: Don’t display inputs with "hidden" type * `#1485 `_: Allow quotes in url() syntax for SVG, Use better approximations for font ascent and descent values in SVG * `#1486 `_: Fix images embedded from multiple pages * `#1489 `_: Use a better hash for fonts to avoid collisions * `abd54c4 `_: Set SVG ratio when width and height are 0 Contributors: * Guillaume Ayoub * Lucie Anglade Backers and sponsors: * Grip Angebotssoftware * SimonSoft * Menutech * Manuel Barkhau * Simon Sapin * KontextWork * René Fritz * Maykin Media * NCC Group * Crisp BV * Des images et des mots * Andreas Zettl * Nathalie Gutton * Tom Pohl * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * G. Allard * Gábor Version 53.3 ------------ Released on 2021-09-10. Bug fixes: * `#1431 `_, `#1440 `_: Fix crashes and malformed PDF files * `#1430 `_: Handle cx and cy in SVG rotations * `#1436 `_: Fix marker-start being drawn on mid vertices Contributors: * Guillaume Ayoub * Rian McGuire * Lucie Anglade Backers and sponsors: * Grip Angebotssoftware * SimonSoft * Menutech * Manuel Barkhau * Simon Sapin * KontextWork * René Fritz * Maykin Media * NCC Group * Des images et des mots * Andreas Zettl * Nathalie Gutton * Tom Pohl * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla Version 53.2 ------------ Released on 2021-08-27. New features: * `#1428 `_: Re-add the ``make_bookmark_tree()`` method Bug fixes: * `#1429 `_: Fix package deployed on PyPI Contributors: * Guillaume Ayoub Backers and sponsors: * Grip Angebotssoftware * PDF Blocks * SimonSoft * Menutech * Manuel Barkhau * Simon Sapin * KontextWork * René Fritz * Maykin Media * NCC Group * Des images et des mots * Andreas Zettl * Nathalie Gutton * Tom Pohl * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla Version 53.1 ------------ Released on 2021-08-22. Bug fixes: * `#1409 `_: Don’t crash when leaders are in floats * `#1414 `_: Embed images once * `#1417 `_: Fix crash with SVG intrinsic ratio Documentation: * `#1422 `_: Include ``weasyprint.tools`` removal in documentation Contributors: * Guillaume Ayoub Backers and sponsors: * Grip Angebotssoftware * PDF Blocks * SimonSoft * Menutech * Manuel Barkhau * Simon Sapin * KontextWork * René Fritz * Maykin Media * NCC Group * Des images et des mots * Andreas Zettl * Nathalie Gutton * Tom Pohl * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla Version 53.0 ------------ Released on 2021-07-31. This version also includes the changes from unstable b1 and b2 versions listed below. Dependencies: * Pango 1.44.0+ is now needed. * pydyf 0.0.3+ is now needed. * fontTools 4.0.0+ is now needed. * html5lib 1.0.1+ is now needed. API changes: * ``FontConfiguration`` is now in the ``weasyprint.text.fonts`` module. * ``--format`` and ``--resolution`` options have been deprecated, PDF is the only output format supported. * ``--optimize-images`` option has been deprecated and replaced by ``--optimize-size``, allowing ``images``, ``fonts``, ``all`` and ``none`` values. * ``weasyprint.tools`` have been removed. * ``Document.resolve_links``, ``Document.make_bookmark_tree`` and ``Document.add_hyperlinks`` have been removed. Performance: * Improve image management New features: * `#1374 `_: Support basic "clipPath" in SVG Bug fixes: * `#1369 `_: Render use path in SVG * `#1370 `_: Fix fill color on use path in SVG * `#1371 `_: Handle stroke-opacity and fill-opacity * `#1378 `_: Fix crash with borders whose widths are in em * `#1394 `_: Fix crash on draw_pattern * `#880 `_: Handle stacking contexts put in contexts by previous generations * `#1386 `_: Catch font subsetting errors * `#1403 `_: Fix how x and y attributes are handled in SVG * `#1399 `_, `#1401 `_: Don’t crash when use tags reference non-existing element * `#1393 `_: Handle font collections * `#1408 `_: Handle x and y attributes in use tags Documentation: * `#1391 `_, `#1405 `_: Add documentation for installation Contributors: * Guillaume Ayoub * Lucie Anglade * Pelle Bo Regener * aschmitz * John Jackson * Felix Schwarz * Syrus Dark * Christoph Päper Backers and sponsors: * OpenEdition * Grip Angebotssoftware * Simonsoft * PDF Blocks * Menutech * Manuel Barkhau * print-css.rocks * Simon Sapin * KontextWork * René Fritz * Maykin Media * Nathalie Gutton * Andreas Zettl * Tom Pohl * NCC Group * Moritz Mahringer * Florian Demmer * Des images et des mots * Mohammed Y. Alnajdi * Yanal-Yvez Fargialla * Yevhenii Hyzyla Version 53.0b2 -------------- Released on 2021-05-30. **This version is experimental, don't use it in production. If you find bugs, please report them!** New features: * `#359 `_: Embed full sets of fonts in PDF Bug fixes: * `#1345 `_: Fix position of SVG use tags * `#1346 `_: Handle "stroke-dasharray: none" * `#1352 `_, `#1358 `_: Sort link target identifiers * `#1357 `_: Fix font information * `#1362 `_: Handle visibility and display properties in SVG * `#1365 `_: Cascade inherited attributes for use tags * `#1366 `_: Correctly handle style attributes in SVG * `#1367 `_: Include line stroke in box bounding Documentation: * `#1341 `_: Fix typos Contributors: * Guillaume Ayoub * aschmitz * John Jackson * Lucie Anglade * Pelle Bo Regener Backers and sponsors: * OpenEdition * print-css.rocks * Simonsoft * PDF Blocks * Menutech * Manuel Barkhau * Simon Sapin * Grip Angebotssoftware * KontextWork * René Fritz * Nathalie Gutton * Andreas Zettl * Tom Pohl * Maykin Media * Moritz Mahringer * Florian Demmer * Mohammed Y. Alnajdi * NCC Group * Des images et des mots * Yanal-Yvez Fargialla * Yevhenii Hyzyla Version 53.0b1 -------------- Released on 2021-04-22. **This version is experimental, don't use it in production. If you find bugs, please report them!** Dependencies: * This version uses its own PDF generator instead of Cairo. Rendering may be different for text, gradients, SVG images… * Packaging is now done with Flit. New features: * `#1328 `_: Add ISO and JIS paper sizes * `#1309 `_: Leader support, with financial support from Simonsoft Bug fixes: * `#504 `_: Fix rendering bugs with PDF gradients * `#606 `_: Fix rounding errors on PDF dimensions * `#1264 `_: Include witdh/height when calculating auto margins of absolute boxes * `#1191 `_: Don’t try to get an earlier page break between columns * `#1235 `_: Include padding, border, padding when calculating inline-block width * `#1199 `_: Fix kerning issues with small fonts Documentation: * `#1298 `_: Rewrite documentation Contributors: * Guillaume Ayoub * Lucie Anglade * Felix Schwarz * Syrus Dark * Christoph Päper Backers and sponsors: * Simonsoft * PDF Blocks * Menutech * Manuel Barkhau * Simon Sapin * Nathalie Gutton * Andreas Zettl * René Fritz * Tom Pohl * KontextWork * Moritz Mahringer * Florian Demmer * Maykin Media * Yanal-Yvez Fargialla * Des images et des mots * Yevhenii Hyzyla Version 52.5 ------------ Released on 2021-04-17. Bug fixes: * `#1336 `_: Fix text breaking exception * `#1318 `_: Fix @font-face rules with Pango 1.48.3+ Contributors: * Guillaume Ayoub Backers and sponsors: * Simonsoft * PDF Blocks * Menutech * Manuel Barkhau * Simon Sapin * Nathalie Gutton * Andreas Zettl * René Fritz * Tom Pohl * KontextWork * Moritz Mahringer * Florian Demmer * Maykin Media * Yanal-Yvez Fargialla * Des images et des mots * Yevhenii Hyzyla Version 52.4 ------------ Released on 2021-03-11. Bug fixes: * `#1304 `_: Don’t try to draw SVG files with no size * `ece5f066 `_: Avoid crash on last word detection * `4ee42e48 `_: Remove last word before ellipses when hyphenated Contributors: * Guillaume Ayoub Backers and sponsors: * PDF Blocks * Simonsoft * Menutech * Simon Sapin * Manuel Barkhau * Andreas Zettl * Nathalie Gutton * Tom Pohl * René Fritz * Moritz Mahringer * Florian Demmer * KontextWork * Michele Mostarda Version 52.3 ------------ Released on 2021-03-02. Bug fixes: * `#1299 `_: Fix imports with url() and quotes New features: * `#1300 `_: Add support of line-clamp, with financial support from expert Germany Contributors: * Guillaume Ayoub * Lucie Anglade Backers and sponsors: * PDF Blocks * Simonsoft * Menutech * Simon Sapin * Manuel Barkhau * Andreas Zettl * Nathalie Gutton * Tom Pohl * Moritz Mahringer * Florian Demmer * KontextWork * Michele Mostarda Version 52.2 ------------ Released on 2020-12-06. Bug fixes: * `238e214 `_: Fix URL handling with tinycss2 * `#1248 `_: Include missing test data * `#1254 `_: Top margins removed from children when tables are displayed on multiple pages * `#1250 `_: Correctly draw borders on the last line of split tables * `a6f9c80 `_: Add a nice gif to please gdk-pixbuf 2.42.0 Contributors: * Guillaume Ayoub * Lucie Anglade * Felix Schwarz Backers and sponsors: * PDF Blocks * Simonsoft * Menutech * Simon Sapin * Nathalie Gutton * Andreas Zetti * Tom Pohl * Florian Demmer * Moritz Mahringer Version 52.1 ------------ Released on 2020-11-02. Bug fixes: * `238e214 `_: Fix URL handling with tinycss2 Contributors: * Guillaume Ayoub Backers and sponsors: * Simonsoft * Simon Sapin * Nathalie Gutton * Andreas Zettl * Florian Demmer * Moritz Mahringer Version 52 ---------- Released on 2020-10-29. Dependencies: * Python 3.6+ is now needed, Python 3.5 is not supported anymore * WeasyPrint now depends on Pillow New features: * `#1019 `_: Implement ``counter-set`` * `#1080 `_: Don’t display ``template`` tags * `#1210 `_: Use ``download`` attribute in ``a`` tags for attachment's filename * `#1206 `_: Handle strings in ``list-style-type`` * `#1165 `_: Add support for concatenating ``var()`` functions in ``content`` declarations * `c56b96b `_: Add an option to optimize embedded images size, with financial support from Hashbang * `#969 `_: Add an image cache that can be shared between documents, with financial support from Hashbang Bug fixes: * `#1141 `_: Don’t clip page margins on account of ``body`` overflow * `#1000 `_: Don’t apply ``text-indent`` twice on inline blocks * `#1051 `_: Avoid random line breaks * `#1120 `_: Gather target counters in page margins * `#1110 `_: Handle most cases for boxes avoiding floats in rtl containers, with financial support from Innovative Software * `#1111 `_: Fix horizontal position of last rtl line, with financial support from Innovative Software * `#1114 `_: Fix bug with transparent borders in tables * `#1146 `_: Don’t gather bookmarks twice for blocks that are displayed on two pages * `#1237 `_: Use fallback fonts on unsupported WOFF2 and WOFF fonts * `#1025 `_: Don’t insert the same layout attributes multiple times * `#1027 `_: Don’t try to break tables after the header or before the footer * `#1050 `_: Don’t crash on absolute SVG files with no intrinsic size * `#1204 `_: Fix a crash with a flexbox corner case * `#1030 `_: Fix frozen builds * `#1089 `_: Fix Pyinstaller builds * `#1216 `_: Fix embedded files * `#1225 `_: Initial support of RTL direction in flexbox layout Documentation: * `#1149 `_: Add the ``--quiet`` CLI option in the documentation * `#1061 `_: Update install instructions on Windows Tests: * `#1209 `_: Use GitHub Actions instead of Travis Contributors: * Guillaume Ayoub * Lucie Anglade * Tontyna * Mohammed Y. Alnajdi * Mike Voets * Bjarni Þórisson * Balázs Dukai * Bart Broere * Endalkachew * Felix Schwarz * Julien Sanchez * Konstantin Alekseev * Nicolas Hart * Nikolaus Schlemm * Thomas J. Lampoltshammer * mPyth * nempoBu4 * saddy001 Backers and sponsors: * Hashbang * Innovative Software * Screenbreak * Simon Sapin * Lisa Warshaw * Nathalie Gutton * Andreas Zettl * Florian Demmer * Moritz Mahringer Version 51 ---------- Released on 2019-12-23. Dependencies: * Pyphen 0.9.1+ is now needed New features: * `#882 `_: Add support of ``element()`` and ``running()`` * `#972 `_: Add HTML element to Box class * `7a4d6f8 `_: Support ``larger`` and ``smaller`` values for ``font-size`` Bug fixes: * `#960 `_: Fix how fonts used for macOS tests are installed * `#956 `_: Fix various crashes due to line breaking bugs * `#983 `_: Fix typo in variable name * `#975 `_: Don’t crash when ``string-set`` is set to ``none`` * `#998 `_: Keep font attributes when text lines are modified * `#1005 `_: Don’t let presentational hints add decorations on tables with no borders * `#974 `_: Don’t crash on improper ``var()`` values * `#1012 `_: Fix rendering of header and footer for empty tables * `#1013 `_: Avoid quadratic time relative to tree depth when setting page names Contributors: - Lucie Anglade - Guillaume Ayoub - Guillermo Bonvehí - Holger Brunn - Felix Schwarz - Tontyna Version 50 ---------- Released on 2019-09-19. New features: * `#209 `_: Make ``break-*`` properties work inside tables * `#661 `_: Make blocks with ``overflow: auto`` grow to include floating children Bug fixes: * `#945 `_: Don't break pages between a list item and its marker * `#727 `_: Avoid tables lost between pages * `#831 `_: Ignore auto margins on flex containers * `#923 `_: Fix a couple of crashes when splitting a line twice * `#896 `_: Fix skip stack order when using a reverse flex direction Contributors: - Lucie Anglade - Guillaume Ayoub Version 49 ---------- Released on 2019-09-11. Performance: * Speed and memory use have been largely improved. New features: * `#700 `_: Handle ``::marker`` pseudo-selector * `135dc06c `_: Handle ``recto`` and ``verso`` parameters for page breaks * `#907 `_: Provide a clean way to build layout contexts Bug fixes: * `#937 `_: Fix rendering of tables with empty lines and rowspans * `#897 `_: Don't crash when small columns are wrapped in absolute blocks * `#913 `_: Fix a test about gradient colors * `#924 `_: Fix title for document with attachments * `#917 `_: Fix tests with Pango 1.44 * `#919 `_: Fix padding and margin management for column flex boxes * `#901 `_: Fix width of replaced boxes with no intrinsic width * `#906 `_: Don't respect table cell width when content doesn't fit * `#927 `_: Don't use deprecated ``logger.warn`` anymore * `a8662794 `_: Fix margin collapsing between caption and table wrapper * `87d9e84f `_: Avoid infinite loops when rendering columns * `789b80e6 `_: Only use in flow children to set columns height * `615e298a `_: Don't include floating elements each time we try to render a column * `48d8632e `_: Avoid not in flow children to compute column height * `e7c452ce `_: Fix collapsing margins for columns * `fb0887cf `_: Fix crash when using currentColor in gradients * `f66df067 `_: Don't crash when using ex units in word-spacing in letter-spacing * `c790ff20 `_: Don't crash when properties needing base URL use var functions * `d63eac31 `_: Don't crash with object-fit: non images with no intrinsic size Documentation: * `#900 `_: Add documentation about semantic versioning * `#692 `_: Add a snippet about PDF magnification * `#899 `_: Add .NET wrapper link * `#893 `_: Fixed wrong nested list comprehension example * `#902 `_: Add ``state`` to the ``make_bookmark_tree`` documentation * `#921 `_: Fix typos in the documentation * `#328 `_: Add CSS sample for forms Contributors: - Lucie Anglade - Guillaume Ayoub - Raphael Gaschignard - Stani - Szmen - Thomas Dexter - Tontyna Version 48 ---------- Released on 2019-07-08. Dependencies: * CairoSVG 2.4.0+ is now needed New features: * `#891 `_: Handle ``text-overflow`` * `#878 `_: Handle ``column-span`` * `#855 `_: Handle all the ``text-decoration`` features * `#238 `_: Don't repeat background images when it's not needed * `#875 `_: Handle ``object-fit`` and ``object-position`` * `#870 `_: Handle ``bookmark-state`` Bug fixes: * `#686 `_: Fix column balance when children are not inline * `#885 `_: Actually use the content box to resolve flex items percentages * `#867 `_: Fix rendering of KaTeX output, including (1) set row baseline of tables when no cells are baseline-aligned, (2) set baseline for inline tables, (3) don't align lines larger than their parents, (4) force CairoSVG to respect image size defined by CSS. * `#873 `_: Set a minimum height for empty list elements with outside marker * `#811 `_: Don't use translations to align flex items * `#851 `_, `#860 `_: Don't cut pages when content overflows a very little bit * `#862 `_: Don't crash when using UTC dates in metadata Documentation: * `#854 `_: Add a "Tips & Tricks" section Contributors: - Gabriel Corona - Guillaume Ayoub - Manuel Barkhau - Nathan de Maestri - Lucie Anglade - theopeek Version 47 ---------- Released on 2019-04-12. New features: * `#843 `_: Handle CSS variables * `#846 `_: Handle ``:nth()`` page selector * `#847 `_: Allow users to use a custom SSL context for HTTP requests Bug fixes: * `#797 `_: Fix underlined justified text * `#836 `_: Fix crash when flex items are replaced boxes * `#835 `_: Fix ``margin-break: auto`` Version 46 ---------- Released on 2019-03-20. New features: * `#771 `_: Handle ``box-decoration-break`` * `#115 `_: Handle ``margin-break`` * `#821 `_: Continuous integration includes tests on Windows Bug fixes: * `#765 `_, `#754 `_, `#800 `_: Fix many crashes related to the flex layout * `#783 `_: Fix a couple of crashes with strange texts * `#827 `_: Named strings and counters are case-sensitive * `#823 `_: Shrink min/max-height/width according to box-sizing * `#728 `_, `#171 `_: Don't crash when fixed boxes are nested * `#610 `_, `#828 `_: Don't crash when preformatted text lines end with a space * `#808 `_, `#387 `_: Fix position of some images * `#813 `_: Don't crash when long preformatted text lines end with ``\n`` Documentation: * `#815 `_: Add documentation about custom ``url_fetcher`` Version 45 ---------- Released on 2019-02-20. WeasyPrint now has a `code of conduct `_. A new website has been launched, with beautiful and useful graphs about speed and memory use across versions: check `WeasyPerf `_. Dependencies: * Python 3.5+ is now needed, Python 3.4 is not supported anymore Bug fixes: * `#798 `_: Prevent endless loop and index out of range in pagination * `#767 `_: Add a ``--quiet`` CLI parameter * `#784 `_: Fix library loading on Alpine * `#791 `_: Use path2url in tests for Windows * `#789 `_: Add LICENSE file to distributed sources * `#788 `_: Fix pending references * `#780 `_: Don't draw patterns for empty page backgrounds * `#774 `_: Don't crash when links include quotes * `#637 `_: Fix a problem with justified text * `#763 `_: Launch tests with Python 3.7 * `#704 `_: Fix a corner case with tables * `#804 `_: Don't logger handlers defined before importing WeasyPrint * `#109 `_, `#748 `_: Don't include punctuation for hyphenation * `#770 `_: Don't crash when people use uppercase words from old-fashioned Microsoft fonts in tables, especially when there's an 5th column * Use a `separate logger `_ to report the rendering process * Add a ``--debug`` CLI parameter and set debug level for unknown prefixed CSS properties * Define minimal versions of Python and setuptools in setup.cfg Documentation: * `#796 `_: Fix a small typo in the tutorial * `#792 `_: Document no alignement character support * `#773 `_: Fix phrasing in Hacking section * `#402 `_: Add a paragraph about fontconfig error * `#764 `_: Fix list of dependencies for Alpine * Fix API documentation of HTML and CSS classes Version 44 ---------- Released on 2018-12-29. Bug fixes: * `#742 `_: Don't crash during PDF generation when locale uses commas as decimal separator * `#746 `_: Close file when reading VERSION * Improve speed and memory usage for long texts. Documentation: * `#733 `_: Small documentation fixes * `#735 `_: Fix broken links in NEWS.rst Version 43 ---------- Released on 2018-11-09. Bug fixes: * `#726 `_: Make empty strings clear previous values of named strings * `#729 `_: Include tools in packaging This version also includes the changes from unstable rc1 and rc2 versions listed below. Version 43rc2 ------------- Released on 2018-11-02. **This version is experimental, don't use it in production. If you find bugs, please report them!** Bug fixes: * `#706 `_: Fix text-indent at the beginning of a page * `#687 `_: Allow query strings in file:// URIs * `#720 `_: Optimize minimum size calculation of long inline elements * `#717 `_: Display
tags as blocks * `#691 `_: Don't recalculate max content widths when distributing extra space for tables * `#722 `_: Fix bookmarks and strings set on images * `#723 `_: Warn users when string() is not used in page margin Version 43rc1 ------------- Released on 2018-10-15. **This version is experimental, don't use it in production. If you find bugs, please report them!** Dependencies: * Python 3.4+ is now needed, Python 2.x is not supported anymore * Cairo 1.15.4+ is now needed, but 1.10+ should work with missing features (such as links, outlines and metadata) * Pdfrw is not needed anymore New features: * `Beautiful website `_ * `#579 `_: Initial support of flexbox * `#592 `_: Support @font-face on Windows * `#306 `_: Add a timeout parameter to the URL fetcher functions * `#594 `_: Split tests using modern pytest features * `#599 `_: Make tests pass on Windows * `#604 `_: Handle target counters and target texts * `#631 `_: Enable counter-increment and counter-reset in page context * `#622 `_: Allow pathlib.Path objects for HTML, CSS and Attachment classes * `#674 `_: Add extensive installation instructions for Windows Bug fixes: * `#558 `_: Fix attachments * `#565 `_, `#596 `_, `#539 `_: Fix many PDF rendering, printing and compatibility problems * `#614 `_: Avoid crashes and endless loops caused by a Pango bug * `#662 `_: Fix warnings and errors when generating documentation * `#666 `_, `#685 `_: Fix many table layout rendering problems * `#680 `_: Don't crash when there's no font available * `#662 `_: Fix support of some align values in tables Version 0.42.3 -------------- Released on 2018-03-27. Bug fixes: * `#583 `_: Fix floating-point number error to fix floating box layout * `#586 `_: Don't optimize resume_at when splitting lines with trailing spaces * `#582 `_: Fix table layout with no overflow * `#580 `_: Fix inline box breaking function * `#576 `_: Split replaced_min_content_width and replaced_max_content_width * `#574 `_: Respect text direction and don't translate rtl columns twice * `#569 `_: Get only first line's width of inline children to get linebox width Version 0.42.2 -------------- Released on 2018-02-04. Bug fixes: * `#560 `_: Fix a couple of crashes and endless loops when breaking lines. Version 0.42.1 -------------- Released on 2018-02-01. Bug fixes: * `#566 `_: Don't crash when using @font-config. * `#567 `_: Fix text-indent with text-align: justify. * `#465 `_: Fix string(\*, start). * `#562 `_: Handle named pages with pseudo-class. * `#507 `_: Fix running headers. * `#557 `_: Avoid infinite loops in inline_line_width. * `#555 `_: Fix margins, borders and padding in column layouts. Version 0.42 ------------ Released on 2017-12-26. WeasyPrint is not tested with (end-of-life) Python 3.3 anymore. **This release is probably the last version of the 0.x series.** Next version may include big changes: - end of Python 2.7 support, - initial support of bidirectional text, - initial support of flexbox, - improvements for speed and memory usage. New features: * `#532 `_: Support relative file URIs when using CLI. Bug fixes: * `#553 `_: Fix slow performance for pre-formatted boxes with a lot of children. * `#409 `_: Don't crash when rendering some tables. * `#39 `_: Fix rendering of floats in inlines. * `#301 `_: Split lines carefully. * `#530 `_: Fix root when frozen with Pyinstaller. * `#534 `_: Handle SVGs containing images embedded as data URIs. * `#360 `_: Fix border-radius rendering problem with some PDF readers. * `#525 `_: Fix pipenv support. * `#227 `_: Smartly handle replaced boxes with percentage width in auto-width parents. * `#520 `_: Don't ignore CSS @page rules that are imported by an @import rule. Version 0.41 ------------ Released on 2017-10-05. WeasyPrint now depends on pdfrw >= 0.4. New features: * `#471 `_: Support page marks and bleed. Bug fixes: * `#513 `_: Don't crash on unsupported image-resolution values. * `#506 `_: Fix @font-face use with write_* methods. * `#500 `_: Improve readability of _select_source function. * `#498 `_: Use CSS prefixes as recommanded by the CSSWG. * `#441 `_: Fix rendering problems and crashes when using @font-face. * `bb3a4db `_: Try to break pages after a block before trying to break inside it. * `1d1654c `_: Fix and test corner cases about named pages. Documentation: * `#508 `_: Add missing libpangocairo dependency for Debian and Ubuntu. * `a7b17fb `_: Add documentation on logged rendering steps. Version 0.40 ------------ Released on 2017-08-17. WeasyPrint now depends on cssselect2 instead of cssselect and lxml. New features: * `#57 `_: Named pages. * Unprefix properties, see `#498 `_. * Add a "verbose" option logging the document generation steps. Bug fixes: * `#483 `_: Fix slow performance with long pre-formatted texts. * `#70 `_: Improve speed and memory usage for long documents. * `#487 `_: Don't crash on local() fonts with a space and no quotes. Version 0.39 ------------ Released on 2017-06-24. Bug fixes: * Fix the use of WeasyPrint's URL fetcher with CairoSVG. Version 0.38 ------------ Released on 2017-06-16. Bug fixes: * `#477 `_: Don't crash on font-face's src attributes with local functions. Version 0.37 ------------ Released on 2017-06-15. WeasyPrint now depends on tinycss2 instead of tinycss. New features: * `#437 `_: Support local links in generated PDFs. Bug fixes: * `#412 `_: Use a NullHandler log handler when WeasyPrint is used as a library. * `#417 `_, `#472 `_: Don't crash on some line breaks. * `#327 `_: Don't crash with replaced elements with height set in percentages. * `#467 `_: Remove incorrect line breaks. * `#446 `_: Let the logging module do the string interpolation. Version 0.36 ------------ Released on 2017-02-25. New features: * `#407 `_: Handle ::first-letter. * `#423 `_: Warn user about broken cairo versions. Bug fixes: * `#411 `_: Typos fixed in command-line help. Version 0.35 ------------ Released on 2017-02-25. Bug fixes: * `#410 `_: Fix AssertionError in split_text_box. Version 0.34 ------------ Released on 2016-12-21. Bug fixes: * `#398 `_: Honor the presentational_hints option for PDFs. * `#399 `_: Avoid CairoSVG-2.0.0rc* on Python 2. * `#396 `_: Correctly close files open by mkstemp. * `#403 `_: Cast the number of columns into int. * Fix multi-page multi-columns and add related tests. Version 0.33 ------------ Released on 2016-11-28. New features: * `#393 `_: Add tests on MacOS. * `#370 `_: Enable @font-face on MacOS. Bug fixes: * `#389 `_: Always update resume_at when splitting lines. * `#394 `_: Don't build universal wheels. * `#388 `_: Fix logic when finishing block formatting context. Version 0.32 ------------ Released on 2016-11-17. New features: * `#28 `_: Support @font-face on Linux. * Support CSS fonts level 3 almost entirely, including OpenType features. * `#253 `_: Support presentational hints (optional). * Support break-after, break-before and break-inside for pages and columns. * `#384 `_: Major performance boost. Bux fixes: * `#368 `_: Respect white-space for shrink-to-fit. * `#382 `_: Fix the preferred width for column groups. * Handle relative boxes in column-layout boxes. Documentation: * Add more and more documentation about Windows installation. * `#355 `_: Add fonts requirements for tests. Version 0.31 ------------ Released on 2016-08-28. New features: * `#124 `_: Add MIME sniffing for images. * `#60 `_: CSS Multi-column Layout. * `#197 `_: Add hyphens at line breaks activated by a soft hyphen. Bux fixes: * `#132 `_: Fix Python 3 compatibility on Windows. Documentation: * `#329 `_: Add documentation about installation on Windows. Version 0.30 ------------ Released on 2016-07-18. WeasyPrint now depends on html5lib-0.999999999. Bux fixes: * Fix Acid2 * `#325 `_: Cutting lines is broken in page margin boxes. * `#334 `_: Newest html5lib 0.999999999 breaks rendering. Version 0.29 ------------ Released on 2016-06-17. Bug fixes: * `#263 `_: Don't crash with floats with percents in positions. * `#323 `_: Fix CairoSVG 2.0 pre-release dependency in Python 2.x. Version 0.28 ------------ Released on 2016-05-16. Bug fixes: * `#189 `_: ``white-space: nowrap`` still wraps on hyphens * `#305 `_: Fix crashes on some tables * Don't crash when transform matrix isn't invertible * Don't crash when rendering ratio-only SVG images * Fix margins and borders on some tables Version 0.27 ------------ Released on 2016-04-08. New features: * `#295 `_: Support the 'rem' unit. * `#299 `_: Enhance the support of SVG images. Bug fixes: * `#307 `_: Fix the layout of cells larger than their tables. Documentation: * The website is now on GitHub Pages, the documentation is on Read the Docs. * `#297 `_: Rewrite the CSS chapter of the documentation. Version 0.26 ------------ Released on 2016-01-29. New features: * Support the `empty-cells` attribute. * Respect table, column and cell widths. Bug fixes: * `#172 `_: Unable to set table column width on tables td's. * `#151 `_: Table background colour bleeds beyond table cell boundaries. * `#260 `_: TypeError: unsupported operand type(s) for +: 'float' and 'str'. * `#288 `_: Unwanted line-breaks in bold text. * `#286 `_: AttributeError: 'Namespace' object has no attribute 'attachments'. Version 0.25 ------------ Released on 2015-12-17. New features: * Support the 'q' unit. Bug fixes: * `#285 `_: Fix a crash happening when splitting lines. * `#284 `_: Escape parenthesis in PDF links. * `#280 `_: Replace utf8 with utf-8 for gettext/django compatibility. * `#269 `_: Add support for use when frozen. * `#250 `_: Don't crash when attachments are not available. Version 0.24 ------------ Released on 2015-08-04. New features: * `#174 `_: Basic support for Named strings. Bug fixes: * `#207 `_: Draw rounded corners on replaced boxes. * `#224 `_: Rely on the font size for rounding bug workaround. * `#31 `_: Honor the vertical-align property in fixed-height cells. * `#202 `_: Remove unreachable area/border at bottom of page. * `#225 `_: Don't allow unknown units during line-height validation. * Fix some wrong conflict resolutions for table borders with inset and outset styles. Version 0.23 ------------ Released on 2014-09-16. Bug fixes: * `#196 `_: Use the default image sizing algorithm for images’s preferred size. * `#194 `_: Try more library aliases with ``dlopen()``. * `#201 `_: Consider ``page-break-after-avoid`` when pushing floats to the next page. * `#217 `_: Avoid a crash on zero-sized background images. Release process: * Start testing on Python 3.4 on Travis-CI. Version 0.22 ------------ Released on 2014-05-05. New features: * `#86 `_: Support gzip and deflate encoding in HTTP responses * `#177 `_: Support for PDF attachments. Bug fixes: * `#169 `_: Fix a crash on percentage-width columns in an auto-width table. * `#168 `_: Make ``
`` a block in the user-agent stylesheet. * `#175 `_: Fix some ``dlopen()`` library loading issues on OS X. * `#183 `_: Break to the next page before a float that would overflow the page. (It might still overflow if it’s bigger than the page.) * `#188 `_: Require a recent enough version of Pyphen Release process: * Drop Python 3.1 support. * Set up [Travis CI](http://travis-ci.org/) to automatically test all pushes and pull requests. * Start testing on Python 3.4 locally. (Travis does not support 3.4 yet.) Version 0.21 ------------ Released on 2014-01-11. New features: * Add the `overflow-wrap `_ property, allowing line breaks inside otherwise-unbreakable words. Thanks Frédérick Deslandes! * Add the `image-resolution `_ property, allowing images to be sized proportionally to their intrinsic size at a resolution other than 96 image pixels per CSS ``in`` (ie. one image pixel per CSS ``px``) Bug fixes: * `#145 `_: Fix parsing HTML from an HTTP URL on Python 3.x * `#40 `_: Use more general hyphenation dictionnaries for specific document languages. (E.g. use ``hyph_fr.dic`` for ``lang="fr_FR"``.) * `#26 `_: Fix ``min-width`` and ``max-width`` on floats. * `#100 `_: Fix a crash on trailing whitespace with ``font-size: 0`` * `#82 `_: Borders on tables with ``border-collapse: collapse`` were sometimes drawn at an incorrect position. * `#30 `_: Fix positioning of images with ``position: absolute``. * `#118 `_: Fix a crash when using ``position: absolute`` inside a ``position: relative`` element. * Fix ``visibility: collapse`` to behave like ``visibility: hidden`` on elements other than table rows and table columns. * `#147 `_ and `#153 `_: Fix dependencies to require lxml 3.0 or a more recent version. Thanks gizmonerd and Thomas Grainger! * `#152 `_: Fix a crash on percentage-sized table cells in auto-sized tables. Thanks Johannes Duschl! Version 0.20.2 -------------- Released on 2013-12-18. * Fix `#146 `_: don't crash when drawing really small boxes with dotted/dashed borders Version 0.20.1 -------------- Released on 2013-12-16. * Depend on html5lib >= 0.99 instead of 1.0b3 to fix pip 1.4 support. * Fix `#74 `_: don't crash on space followed by dot at line break. * Fix `#78 `_: nicer colors for border-style: ridge/groove/inset/outset. Version 0.20 ------------ Released on 2013-12-14. * Add support for ``border-radius``. * Feature `#77 `_: Add PDF metadata from HTML. * Feature `#12 `_: Use html5lib. * Tables: handle percentages for column groups, columns and cells, and values for row height. * Bug fixes: * Fix `#84 `_: don't crash when stylesheets are not available. * Fix `#101 `_: use page ids instead of page numbers in PDF bookmarks. * Use ``logger.warning`` instead of deprecated ``logger.warn``. * Add 'font-stretch' in the 'font' shorthand. Version 0.19.2 -------------- Released on 2013-06-18. Bug fix release: * Fix `#88 `_: ``text-decoration: overline`` not being drawn above the text * Bug fix: Actually draw multiple lines when multiple values are given to ``text-decoration``. * Use the font metrics for text decoration positioning. * Bug fix: Don't clip the border with ``overflow: hidden``. * Fix `#99 `_: Regression: JPEG images not loading with cairo 1.8.x. Version 0.19.1 -------------- Released on 2013-04-30. Bug fix release: * Fix incorrect intrinsic width calculation leading to unnecessary line breaks in floats, tables, etc. * Tweak border painting to look better * Fix unnecessary page break before big tables. * Fix table row overflowing at the bottom of the page when there are margins above the table. * Fix ``position: fixed`` to actually repeat on every page. * Fix `#76 `_: repeat ```` and ```` elements on every page, even with table border collapsing. Version 0.19 ------------ Released on 2013-04-18. * Add support for ``linear-gradient()`` and ``radial-gradient`` in background images. * Add support for the ``ex`` and ``ch`` length units. (``1ex`` is based on the font instead of being always ``0.5em`` as before.) * Add experimental support for Level 4 hyphenation properties. * Drop support for CFFI < 0.6 and cairocffi < 0.4. * Many bug fixes, including: * Fix `#54 `_: min/max-width/height on block-level images. * Fix `#71 `_: Crash when parsing nested functional notation. Version 0.18 ------------ Released on 2013-03-30. * Add support for Level 3 backgrounds, including multiple background layers per element/box. * Forward-compatibility with (future releases of) cairocffi 0.4+ and CFFI 0.6+. * Bug fixes: * Avoid some unnecessary line breaks for elements sized based on their content (aka. “shrink-to-fit”) such as floats and page headers. * Allow page breaks between empty blocks. * Fix `#66 `_: Resolve images’ auto width from non-auto height and intrinsic ratio. * Fix `#21 `_: The ``data:`` URL scheme is case-insensitive. * Fix `#53 `_: Crash when backtracking for ``break-before/after: avoid``. Version 0.17.1 -------------- Released on 2013-03-18. Bug fixes: * Fix `#41 `_: GObject initialization when GDK-PixBuf is not installed. * Fix `#42 `_: absolute URLs without a base URL (ie. document parsed from a string.) * Fix some whitespace collapsing bugs. * Fix absolutely-positioned elements inside inline elements. * Fix URL escaping of image references from CSS. * Fix `#49 `_: Division by 0 on dashed or dotted border smaller than one dot/dash. * Fix `#44 `_: bad interaction of ``page-break-before/after: avoid`` and floats. Version 0.17 ------------ Released on 2013-02-27. * Added `text hyphenation`_ with the ``-weasy-hyphens`` property. * When a document includes JPEG images, embed them as JPEG in the PDF output. This often results in smaller PDF file size compared to the default *deflate* compression. * Switched to using CFFI instead of PyGTK or PyGObject-introspection. * Layout bug fixes: - Correctly trim whitespace at the end of lines. - Fix some cases with floats within inline content. .. _text hyphenation: https://weasyprint.readthedocs.io/en/latest/features.html#css-text-module-level-3-4 Version 0.16 ------------ Released on 2012-12-13. * Add the ``zoom`` parameter to ``HTML.write_pdf`` and ``Document.write_pdf() `` * Fix compatibility with old (and buggy) pycairo versions. WeasyPrint is now tested on 1.8.8 in addition to the latest. * Fix layout bugs related to line trailing spaces. Version 0.15 ------------ Released on 2012-10-09. * Add a low-level API that enables painting pages individually on any cairo surface. * **Backward-incompatible change**: remove the ``HTML.get_png_pages`` method. The new low-level API covers this functionality and more. * Add support for the ``font-stretch`` property. * Add support for ``@page:blank`` to select blank pages. * New Sphinx-based and improved docs * Bug fixes: - Importing Pango in some PyGTK installations. - Layout of inline-blocks with `vertical-align: top` or `bottom`. - Do not repeat a block’s margin-top or padding-top after a page break. - Performance problem with large tables split across many pages. - Anchors and hyperlinks areas now follow CSS transforms. Since PDF links have to be axis-aligned rectangles, the bounding box is used. This may be larger than expected with rotations that are not a multiple of 90 degrees. Version 0.14 ------------ Released on 2012-08-03. * Add a public API to choose media type used for @media. (It still defaults to ``print``). Thanks Chung Lu! * Add ``--base-url`` and ``--resolution`` to the command-line API, making it as complete as the Python one. * Add support for the ```` element in HTML. * Add support for CSS outlines * Switch to gdk-pixbuf instead of Pystacia for loading raster images. * Bug fixes: - Handling of filenames and URLs on Windows - Unicode filenames with older version of py2cairo - ``base_url`` now behaves as expected when set to a directory name. - Make some tests more robust Version 0.13 ------------ Released on 2012-07-23. * Add support for PyGTK, as an alternative to PyGObject + introspection. This should make WeasyPrint easier to run on platforms that not not have packages for PyGObject 3.x yet. * Bug fix: crash in PDF outlines for some malformed HTML documents Version 0.12 ------------ Released on 2012-07-19. * Add support for collapsed borders on tables. This is currently incompatible with repeating header and footer row groups on each page: headers and footers are treated as normal row groups on table with ``border-collapse: collapse``. * Add ``url_fetcher`` to the public API. This enables users to hook into WeasyPrint for fetching linked stylesheets or images, eg. to generate them on the fly without going through the network. This enables the creation of `Flask-WeasyPrint `_. Version 0.11 ------------ Released on 2012-07-04. * Add support for floats and clear. Together with various bug fixes, this enables WeasyPrint to pass the Acid2 test! Acid2 is now part of our automated test suite. * Add support for the width, min-width, max-width, height, min-height and max-height properties in @page. The size property is now the size of the page’s containing block. * Switch the Variable Dimension rules to `the new proposal `_. The previous implementation was broken in many cases. * The ``image-rendering``, ``transform``, ``transform-origin`` and ``size`` properties are now unprefixed. The prefixed form (eg. -weasy-size) is ignored but gives a specific warning. Version 0.10 ------------ Released on 2012-06-25. * Add ``get_png_pages()`` to the public API. It returns each page as a separate PNG image. * Add a ``resolution`` parameter for PNG. * Add *WeasyPrint Navigator*, a web application that shows WeasyPrint’s output with clickable links. Yes, that’s a browser in your browser. Start it with ``python -m weasyprint.navigator`` * Add support for `vertical-align: top` and `vertical-align: bottom` * Add support for `page-break-before: avoid` and `page-break-after: avoid` * Bug fixes Version 0.9 ----------- Released on 2012-06-04. * Relative, absolute and fixed positioning * Proper painting order (z-index) * In PDF: support for internal and external hyperlinks as well as bookmarks. * Added the ``tree`` parameter to the ``HTML`` class: accepts a parsed lxml object. * Bug fixes, including many crashes. Bookmarks can be controlled by the ``-weasy-bookmark-level`` and ``-weasy-bookmark-label`` properties, as described in `CSS Generated Content for Paged Media Module `_. The default UA stylesheet sets a matching bookmark level on all ``

`` to ``

`` elements. Version 0.8 ----------- Released on 2012-05-07. * Switch from cssutils to tinycss_ as the CSS parser. * Switch to the new cssselect_, almost all level 3 selectors are supported now. * Support for inline blocks and inline tables * Automatic table layout (column widths) * Support for the ``min-width``, ``max-width``, ``min-height`` and ``max-height`` properties, except on table-related and page-related boxes. * Speed improvements on big stylesheets / small documents thanks to tinycss. * Many bug fixes .. _tinycss: http://packages.python.org/tinycss/ .. _cssselect: http://packages.python.org/cssselect/ Version 0.7.1 ------------- Released on 2012-03-21. Change the license from AGPL to BSD. Version 0.7 ----------- Released on 2012-03-21. * Support page breaks between table rows * Support for the ``orphans`` and ``widows`` properties. * Support for ``page-break-inside: avoid`` * Bug fixes Only avoiding page breaks before/after an element is still missing. Version 0.6.1 ------------- Released on 2012-03-01. Fix a packaging bug. (Remove use_2to3 in setup.py. We use the same codebase for Python 2 and 3.) Version 0.6 ----------- Released on 2012-02-29. * *Backward incompatible*: completely change the Python API. See the documentation: https://weasyprint.readthedocs.io/en/latest/tutorial.html#as-a-python-library * *Backward incompatible*: Proper margin collapsing. This changes how blocks are rendered: adjoining margins "collapse" (their maximum is used) instead of accumulating. * Support images in ``embed`` or ``object`` elements. * Switch to pystacia instead of PIL for raster images * Add compatibility with CPython 2.6 and 3.2. (Previously only 2.7 was supported) * Many bug fixes Version 0.5 ----------- Released on 2012-02-08. * Support for the ``overflow`` and ``clip`` properties. * Support for the ``opacity`` property from CSS3 Colors. * Support for CSS 2D Transforms. These are prefixed, so you need to use ``-weasy-transform`` and ``-weasy-transform-origin``. Version 0.4 ----------- Released on 2012-02-07. * Support ``text-align: justify``, ``word-spacing`` and ``letter-spacing``. * Partial support for CSS3 Paged Media: page size and margin boxes with page-based counters. * All CSS 2.1 border styles * Fix SVG images with non-pixel units. Requires CairoSVG 0.3 * Support for ``page-break-before`` and ``page-break-after``, except for the value ``avoid``. * Support for the ``background-clip``, ``background-origin`` and ``background-size`` from CSS3 (but still with a single background per element) * Support for the ``image-rendering`` from SVG. This one is prefixed, use ``-weasy-image-rendering``. It only has an effect on PNG output. Version 0.3.1 ------------- Released on 2011-12-14. Compatibility with CairoSVG 0.1.2 Version 0.3 ----------- Released on 2011-12-13. * **Backward-incompatible change:** the 'size' property is now prefixed (since it is in an experimental specification). Use '-weasy-size' instead. * cssutils 0.9.8 or higher is now required. * Support SVG images with CairoSVG * Support generated content: the ``:before`` and ``:after`` pseudo-elements, the ``content``, ``quotes`` and ``counter-*`` properties. * Support ordered lists: all CSS 2.1 values of the ``list-style-type`` property. * New user-agent stylesheet with HTML 5 elements and automatic quotes for many languages. Thanks Peter Moulder! * Disable cssutils validation warnings, they are redundant with WeasyPrint’s. * Add ``--version`` to the command-line script. * Various bug fixes Version 0.2 ----------- Released on 2011-11-25. * Support for tables. * Support the `box-sizing` property from CSS 3 Basic User Interface * Support all values of vertical-align except top and bottom. They are interpreted as text-top and text-bottom. * Minor bug fixes Tables have some limitations: Only the fixed layout and separate border model are supported. There are also no page break inside tables so a table higher than a page will overflow. Version 0.1 ----------- Released on 2011-10-28. First packaged release. Supports "simple" CSS 2.1 pages: there is no support for floats, tables, or absolute positioning. Other than that most of CSS 2.1 is supported, as well as CSS 3 Colors and Selectors. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0223079 weasyprint-54.1/docs/common_use_cases.rst0000644000000000000000000001121100000000000016777 0ustar0000000000000000Common Use Cases ================ Include in Web Applications --------------------------- Using WeasyPrint in web applications sometimes requires attention on some details. Security Problems ................. First of all, rendering untrusted HTML and CSS files can lead to :ref:`security problems `. Please be sure to carefully follow the different proposed solutions if you allow your users to modify the source of the rendered documents in any way. Rights Management ................. Another problem is rights management: you often need to render templates that can only be accessed by authenticated users, and WeasyPrint installed on the server doesn’t send the same cookies as the ones sent by the users. Extensions such as Flask-WeasyPrint_ (for Flask_) or Django-WeasyPrint_ (for Django_) solve this issue with a small amount of code. If you use another framework, you can read these extensions and probably find an equivalent workaround. .. _Flask-Weasyprint: https://github.com/Kozea/Flask-WeasyPrint .. _Flask: http://flask.pocoo.org/ .. _Django-WeasyPrint: https://github.com/fdemmer/django-weasyprint .. _Django: https://www.djangoproject.com/ Server Side Requests & Self-Signed SSL Certificates ................................................... If your server is requesting data from itself, you may encounter a self-signed certificate error, even if you have a valid certificate. You need to add yourself as a Certificate Authority, so that your self-signed SSL certificates can be requested. .. code-block:: bash # If you have not yet created a certificate. sudo openssl req -x509 \ -sha256 \ -nodes \ -newkey rsa:4096 \ -days 365 \ -keyout localhost.key \ -out localhost.crt # Follow the prompts about your certificate and the domain name. openssl x509 -text -noout -in localhost.crt Add your new self-signed SSL certificate to your nginx.conf, below the line ``server_name 123.123.123.123;``: .. code-block:: bash ssl_certificate /etc/ssl/certs/localhost.crt; ssl_certificate_key /etc/ssl/private/localhost.key; The SSL certificate will be valid when accessing your website from the internet. However, images will not render when requesting files from the same server. You will need to add your new self-signed certificates as trusted: .. code-block:: bash sudo cp /etc/ssl/certs/localhost.crt /usr/local/share/ca-certificates/localhost.crt sudo cp /etc/ssl/private/localhost.key /usr/local/share/ca-certificates/localhost.key # Update the certificate authority trusted certificates. sudo update-ca-certificates # Export your newly updated Certificate Authority Bundle file. # If using Django, it will use the newly signed certificate authority as # valid and images will load properly. sudo tee -a /etc/environment <<< 'export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt' Adjust Document Dimensions -------------------------- WeasyPrint does not provide support for adjusting page size or document margins via command-line flags. This is best accomplished with the CSS ``@page`` at-rule. Consider the following example: .. code-block:: css @page { size: Letter; /* Change from the default size of A4 */ margin: 3cm; /* Set margin on each page */ } There is much more which can be achieved with the ``@page`` at-rule, such as page numbers, headers, etc. Read more about the page_ at-rule. .. _page: https://developer.mozilla.org/en-US/docs/Web/CSS/@page Improve Rendering Speed and Memory Use -------------------------------------- WeasyPrint is often slower than other web engines. Python is the usual suspect, but it’s not the main culprit here. :ref:`Optimization is not the main goal of WeasyPrint ` and it may lead to unbearable long rendering times. First of all: WeasyPrint’s performance gets generally better with time. You can check WeasyPerf_ to compare time and memory needed across versions. Some tips may help you to get better results. - A high number of CSS properties with a high number of HTML tags can lead to a huge amount of time spent for the cascade. Avoiding large CSS frameworks can drastically reduce the rendering time. - Tables are known to be slow, especially when they are rendered on multiple pages. When possible, using a common block layout instead gives much faster layouts. - Optimizing images and fonts can reduce the PDF size, but increase the rendering time. Moreover, caching images gives the possibility to read and optimize images only once, and thus to save time when the same image is used multiple times. See :ref:`Image Cache and Optimization`. .. _WeasyPerf: https://kozea.github.io/WeasyPerf/ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0223079 weasyprint-54.1/docs/conf.py0000644000000000000000000000517200000000000014233 0ustar0000000000000000# WeasyPrint documentation build configuration file. import weasyprint # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.autosectionlabel'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'WeasyPrint' copyright = 'Simon Sapin and contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. release = weasyprint.__version__ # The short X.Y version. version = release # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'monokai' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' html_theme_options = { 'collapse_navigation': False, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # These paths are either relative to html_static_path # or fully qualified paths (eg. https://...) html_css_files = [ 'https://www.courtbouillon.org/static/docs.css', ] # Output file base name for HTML help builder. htmlhelp_basename = 'weasyprintdoc' # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'weasyprint', 'WeasyPrint Documentation', ['Simon Sapin and contributors'], 1) ] # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [( 'index', 'WeasyPrint', 'WeasyPrint Documentation', 'Simon Sapin', 'WeasyPrint', 'The Awesome Document Factory', 'Miscellaneous'), ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'python': ('https://docs.python.org/3/', None), 'pydyf': ('https://doc.courtbouillon.org/pydyf/stable/', None), } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641212595.6386926 weasyprint-54.1/docs/contribute.rst0000644000000000000000000000332500000000000015642 0ustar0000000000000000Contribute ========== You want to add some code to WeasyPrint, launch its tests or improve its documentation? Thank you very much! Here are some tips to help you play with WeasyPrint in good conditions. The first step is to clone the repository, create a virtual environment and install WeasyPrint dependencies. .. code-block:: shell git clone https://github.com/Kozea/WeasyPrint.git cd WeasyPrint python -m venv venv venv/bin/pip install -e .[doc,test] You can then launch Python to test your changes. .. code-block:: shell venv/bin/python Code & Issues ------------- If you’ve found a bug in WeasyPrint, it’s time to report it, and to fix it if you can! You can report bugs and feature requests on `GitHub`_. If you want to add or fix some code, please fork the repository and create a pull request, we’ll be happy to review your work. You can find more information about the code architecture in the :ref:`Dive into the Source` section. .. _GitHub: https://github.com/Kozea/WeasyPrint Tests ----- Tests are stored in the ``tests`` folder at the top of the repository. They use the `pytest`_ library. You can launch tests (with code coverage and lint) using the following command:: venv/bin/python -m pytest .. _pytest: https://docs.pytest.org/ Documentation ------------- Documentation is stored in the ``docs`` folder at the top of the repository. It relies on the `Sphinx`_ library. You can build the documentation using the following command:: venv/bin/sphinx-build docs docs/_build The documentation home page can now be found in the ``/path/to/weasyprint/docs/_build/index.html`` file. You can open this file in a browser to see the final rendering. .. _Sphinx: https://www.sphinx-doc.org/ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639605413.6057737 weasyprint-54.1/docs/first_steps.rst0000644000000000000000000005623700000000000016043 0ustar0000000000000000First Steps =========== .. currentmodule:: weasyprint Installation ------------ WeasyPrint |version| depends on: * Python_ ≥ 3.6.0 * Pango_ ≥ 1.44.0 * pydyf_ ≥ 0.0.3 * CFFI_ ≥ 0.6 * html5lib_ ≥ 1.1 * tinycss2_ ≥ 1.0.0 * cssselect2_ ≥ 0.1 * Pyphen_ ≥ 0.9.1 * Pillow_ ≥ 4.0.0 * fontTools_ ≥ 4.0.0 .. _Python: http://www.python.org/ .. _Pango: http://www.pango.org/ .. _CFFI: https://cffi.readthedocs.io/ .. _html5lib: https://html5lib.readthedocs.io/ .. _pydyf: https://doc.courtbouillon.org/pydyf/ .. _tinycss2: https://doc.courtbouillon.org/tinycss2/ .. _cssselect2: https://doc.courtbouillon.org/cssselect2/ .. _Pyphen: http://pyphen.org/ .. _Pillow: https://python-pillow.org/ .. _fontTools: https://github.com/fonttools/fonttools There are many ways to install WeasyPrint, depending on the system you use. Linux ~~~~~ The easiest way to install WeasyPrint on Linux is to use the package manager of your distribution. WeasyPrint is packaged for recent versions of Debian_, Ubuntu_, Fedora_, Archlinux_, Gentoo_… .. _Debian: https://packages.debian.org/search?keywords=weasyprint&searchon=names&suite=all§ion=all .. _Ubuntu: https://packages.ubuntu.com/search?keywords=weasyprint&searchon=names&suite=all§ion=all .. _Fedora: https://src.fedoraproject.org/rpms/weasyprint .. _Archlinux: https://aur.archlinux.org/packages/python-weasyprint .. _Gentoo: https://packages.gentoo.org/packages/dev-python/weasyprint If WeasyPrint is not available on your distribution, or if you want to use a more recent version of WeasyPrint, you have to be sure that Python_ (at least version 3.6.0) and Pango_ (at least version 1.44.0) are installed on your system. You can verify this by launching:: python3 --version pango-view --version If the version of Pango provided by your distribution is too old, you can use version 52.5 of WeasyPrint which does not need recent Pango features. When everything is OK, you can install WeasyPrint directly on your system or in a `virtual environment`_ using `pip`_:: python3 -m venv venv source venv/bin/activate pip install weasyprint weasyprint --info .. _virtual environment: https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/ .. _pip: https://pip.pypa.io/ Alpine ≥ 3.12 +++++++++++++ To install WeasyPrint without a virtualenv, you need the following packages:: apk add py3-pip py3-pillow py3-cffi py3-brotli gcc musl-dev python3-dev pango To install WeasyPrint inside a virtualenv using wheels (if possible), you need the following packages:: apk add py3-pip gcc musl-dev python3-dev pango zlib-dev jpeg-dev openjpeg-dev g++ libffi-dev To install WeasyPrint inside a virtualenv without using wheels, you need the following packages:: apk add py3-pip gcc musl-dev python3-dev pango zlib-dev jpeg-dev openjpeg-dev g++ libffi-dev Archlinux +++++++++ To install WeasyPrint without a virtualenv, you need the following packages:: pacman -S python-pip pango python-cffi python-pillow python-brotli python-zopfli To install WeasyPrint inside a virtualenv using wheels (if possible), you need the following packages:: pacman -S python-pip pango To install WeasyPrint inside a virtualenv without using wheels, you need the following packages:: pacman -S python-pip pango gcc libjpeg-turbo openjpeg2 Debian ≥ 11 +++++++++++ To install WeasyPrint without a virtualenv, you need the following packages:: apt install python3-pip python3-cffi python3-brotli libpango-1.0-0 libpangoft2-1.0-0 To install WeasyPrint inside a virtualenv using wheels (if possible), you need the following packages:: apt install python3-pip libpango-1.0-0 libpangoft2-1.0-0 To install WeasyPrint inside a virtualenv without using wheels, you need the following packages:: apt install python3-pip libpango-1.0-0 libpangoft2-1.0-0 libjpeg-dev libopenjp2-7-dev libffi-dev Fedora ≥ 34 +++++++++++ To install WeasyPrint without a virtualenv, you need the following packages:: yum install python-pip python-pillow python-cffi python3-brotli pango To install WeasyPrint inside a virtualenv using wheels (if possible), you need the following packages:: yum install python-pip pango To install WeasyPrint inside a virtualenv without using wheels, you need the following packages:: yum install python-pip pango gcc python3-devel gcc-c++ zlib-devel libjpeg-devel openjpeg2-devel libffi-devel Ubuntu ≥ 20.04 ++++++++++++++ To install WeasyPrint without a virtualenv, you need the following packages:: apt install python3-pip python3-cffi python3-brotli libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 To install WeasyPrint inside a virtualenv using wheels (if possible), you need the following packages:: apt install python3-pip libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 To install WeasyPrint inside a virtualenv without using wheels, you need the following packages:: apt install python3-pip libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 libffi-dev libjpeg-dev libopenjp2-7-dev macOS ~~~~~ The easiest way to install WeasyPrint on macOS is to use Homebrew_. When Homebrew is installed, install Python, Pango and libffi:: brew install python pango libffi You can then install WeasyPrint in a `virtual environment`_ using `pip`_:: python3 -m venv venv source venv/bin/activate pip install weasyprint weasyprint --info .. _Homebrew: https://brew.sh/ Windows ~~~~~~~ Installing WeasyPrint on Windows requires to follow a few steps that may not be easy. Please read this chapter carefully. Only Windows 10 64-bit is supported. You can find this information in the Control Panel → System and Security → System. The first step is to install the latest version of Python from the `Microsoft Store`_. When Python is installed, you have to install GTK. Download the latest `GTK3 installer`_ and launch it. If you don’t know what some options mean, you can safely keep the default options selected. When everything is OK, you can launch a command prompt by clicking on the Start menu, typing "cmd" and clicking the "Command Prompt" icon. You can then install WeasyPrint in a `virtual environment`_ using `pip`_:: python3 -m venv venv venv\Scripts\activate.bat python3 -m pip install weasyprint python3 -m weasyprint --info .. _Microsoft Store: https://www.microsoft.com/en-us/search?q=python .. _GTK3 installer: https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases Other Solutions ~~~~~~~~~~~~~~~ Other solutions are available to install WeasyPrint. These solutions are not tested but they are known to work for some use cases on specific platforms. Macports ++++++++ On macOS, you can install WeasyPrint’s dependencies with Macports_:: sudo port install py-pip pango libffi You can then install WeasyPrint in a `virtual environment`_ using `pip`_:: python3 -m venv venv source venv/bin/activate pip install weasyprint weasyprint --info .. _Macports: https://www.macports.org/ Conda +++++ On Linux and macOS, WeasyPrint is available on Conda_, with `a WeasyPrint package on Conda Forge`_. .. _Conda: https://docs.conda.io/projects/conda/en/latest/ .. _a WeasyPrint package on Conda Forge: https://anaconda.org/conda-forge/weasyprint WSL +++ On Windows, you can also use WSL_ and install WeasyPrint the same way it has to be installed on Linux. .. _WSL: https://docs.microsoft.com/en-us/windows/wsl/ .NET Wrapper ++++++++++++ On Windows, Bader Albarrak maintains `a .NET wrapper`_. .. _a .NET wrapper: https://github.com/balbarak/WeasyPrint-netcore AWS +++ Kotify maintains `an AWS Lambda layer`_, see issue `#1003`_ for more information. .. _an AWS Lambda layer: https://github.com/kotify/cloud-print-utils .. _#1003: https://github.com/Kozea/WeasyPrint/issues/1003 Troubleshooting ~~~~~~~~~~~~~~~ Most of the installation problems have already been met, and some `issues on GitHub`_ could help you to solve them. Missing Library +++++++++++++++ On Windows, most of the problems come from unreachable libraries. If you get an error like ``cannot load library 'xxx': error xxx``, it means that WeasyPrint can’t find this library. You can set the ``WEASYPRINT_DLL_DIRECTORIES`` environment variable to list the folders where the libraries can be found. For example, in ``cmd.exe``: .. code-block:: doscon > set WEASYPRINT_DLL_DIRECTORIES=C:\GTK3\bin;D:\GTK3\bin You can find more about this issue in `#589`_, `#721`_ or `#1240`_. .. _issues on GitHub: https://github.com/Kozea/WeasyPrint/issues .. _#589: https://github.com/Kozea/WeasyPrint/issues/589 .. _#721: https://github.com/Kozea/WeasyPrint/issues/721 .. _#1240: https://github.com/Kozea/WeasyPrint/issues/1240 Missing Fonts +++++++++++++ If no character is drawn in the generated PDF, or if you get squares instead of letters, you have to install fonts and make them available to WeasyPrint. Following the standard way to install fonts on your system should be enough. You can also use ``@font-face`` rules to explicitly reference fonts using URLs. Command-Line ------------ Using the WeasyPrint command line interface can be as simple as this: .. code-block:: sh weasyprint http://weasyprint.org /tmp/weasyprint-website.pdf You may see warnings on the standard error output about unsupported CSS properties. See :ref:`Command-Line API` for the details of all available options. In particular, the ``-s`` option can add a filename for a :ref:`user stylesheet `. For quick experimentation however, you may not want to create a file. In bash or zsh, you can use the shell’s redirection instead: .. code-block:: sh weasyprint http://weasyprint.org /tmp/weasyprint-website.pdf \ -s <(echo 'body { font-family: serif !important }') If you have many documents to convert you may prefer using the Python API in long-lived processes to avoid paying the start-up costs every time. Python Library -------------- .. attention:: Using WeasyPrint with untrusted HTML or untrusted CSS may lead to various :ref:`security problems `. Quickstart ~~~~~~~~~~ The Python version of the above example goes like this: .. code-block:: python from weasyprint import HTML HTML('http://weasyprint.org/').write_pdf('/tmp/weasyprint-website.pdf') … or with the inline stylesheet: .. code-block:: python from weasyprint import HTML, CSS HTML('http://weasyprint.org/').write_pdf('/tmp/weasyprint-website.pdf', stylesheets=[CSS(string='body { font-family: serif !important }')]) Instantiating HTML and CSS Objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you have a file name, an absolute URL or a readable :term:`file object`, you can just pass it to :class:`HTML` or :class:`CSS` to create an instance. Alternatively, use a named argument so that no guessing is involved: .. code-block:: python from weasyprint import HTML HTML('../foo.html') # Same as … HTML(filename='../foo.html') HTML('http://weasyprint.org') # Same as … HTML(url='http://weasyprint.org') HTML(sys.stdin) # Same as … HTML(file_obj=sys.stdin) If you have a byte string or Unicode string already in memory you can also pass that, although the argument must be named: .. code-block:: python from weasyprint import HTML, CSS # HTML('

foo') would be filename HTML(string='''

The title

Content goes here ''') CSS(string='@page { size: A3; margin: 1cm }') If you have ``@font-face`` rules in your CSS, you have to create a ``FontConfiguration`` object: .. code-block:: python from weasyprint import HTML, CSS from weasyprint.text.fonts import FontConfiguration font_config = FontConfiguration() html = HTML(string='

The title

') css = CSS(string=''' @font-face { font-family: Gentium; src: url(http://example.com/fonts/Gentium.otf); } h1 { font-family: Gentium }''', font_config=font_config) html.write_pdf( '/tmp/example.pdf', stylesheets=[css], font_config=font_config) Rendering to a Single File ~~~~~~~~~~~~~~~~~~~~~~~~~~ Once you have a :class:`HTML` object, call its :meth:`HTML.write_pdf` method to get the rendered document in a single PDF file. Without arguments, this method returns a byte string in memory. If you pass a file name or a writable :term:`file object`, they will write there directly instead. (**Warning**: with a filename, these methods will overwrite existing files silently.) Individual Pages & Meta-Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you want more than a single PDF, the :meth:`HTML.render` method gives you a :class:`document.Document` object with access to individual :class:`document.Page` objects. Thus you can get the number of pages, their size\ [#]_, the details of hyperlinks and bookmarks, etc. Documents also have a :meth:`document.Document.write_pdf` method, and you can get a subset of the pages with :meth:`document.Document.copy()`. Finally, for ultimate control, :meth:`document.Page.paint` individual pages anywhere on any :class:`pydyf.Stream`. .. [#] Pages in the same document do not always have the same size. See the :ref:`Python API` for details. A few random examples: .. code-block:: python # Write odd and even pages separately: # Lists count from 0 but page numbers usually from 1 # [::2] is a slice of even list indexes but odd-numbered pages. document.copy(document.pages[::2]).write_pdf('odd_pages.pdf') document.copy(document.pages[1::2]).write_pdf('even_pages.pdf') .. code-block:: python # Print the outline of the document. # Output on http://www.w3.org/TR/CSS21/intro.html # 1. Introduction to CSS 2.1 (page 2) # 1. A brief CSS 2.1 tutorial for HTML (page 2) # 2. A brief CSS 2.1 tutorial for XML (page 5) # 3. The CSS 2.1 processing model (page 6) # 1. The canvas (page 7) # 2. CSS 2.1 addressing model (page 7) # 4. CSS design principles (page 8) def print_outline(bookmarks, indent=0): for i, bookmark in enumerate(bookmarks, 1): page = bookmark.destination[0] print('%s%d. %s (page %d)' % ( ' ' * indent, i, bookmark.label.lstrip('0123456789. '), page)) print_outline(bookmark.children, indent + 2) print_outline(document.make_bookmark_tree()) URL Fetchers ~~~~~~~~~~~~ WeasyPrint goes through a *URL fetcher* to fetch external resources such as images or CSS stylesheets. The default fetcher can natively open file and HTTP URLs, but the HTTP client does not support advanced features like cookies or authentication. This can be worked-around by passing a custom ``url_fetcher`` callable to the :class:`HTML` or :class:`CSS` classes. It must have the same signature as :func:`default_url_fetcher`. Custom fetchers can choose to handle some URLs and defer others to the default fetcher: .. code-block:: python from weasyprint import default_url_fetcher, HTML def my_fetcher(url): if url.startswith('graph:'): graph_data = map(float, url[6:].split(',')) return dict(string=generate_graph(graph_data), mime_type='image/png') return default_url_fetcher(url) source = '' HTML(string=source, url_fetcher=my_fetcher).write_pdf('out.pdf') Flask-WeasyPrint_ for Flask_ and Django-Weasyprint_ for Django_ both make use of a custom URL fetcher to integrate WeasyPrint and use the filesystem instead of a network call for static and media files. A custom fetcher should be returning a :obj:`dict` with * One of ``string`` (a :obj:`bytestring `) or ``file_obj`` (a :term:`file object`). * Optionally: ``mime_type``, a MIME type extracted e.g. from a *Content-Type* header. If not provided, the type is guessed from the file extension in the URL. * Optionally: ``encoding``, a character encoding extracted e.g. from a *charset* parameter in a *Content-Type* header * Optionally: ``redirected_url``, the actual URL of the resource if there were e.g. HTTP redirects. * Optionally: ``filename``, the filename of the resource. Usually derived from the *filename* parameter in a *Content-Disposition* header If a ``file_obj`` is given, the resource will be closed automatically by the function internally used by WeasyPrint to retreive data. .. _Flask-Weasyprint: https://github.com/Kozea/Flask-WeasyPrint .. _Flask: http://flask.pocoo.org/ .. _Django-WeasyPrint: https://github.com/fdemmer/django-weasyprint .. _Django: https://www.djangoproject.com/ Image Cache and Optimization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ WeasyPrint provides two options to deal with images: ``optimize_size`` and ``image_cache``. ``optimize_size`` can enable size optimization for images, but also for fonts. When enabled, the generated PDF will include smaller images and fonts, but the rendering time may be slightly increased. .. code-block:: python # No size optimization, faster, but generated PDF is larger HTML('http://example.org/').write_pdf( 'example.pdf', optimize_size=()) # Full size optimization, slower, but generated PDF is smaller HTML('http://example.org/').write_pdf( 'example.pdf', optimize_size=('fonts', 'images')) ``image_cache`` gives the possibility to use a cache for images, avoiding to download, parse and optimize them each time they are used. By default, the cache is used document by document, but you can share it between documents if needed. This feature can save a lot of network and CPU time when you render a lot of documents that use the same images. .. code-block:: python cache = {} for i in range(10): HTML(f'http://example.org/?id={i}').write_pdf( f'example-{i}.pdf', image_cache=cache) Logging ~~~~~~~ Most errors (unsupported CSS property, missing image, ...) are not fatal and will not prevent a document from being rendered. WeasyPrint uses the :mod:`logging` module from the Python standard library to log these errors and let you know about them. When WeasyPrint is launched in a terminal, logged messaged will go to *stderr* by default. You can change that by configuring the ``weasyprint`` logger object: .. code-block:: python import logging logger = logging.getLogger('weasyprint') logger.addHandler(logging.FileHandler('/path/to/weasyprint.log')) The ``weasyprint.progress`` logger is used to report the rendering progress. It is useful to get feedback when WeasyPrint is launched in a terminal (using the ``--verbose`` or ``--debug`` option), or to give this feedback to end users when used as a library. See the documentation of the :mod:`logging` module for details. Security -------- When used with untrusted HTML or untrusted CSS, WeasyPrint can meet security problems. You will need extra configuration in your Python application to avoid high memory use, endless renderings or local files leaks. *This section has been added thanks to the very useful reports and advice from Raz Becker.* Long Renderings ~~~~~~~~~~~~~~~ WeasyPrint is pretty slow and can take a long time to render long documents or specially crafted HTML pages. When WeasyPrint used on a server with HTML or CSS files from untrusted sources, this problem can lead to very long time renderings, with processes with high CPU and memory use. Even small documents may lead to really long rendering times, restricting HTML document size is not enough. If you use WeasyPrint on a server with HTML or CSS samples coming from untrusted users, you should: - limit rendering time and memory use of your process, for example using ``evil-reload-on-as`` and ``harakiri`` options if you use uWSGI, - limit memory use at the OS level, for example with ``ulimit`` on Linux, - automatically kill the process when it uses too much memory or when the rendering time is too high, by regularly launching a script to do so if no better option is available, - truncate and sanitize HTML and CSS input to avoid very long documents and access to external URLs. Infinite Requests ~~~~~~~~~~~~~~~~~ WeasyPrint can reach files on the network, for example using ``http://`` URIs. For various reasons, HTTP requests may take a long time and lead to problems similar to :ref:`Long Renderings`. WeasyPrint has a default timeout of 10 seconds for HTTP, HTTPS and FTP resources. This timeout has no effect with other protocols, including access to ``file://`` URIs. If you use WeasyPrint on a server with HTML or CSS samples coming from untrusted users, or need to reach network resources, you should: - use a custom :ref:`URL fetcher `, - follow solutions listed in :ref:`Long Renderings`. Infinite Loops ~~~~~~~~~~~~~~ WeasyPrint has been hit by a large number of bugs, including infinite loops. Specially crafted HTML and CSS files can quite easily lead to infinite loops and infinite rendering times. If you use WeasyPrint on a server with HTML or CSS samples coming from untrusted users, you should: - follow solutions listed in :ref:`Long Renderings`. Huge Values ~~~~~~~~~~~ WeasyPrint doesn't restrict integer and float values used in CSS. Using huge values for some properties (page sizes, font sizes, block sizes) can lead to various problems, including infinite rendering times, huge PDF files, high memory use and crashes. This problem is really hard to avoid. Even parsing CSS stylesheets and searching for huge values is not enough, as it is quite easy to trick CSS pre-processors using relative units (``em`` and ``%`` for example). If you use WeasyPrint on a server with HTML or CSS samples coming from untrusted users, you should: - follow solutions listed in :ref:`Long Renderings`. Access to Local Files ~~~~~~~~~~~~~~~~~~~~~ As any web renderer, WeasyPrint can reach files on the local filesystem using ``file://`` URIs. These files can be shown in ``img`` or ``embed`` tags for example. When WeasyPrint used on a server with HTML or CSS files from untrusted sources, this feature may be used to know if files are present on the server filesystem, and to embed them in generated documents. Unix-like systems also have special local files with infinite size, like ``/dev/urandom``. Referencing these files in HTML or CSS files obviously lead to infinite time renderings. If you use WeasyPrint on a server with HTML or CSS samples coming from untrusted users, you should: - restrict your process access to trusted files using sandboxing solutions, - use a custom :ref:`URL fetcher ` that doesn't allow ``file://`` URLs or filters access depending on given paths. - follow solutions listed in :ref:`Long Renderings`. System Information Leaks ~~~~~~~~~~~~~~~~~~~~~~~~ WeasyPrint relies on many libraries that can leak hardware and software information. Even when this information looks useless, it can be used by attackers to exploit other security breaches. Leaks can include (but are not restricted to): - locally installed fonts (using ``font-family`` and ``@font-face``), - network configuration (IPv4 and IPv6 support, IP addressing, firewall configuration, using ``http://`` URIs and tracking time used to render documents), - Python, Pango and other libraries versions (implementation details lead to different renderings). SVG Images ~~~~~~~~~~ Rendering SVG images more or less suffers from the same problems as the ones listed here for WeasyPrint. Security advices apply for untrusted SVG files as they apply for untrusted HTML and CSS documents. Note that WeasyPrint’s URL fetcher is used to render SVG files. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/docs/going_further.rst0000644000000000000000000002674100000000000016335 0ustar0000000000000000Going Further ============= .. currentmodule:: weasyprint Why WeasyPrint? --------------- Automatic document generation is a common need of many applications, even if a lot of operations do not require printed paper anymore. Invoices, tickets, leaflets, diplomas, documentation, books… All these documents are read and used on paper, but also on electronical readers, on smartphones, on computers. PDF is a great format to store and display them in a reliable way, with pagination. Using HTML and CSS to generate static and paged content can be strange at first glance: browsers display only one page, with variable dimensions, often in a very dynamic way. But paged media layout is actually included in CSS2_, which was already a W3C recommendation in 1998. Other well-known tools can be used to automatically generate PDF documents, like LaTeX and LibreOffice, but they miss many advantages that HTML and CSS offer. HTML and CSS are very widely known, by developers but also by webdesigners. They are specified in a backwards-compatible way, and regularly adapted to please the use of billions of people. They are really easy to write and generate, with a ridiculous amount of tools that are finely adapted to the needs and taste of their users. However, the web engines that are used for browsers were very limited for pagination when WeasyPrint was created in 2011. Even now, they lack a lot of basic features. That’s why projects such as wkhtmltopdf_ and PagedJS_ have been created: they add some of these features to existing browsers. Other solutions have beed developed, including web engine dedicated to paged media. Prince_, Antennahouse_ or `Typeset.sh`_ created original renderers supporting many features related to pagination. These tools are very powerful, but they are not open source. Building a free and open source web renderer generating high-quality documents is the main goal of WeasyPrint. Do you think that it was a little bit crazy to create such a big project from scratch? Here is what `Simon Sapin`_ wrote in WeasyPrint’s documentation one month after the beginning: Are we crazy? Yes. But not that much. Each modern web browser did take many developers’ many years of work to get where they are now, but WeasyPrint’s scope is much smaller: there is no user-interaction, no JavaScript, no live rendering (the document doesn’t changed after it was first parsed) and no quirks mode (we don’t need to support every broken page of the web.) We still need however to implement the whole CSS box model and visual rendering. This is a lot of work, but we feel we can get something useful much quicker than “Let’s build a rendering engine!” may seem. Simon is often right. .. _CSS2: https://www.w3.org/TR/1998/REC-CSS2-19980512/ .. _wkhtmltopdf: https://wkhtmltopdf.org/ .. _PagedJS: https://www.pagedjs.org/ .. _Prince: https://www.princexml.com/ .. _Antennahouse: https://www.antennahouse.com/ .. _Typeset.sh: https://typeset.sh/ .. _Simon Sapin: https://exyr.org/ Why Python? ----------- Python is a really good language to design a small, OS-agnostic parser. As it is object-oriented, it gives the possibility to follow the specification with high-level classes and a small amount of very simple code. Speed is not WeasyPrint’s main goal. Web rendering is a very complex task, and following :pep:`the Zen of Python <20>` helped a lot to keep our sanity (both in our code and in our heads): code simplicity, maintainability and flexibility are the most important goals for this library, as they give the ability to stay really close to the specification and to fix bugs easily. Dive into the Source -------------------- This chapter is a high-level overview of WeasyPrint’s source code. For more details, see the various docstrings or even the code itself. When in doubt, feel free to :ref:`ask `! Much `like in web browsers`_, the rendering of a document in WeasyPrint goes like this: 1. The HTML document is fetched and parsed into a tree of elements (like DOM). 2. CSS stylesheets (either found in the HTML or supplied by the user) are fetched and parsed. 3. The stylesheets are applied to the DOM-like tree. 4. The DOM-like tree with styles is transformed into a *formatting structure* made of rectangular boxes. 5. These boxes are *laid-out* with fixed dimensions and position onto pages. 6. For each page, the boxes are re-ordered to observe stacking rules, and are drawn on a PDF page. 7. Metadata −such as document information, attachments, embedded files, hyperlinks, and PDF trim and bleed boxes− are added to the PDF. .. _like in web browsers: http://www.html5rocks.com/en/tutorials/internals/howbrowserswork/#The_main_flow Parsing HTML ............ Not much to see here. The :class:`HTML` class handles step 1 and gives a tree of HTML *elements*. Although the actual API is different, this tree is conceptually the same as what web browsers call *the DOM*. Parsing CSS ........... As with HTML, CSS stylesheets are parsed in the :class:`CSS` class with an external library, tinycss2_. In addition to the actual parsing, the ``css`` and ``css.validation`` modules do some pre-processing: * Unknown and unsupported declarations are ignored with warnings. Remaining property values are parsed in a property-specific way from raw tinycss2 tokens into a higher-level form. * Shorthand properties are expanded. For example, ``margin`` becomes ``margin-top``, ``margin-right``, ``margin-bottom`` and ``margin-left``. * Hyphens in property names are replaced by underscores (``margin-top`` becomes ``margin_top``). This transformation is safe since none of the known (not ignored) properties have an underscore character. * Selectors are pre-compiled with cssselect2_. .. _tinycss2: https://pypi.python.org/pypi/tinycss2 .. _cssselect2: https://pypi.python.org/pypi/cssselect2 The Cascade ........... After that and still in the ``css`` package, the cascade_ (that’s the C in CSS!) applies the stylesheets to the element tree. Selectors associate property declarations to elements. In case of conflicting declarations (different values for the same property on the same element), the one with the highest *weight* wins. Weights are based on the stylesheet’s :ref:`origin `, ``!important`` markers, selector specificity and source order. Missing values are filled in through *inheritance* (from the parent element) or the property’s *initial value*, so that every element has a *specified value* for every property. .. _cascade: http://www.w3.org/TR/CSS21/cascade.html These *specified values* are turned into *computed values* in the ``css.computed_values`` module. Keywords and lengths in various units are converted to pixels, etc. At this point the value for some properties can be represented by a single number or string, but some require more complex objects. For example, a ``Dimension`` object can be either an absolute length or a percentage. The final result of the ``css.get_all_computed_styles`` function is a big dict where keys are ``(element, pseudo_element_type)`` tuples, and keys are style dict objects. Elements are ElementTree elements, while the type of pseudo-element is a string for eg. ``::first-line`` selectors, or :obj:`None` for “normal” elements. Style dict objects are dicts mapping property names to the computed values. (The return value is not the dict itself, but a convenience ``style_for`` function for accessing it.) Formatting Structure .................... The `visual formatting model`_ explains how *elements* (from the ElementTree tree) generate *boxes* (in the formatting structure). This is step 4 above. Boxes may have children and thus form a tree, much like elements. This tree is generally close but not identical to the ElementTree tree: some elements generate more than one box or none. .. _visual formatting model: http://www.w3.org/TR/CSS21/visuren.html Boxes are of a lot of different kinds. For example you should not confuse *block-level boxes* and *block containers*, though *block boxes* are both. The ``formatting_structure.boxes`` module has a whole hierarchy of classes to represent all these boxes. We won’t go into the details here, see the module and class docstrings. The ``formatting_structure.build`` module takes an ElementTree tree with associated computed styles, and builds a formatting structure. It generates the right boxes for each element and ensures they conform to the models rules (eg. an inline box can not contain a block). Each box has a ``style`` attribute containing the style dict of computed values. The main logic is based on the ``display`` property, but it can be overridden for some elements by adding a handler in the ``html`` module. This is how ```` and ```` are currently implemented, for example. This module is rather short as most of HTML is defined in CSS rather than in Python, in the `user agent stylesheet`_. The ``formatting_structure.build.build_formatting_structure`` function returns the box for the root element (and, through its ``children`` attribute, the whole tree). .. _user agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/master/weasyprint/css/html5_ua.css Layout ...... Step 5 is the layout. You could say the everything else is glue code and this is where the magic happens. During the layout the document’s content is, well, laid out on pages. This is when we decide where to do line breaks and page breaks. If a break happens inside of a box, that box is split into two (or more) boxes in the layout result. According to the `box model`_, each box has rectangular margin, border, padding and content areas: .. _box model: http://www.w3.org/TR/CSS21/box.html .. image:: https://www.w3.org/TR/CSS21/images/boxdim.png :alt: CSS Box Model While ``box.style`` contains computed values, the `used values`_ are set as attributes of the ``Box`` object itself during the layout. This include resolving percentages and especially ``auto`` values into absolute, pixel lengths. Once the layout done, each box has used values for margins, border width, padding of each four sides, as well as the ``width`` and ``height`` of the content area. They also have ``position_x`` and ``position_y``, the absolute coordinates of the top-left corner of the margin box (**not** the content box) from the top-left corner of the page.\ [#]_ Boxes also have helpers methods such as ``content_box_y`` and ``margin_width`` that give other metrics that can be useful in various parts of the code. The final result of the layout is a list of ``PageBox`` objects. .. [#] These are the coordinates *if* no `CSS transform`_ applies. Transforms change the actual location of boxes, but they are applied later during drawing and do not affect layout. .. _used values: http://www.w3.org/TR/CSS21/cascade.html#used-value .. _CSS transform: http://www.w3.org/TR/css3-transforms/ Stacking & Drawing .................. In step 6, the boxes are reordered by the ``stacking`` module to observe `stacking rules`_ such as the ``z-index`` property. The result is a tree of *stacking contexts*. Next, each laid-out page is *drawn* onto a PDF page. Since each box has absolute coordinates on the page from the layout step, the logic here should be minimal. If you find yourself adding a lot of logic here, maybe it should go in the layout or stacking instead. The code lives in the ``draw`` module. .. _stacking rules: http://www.w3.org/TR/CSS21/zindex.html Metadata ........ Finally (step 7), the ``pdf`` adds metadata to the PDF file: document information, attachments, hyperlinks, embedded files, trim box and bleed box. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/docs/index.rst0000644000000000000000000000046400000000000014574 0ustar0000000000000000WeasyPrint ========== .. currentmodule:: weasyprint .. include:: ../README.rst .. toctree:: :caption: Documentation :maxdepth: 3 first_steps common_use_cases api_reference going_further .. toctree:: :caption: Extra Information :maxdepth: 3 changelog contribute support ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/docs/support.rst0000644000000000000000000000156200000000000015201 0ustar0000000000000000Support ======= Sponsorship ----------- With `donations and sponsorship`, you help make the projects better. Donations allow the CourtBouillon team to have more time dedicated to add new features, fix bugs, and improve documentation. .. _donations and sponsorship: https://opencollective.com/courtbouillon Professional Support -------------------- You can improve your experience with CourtBouillon’s tools thanks to our professional support. You want bugs fixed as soon as possible? Your projects would highly benefit from some new features? You or your team would like to get new skills with one of the technologies we master? Please contact us by mail, by chat, or by tweet to get in touch and find the best way we can help you. .. _mail: mailto:contact@courtbouillon.org .. _chat: https://gitter.im/CourtBouillon/tinycss2 .. _tweet: https://twitter.com/BouillonCourt ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/pyproject.toml0000644000000000000000000000461700000000000014723 0ustar0000000000000000[build-system] requires = ['flit_core >=3.2,<4'] build-backend = 'flit_core.buildapi' [project] name = 'weasyprint' description = 'The Awesome Document Factory' keywords = ['html', 'css', 'pdf', 'converter'] authors = [{name = 'Simon Sapin', email = 'simon.sapin@exyr.org'}] maintainers = [{name = 'CourtBouillon', email = 'contact@courtbouillon.org'}] requires-python = '>=3.6' readme = {file = 'README.rst', content-type = 'text/x-rst'} license = {file = 'LICENSE'} dependencies = [ 'pydyf >=0.0.3', 'cffi >=0.6', 'html5lib >=1.1', 'tinycss2 >=1.0.0', 'cssselect2 >=0.1', 'Pyphen >=0.9.1', 'Pillow >=4.0.0', 'fonttools[woff] >=4.0.0', ] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Text Processing :: Markup :: HTML', 'Topic :: Multimedia :: Graphics :: Graphics Conversion', 'Topic :: Printing', ] dynamic = ['version'] [project.urls] Homepage = 'https://www.courtbouillon.org/weasyprint' Documentation = 'https://doc.courtbouillon.org/weasyprint/' Code = 'https://github.com/Kozea/WeasyPrint' Issues = 'https://github.com/Kozea/WeasyPrint/issues' Changelog = 'https://github.com/Kozea/WeasyPrint/releases' Donation = 'https://opencollective.com/courtbouillon' [project.optional-dependencies] doc = ['sphinx', 'sphinx_rtd_theme'] test = ['pytest', 'pytest-cov', 'pytest-flake8', 'pytest-isort', 'coverage[toml]'] [project.scripts] weasyprint = 'weasyprint.__main__:main' [tool.flit.sdist] exclude = ['.*', 'tests/results'] [tool.pytest.ini_options] addopts = '--isort --flake8 --cov --no-cov-on-fail' [tool.coverage.run] branch = true include = ['tests/*', 'weasyprint/*'] [tool.coverage.report] exclude_lines = ['pragma: no cover', 'def __repr__', 'raise NotImplementedError'] omit = ['.*'] [tool.isort] default_section = 'FIRSTPARTY' multi_line_output = 4 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/__init__.py0000644000000000000000000000012300000000000015246 0ustar0000000000000000""" weasyprint.tests ---------------- The Weasyprint test suite. """ ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1643022019.494474 weasyprint-54.1/tests/conftest.py0000644000000000000000000000563300000000000015347 0ustar0000000000000000""" weasyprint.tests.conftest ------------------------- Configuration for WeasyPrint tests. This module adds a PNG export based on Ghostscript. Note that Ghostscript is released under AGPL. """ import io import os import shutil from subprocess import PIPE, run from tempfile import NamedTemporaryFile import pytest from PIL import Image from weasyprint import HTML from weasyprint.document import Document MAGIC_NUMBER = b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a' def document_write_png(self, target=None, resolution=96, antialiasing=1, zoom=4/30, split_images=False): # Use temporary files because gs on Windows doesn’t accept binary on stdin with NamedTemporaryFile(delete=False) as pdf: pdf.write(self.write_pdf(zoom=zoom)) command = [ 'gs', '-q', '-dNOPAUSE', '-dBATCH', f'-dTextAlphaBits={antialiasing}', f'-dGraphicsAlphaBits={antialiasing}', '-sDEVICE=png16m', f'-r{resolution / zoom}', '-sOutputFile=-', pdf.name] pngs = run(command, stdout=PIPE).stdout os.remove(pdf.name) assert pngs.startswith(MAGIC_NUMBER), ( 'Ghostscript error: ' f'{pngs.split(MAGIC_NUMBER)[0].decode("ascii").strip()}') if split_images: assert target is None # TODO: use a different way to find PNG files in stream magic_numbers = pngs.count(MAGIC_NUMBER) if magic_numbers == 1: if target is None: return [pngs] if split_images else pngs png = io.BytesIO(pngs) else: images = [MAGIC_NUMBER + png for png in pngs[8:].split(MAGIC_NUMBER)] if split_images: return images images = [Image.open(io.BytesIO(image)) for image in images] width = max(image.width for image in images) height = sum(image.height for image in images) output_image = Image.new('RGBA', (width, height)) top = 0 for image in images: output_image.paste(image, (int((width - image.width) / 2), top)) top += image.height png = io.BytesIO() output_image.save(png, format='png') png.seek(0) if target is None: return png.read() if hasattr(target, 'write'): shutil.copyfileobj(png, target) else: with open(target, 'wb') as fd: shutil.copyfileobj(png, fd) def html_write_png(self, target=None, stylesheets=None, resolution=96, presentational_hints=False, optimize_size=('fonts',), font_config=None, counter_style=None, image_cache=None): return self.render( stylesheets, presentational_hints=presentational_hints, optimize_size=optimize_size, font_config=font_config, counter_style=counter_style, image_cache=image_cache).write_png( target, resolution) @pytest.fixture(autouse=True) def monkey_write_png(monkeypatch): Document.write_png = document_write_png HTML.write_png = html_write_png ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/__init__.py0000644000000000000000000001242600000000000016214 0ustar0000000000000000""" weasyprint.tests.test_draw -------------------------- Test the final, drawn results and compare PNG images pixel per pixel. """ import io import os from itertools import zip_longest from PIL import Image from ..testing_utils import FakeHTML, resource_filename PIXELS_BY_CHAR = dict( _=(255, 255, 255), # white R=(255, 0, 0), # red B=(0, 0, 255), # blue G=(0, 255, 0), # lime green V=(191, 0, 64), # average of 1*B and 3*R. S=(255, 63, 63), # R above R above #fff r=(255, 0, 0), # red g=(0, 128, 0), # half green b=(0, 0, 128), # half blue v=(128, 0, 128), # average of B and R. h=(64, 0, 64), # half average of B and R. a=(0, 0, 254), # JPG is lossy... p=(192, 0, 63), # R above R above B above #fff. z=None, ) # NOTE: "r" is not half red on purpose. In the pixel strings it has # better contrast with "B" than does "R". eg. "rBBBrrBrB" vs "RBBBRRBRB". def parse_pixels(pixels, pixels_overrides=None): chars = dict(PIXELS_BY_CHAR, **(pixels_overrides or {})) lines = (line.split('#')[0].strip() for line in pixels.splitlines()) return tuple(chars[char] for line in lines if line for char in line) def assert_pixels(name, expected_width, expected_height, expected_pixels, html): """Helper testing the size of the image and the pixels values.""" if isinstance(expected_pixels, str): expected_pixels = parse_pixels(expected_pixels) assert len(expected_pixels) == expected_height * expected_width, ( f'Expected {len(expected_pixels)} pixels, ' f'got {expected_height * expected_width}') pixels = html_to_pixels(name, expected_width, expected_height, html) assert_pixels_equal( name, expected_width, expected_height, pixels, expected_pixels) def assert_same_rendering(expected_width, expected_height, documents, tolerance=0): """Render HTML documents to PNG and check that they render the same. Each document is passed as a (name, html_source) tuple. """ pixels_list = [] for name, html in documents: pixels = html_to_pixels( name, expected_width, expected_height, html) pixels_list.append((name, pixels)) _name, reference = pixels_list[0] for name, pixels in pixels_list[1:]: assert_pixels_equal( name, expected_width, expected_height, pixels, reference, tolerance) def assert_different_renderings(expected_width, expected_height, documents): """Render HTML documents to PNG and check that they don't render the same. Each document is passed as a (name, html_source) tuple. """ pixels_list = [] for name, html in documents: pixels = html_to_pixels(name, expected_width, expected_height, html) pixels_list.append((name, pixels)) for i, (name_1, pixels_1) in enumerate(pixels_list): for name_2, pixels_2 in pixels_list[i + 1:]: if pixels_1 == pixels_2: # pragma: no cover write_png(name_1, pixels_1, expected_width, expected_height) # Same as "assert pixels_1 != pixels_2" but the output of # the assert hook would be gigantic and useless. assert False, f'{name_1} and {name_2} are the same' def write_png(basename, pixels, width, height): # pragma: no cover """Take a pixel matrix and write a PNG file.""" directory = os.path.join(os.path.dirname(__file__), 'results') if not os.path.isdir(directory): os.mkdir(directory) filename = os.path.join(directory, basename + '.png') image = Image.new('RGB', (width, height)) image.putdata(pixels) image.save(filename) def html_to_pixels(name, expected_width, expected_height, html): """Render an HTML document to PNG, checks its size and return pixel data. Also return the document to aid debugging. """ document = FakeHTML( string=html, # Dummy filename, but in the right directory. base_url=resource_filename('')) pixels = document_to_pixels( document, name, expected_width, expected_height) return pixels def document_to_pixels(document, name, expected_width, expected_height): """Render an HTML document to PNG, check its size and return pixel data.""" return Image.open(io.BytesIO(document.write_png())).getdata() def assert_pixels_equal(name, width, height, raw, expected_raw, tolerance=0): """Take 2 matrices of pixels and assert that they are the same.""" if raw != expected_raw: # pragma: no cover pixels = zip_longest(raw, expected_raw, fillvalue=(-1, -1, -1)) for i, (value, expected) in enumerate(pixels): if expected is None: continue if any(abs(value - expected) > tolerance for value, expected in zip(value, expected)): actual_height = len(raw) // width write_png(name, raw, width, actual_height) expected_raw = [ pixel or (255, 255, 255) for pixel in expected_raw] write_png(name + '.expected', expected_raw, width, height) x = i % width y = i // width assert 0, ( f'Pixel ({x}, {y}) in {name}: ' f'expected rgba{expected}, got rgba{value}') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/svg/__init__.py0000644000000000000000000000000000000000000016774 0ustar0000000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/svg/test_bounding_box.py0000644000000000000000000002100600000000000020762 0ustar0000000000000000""" weasyprint.tests.test_draw.svg.test_bounding_box ------------------------------------------------ Test how bounding boxes are defined for SVG tags. """ import pytest from ...testing_utils import assert_no_logs from .. import assert_pixels @assert_no_logs def test_bounding_box_rect(): assert_pixels('bounding_box_rect', 5, 5, ''' BBBBB BBBBR BBBRR BBRRR BRRRR ''', ''' ''') @assert_no_logs def test_bounding_box_circle(): assert_pixels('bounding_box_circle', 10, 10, ''' __________ __BBBBBB__ _BBBBBBBR_ _BBBBBBRR_ _BBBBBRRR_ _BBBBRRRR_ _BBBRRRRR_ _BBRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_bounding_box_ellipse(): assert_pixels('bounding_box_ellipse', 10, 10, ''' __________ __BBBBBB__ _BBBBBBBR_ _BBBBBBRR_ _BBBBBRRR_ _BBBBRRRR_ _BBBRRRRR_ _BBRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_bounding_box_line(): assert_pixels('bounding_box_line', 5, 5, ''' BB___ BBB__ _BRR_ __RRR ___RR ''', ''' ''') @assert_no_logs def test_bounding_box_polygon(): assert_pixels('bounding_box_polygon', 5, 5, ''' BBBBB BBBBR BBBRR BBRRR BRRRR ''', ''' ''') @assert_no_logs def test_bounding_box_polyline(): assert_pixels('bounding_box_polyline', 5, 5, ''' BBBBB BBBBR BBBRR BBRRR BRRRR ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_bounding_box_text(): assert_pixels('bounding_box_text', 2, 2, ''' BB BR ''', ''' A ''') @assert_no_logs def test_bounding_box_path_hv(): assert_pixels('bounding_box_path_hv', 5, 5, ''' BBBBB BBBBR BBBRR BBRRR BRRRR ''', ''' ''') @assert_no_logs def test_bounding_box_path_l(): assert_pixels('bounding_box_path_l', 5, 5, ''' BBBBB BBBBR BBBRR BBRRR BRRRR ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_bounding_box_path_c(): assert_pixels('bounding_box_path_c', 5, 5, ''' BBB__ BBR__ _____ BBB__ BBR__ ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_bounding_box_path_s(): assert_pixels('bounding_box_path_s', 5, 5, ''' BBB__ BBR__ _____ BBB__ BBR__ ''', ''' ''') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/svg/test_clip.py0000644000000000000000000000476300000000000017247 0ustar0000000000000000""" weasyprint.tests.test_draw.svg.test_clip ---------------------------------------- Test clip-path attribute. """ import pytest from ...testing_utils import assert_no_logs from .. import assert_pixels @assert_no_logs def test_clip_path(): assert_pixels('clip_path', 9, 9, ''' _________ _________ __RRRRR__ __RBBBR__ __RBBBR__ __RBBBR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_clip_path_on_group(): assert_pixels('clip_path_on_group', 9, 9, ''' _________ _________ __BBBB___ __BRRRR__ __BRRRR__ __BRRRR__ ___RRRR__ _________ _________ ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_clip_path_group_on_group(): assert_pixels('clip_path_group_on_group', 9, 9, ''' _________ _________ __BB_____ __BR_____ _________ _____RR__ _____RR__ _________ _________ ''', ''' ''') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/svg/test_defs.py0000644000000000000000000000170200000000000017227 0ustar0000000000000000""" weasyprint.tests.test_draw.svg.test_defs ---------------------------------------- Test how SVG definitions are drawn. """ from ...testing_utils import assert_no_logs from .. import assert_pixels @assert_no_logs def test_use(): assert_pixels('use', 10, 10, ''' RRRRR_____ RRRRR_____ __________ ___RRRRR__ ___RRRRR__ __________ _____RRRRR _____RRRRR __________ __________ ''', ''' ''') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/svg/test_gradients.py0000644000000000000000000003462500000000000020300 0ustar0000000000000000""" weasyprint.tests.test_draw.svg.test_gradients ------------------------------------------ Test how SVG simple gradients are drawn. """ import pytest from ...testing_utils import assert_no_logs from .. import assert_pixels @assert_no_logs def test_linear_gradient(): assert_pixels('linear_gradient', 10, 10, ''' BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR ''', ''' ''') @assert_no_logs def test_linear_gradient_userspace(): assert_pixels('linear_gradient_userspace', 10, 10, ''' BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR ''', ''' ''') @assert_no_logs def test_linear_gradient_multicolor(): assert_pixels('linear_gradient_multicolor', 10, 8, ''' BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv ''', ''' ''') @assert_no_logs def test_linear_gradient_multicolor_userspace(): assert_pixels('linear_gradient_multicolor_userspace', 10, 8, ''' BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_linear_gradient_transform(): assert_pixels('linear_gradient_transform', 10, 8, ''' BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv vvvvvvvvvv vvvvvvvvvv vvvvvvvvvv ''', ''' ''') @assert_no_logs def test_linear_gradient_repeat(): assert_pixels('linear_gradient_repeat', 10, 16, ''' BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_linear_gradient_repeat_long(): assert_pixels('linear_gradient_repeat_long', 10, 16, ''' BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv ''', ''' ''') @assert_no_logs def test_linear_gradient_reflect(): assert_pixels('linear_gradient_reflect', 10, 16, ''' BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv vvvvvvvvvv vvvvvvvvvv GGGGGGGGGG GGGGGGGGGG RRRRRRRRRR RRRRRRRRRR BBBBBBBBBB BBBBBBBBBB ''', ''' ''') @assert_no_logs def test_radial_gradient(): assert_pixels('radial_gradient', 10, 10, ''' rrrrrrrrrr rrrrrrrrrr rrrrBBrrrr rrrBBBBrrr rrBBBBBBrr rrBBBBBBrr rrrBBBBrrr rrrrBBrrrr rrrrrrrrrr rrrrrrrrrr ''', ''' ''') @assert_no_logs def test_radial_gradient_userspace(): assert_pixels('radial_gradient_userspace', 10, 10, ''' rrrrrrrrrr rrrrrrrrrr rrrrBBrrrr rrrBBBBrrr rrBBBBBBrr rrBBBBBBrr rrrBBBBrrr rrrrBBrrrr rrrrrrrrrr rrrrrrrrrr ''', ''' ''') @assert_no_logs def test_radial_gradient_multicolor(): assert_pixels('radial_gradient_multicolor', 10, 10, ''' rrrrrrrrrr rrrGGGGrrr rrGGBBGGrr rGGBBBBGGr rGBBBBBBGr rGBBBBBBGr rGGBBBBGGr rrGGBBGGrr rrrGGGGrrr rrrrrrrrrr ''', ''' ''') @assert_no_logs def test_radial_gradient_multicolor_userspace(): assert_pixels('radial_gradient_multicolor_userspace', 10, 10, ''' rrrrrrrrrr rrrGGGGrrr rrGGBBGGrr rGGBBBBGGr rGBBBBBBGr rGBBBBBBGr rGGBBBBGGr rrGGBBGGrr rrrGGGGrrr rrrrrrrrrr ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_radial_gradient_repeat(): assert_pixels('radial_gradient_repeat', 10, 10, ''' GBrrrrrrBG BrrGGGGrrB rrGGBBGGrr rGGBBBBGGr rGBBBBBBGr rGBBBBBBGr rGGBBBBGGr rrGGBBGGrr BrrGGGGrrB GBrrrrrrBG ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_radial_gradient_reflect(): assert_pixels('radial_gradient_reflect', 10, 10, ''' BGrrrrrrGB GrrGGGGrrG rrGGBBGGrr rGGBBBBGGr rGBBBBBBGr rGBBBBBBGr rGGBBBBGGr rrGGBBGGrr GrrGGGGrrG BGrrrrrrGB ''', ''' ''') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/svg/test_images.py0000644000000000000000000001556000000000000017562 0ustar0000000000000000""" weasyprint.tests.test_draw.svg.test_images ------------------------------------------ Test how images are drawn in SVG. """ import pytest from weasyprint.urls import path2url from ...testing_utils import assert_no_logs, resource_filename from .. import assert_pixels @assert_no_logs def test_image_svg(): assert_pixels('test_image_svg', 4, 4, ''' ____ ____ __B_ ____ ''', ''' ''') @assert_no_logs def test_image_svg_viewbox(): assert_pixels('test_image_svg_viewbox', 4, 4, ''' ____ ____ __B_ ____ ''', ''' ''') @assert_no_logs def test_image_svg_align_default(): assert_pixels('test_image_svg_align_default', 8, 8, ''' __BRRR__ __BRRR__ __RRRG__ __RRRG__ ________ ________ ________ ________ ''', ''' ''') @assert_no_logs def test_image_svg_align_none(): assert_pixels('test_image_svg_align_none', 8, 8, ''' BBRRRRRR BBRRRRRR RRRRRRGG RRRRRRGG ________ ________ ________ ________ ''', ''' ''') @assert_no_logs def test_image_svg_align_meet_x(): assert_pixels('test_image_svg_align_meet_x', 8, 8, ''' ____BRRR ____BRRR ____RRRG ____RRRG ________ ________ ________ ________ ''', ''' ''') @assert_no_logs def test_image_svg_align_meet_y(): assert_pixels('test_image_svg_align_meet_y', 8, 8, ''' ________ ________ ________ ________ BRRR____ BRRR____ RRRG____ RRRG____ ''', ''' ''') @assert_no_logs def test_image_svg_align_slice_x(): assert_pixels('test_image_svg_align_slice_x', 8, 8, ''' BBRRRRRR BBRRRRRR BBRRRRRR BBRRRRRR ________ ________ ________ ________ ''', ''' ''') @assert_no_logs def test_image_svg_align_slice_y(): assert_pixels('test_image_svg_align_slice_y', 8, 8, ''' BBRR____ BBRR____ BBRR____ BBRR____ RRRR____ RRRR____ RRRR____ RRRR____ ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_image_svg_percentage(): assert_pixels('test_image_svg_percentage', 4, 4, ''' ____ ____ __B_ ____ ''', ''' ''') def test_image_svg_wrong(): assert_pixels('test_image_svg_wrong', 4, 4, ''' ____ ____ ____ ____ ''', ''' ''') @assert_no_logs def test_image_image(): assert_pixels('test_image_image', 4, 4, ''' rBBB BBBB BBBB BBBB ''', ''' ''' % path2url(resource_filename('pattern.png'))) def test_image_image_wrong(): assert_pixels('test_image_image_wrong', 4, 4, ''' ____ ____ ____ ____ ''', ''' ''') ������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1639425971.0233078 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-54.1/tests/draw/svg/test_opacity.py������������������������������������������������������0000644�0000000�0000000�00000012067�00000000000�017764� 0����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������""" weasyprint.tests.test_draw.svg.test_opacity ------------------------------------------- Test how opacity is handled for SVG. """ import pytest from ...testing_utils import assert_no_logs from .. import assert_same_rendering # TODO: xfail tests fail because of GhostScript and are supposed to work with # real PDF files. opacity_source = ''' %s''' @assert_no_logs def test_opacity(): assert_same_rendering(9, 9, ( ('opacity_reference', opacity_source % ''' '''), ('opacity', opacity_source % ''' '''), )) @assert_no_logs def test_fill_opacity(): assert_same_rendering(9, 9, ( ('fill_opacity_reference', opacity_source % ''' '''), ('fill_opacity', opacity_source % ''' '''), )) @pytest.mark.xfail @assert_no_logs def test_stroke_opacity(): assert_same_rendering(9, 9, ( ('stroke_opacity_reference', opacity_source % ''' '''), ('stroke_opacity', opacity_source % ''' '''), )) @pytest.mark.xfail @assert_no_logs def test_stroke_fill_opacity(): assert_same_rendering(9, 9, ( ('stroke_fill_opacity_reference', opacity_source % ''' '''), ('stroke_fill_opacity', opacity_source % ''' '''), )) @pytest.mark.xfail @assert_no_logs def test_pattern_gradient_stroke_fill_opacity(): assert_same_rendering(9, 9, ( ('pattern_gradient_stroke_fill_opacity_reference', opacity_source % ''' '''), ('pattern_gradient_stroke_fill_opacity', opacity_source % ''' '''), )) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1639425971.0233078 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-54.1/tests/draw/svg/test_paths.py��������������������������������������������������������0000644�0000000�0000000�00000032014�00000000000�017425� 0����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������""" weasyprint.tests.test_draw.svg.test_paths ------------------------------------------ Test how SVG simple paths are drawn. """ from ...testing_utils import assert_no_logs from .. import assert_pixels @assert_no_logs def test_path_Hh(): assert_pixels('path_Hh', 10, 10, ''' BBBBBBBB__ BBBBBBBB__ __________ RRRRRRRR__ RRRRRRRR__ __________ GGGGGGGG__ GGGGGGGG__ BBBBBBBB__ BBBBBBBB__ ''', ''' ''') @assert_no_logs def test_path_Vv(): assert_pixels('path_Vv', 10, 10, ''' BB____GG__ BB____GG__ BB____GG__ BB____GG__ ___RR_____ ___RR_____ ___RR___BB ___RR___BB ___RR___BB ___RR___BB ''', ''' ''') @assert_no_logs def test_path_Ll(): assert_pixels('path_Ll', 10, 10, ''' ______RR__ ______RR__ ______RR__ ___BB_RR__ ___BB_RR__ ___BB_RR__ ___BB_____ ___BB_____ ___BB_____ ___BB_____ ''', ''' ''') @assert_no_logs def test_path_Zz(): assert_pixels('path_Zz', 10, 10, ''' BBBBBBB___ BBBBBBB___ BB___BB___ BB___BB___ BBBBBBB___ BBBBBBB___ ____RRRRRR ____RRRRRR ____RR__RR ____RRRRRR ''', ''' ''') @assert_no_logs def test_path_Zz_fill(): assert_pixels('path_Zz_fill', 10, 10, ''' BBBBBBB___ BBBBBBB___ BBGGGBB___ BBGGGBB___ BBBBBBB___ BBBBBBB___ ____RRRRRR ____RRRRRR ____RRGGRR ____RRRRRR ''', ''' ''') @assert_no_logs def test_path_Cc(): assert_pixels('path_Cc', 10, 10, ''' __________ __________ __________ __________ __BBB_____ __BBB_____ __________ __RRR_____ __RRR_____ __________ ''', ''' ''') @assert_no_logs def test_path_Ss(): assert_pixels('path_Ss', 10, 10, ''' __________ __________ __________ __________ __BBB_____ __BBB_____ __________ __RRR_____ __RRR_____ __________ ''', ''' ''') @assert_no_logs def test_path_CcSs(): assert_pixels('path_CcSs', 10, 12, ''' __BBBBBB__ __BBBBBBB_ _____BBBB_ __RRRRRR__ __RRRRRRR_ _____RRRR_ __GGGGGG__ __GGGGGGG_ _____GGGG_ __BBBBBB__ __BBBBBBB_ _____BBBB_ ''', ''' ''') @assert_no_logs def test_path_Qq(): assert_pixels('path_Qq', 10, 10, ''' __________ __________ __________ __________ __BBBB____ __BBBB____ __________ __RRRR____ __RRRR____ __________ ''', ''' ''') @assert_no_logs def test_path_Tt(): assert_pixels('path_Tt', 10, 10, ''' __________ __________ __________ __________ __BBBB____ __BBBB____ __________ __RRRR____ __RRRR____ __________ ''', ''' ''') @assert_no_logs def test_path_QqTt(): assert_pixels('path_QqTt', 12, 12, ''' _BBBB_______ BBBBBBB_____ BBBBBBBB__BB BB__BBBBBBBB _____BBBBBBB _______BBBB_ _RRRR_______ RRRRRRR_____ RRRRRRRR__RR RR__RRRRRRRR _____RRRRRRR _______RRRR_ ''', ''' ''') @assert_no_logs def test_path_QqTt2(): assert_pixels('path_QqTt2', 12, 12, ''' _BBBB_______ BBBBBBB_____ BBBBBBBB__BB BB__BBBBBBBB _____BBBBBBB _______BBBB_ _RRRR_______ RRRRRRR_____ RRRRRRRR__RR RR__RRRRRRRR _____RRRRRRR _______RRRR_ ''', ''' ''') @assert_no_logs def test_path_Aa(): assert_pixels('path_Aa', 12, 12, ''' __BBBB______ _BBBBB______ BBBBBB______ BBBB________ BBB_________ BBB____RRRR_ ______RRRRR_ _____RRRRRR_ _____RRRR___ _____RRR____ _____RRR____ ____________ ''', ''' ''') @assert_no_logs def test_path_Aa2(): assert_pixels('path_Aa2', 12, 12, ''' ______GGGG__ ______GGGGG_ ______GGGGGG ________GGGG _________GGG _________GGG GGG______GGG GGG______GGG GGGG____GGGG GGGGGGGGGGGG _GGGGGGGGGG_ __GGGGGGGG__ ''', ''' ''') @assert_no_logs def test_path_Aa3(): assert_pixels('path_Aa3', 12, 12, ''' ______GGGG__ ______GGGGG_ ______GGGGGG ________GGGG _________GGG _________GGG GGG______GGG GGG______GGG GGGG____GGGG GGGGGGGGGGGG _GGGGGGGGGG_ __GGGGGGGG__ ''', ''' ''') @assert_no_logs def test_path_Aa4(): assert_pixels('path_Aa4', 12, 12, ''' ____________ ____BBB_____ ____BBB_____ ___BBBB_____ _BBBBBB_____ _BBBBB______ _BBBB____RRR _________RRR ________RRRR ______RRRRRR ______RRRRR_ ______RRRR__ ''', ''' ''') @assert_no_logs def test_path_Aa5(): assert_pixels('path_Aa5', 12, 12, ''' __BBBBBBBB__ _BBBBBBBBBB_ BBBBBBBBBBBB BBBB____BBBB BBB______BBB BBB______BBB BBB_________ BBB_________ BBBB________ BBBBBB______ _BBBBB______ __BBBB______ ''', ''' ''') @assert_no_logs def test_path_Aa6(): assert_pixels('path_Aa6', 12, 12, ''' __BBBBBBBB__ _BBBBBBBBBB_ BBBBBBBBBBBB BBBB____BBBB BBB______BBB BBB______BBB BBB_________ BBB_________ BBBB________ BBBBBB______ _BBBBB______ __BBBB______ ''', ''' ''') @assert_no_logs def test_path_Aa7(): assert_pixels('path_Aa7', 12, 12, ''' ____________ ____________ ____________ ____________ ____________ ____________ GGG______GGG GGG______GGG GGGG____GGGG GGGGGGGGGGGG _GGGGGGGGGG_ __GGGGGGGG__ ''', ''' ''') ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1639425971.0233078 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-54.1/tests/draw/svg/test_patterns.py�����������������������������������������������������0000644�0000000�0000000�00000007706�00000000000�020160� 0����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������""" weasyprint.tests.test_draw.svg.test_patterns ------------------------------------------ Test how SVG simple patterns are drawn. """ from ...testing_utils import assert_no_logs from .. import assert_pixels @assert_no_logs def test_pattern(): assert_pixels('pattern', 8, 8, ''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') @assert_no_logs def test_pattern_2(): assert_pixels('pattern_2', 8, 8, ''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') @assert_no_logs def test_pattern_3(): assert_pixels('pattern_3', 8, 8, ''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') @assert_no_logs def test_pattern_4(): assert_pixels('pattern_4', 8, 8, ''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') ����������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1639425971.0233078 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-54.1/tests/draw/svg/test_shapes.py�������������������������������������������������������0000644�0000000�0000000�00000023347�00000000000�017602� 0����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������""" weasyprint.tests.test_draw.svg.test_shapes ------------------------------------------ Test how SVG simple shapes are drawn. """ from ...testing_utils import assert_no_logs from .. import assert_pixels @assert_no_logs def test_rect_stroke(): assert_pixels('rect_stroke', 9, 9, ''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_rect_fill(): assert_pixels('rect_fill', 9, 9, ''' _________ _________ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_rect_stroke_fill(): assert_pixels('rect_stroke_fill', 9, 9, ''' _________ _RRRRRRR_ _RRRRRRR_ _RRBBBRR_ _RRBBBRR_ _RRBBBRR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_rect_round(): assert_pixels('rect_round', 9, 9, ''' _zzzzzzz_ zzzzzzzzz zzRRRRRzz zzRRRRRzz zzRRRRRzz zzRRRRRzz zzRRRRRzz zzzzzzzzz _zzzzzzz_ ''', ''' ''') @assert_no_logs def test_rect_round_zero(): assert_pixels('rect_round_zero', 9, 9, ''' RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR ''', ''' ''') @assert_no_logs def test_line(): assert_pixels('line', 9, 9, ''' _________ _________ _________ _________ RRRRRR___ RRRRRR___ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_polyline(): assert_pixels('polyline', 9, 9, ''' _________ RRRRRR___ RRRRRR___ RR__RR___ RR__RR___ RR__RR___ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_polyline_fill(): assert_pixels('polyline_fill', 9, 9, ''' _________ RRRRRR___ RRRRRR___ RRBBRR___ RRBBRR___ RRBBRR___ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_polygon(): assert_pixels('polygon', 9, 9, ''' _________ RRRRRR___ RRRRRR___ RR__RR___ RR__RR___ RRRRRR___ RRRRRR___ _________ _________ ''', ''' ''') @assert_no_logs def test_polygon_fill(): assert_pixels('polygon_fill', 9, 9, ''' _________ RRRRRR___ RRRRRR___ RRBBRR___ RRBBRR___ RRRRRR___ RRRRRR___ _________ _________ ''', ''' ''') @assert_no_logs def test_circle_stroke(): assert_pixels('circle_stroke', 10, 10, ''' __________ __RRRRRR__ _RRRRRRRR_ _RRRRRRRR_ _RRR__RRR_ _RRR__RRR_ _RRRRRRRR_ _RRRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_circle_fill(): assert_pixels('circle_fill', 10, 10, ''' __________ __RRRRRR__ _RRRRRRRR_ _RRRRRRRR_ _RRRBBRRR_ _RRRBBRRR_ _RRRRRRRR_ _RRRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_ellipse_stroke(): assert_pixels('ellipse_stroke', 10, 10, ''' __________ __RRRRRR__ _RRRRRRRR_ _RRRRRRRR_ _RRR__RRR_ _RRR__RRR_ _RRRRRRRR_ _RRRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_ellipse_fill(): assert_pixels('ellipse_fill', 10, 10, ''' __________ __RRRRRR__ _RRRRRRRR_ _RRRRRRRR_ _RRRBBRRR_ _RRRBBRRR_ _RRRRRRRR_ _RRRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_rect_in_g(): assert_pixels('rect_in_g', 9, 9, ''' RRRRR____ RRRRR____ RRRRR____ RRRRR____ RRRRR____ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_rect_x_y_in_g(): assert_pixels('rect_x_y_in_g', 9, 9, ''' _________ _________ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_rect_stroke_zero(): assert_pixels('rect_stroke_zero', 9, 9, ''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_rect_width_height_zero(): assert_pixels('rect_fill', 9, 9, ''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000033�00000000000�011451� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������27 mtime=1643655079.358553 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-54.1/tests/draw/svg/test_text.py���������������������������������������������������������0000644�0000000�0000000�00000020363�00000000000�017276� 0����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������""" weasyprint.tests.test_draw.svg.test_text ---------------------------------------- Test how SVG text is drawn. """ from ...testing_utils import assert_no_logs from .. import assert_pixels @assert_no_logs def test_text_fill(): assert_pixels('text_fill', 20, 2, ''' BBBBBB__BBBBBB______ BBBBBB__BBBBBB______ ''', ''' ABC DEF ''') @assert_no_logs def test_text_stroke(): assert_pixels('text_stroke', 20, 4, ''' _BBBBBBBBBBBB_______ _BBBBBBBBBBBB_______ _BBBBBBBBBBBB_______ _BBBBBBBBBBBB_______ ''', ''' A B C ''') @assert_no_logs def test_text_x(): assert_pixels('text_x', 20, 2, ''' BB__BB_BBBB_________ BB__BB_BBBB_________ ''', ''' ABCD ''') @assert_no_logs def test_text_y(): assert_pixels('text_y', 30, 10, ''' __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB BBBBBBBBBB_____BBBBB__________ BBBBBBBBBB_____BBBBB__________ BBBBBBBBBB_____BBBBB__________ BBBBBBBBBB_____BBBBB__________ BBBBBBBBBB_____BBBBB__________ ''', ''' ABCDEF ''') @assert_no_logs def test_text_xy(): assert_pixels('text_xy', 30, 10, ''' __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB BBBBB__________BBBBB__________ BBBBB__________BBBBB__________ BBBBB__________BBBBB__________ BBBBB__________BBBBB__________ BBBBB__________BBBBB__________ ''', ''' ABCDE ''') @assert_no_logs def test_text_dx(): assert_pixels('text_dx', 20, 2, ''' BB__BB_BBBB_________ BB__BB_BBBB_________ ''', ''' ABCD ''') @assert_no_logs def test_text_dy(): assert_pixels('text_dy', 30, 10, ''' __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB BBBBBBBBBB_____BBBBB__________ BBBBBBBBBB_____BBBBB__________ BBBBBBBBBB_____BBBBB__________ BBBBBBBBBB_____BBBBB__________ BBBBBBBBBB_____BBBBB__________ ''', ''' ABCDEF ''') @assert_no_logs def test_text_dx_dy(): assert_pixels('text_dx_dy', 30, 10, ''' __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB __________BBBBB_____BBBBBBBBBB BBBBB__________BBBBB__________ BBBBB__________BBBBB__________ BBBBB__________BBBBB__________ BBBBB__________BBBBB__________ BBBBB__________BBBBB__________ ''', ''' ABCDE ''') @assert_no_logs def test_text_anchor_start(): assert_pixels('text_anchor_start', 20, 4, ''' __BBBBBB____________ __BBBBBB____________ ____BBBBBB__________ ____BBBBBB__________ ''', ''' ABC ABC ''') @assert_no_logs def test_text_anchor_middle(): assert_pixels('text_anchor_middle', 20, 2, ''' _______BBBBBB_______ _______BBBBBB_______ ''', ''' ABC ''') @assert_no_logs def test_text_anchor_end(): assert_pixels('text_anchor_end', 20, 2, ''' ____________BBBBBB__ ____________BBBBBB__ ''', ''' ABC ''') @assert_no_logs def test_text_tspan(): assert_pixels('text_tspan', 20, 2, ''' BBBBBB__BBBBBB______ BBBBBB__BBBBBB______ ''', ''' ABC DEF ''') �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1639425971.0233078 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-54.1/tests/draw/svg/test_visibility.py���������������������������������������������������0000644�0000000�0000000�00000011311�00000000000�020472� 0����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������""" weasyprint.tests.test_draw.svg.test_visibility ---------------------------------------------- Test how the visibility is controlled with "visibility" and "display" attributes. """ from ...testing_utils import assert_no_logs from .. import assert_pixels @assert_no_logs def test_visibility_visible(): assert_pixels('visibility_visible', 9, 9, ''' _________ _________ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_visibility_hidden(): assert_pixels('visibility_hidden', 9, 9, ''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_visibility_inherit_hidden(): assert_pixels('visibility_inherit_hidden', 9, 9, ''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_visibility_inherit_visible(): assert_pixels('visibility_inherit_visible', 9, 9, ''' _________ _________ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_display_inline(): assert_pixels('display_inline', 9, 9, ''' _________ _________ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_display_none(): assert_pixels('display_none', 9, 9, ''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_display_inherit_none(): assert_pixels('display_inherit_none', 9, 9, ''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_display_inherit_inline(): assert_pixels('display_inherit_inline', 9, 9, ''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1639425971.0233078 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-54.1/tests/draw/test_absolute.py���������������������������������������������������������0000644�0000000�0000000�00000021134�00000000000�017326� 0����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������""" weasyprint.tests.test_draw.test_absolute ---------------------------------------- Test how absolutes are drawn. """ import pytest from ..testing_utils import assert_no_logs from . import assert_pixels @assert_no_logs def test_absolute_split_1(): expected_pixels = ''' BBBBRRRRRRRR____ BBBBRRRRRRRR____ BBBBRR__________ BBBBRR__________ ''' html = '''
aa aa
bbbbbb bbb
''' assert_pixels('absolute_split_1', 16, 4, expected_pixels, html) @assert_no_logs def test_absolute_split_2(): expected_pixels = ''' RRRRRRRRRRRRBBBB RRRRRRRRRRRRBBBB RRRR________BBBB RRRR________BBBB ''' html = '''
aa aa
bbbbbb bb
''' assert_pixels('absolute_split_2', 16, 4, expected_pixels, html) @assert_no_logs def test_absolute_split_3(): expected_pixels = ''' BBBBRRRRRRRR____ BBBBRRRRRRRR____ RRRRRRRRRR______ RRRRRRRRRR______ ''' html = '''
aa
bbbbbb bbbbb
''' assert_pixels('absolute_split_3', 16, 4, expected_pixels, html) @assert_no_logs def test_absolute_split_4(): expected_pixels = ''' RRRRRRRRRRRRBBBB RRRRRRRRRRRRBBBB RRRRRRRRRR______ RRRRRRRRRR______ ''' html = '''
aa
bbbbbb bbbbb
''' assert_pixels('absolute_split_4', 16, 4, expected_pixels, html) @assert_no_logs def test_absolute_split_5(): expected_pixels = ''' BBBBRRRR____gggg BBBBRRRR____gggg BBBBRRRRRR__gggg BBBBRRRRRR__gggg ''' html = '''
aa aa
cc cc
bbbb bbbbb
''' assert_pixels('absolute_split_5', 16, 4, expected_pixels, html) @assert_no_logs def test_absolute_split_6(): expected_pixels = ''' BBBBRRRR____gggg BBBBRRRR____gggg BBBBRRRRRR______ BBBBRRRRRR______ ''' html = '''
aa aa
cc
bbbb bbbbb
''' assert_pixels('absolute_split_6', 16, 4, expected_pixels, html) @assert_no_logs def test_absolute_split_7(): expected_pixels = ''' BBBBRRRRRRRRgggg BBBBRRRRRRRRgggg ____RRRR____gggg ____RRRR____gggg ''' html = '''
aa
cc cc
bbbb bb
''' assert_pixels('absolute_split_7', 16, 4, expected_pixels, html) @pytest.mark.xfail @assert_no_logs def test_absolute_next_page(): # TODO: currently, the layout of absolute boxes forces to render a box, # even when it doesn’t fit in the page. This workaround avoids placeholders # with no box. Instead, we should remove these placeholders, or avoid # crashes when they’re rendered. expected_pixels = ''' RRRRRRRRRR______ RRRRRRRRRR______ RRRRRRRRRR______ RRRRRRRRRR______ BBBBBBRRRR______ BBBBBBRRRR______ BBBBBB__________ ________________ ''' html = ''' aaaaa aaaaa
bb
aaaaa ''' assert_pixels('absolute_next_page', 16, 8, expected_pixels, html) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/test_background.py0000644000000000000000000006313500000000000017636 0ustar0000000000000000""" weasyprint.tests.test_draw.test_background ------------------------------------------ Test how backgrounds are drawn. """ import pytest from ..testing_utils import assert_no_logs from . import assert_pixels @assert_no_logs @pytest.mark.parametrize( 'name, expected_width, expected_height, expected_pixels, html', ( ('all_blue', 10, 10, (10 * (10 * 'B' + '\n')), ''' '''), ('blocks', 10, 10, ''' rrrrrrrrrr rrrrrrrrrr rrBBBBBBrr rrBBBBBBrr rrBBBBBBrr rrBBBBBBrr rrBBBBBBrr rrrrrrrrrr rrrrrrrrrr rrrrrrrrrr ''', ''' '''), )) def test_canvas_background(name, expected_width, expected_height, expected_pixels, html): assert_pixels(name, expected_width, expected_height, expected_pixels, html) def test_canvas_background_size(): expected_pixels = ''' __________ __________ __RRRRRR__ __RGGGGR__ __RRRRRR__ __BBBBBB__ __BBBBBB__ __BBBBBB__ __________ __________ ''' html = ''' ''' assert_pixels('background-size', 10, 10, expected_pixels, html) @assert_no_logs @pytest.mark.parametrize('name, css, pixels', ( ('repeat', 'url(pattern.png)', ''' ______________ ______________ __rBBBrBBBrB__ __BBBBBBBBBB__ __BBBBBBBBBB__ __BBBBBBBBBB__ __rBBBrBBBrB__ __BBBBBBBBBB__ __BBBBBBBBBB__ __BBBBBBBBBB__ __rBBBrBBBrB__ __BBBBBBBBBB__ ______________ ______________ ______________ ______________ '''), ('repeat_x', 'url(pattern.png) repeat-x', ''' ______________ ______________ __rBBBrBBBrB__ __BBBBBBBBBB__ __BBBBBBBBBB__ __BBBBBBBBBB__ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('repeat_y', 'url(pattern.png) repeat-y', ''' ______________ ______________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ __rBBB________ __BBBB________ ______________ ______________ ______________ ______________ '''), ('left_top', 'url(pattern.png) no-repeat 0 0%', ''' ______________ ______________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('center_top', 'url(pattern.png) no-repeat 50% 0px', ''' ______________ ______________ _____rBBB_____ _____BBBB_____ _____BBBB_____ _____BBBB_____ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('right_top', 'url(pattern.png) no-repeat 6px top', ''' ______________ ______________ ________rBBB__ ________BBBB__ ________BBBB__ ________BBBB__ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('bottom_6_right_0', 'url(pattern.png) no-repeat bottom 6px right 0', ''' ______________ ______________ ________rBBB__ ________BBBB__ ________BBBB__ ________BBBB__ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('left_center', 'url(pattern.png) no-repeat left center', ''' ______________ ______________ ______________ ______________ ______________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('center_left', 'url(pattern.png) no-repeat center left', ''' ______________ ______________ ______________ ______________ ______________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('center_center', 'url(pattern.png) no-repeat 3px 3px', ''' ______________ ______________ ______________ ______________ ______________ _____rBBB_____ _____BBBB_____ _____BBBB_____ _____BBBB_____ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('right_center', 'url(pattern.png) no-repeat 100% 50%', ''' ______________ ______________ ______________ ______________ ______________ ________rBBB__ ________BBBB__ ________BBBB__ ________BBBB__ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('left_bottom', 'url(pattern.png) no-repeat 0% bottom', ''' ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ ______________ ______________ ______________ ______________ '''), ('center_bottom', 'url(pattern.png) no-repeat center 6px', ''' ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ _____rBBB_____ _____BBBB_____ _____BBBB_____ _____BBBB_____ ______________ ______________ ______________ ______________ '''), ('bottom_center', 'url(pattern.png) no-repeat bottom center', ''' ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ _____rBBB_____ _____BBBB_____ _____BBBB_____ _____BBBB_____ ______________ ______________ ______________ ______________ '''), ('right_bottom', 'url(pattern.png) no-repeat 6px 100%', ''' ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ________rBBB__ ________BBBB__ ________BBBB__ ________BBBB__ ______________ ______________ ______________ ______________ '''), ('repeat_x_1px_2px', 'url(pattern.png) repeat-x 1px 2px', ''' ______________ ______________ ______________ ______________ __BrBBBrBBBr__ __BBBBBBBBBB__ __BBBBBBBBBB__ __BBBBBBBBBB__ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('repeat_y_local_2px_1px', 'url(pattern.png) repeat-y local 2px 1px', ''' ______________ ______________ ____BBBB______ ____rBBB______ ____BBBB______ ____BBBB______ ____BBBB______ ____rBBB______ ____BBBB______ ____BBBB______ ____BBBB______ ____rBBB______ ______________ ______________ ______________ ______________ '''), ('fixed', 'url(pattern.png) no-repeat fixed', ''' # The image is actually here: ####### ______________ ______________ __BB__________ __BB__________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('fixed_right', 'url(pattern.png) no-repeat fixed right 3px', ''' # x x x x ______________ ______________ ______________ __________rB__ # __________BB__ # __________BB__ # __________BB__ # ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('fixed_center_center', 'url(pattern.png)no-repeat fixed 50%center', ''' ______________ ______________ ______________ ______________ ______________ ______________ _____rBBB_____ _____BBBB_____ _____BBBB_____ _____BBBB_____ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('multi_under', '''url(pattern.png) no-repeat, url(pattern.png) no-repeat 2px 1px''', ''' ______________ ______________ __rBBB________ __BBBBBB______ __BBBBBB______ __BBBBBB______ ____BBBB______ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('multi_over', '''url(pattern.png) no-repeat 2px 1px, url(pattern.png) no-repeat''', ''' ______________ ______________ __rBBB________ __BBrBBB______ __BBBBBB______ __BBBBBB______ ____BBBB______ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), )) def test_background_image(name, css, pixels): # pattern.png looks like this: # rBBB # BBBB # BBBB # BBBB assert_pixels(f'background_{name}', 14, 16, pixels, '''

 ''' % css) @assert_no_logs def test_background_image_zero_size_background(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/217 assert_pixels('zero_size_background', 10, 10, ''' __________ __________ __________ __________ __________ __________ __________ __________ __________ __________ ''', ''' ''') @assert_no_logs def test_background_origin(): """Test the background-origin property.""" def test_value(value, pixels, css=None): assert_pixels(f'background_origin_{value}', 12, 12, pixels, ''' ''' % (css or value,)) test_value('border-box', ''' ____________ ____________ ____________ ____________ ____________ ____________ ____________ _______rBBB_ _______BBBB_ _______BBBB_ _______BBBB_ ____________ ''') test_value('padding-box', ''' ____________ ____________ ____________ ____________ ____________ ____________ ______rBBB__ ______BBBB__ ______BBBB__ ______BBBB__ ____________ ____________ ''') test_value('content-box', ''' ____________ ____________ ____________ ____________ ____________ _____rBBB___ _____BBBB___ _____BBBB___ _____BBBB___ ____________ ____________ ____________ ''') test_value('border-box_clip', ''' ____________ ____________ ____________ ____________ ____________ ____________ ____________ _______rB___ _______BB___ ____________ ____________ ____________ ''', css='border-box; background-clip: content-box') @assert_no_logs def test_background_repeat_space_1(): assert_pixels('background_repeat_space', 12, 16, ''' ____________ _rBBB__rBBB_ _BBBB__BBBB_ _BBBB__BBBB_ _BBBB__BBBB_ ____________ _rBBB__rBBB_ _BBBB__BBBB_ _BBBB__BBBB_ _BBBB__BBBB_ ____________ _rBBB__rBBB_ _BBBB__BBBB_ _BBBB__BBBB_ _BBBB__BBBB_ ____________ ''', ''' ''') @assert_no_logs def test_background_repeat_space_2(): assert_pixels('background_repeat_space', 12, 14, ''' ____________ _rBBB__rBBB_ _BBBB__BBBB_ _BBBB__BBBB_ _BBBB__BBBB_ _rBBB__rBBB_ _BBBB__BBBB_ _BBBB__BBBB_ _BBBB__BBBB_ _rBBB__rBBB_ _BBBB__BBBB_ _BBBB__BBBB_ _BBBB__BBBB_ ____________ ''', ''' ''') @assert_no_logs def test_background_repeat_space_3(): assert_pixels('background_repeat_space', 12, 13, ''' ____________ _rBBBrBBBrB_ _BBBBBBBBBB_ _BBBBBBBBBB_ _BBBBBBBBBB_ ____________ ____________ ____________ _rBBBrBBBrB_ _BBBBBBBBBB_ _BBBBBBBBBB_ _BBBBBBBBBB_ ____________ ''', ''' ''') @assert_no_logs def test_background_repeat_round_1(): assert_pixels('background_repeat_round', 10, 14, ''' __________ _rrBBBBBB_ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _rrBBBBBB_ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ ''', ''' ''') @assert_no_logs def test_background_repeat_round_2(): assert_pixels('background_repeat_round', 10, 18, ''' __________ _rrBBBBBB_ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _rrBBBBBB_ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ ''', ''' ''') @assert_no_logs def test_background_repeat_round_3(): assert_pixels('background_repeat_round', 10, 14, ''' __________ _rrBBBBBB_ _rrBBBBBB_ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ ''', ''' ''') @assert_no_logs def test_background_repeat_round_4(): assert_pixels('background_repeat_round', 10, 14, ''' __________ _rBBBrBBB_ _rBBBrBBB_ _rBBBrBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ ''', ''' ''') @assert_no_logs @pytest.mark.parametrize('value, pixels', ( ('#00f border-box', ''' ________ _BBBBBB_ _BBBBBB_ _BBBBBB_ _BBBBBB_ _BBBBBB_ _BBBBBB_ ________ '''), ('#00f padding-box', ''' ________ ________ __BBBB__ __BBBB__ __BBBB__ __BBBB__ ________ ________ '''), ('#00f content-box', ''' ________ ________ ________ ___BB___ ___BB___ ________ ________ ________ '''), ('url(pattern.png) padding-box, #0f0', ''' ________ _GGGGGG_ _GrBBBG_ _GBBBBG_ _GBBBBG_ _GBBBBG_ _GGGGGG_ ________ '''), )) def test_background_clip(value, pixels): assert_pixels(f'background_clip_{value}', 8, 8, pixels, ''' ''' % value) @assert_no_logs @pytest.mark.parametrize( 'name, expected_width, expected_height, expected_pixels, html', ( ('background_size', 12, 12, ''' ____________ ____________ ____________ ___rrBBBBBB_ ___rrBBBBBB_ ___BBBBBBBB_ ___BBBBBBBB_ ___BBBBBBBB_ ___BBBBBBBB_ ___BBBBBBBB_ ___BBBBBBBB_ ____________ ''', ''' '''), ('background_size_auto', 12, 12, ''' ____________ ____________ ____________ ____________ ____________ ____________ ____________ _______rBBB_ _______BBBB_ _______BBBB_ _______BBBB_ ____________ ''', ''' '''), ('background_size_contain', 14, 10, ''' ______________ _rrBBBBBB_____ _rrBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ ______________ ''', ''' '''), ('background_size_mixed', 14, 10, ''' ______________ _rrBBBBBB_____ _rrBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ ______________ ''', ''' '''), ('background_size_double', 14, 10, ''' ______________ _rrBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ ______________ ______________ ______________ ______________ ______________ ''', ''' '''), ('background_size_cover', 14, 10, ''' ______________ _rrrBBBBBBBBB_ _rrrBBBBBBBBB_ _rrrBBBBBBBBB_ _BBBBBBBBBBBB_ _BBBBBBBBBBBB_ _BBBBBBBBBBBB_ _BBBBBBBBBBBB_ _BBBBBBBBBBBB_ ______________ ''', ''' '''), ) ) def test_background_size(name, expected_width, expected_height, expected_pixels, html): assert_pixels( name, expected_width, expected_height, expected_pixels, html) @assert_no_logs def test_bleed_background_size(): expected_pixels = ''' RRRR RRRR RRRR RRRR ''' html = ''' ''' assert_pixels('bleed_background_size', 4, 4, expected_pixels, html) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/test_before_after.py0000644000000000000000000000461500000000000020140 0ustar0000000000000000""" weasyprint.tests.test_draw.test_before_after -------------------------------------------- Test how before and after pseudo elements are drawn. """ from ..testing_utils import assert_no_logs from . import assert_same_rendering @assert_no_logs def test_before_after_1(): assert_same_rendering(300, 30, [ ('pseudo_before', '''

some content

'''), ('pseudo_before_reference', '''

[some url] some content

''') ], tolerance=10) @assert_no_logs def test_before_after_2(): assert_same_rendering(500, 30, [ ('pseudo_quotes', '''

Lorem ipsum dolor sit amet

'''), ('pseudo_quotes_reference', '''

« Lorem ipsum “ dolor ” sit amet »

''') ], tolerance=10) @assert_no_logs def test_before_after_3(): assert_same_rendering(100, 30, [ ('pseudo_url', '''

c

'''), ('pseudo_url_reference', '''

aMissing imagebc

''') ], tolerance=10) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/test_box.py0000644000000000000000000001114000000000000016274 0ustar0000000000000000""" weasyprint.tests.test_draw.test_box ----------------------------------- Test how boxes, borders, outlines are drawn. """ import itertools import pytest from weasyprint import HTML from ..testing_utils import assert_no_logs from . import PIXELS_BY_CHAR, assert_different_renderings, assert_pixels @assert_no_logs def test_borders(margin='10px', prop='border'): """Test the rendering of borders""" source = ''' ''' # Do not test the exact rendering of earch border style but at least # check that they do not do the same. assert_different_renderings(140, 110, [ (f'{prop}_{border_style}', source % (margin, prop, border_style)) for border_style in [ 'none', 'solid', 'dashed', 'dotted', 'double', 'inset', 'outset', 'groove', 'ridge']]) css_margin = margin width = 140 height = 110 margin = 10 border = 10 solid_pixels = [PIXELS_BY_CHAR['_'] for i in range(width * height)] for x in range(margin, width - margin): for y in itertools.chain( range(margin, margin + border), range(height - margin - border, height - margin)): solid_pixels[y * width + x] = PIXELS_BY_CHAR['B'] for y in range(margin, height - margin): for x in itertools.chain( range(margin, margin + border), range(width - margin - border, width - margin)): solid_pixels[y * width + x] = PIXELS_BY_CHAR['B'] assert_pixels( f'{prop}_solid', 140, 110, solid_pixels, source % (css_margin, prop, 'solid')) @assert_no_logs def test_outlines(): return test_borders(margin='20px', prop='outline') @assert_no_logs @pytest.mark.parametrize('border_style', ('none', 'solid', 'dashed', 'dotted')) def test_small_borders_1(border_style): # Regression test for ZeroDivisionError on dashed or dotted borders # smaller than a dash/dot. # https://github.com/Kozea/WeasyPrint/issues/49 html = ''' ''' % border_style HTML(string=html).write_pdf() @assert_no_logs @pytest.mark.parametrize('border_style', ('none', 'solid', 'dashed', 'dotted')) def test_small_borders_2(border_style): # Regression test for ZeroDivisionError on dashed or dotted borders # smaller than a dash/dot. # https://github.com/Kozea/WeasyPrint/issues/146 html = ''' ''' % border_style HTML(string=html).write_pdf() @assert_no_logs def test_em_borders(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1378 html = '' HTML(string=html).write_pdf() @assert_no_logs def test_margin_boxes(): assert_pixels('margin_boxes', 15, 15, ''' _______________ _GGG______BBBB_ _GGG______BBBB_ _______________ _____RRRR______ _____RRRR______ _____RRRR______ _____RRRR______ _______________ _bbb______gggg_ _bbb______gggg_ _bbb______gggg_ _bbb______gggg_ _bbb______gggg_ _______________ ''', ''' ''') @assert_no_logs def test_display_inline_block_twice(): # Regression test for inline blocks displayed twice. # https://github.com/Kozea/WeasyPrint/issues/880 html = '
' document = HTML(string=html).render() assert document.write_pdf() == document.write_pdf() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/test_column.py0000644000000000000000000000271200000000000017006 0ustar0000000000000000""" weasyprint.tests.test_draw.test_column -------------------------------------- Test how columns are drawn. """ from ..testing_utils import assert_no_logs from . import assert_pixels @assert_no_logs def test_column_rule_1(): assert_pixels('solid', 5, 3, ''' a_r_a a_r_a _____ ''', '''
''') @assert_no_logs def test_column_rule_2(): assert_pixels('dotted', 5, 3, ''' a_r_a a___a a_r_a ''', '''
''') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639425971.0233078 weasyprint-54.1/tests/draw/test_current_color.py0000644000000000000000000000316600000000000020375 0ustar0000000000000000""" weasyprint.tests.test_draw.test_current_color --------------------------------------------- Test the currentColor value. """ from ..testing_utils import assert_no_logs from . import assert_pixels GREEN_2x2 = ''' GG GG ''' @assert_no_logs def test_current_color_1(): assert_pixels('background_current_color', 2, 2, GREEN_2x2, ''' ''') @assert_no_logs def test_current_color_2(): assert_pixels('border_current_color', 2, 2, GREEN_2x2, ''' ''') @assert_no_logs def test_current_color_3(): assert_pixels('outline_current_color', 2, 2, GREEN_2x2, ''' ''') @assert_no_logs def test_current_color_4(): assert_pixels('border_collapse_current_color', 2, 2, GREEN_2x2, ''' assert td_1.width == 20 # 30 - 5 (border-left) - 1 (border-right) - 2*2 assert td_2.position_x == 35 assert td_2.width == 34 assert td_2.margin_width() == 60 # 34 + 2*10 + 5 + 1 assert table.width == 90 # 30 + 60 assert table.margin_width() == 100 # 90 + 2*5 (border) @assert_no_logs def test_layout_table_auto_1(): page, = render_pages('''
''') ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/draw/test_float.py0000644000000000000000000004333600000000000016625 0ustar0000000000000000""" weasyprint.tests.test_draw.test_float ------------------------------------- Test how floats are drawn. """ import pytest from ..testing_utils import assert_no_logs from . import assert_pixels @assert_no_logs def test_float(): assert_pixels('float', 10, 5, ''' rBBB__aaaa BBBB__aaaa BBBB__aaaa BBBB__aaaa __________ ''', '''
''') @assert_no_logs def test_float_rtl(): assert_pixels('float_rtl', 10, 5, ''' rBBB__aaaa BBBB__aaaa BBBB__aaaa BBBB__aaaa __________ ''', '''
''') @assert_no_logs def test_float_inline(): assert_pixels('float_inline', 15, 5, ''' rBBBGG_____aaaa BBBBGG_____aaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_inline_rtl(): assert_pixels('float_inline_rtl', 15, 5, ''' rBBB_____GGaaaa BBBB_____GGaaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_inline_block(): assert_pixels('float_inline_block', 15, 5, ''' rBBBGG_____aaaa BBBBGG_____aaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_inline_block_rtl(): assert_pixels('float_inline_block_rtl', 15, 5, ''' rBBB_____GGaaaa BBBB_____GGaaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_table(): assert_pixels('float_table', 15, 5, ''' rBBBGG_____aaaa BBBBGG_____aaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_table_rtl(): assert_pixels('float_table_rtl', 15, 5, ''' rBBB_____GGaaaa BBBB_____GGaaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_inline_table(): assert_pixels('float_inline_table', 15, 5, ''' rBBBGG_____aaaa BBBBGG_____aaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_inline_table_rtl(): assert_pixels('float_inline_table_rtl', 15, 5, ''' rBBB_____GGaaaa BBBB_____GGaaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_replaced_block(): assert_pixels('float_replaced_block', 15, 5, ''' rBBBaaaa___rBBB BBBBaaaa___BBBB BBBBaaaa___BBBB BBBBaaaa___BBBB _______________ ''', '''
''') @assert_no_logs def test_float_replaced_block_rtl(): assert_pixels('float_replaced_block_rtl', 15, 5, ''' rBBB___aaaarBBB BBBB___aaaaBBBB BBBB___aaaaBBBB BBBB___aaaaBBBB _______________ ''', '''
''') @pytest.mark.xfail @assert_no_logs def test_float_replaced_inline(): assert_pixels('float_replaced_inline', 15, 5, ''' rBBBaaaa___rBBB BBBBaaaa___BBBB BBBBaaaa___BBBB BBBBaaaa___BBBB _______________ ''', '''
''') @pytest.mark.xfail @assert_no_logs def test_float_replaced_inline_rtl(): assert_pixels('float_replaced_inline_rtl', 15, 5, ''' rBBB___aaaarBBB BBBB___aaaaBBBB BBBB___aaaaBBBB BBBB___aaaaBBBB _______________ ''', '''
''') @assert_no_logs def test_float_margin(): expected_pixels = ''' BBBBRRRRRRRRRR__ BBBBRRRRRRRRRR__ __RRRRRRRRRR____ __RRRRRRRRRR____ ''' html = '''
aa
bbbbb bbbbb
''' assert_pixels('float_split_10', 16, 4, expected_pixels, html) @assert_no_logs def test_float_split_1(): expected_pixels = ''' BBBBRRRRRRRRRRRR BBBBRRRRRRRRRRRR BBBBRRRR________ BBBBRRRR________ ''' html = '''
aa aa
bbbbbb bb
''' assert_pixels('float_split_1', 16, 4, expected_pixels, html) @assert_no_logs def test_float_split_2(): expected_pixels = ''' RRRRRRRRRRRRBBBB RRRRRRRRRRRRBBBB RRRR________BBBB RRRR________BBBB ''' html = '''
aa aa
bbbbbb bb
''' assert_pixels('float_split_2', 16, 4, expected_pixels, html) @assert_no_logs def test_float_split_3(): expected_pixels = ''' BBBBRRRRRRRRRRRR BBBBRRRRRRRRRRRR RRRRRRRRRR______ RRRRRRRRRR______ ''' html = '''
aa
bbbbbb bbbbb
''' assert_pixels('float_split_3', 16, 4, expected_pixels, html) @assert_no_logs def test_float_split_4(): expected_pixels = ''' RRRRRRRRRRRRBBBB RRRRRRRRRRRRBBBB RRRRRRRRRR______ RRRRRRRRRR______ ''' html = '''
aa
bbbbbb bbbbb
''' assert_pixels('float_split_4', 16, 4, expected_pixels, html) @assert_no_logs def test_float_split_5(): expected_pixels = ''' BBBBRRRRRRRRgggg BBBBRRRRRRRRgggg BBBBRRRR____gggg BBBBRRRR____gggg ''' html = '''
aa aa
cc cc
bbbb bb
''' assert_pixels('float_split_5', 16, 4, expected_pixels, html) @assert_no_logs def test_float_split_6(): expected_pixels = ''' BBBBRRRRRRRRgggg BBBBRRRRRRRRgggg BBBBRRRR________ BBBBRRRR________ ''' html = '''
aa aa
cc
bbbb bb
''' assert_pixels('float_split_6', 16, 4, expected_pixels, html) @assert_no_logs def test_float_split_7(): expected_pixels = ''' BBBBRRRRRRRRgggg BBBBRRRRRRRRgggg RRRR________gggg RRRR________gggg ''' html = '''
aa
cc cc
bbbb bb
''' assert_pixels('float_split_7', 16, 4, expected_pixels, html) @assert_no_logs def test_float_split_8(): expected_pixels = ''' BBBB__RRRRRRRRRR BBBB__RRRRRRRRRR BBBB__RRRR______ BBBB__RRRR______ ''' html = '''
aa aa
bbbbb bb
''' assert_pixels('float_split_8', 16, 4, expected_pixels, html) @assert_no_logs def test_float_split_9(): expected_pixels = ''' RRRRRRRRRRBBBB__ RRRRRRRRRRBBBB__ RRRR______BBBB__ RRRR______BBBB__ ''' html = '''
aa aa
bbbbb bb
''' assert_pixels('float_split_9', 16, 4, expected_pixels, html) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/draw/test_footnotes.py0000644000000000000000000000546600000000000017542 0ustar0000000000000000""" weasyprint.tests.test_draw.test_footnotes ----------------------------------------- Test how footnotes are drawn. """ from ..testing_utils import assert_no_logs from . import assert_pixels @assert_no_logs def test_inline_footnote(): assert_pixels('span_footnote', 9, 7, ''' RRRRRRRR_ RRRRRRRR_ _________ _________ _________ RRRRRRRR_ RRRRRRRR_ ''', '''
abcde
''') @assert_no_logs def test_block_footnote(): assert_pixels('div_footnote', 9, 7, ''' RRRRRRRR_ RRRRRRRR_ _________ _________ _________ RRRRRRRR_ RRRRRRRR_ ''', '''
abc
de
''') @assert_no_logs def test_long_footnote(): assert_pixels('long_footnote', 9, 7, ''' RRRRRRRR_ RRRRRRRR_ _________ RRRRRRRR_ RRRRRRRR_ RR_______ RR_______ ''', '''
abcde f
''') @assert_no_logs def test_footnote_margin(): assert_pixels('footnote_margin', 9, 7, ''' RRRRRRRR_ RRRRRRRR_ _________ _________ _RRRRRR__ _RRRRRR__ _________ ''', '''
abcd
''') ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/draw/test_gradient.py0000644000000000000000000001477300000000000017320 0ustar0000000000000000""" weasyprint.tests.test_draw.test_gradient ---------------------------------------- Test how gradients are drawn. """ from ..testing_utils import assert_no_logs from . import assert_pixels @assert_no_logs def test_linear_gradients_1(): assert_pixels('linear_gradient_1', 5, 9, ''' _____ _____ _____ BBBBB BBBBB RRRRR RRRRR RRRRR RRRRR ''', '''
''' % filename) @assert_no_logs @pytest.mark.parametrize('filename', ( 'pattern.svg', 'pattern.png', 'pattern.palette.png', 'pattern.gif', )) def test_resized_images(filename): assert_pixels(f'resized_image_{filename}', 12, 12, resized_image, '''
''' % filename) @assert_no_logs @pytest.mark.parametrize('viewbox, width, height', ( (None, None, None), (None, 4, None), (None, None, 4), (None, 4, 4), ('0 0 4 4', 4, None), ('0 0 4 4', None, 4), ('0 0 4 4', 4, 4), ('0 0 4 4', 4, 4), )) def test_svg_sizing(viewbox, width, height): assert_pixels( f'svg_sizing_{viewbox}_{width}_{height}', 8, 8, centered_image, ''' ''' % ( f'width="{width}"' if width else '', f'height="{height}px"' if height else '', f'viewbox="{viewbox}"' if viewbox else '')) @assert_no_logs @pytest.mark.parametrize('viewbox, width, height, image', ( (None, None, None, small_resized_image), (None, 8, None, small_resized_image), (None, None, 8, small_resized_image), (None, 8, 8, small_resized_image), ('0 0 4 4', None, None, resized_image), ('0 0 4 4', 8, None, resized_image), ('0 0 4 4', None, 8, resized_image), ('0 0 4 4', 8, 8, resized_image), ('0 0 4 4', 800, 800, resized_image), )) def test_svg_resizing(viewbox, width, height, image): assert_pixels( f'svg_resizing_{viewbox}_{width}_{height}', 12, 12, image, ''' ''' % ( f'width="{width}"' if width else '', f'height="{height}px"' if height else '', f'viewbox="{viewbox}"' if viewbox else '')) @assert_no_logs def test_images_block(): assert_pixels('block_image', 8, 8, centered_image, '''
''') @assert_no_logs def test_images_not_found(): with capture_logs() as logs: assert_pixels('image_not_found', 8, 8, no_image, '''
''') assert len(logs) == 1 assert 'ERROR: Failed to load image' in logs[0] assert 'inexistent1.png' in logs[0] @assert_no_logs def test_images_no_src(): assert_pixels('image_no_src', 8, 8, no_image, '''
''') @assert_no_logs def test_images_alt(): with capture_logs() as logs: assert_same_rendering(200, 30, [ (name, '''
%s
''' % html) for name, html in [ ('image_alt_text_reference', 'Hello, world!'), ('image_alt_text_not_found', 'Hello, world!'), ('image_alt_text_no_src', 'Hello, world!'), ('image_svg_no_intrinsic_size', '''Hello, world!'''), ] ]) assert len(logs) == 1 assert 'ERROR: Failed to load image' in logs[0] assert 'inexistent2.png' in logs[0] @assert_no_logs def test_images_repeat_transparent(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1440 assert_pixels('image_repeat_transparent', 1, 3, '_\n_\n_', '''
''') @assert_no_logs def test_images_no_width(): assert_pixels('image_0x1', 8, 8, no_image, '''
not shown
''') @assert_no_logs def test_images_no_height(): assert_pixels('image_1x0', 8, 8, no_image, '''
not shown
''') @assert_no_logs def test_images_no_width_height(): assert_pixels('image_0x0', 8, 8, no_image, '''
not shown
''') @assert_no_logs def test_images_page_break(): assert_pixels('image_page_break', 8, 3 * 8, page_break, '''
''') @assert_no_logs def test_image_repeat_inline(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/808 assert_pixels('image_page_repeat_inline', 8, 2 * 8, table, '''
''') @assert_no_logs def test_image_repeat_block(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/808 assert_pixels('image_page_repeat_block', 8, 2 * 8, table, '''
''') @assert_no_logs def test_images_padding(): # Regression test: padding used to be ignored on images assert_pixels('image_with_padding', 8, 8, centered_image, '''
''') @assert_no_logs def test_images_in_inline_block(): # Regression test: this used to cause an exception assert_pixels('image_in_inline_block', 8, 8, centered_image, '''

''') @assert_no_logs def test_images_shared_pattern(): # The same image is used in a repeating background, # then in a non-repating . # If Pattern objects are shared carelessly, the image will be repeated. assert_pixels('image_shared_pattern', 12, 12, ''' ____________ ____________ __aaaaaaaa__ __aaaaaaaa__ ____________ __aaaa______ __aaaa______ __aaaa______ __aaaa______ ____________ ____________ ____________ ''', '''
''') @assert_no_logs def test_image_resolution(): assert_same_rendering(20, 20, [ ('image_resolution_ref', '''
'''), ('image_resolution_img', '''
'''), ('image_resolution_content', '''
'''), ('image_resolution_background', '''
'''), ]) @assert_no_logs def test_image_cover(): assert_pixels('image_cover', 8, 8, cover_image, ''' ''') @assert_no_logs def test_image_contain(): assert_pixels('image_contain', 8, 8, centered_image, ''' ''') @assert_no_logs def test_image_none(): assert_pixels('image_none', 8, 8, centered_image, ''' ''') @assert_no_logs def test_image_scale_down(): assert_pixels('image_scale_down', 8, 8, centered_image, ''' ''') @assert_no_logs def test_image_position(): assert_pixels('image_position', 8, 8, centered_image, ''' ''') ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/draw/test_leader.py0000644000000000000000000002147400000000000016753 0ustar0000000000000000""" weasyprint.tests.test_draw.test_leader -------------------------------------- Test how leaders are drawn. """ import pytest from ..testing_utils import assert_no_logs from . import assert_pixels @assert_no_logs def test_leader_simple(): expected_pixels = ''' RR__BBBBBBBB__BB RR__BBBBBBBB__BB RRRR__BBBB__BBBB RRRR__BBBB__BBBB RR__BBBB__BBBBBB RR__BBBB__BBBBBB ''' html = '''
a
bb
c
''' assert_pixels('leader-simple', 16, 6, expected_pixels, html) @assert_no_logs def test_leader_too_long(): expected_pixels = ''' RRRRRRRRRR______ RRRRRRRRRR______ BBBBBBBBBBBB__BB BBBBBBBBBBBB__BB RR__RR__RR__RR__ RR__RR__RR__RR__ RR__RR__RR______ RR__RR__RR______ BBBBBBBBBB__BBBB BBBBBBBBBB__BBBB RR__RR__RR__RR__ RR__RR__RR__RR__ RR__BBBB__BBBBBB RR__BBBB__BBBBBB ''' html = '''
aaaaa
a a a a a a a
a a a a a
''' assert_pixels('leader-too-long', 16, 14, expected_pixels, html) @assert_no_logs def test_leader_alone(): expected_pixels = ''' RRBBBBBBBBBBBBBB RRBBBBBBBBBBBBBB ''' html = '''
a
''' assert_pixels('leader-alone', 16, 2, expected_pixels, html) @assert_no_logs def test_leader_content(): expected_pixels = ''' RR____BB______BB RR____BB______BB ''' html = '''
a
''' assert_pixels('leader-content', 16, 2, expected_pixels, html) @pytest.mark.xfail @assert_no_logs def test_leader_float(): expected_pixels = ''' bbGRR___BB____BB bbGRR___BB____BB GGGRR___BB____BB ___RR___BB____BB ''' html = '''
a
a
a
''' assert_pixels('leader-float', 16, 4, expected_pixels, html) @assert_no_logs def test_leader_in_inline(): expected_pixels = ''' RR__GGBBBBBB__RR RR__GGBBBBBB__RR ''' html = '''
a a a
''' assert_pixels('leader-in-inline', 16, 2, expected_pixels, html) @pytest.mark.xfail @assert_no_logs def test_leader_bad_alignment(): expected_pixels = ''' RRRRRR__________ RRRRRR__________ ______BB______RR ______BB______RR ''' html = '''
aaa
''' assert_pixels('leader-in-inline', 16, 4, expected_pixels, html) @assert_no_logs def test_leader_simple_rtl(): expected_pixels = ''' BB__BBBBBBBB__RR BB__BBBBBBBB__RR BBBB__BBBB__RRRR BBBB__BBBB__RRRR BBBBBB__BBBB__RR BBBBBB__BBBB__RR ''' html = '''
a
bb
c
''' assert_pixels('leader-simple-rtl', 16, 6, expected_pixels, html) @assert_no_logs def test_leader_too_long_rtl(): expected_pixels = ''' ______RRRRRRRRRR ______RRRRRRRRRR BB__BBBBBBBBBBBB BB__BBBBBBBBBBBB __RR__RR__RR__RR __RR__RR__RR__RR ______RR__RR__RR ______RR__RR__RR BBBB__BBBBBBBBBB BBBB__BBBBBBBBBB __RR__RR__RR__RR __RR__RR__RR__RR BBBBBB__BBBB__RR BBBBBB__BBBB__RR ''' html = '''
aaaaa
a a a a a a a
a a a a a
''' assert_pixels('leader-too-long-rtl', 16, 14, expected_pixels, html) @assert_no_logs def test_leader_float_leader(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1409 # Leaders in floats are not displayed at all in many cases with the current # implementation, and this case is not really specified. So… expected_pixels = ''' RR____________BB RR____________BB RRRR__________BB RRRR__________BB RR____________BB RR____________BB ''' html = '''
a
bb
c
''' assert_pixels('leader-float-leader', 16, 6, expected_pixels, html) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/draw/test_list.py0000644000000000000000000000416500000000000016470 0ustar0000000000000000""" weasyprint.tests.test_draw.test_list ------------------------------------ Test how lists are drawn. """ import pytest from ..testing_utils import SANS_FONTS, assert_no_logs from . import assert_pixels @assert_no_logs @pytest.mark.parametrize('position, pixels', ( ('outside', # ++++++++++++++ ++++
  • horizontal margins: 7px 2px # ######
  • width: 12 - 7 - 2 = 3px # -- list marker margin: 0.5em = 2px # ******** list marker image is 4px wide ''' ____________ ____________ ___rBBB_____ ___BBBB_____ ___BBBB_____ ___BBBB_____ ____________ ____________ ____________ ____________ '''), ('inside', # ++++++++++++++ ++++
  • horizontal margins: 7px 2px # ######
  • width: 12 - 7 - 2 = 3px # ******** list marker image is 4px wide: overflow ''' ____________ ____________ _______rBBB_ _______BBBB_ _______BBBB_ _______BBBB_ ____________ ____________ ____________ ____________ ''') )) def test_list_style_image(position, pixels): assert_pixels(f'list_style_image_{position}', 12, 10, pixels, '''
    ''' % (SANS_FONTS, position)) @assert_no_logs def test_list_style_image_none(): assert_pixels('list_style_none', 10, 10, ''' __________ __________ __________ __________ __________ __________ __________ __________ __________ __________ ''', '''
    • ''' % (SANS_FONTS,)) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/draw/test_opacity.py0000644000000000000000000000304100000000000017155 0ustar0000000000000000""" weasyprint.tests.test_draw.test_opacity --------------------------------------- Test opacity. """ from ..testing_utils import assert_no_logs from . import assert_same_rendering opacity_source = ''' %s''' @assert_no_logs def test_opacity_1(): assert_same_rendering(60, 60, [ ('opacity_0_reference', opacity_source % '''
      '''), ('opacity_0', opacity_source % '''
      '''), ]) @assert_no_logs def test_opacity_2(): assert_same_rendering(60, 60, [ ('opacity_color_reference', opacity_source % '''
      '''), ('opacity_color', opacity_source % '''
      '''), ]) @assert_no_logs def test_opacity_3(): assert_same_rendering(60, 60, [ ('opacity_multiplied_reference', opacity_source % '''
      '''), ('opacity_multiplied', opacity_source % '''
      '''), ('opacity_multiplied_2', opacity_source % '''
      '''), # 0.9 * 0.666666 == 0.6 ]) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/draw/test_overflow.py0000644000000000000000000001067100000000000017357 0ustar0000000000000000""" weasyprint.tests.test_draw.test_overflow ---------------------------------------- Test overflow and clipping. """ import pytest from ..testing_utils import assert_no_logs from . import assert_pixels @assert_no_logs def test_overflow_1(): # See test_images assert_pixels('inline_image_overflow', 8, 8, ''' ________ ________ __rBBB__ __BBBB__ ________ ________ ________ ________ ''', '''
      ''') @assert_no_logs def test_overflow_2(): # is only 1px high, but its overflow is propageted to the viewport # ie. the padding edge of the page box. assert_pixels('inline_image_viewport_overflow', 8, 8, ''' ________ ________ __rBBB__ __BBBB__ __BBBB__ ________ ________ ________ ''', '''
      ''') @assert_no_logs def test_overflow_3(): # Assert that the border is not clipped by overflow: hidden assert_pixels('border_box_overflow', 8, 8, ''' ________ ________ __BBBB__ __B__B__ __B__B__ __BBBB__ ________ ________ ''', '''
      ''') @assert_no_logs def test_overflow_4(): # Assert that the page margins aren't clipped by body's overflow assert_pixels('border_box_overflow', 8, 8, ''' rr______ rr______ __BBBB__ __BBBB__ __BBBB__ __BBBB__ ________ ________ ''', ''' ''') @assert_no_logs @pytest.mark.parametrize('number, css, pixels', ( (1, '5px, 5px, 9px, auto', ''' ______________ ______________ ______________ ______________ ______________ ______________ ______rBBBrBg_ ______BBBBBBg_ ______BBBBBBg_ ______BBBBBBg_ ______________ ______________ ______________ ______________ ______________ ______________ '''), (2, '5px, 5px, auto, 10px', ''' ______________ ______________ ______________ ______________ ______________ ______________ ______rBBBr___ ______BBBBB___ ______BBBBB___ ______BBBBB___ ______rBBBr___ ______BBBBB___ ______ggggg___ ______________ ______________ ______________ '''), (3, '5px, auto, 9px, 10px', ''' ______________ ______________ ______________ ______________ ______________ ______________ _grBBBrBBBr___ _gBBBBBBBBB___ _gBBBBBBBBB___ _gBBBBBBBBB___ ______________ ______________ ______________ ______________ ______________ ______________ '''), (4, 'auto, 5px, 9px, 10px', ''' ______________ ______ggggg___ ______rBBBr___ ______BBBBB___ ______BBBBB___ ______BBBBB___ ______rBBBr___ ______BBBBB___ ______BBBBB___ ______BBBBB___ ______________ ______________ ______________ ______________ ______________ ______________ '''), )) def test_clip(number, css, pixels): assert_pixels('background_repeat_clipped_%s' % number, 14, 16, pixels, '''
      ''' % css) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1640088515.6079571 weasyprint-54.1/tests/draw/test_table.py0000644000000000000000000011724700000000000016612 0ustar0000000000000000""" weasyprint.tests.test_draw.test_tables -------------------------------------- Test how tables are drawn. """ import pytest from weasyprint.html import HTML_HANDLERS from ..testing_utils import assert_no_logs from . import assert_pixels, parse_pixels PIX_BY_CHAR_OVERRIDES = { # rgba(255, 0, 0, 0.5) above #fff 'r': (255, 127, 127), # rgba(0, 255, 0, 0.5) above #fff 'g': (127, 255, 127), # r above B above #fff. 'b': (128, 0, 127), } def to_pix(pixels_str): return parse_pixels(pixels_str, PIX_BY_CHAR_OVERRIDES) # TODO: refactor colspan/rowspan into CSS: # td, th { column-span: attr(colspan integer) } HTML_HANDLERS['x-td'] = HTML_HANDLERS['td'] HTML_HANDLERS['x-th'] = HTML_HANDLERS['th'] tables_source = ''' ''' @assert_no_logs def test_tables_1(): assert_pixels('table_borders', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__r____r_r____r_r____r__B_ _B__r____r_r____r_r____r__B_ _B__r____r_r____r_r____r__B_ _B__r____r_r____r_r____r__B_ _B__rrrrrr_r____r_rrrrrr__B_ _B_________r____r_________B_ _B__rrrrrrrSrrrrS_rrrrrr__B_ _B__r______r____S_r____r__B_ _B__r______r____S_r____r__B_ _B__r______r____S_r____r__B_ _B__r______r____S_r____r__B_ _B__rrrrrrrSSSSSS_rrrrrr__B_ _B________________________B_ _B__rrrrrr_rrrrrr_________B_ _B__r____r_r____r_________B_ _B__r____r_r____r_________B_ _B__r____r_r____r_________B_ _B__r____r_r____r_________B_ _B__rrrrrr_rrrrrr_________B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed } x-td { border-color: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_1_rtl(): assert_pixels('table_borders_rtl', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__r____r_r____r_r____r__B_ _B__r____r_r____r_r____r__B_ _B__r____r_r____r_r____r__B_ _B__r____r_r____r_r____r__B_ _B__rrrrrr_r____r_rrrrrr__B_ _B_________r____r_________B_ _B__rrrrrr_SrrrrSrrrrrrr__B_ _B__r____r_S____r______r__B_ _B__r____r_S____r______r__B_ _B__r____r_S____r______r__B_ _B__r____r_S____r______r__B_ _B__rrrrrr_SSSSSSrrrrrrr__B_ _B________________________B_ _B_________rrrrrr_rrrrrr__B_ _B_________r____r_r____r__B_ _B_________r____r_r____r__B_ _B_________r____r_r____r__B_ _B_________r____r_r____r__B_ _B_________rrrrrr_rrrrrr__B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed; direction: rtl; } x-td { border-color: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_2(): assert_pixels('table_collapsed_borders', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBB_________ _BBBBBBBBBBBBBBBBBB_________ _BB____r____r____BB_________ _BB____r____r____BB_________ _BB____r____r____BB_________ _BB____r____r____BB_________ _BBrrrrr____rrrrrBB_________ _BB_________r____BB_________ _BB_________r____BB_________ _BB_________r____BB_________ _BB_________r____BB_________ _BBrrrrrrrrrrrrrrBB_________ _BB____r____r____BB_________ _BB____r____r____BB_________ _BB____r____r____BB_________ _BB____r____r____BB_________ _BBBBBBBBBBBBBBBBBB_________ _BBBBBBBBBBBBBBBBBB_________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border: 2px solid #00f; table-layout: fixed; border-collapse: collapse } x-td { border-color: #ff7f7f } '''}) @assert_no_logs def test_tables_2_rtl(): assert_pixels('table_collapsed_borders_rtl', 28, 28, to_pix(''' ____________________________ _________BBBBBBBBBBBBBBBBBB_ _________BBBBBBBBBBBBBBBBBB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BBrrrrr____rrrrrBB_ _________BB____r_________BB_ _________BB____r_________BB_ _________BB____r_________BB_ _________BB____r_________BB_ _________BBrrrrrrrrrrrrrrBB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BBBBBBBBBBBBBBBBBB_ _________BBBBBBBBBBBBBBBBBB_ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ '''), tables_source % {'extra_css': ''' body { direction: rtl; } x-table { border: 2px solid #00f; table-layout: fixed; border-collapse: collapse; } x-td { border-color: #ff7f7f } '''}) @assert_no_logs def test_tables_3(): assert_pixels('table_collapsed_borders_paged', 28, 52, to_pix(''' ____________________________ _gggggggggggggggggggggggggg_ _g________________________g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BB____r____r____BB_____g_ _g_BB____r____r____BB_____g_ _g_BB____r____r____BB_____g_ _g_BB____r____r____BB_____g_ _g_BBrrrrr____rrrrrBB_____g_ _g_BB_________r____BB_____g_ _g_BB_________r____BB_____g_ _g_BB_________r____BB_____g_ _g_BB_________r____BB_____g_ _g_BBrrrrrrrrrrrrrrBB_____g_ _g________________________g_ _g________________________g_ _g________________________g_ _gggggggggggggggggggggggggg_ ____________________________ ____________________________ _gggggggggggggggggggggggggg_ _g_BBrrrrrrrrrrrrrrBB_____g_ _g_BB____r____r____BB_____g_ _g_BB____r____r____BB_____g_ _g_BB____r____r____BB_____g_ _g_BB____r____r____BB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g_BBBBBBBBBBBBBBBBBB_____g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _gggggggggggggggggggggggggg_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border: solid #00f; border-width: 8px 2px; table-layout: fixed; border-collapse: collapse } x-td { border-color: #ff7f7f } @page { size: 28px 26px; margin: 1px; border: 1px solid rgba(0, 255, 0, 0.5); } '''}) @assert_no_logs def test_tables_3_rtl(): assert_pixels('table_collapsed_borders_paged_rtl', 28, 52, to_pix(''' ____________________________ _gggggggggggggggggggggggggg_ _g________________________g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BBrrrrr____rrrrrBB_g_ _g_____BB____r_________BB_g_ _g_____BB____r_________BB_g_ _g_____BB____r_________BB_g_ _g_____BB____r_________BB_g_ _g_____BBrrrrrrrrrrrrrrBB_g_ _g________________________g_ _g________________________g_ _g________________________g_ _gggggggggggggggggggggggggg_ ____________________________ ____________________________ _gggggggggggggggggggggggggg_ _g_____BBrrrrrrrrrrrrrrBB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _gggggggggggggggggggggggggg_ ____________________________ '''), tables_source % {'extra_css': ''' body { direction: rtl; } x-table { border: solid #00f; border-width: 8px 2px; table-layout: fixed; border-collapse: collapse; } x-td { border-color: #ff7f7f } @page { size: 28px 26px; margin: 1px; border: 1px solid rgba(0, 255, 0, 0.5); } '''}) @assert_no_logs def test_tables_4(): assert_pixels('table_td_backgrounds', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B_________rrrrrr_________B_ _B__rrrrrrrSSSSSS_rrrrrr__B_ _B__rrrrrrrSSSSSS_rrrrrr__B_ _B__rrrrrrrSSSSSS_rrrrrr__B_ _B__rrrrrrrSSSSSS_rrrrrr__B_ _B__rrrrrrrSSSSSS_rrrrrr__B_ _B__rrrrrrrSSSSSS_rrrrrr__B_ _B________________________B_ _B__rrrrrr_rrrrrr_________B_ _B__rrrrrr_rrrrrr_________B_ _B__rrrrrr_rrrrrr_________B_ _B__rrrrrr_rrrrrr_________B_ _B__rrrrrr_rrrrrr_________B_ _B__rrrrrr_rrrrrr_________B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed } x-td { background: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_4_rtl(): assert_pixels('table_td_backgrounds_rtl', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B__rrrrrr_rrrrrr_rrrrrr__B_ _B_________rrrrrr_________B_ _B__rrrrrr_SSSSSSrrrrrrr__B_ _B__rrrrrr_SSSSSSrrrrrrr__B_ _B__rrrrrr_SSSSSSrrrrrrr__B_ _B__rrrrrr_SSSSSSrrrrrrr__B_ _B__rrrrrr_SSSSSSrrrrrrr__B_ _B__rrrrrr_SSSSSSrrrrrrr__B_ _B________________________B_ _B_________rrrrrr_rrrrrr__B_ _B_________rrrrrr_rrrrrr__B_ _B_________rrrrrr_rrrrrr__B_ _B_________rrrrrr_rrrrrr__B_ _B_________rrrrrr_rrrrrr__B_ _B_________rrrrrr_rrrrrr__B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed; direction: rtl; } x-td { background: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_5(): assert_pixels('table_row_backgrounds', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B_________bbbbbb_________B_ _B__bbbbbbbpppppp_bbbbbb__B_ _B__bbbbbbbpppppp_bbbbbb__B_ _B__bbbbbbbpppppp_bbbbbb__B_ _B__bbbbbbbpppppp_bbbbbb__B_ _B__bbbbbbbpppppp_bbbbbb__B_ _B__bbbbbbbpppppp_bbbbbb__B_ _B________________________B_ _B__rrrrrr_rrrrrr_________B_ _B__rrrrrr_rrrrrr_________B_ _B__rrrrrr_rrrrrr_________B_ _B__rrrrrr_rrrrrr_________B_ _B__rrrrrr_rrrrrr_________B_ _B__rrrrrr_rrrrrr_________B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed } x-tbody { background: rgba(0, 0, 255, 1) } x-tr { background: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_5_rtl(): assert_pixels('table_row_backgrounds_rtl', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B_________bbbbbb_________B_ _B__bbbbbb_ppppppbbbbbbb__B_ _B__bbbbbb_ppppppbbbbbbb__B_ _B__bbbbbb_ppppppbbbbbbb__B_ _B__bbbbbb_ppppppbbbbbbb__B_ _B__bbbbbb_ppppppbbbbbbb__B_ _B__bbbbbb_ppppppbbbbbbb__B_ _B________________________B_ _B_________rrrrrr_rrrrrr__B_ _B_________rrrrrr_rrrrrr__B_ _B_________rrrrrr_rrrrrr__B_ _B_________rrrrrr_rrrrrr__B_ _B_________rrrrrr_rrrrrr__B_ _B_________rrrrrr_rrrrrr__B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed; direction: rtl; } x-tbody { background: rgba(0, 0, 255, 1) } x-tr { background: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_6(): assert_pixels('table_column_backgrounds', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__bbbbbb_bbbbbb_rrrrrr__B_ _B__bbbbbb_bbbbbb_rrrrrr__B_ _B__bbbbbb_bbbbbb_rrrrrr__B_ _B__bbbbbb_bbbbbb_rrrrrr__B_ _B__bbbbbb_bbbbbb_rrrrrr__B_ _B__bbbbbb_bbbbbb_rrrrrr__B_ _B_________bbbbbb_________B_ _B__bbbbbbbpppppp_rrrrrr__B_ _B__bbbbbbbpppppp_rrrrrr__B_ _B__bbbbbbbpppppp_rrrrrr__B_ _B__bbbbbbbpppppp_rrrrrr__B_ _B__bbbbbbbpppppp_rrrrrr__B_ _B__bbbbbbbpppppp_rrrrrr__B_ _B________________________B_ _B__bbbbbb_bbbbbb_________B_ _B__bbbbbb_bbbbbb_________B_ _B__bbbbbb_bbbbbb_________B_ _B__bbbbbb_bbbbbb_________B_ _B__bbbbbb_bbbbbb_________B_ _B__bbbbbb_bbbbbb_________B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed;} x-colgroup { background: rgba(0, 0, 255, 1) } x-col { background: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_6_rtl(): assert_pixels('table_column_backgrounds_rtl', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__rrrrrr_bbbbbb_bbbbbb__B_ _B__rrrrrr_bbbbbb_bbbbbb__B_ _B__rrrrrr_bbbbbb_bbbbbb__B_ _B__rrrrrr_bbbbbb_bbbbbb__B_ _B__rrrrrr_bbbbbb_bbbbbb__B_ _B__rrrrrr_bbbbbb_bbbbbb__B_ _B_________bbbbbb_________B_ _B__rrrrrr_ppppppbbbbbbb__B_ _B__rrrrrr_ppppppbbbbbbb__B_ _B__rrrrrr_ppppppbbbbbbb__B_ _B__rrrrrr_ppppppbbbbbbb__B_ _B__rrrrrr_ppppppbbbbbbb__B_ _B__rrrrrr_ppppppbbbbbbb__B_ _B________________________B_ _B_________bbbbbb_bbbbbb__B_ _B_________bbbbbb_bbbbbb__B_ _B_________bbbbbb_bbbbbb__B_ _B_________bbbbbb_bbbbbb__B_ _B_________bbbbbb_bbbbbb__B_ _B_________bbbbbb_bbbbbb__B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed; direction: rtl; } x-colgroup { background: rgba(0, 0, 255, 1) } x-col { background: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_7(): assert_pixels('table_borders_and_row_backgrounds', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bBBBBb_bBBBBb_bBBBBb__B_ _B__bBBBBb_bBBBBb_bBBBBb__B_ _B__bBBBBb_bBBBBb_bBBBBb__B_ _B__bBBBBb_bBBBBb_bBBBBb__B_ _B__bbbbbb_bBBBBb_bbbbbb__B_ _B_________bBBBBb_________B_ _B__rrrrrrrpbbbbp_rrrrrr__B_ _B__r______bBBBBp_r____r__B_ _B__r______bBBBBp_r____r__B_ _B__r______bBBBBp_r____r__B_ _B__r______bBBBBp_r____r__B_ _B__rrrrrrrpppppp_rrrrrr__B_ _B________________________B_ _B__rrrrrr_rrrrrr_________B_ _B__r____r_r____r_________B_ _B__r____r_r____r_________B_ _B__r____r_r____r_________B_ _B__r____r_r____r_________B_ _B__rrrrrr_rrrrrr_________B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed } x-tr:first-child { background: blue } x-td { border-color: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_7_rtl(): assert_pixels('table_borders_and_row_backgrounds_rtl', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__bbbbbb_bbbbbb_bbbbbb__B_ _B__bBBBBb_bBBBBb_bBBBBb__B_ _B__bBBBBb_bBBBBb_bBBBBb__B_ _B__bBBBBb_bBBBBb_bBBBBb__B_ _B__bBBBBb_bBBBBb_bBBBBb__B_ _B__bbbbbb_bBBBBb_bbbbbb__B_ _B_________bBBBBb_________B_ _B__rrrrrr_pbbbbprrrrrrr__B_ _B__r____r_pBBBBb______r__B_ _B__r____r_pBBBBb______r__B_ _B__r____r_pBBBBb______r__B_ _B__r____r_pBBBBb______r__B_ _B__rrrrrr_pppppprrrrrrr__B_ _B________________________B_ _B_________rrrrrr_rrrrrr__B_ _B_________r____r_r____r__B_ _B_________r____r_r____r__B_ _B_________r____r_r____r__B_ _B_________r____r_r____r__B_ _B_________rrrrrr_rrrrrr__B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed; direction: rtl; } x-tr:first-child { background: blue } x-td { border-color: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_8(): assert_pixels('table_borders_and_column_backgrounds', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__bbbbbb_rrrrrr_rrrrrr__B_ _B__bBBBBb_r____r_r____r__B_ _B__bBBBBb_r____r_r____r__B_ _B__bBBBBb_r____r_r____r__B_ _B__bBBBBb_r____r_r____r__B_ _B__bbbbbb_r____r_rrrrrr__B_ _B_________r____r_________B_ _B__bbbbbbbpbbbbp_rrrrrr__B_ _B__bBBBBBBbBBBBp_r____r__B_ _B__bBBBBBBbBBBBp_r____r__B_ _B__bBBBBBBbBBBBp_r____r__B_ _B__bBBBBBBbBBBBp_r____r__B_ _B__bbbbbbbpppppp_rrrrrr__B_ _B________________________B_ _B__bbbbbb_rrrrrr_________B_ _B__bBBBBb_r____r_________B_ _B__bBBBBb_r____r_________B_ _B__bBBBBb_r____r_________B_ _B__bBBBBb_r____r_________B_ _B__bbbbbb_rrrrrr_________B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed } x-col:first-child { background: blue } x-td { border-color: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_8_rtl(): assert_pixels('table_borders_and_column_backgrounds_rtl', 28, 28, to_pix(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__rrrrrr_rrrrrr_bbbbbb__B_ _B__r____r_r____r_bBBBBb__B_ _B__r____r_r____r_bBBBBb__B_ _B__r____r_r____r_bBBBBb__B_ _B__r____r_r____r_bBBBBb__B_ _B__rrrrrr_r____r_bbbbbb__B_ _B_________r____r_________B_ _B__rrrrrr_pbbbbpbbbbbbb__B_ _B__r____r_pBBBBbBBBBBBb__B_ _B__r____r_pBBBBbBBBBBBb__B_ _B__r____r_pBBBBbBBBBBBb__B_ _B__r____r_pBBBBbBBBBBBb__B_ _B__rrrrrr_ppppppbbbbbbb__B_ _B________________________B_ _B_________rrrrrr_bbbbbb__B_ _B_________r____r_bBBBBb__B_ _B_________r____r_bBBBBb__B_ _B_________r____r_bBBBBb__B_ _B_________r____r_bBBBBb__B_ _B_________rrrrrr_bbbbbb__B_ _B________________________B_ _B________________________B_ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ ____________________________ '''), tables_source % {'extra_css': ''' x-table { border-color: #00f; table-layout: fixed; direction: rtl; } x-col:first-child { background: blue } x-td { border-color: rgba(255, 0, 0, 0.5) } '''}) @assert_no_logs def test_tables_9(): assert_pixels('collapsed_border_thead', 22, 36, ''' ______________________ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBB____R____R____BBB_ _BBB____R____R____BBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ __R_____R____R_____R__ __R_____R____R_____R__ __RRRRRRRRRRRRRRRRRR__ __R_____R____R_____R__ __R_____R____R_____R__ __RRRRRRRRRRRRRRRRRR__ ______________________ ______________________ ______________________ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBB____R____R____BBB_ _BBB____R____R____BBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ __R_____R____R_____R__ __RRRRRRRRRRRRRRRRRR__ ______________________ ______________________ ______________________ ______________________ ______________________ ______________________ ______________________ ______________________ ''', ''' ''') @assert_no_logs def test_tables_10(): assert_pixels('collapsed_border_tfoot', 22, 34, ''' ______________________ __RRRRRRRRRRRRRRRRRR__ __R_____R____R_____R__ __R_____R____R_____R__ __RRRRRRRRRRRRRRRRRR__ __R_____R____R_____R__ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBB____R____R____BBB_ _BBB____R____R____BBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ ______________________ ______________________ ______________________ ______________________ __RRRRRRRRRRRRRRRRRR__ __R_____R____R_____R__ __R_____R____R_____R__ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBB____R____R____BBB_ _BBB____R____R____BBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ ______________________ ______________________ ______________________ ______________________ ______________________ ''', '''
      ''') @assert_no_logs def test_tables_11(): # Regression test for inline table with collapsed border and alignment # rendering borders incorrectly # https://github.com/Kozea/WeasyPrint/issues/82 assert_pixels('inline_text_align', 20, 10, ''' ____________________ ________RRRRRRRRRRR_ ________R____R____R_ ________R____R____R_ ________R____R____R_ ________RRRRRRRRRRR_ ____________________ ____________________ ____________________ ____________________ ''', '''
      ''') @assert_no_logs def test_tables_12(): assert_pixels('table_collapsed_borders', 28, 28, to_pix(''' ____________________________ _________BBBBBBBBBBBBBBBBBB_ _________BBBBBBBBBBBBBBBBBB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BBrrrrr____rrrrrBB_ _________BB____r_________BB_ _________BB____r_________BB_ _________BB____r_________BB_ _________BB____r_________BB_ _________BBrrrrrrrrrrrrrrBB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BB____r____r____BB_ _________BBBBBBBBBBBBBBBBBB_ _________BBBBBBBBBBBBBBBBBB_ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ '''), tables_source % {'extra_css': ''' body { direction: rtl } x-table { border: 2px solid #00f; table-layout: fixed; border-collapse: collapse } x-td { border-color: #ff7f7f } '''}) @assert_no_logs def test_tables_13(): assert_pixels('table_collapsed_borders_paged', 28, 52, to_pix(''' ____________________________ _gggggggggggggggggggggggggg_ _g________________________g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BBrrrrr____rrrrrBB_g_ _g_____BB____r_________BB_g_ _g_____BB____r_________BB_g_ _g_____BB____r_________BB_g_ _g_____BB____r_________BB_g_ _g_____BBrrrrrrrrrrrrrrBB_g_ _g________________________g_ _g________________________g_ _g________________________g_ _gggggggggggggggggggggggggg_ ____________________________ ____________________________ _gggggggggggggggggggggggggg_ _g_____BBrrrrrrrrrrrrrrBB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BB____r____r____BB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g_____BBBBBBBBBBBBBBBBBB_g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _g________________________g_ _gggggggggggggggggggggggggg_ ____________________________ '''), tables_source % {'extra_css': ''' body { direction: rtl } x-table { border: solid #00f; border-width: 8px 2px; table-layout: fixed; border-collapse: collapse } x-td { border-color: #ff7f7f } @page { size: 28px 26px; margin: 1px; border: 1px solid rgba(0, 255, 0, 0.5); } '''}) @pytest.mark.xfail @assert_no_logs def test_tables_14(): assert_pixels('table_background_column_paged', 28, 52, to_pix(''' ____________________________ _RRR_RRR_RRR________________ _RRR_RRR_RRR________________ _RRR_RRR_RRR________________ _RRR_RRR_RRR________________ _RRR_RRR_RRR________________ _RRR_RRR_RRR________________ _RRR_RRR_RRR________________ _RRR_RRR_RRR________________ _RRR_RRR_RRR________________ _RRR_RRR_RRR________________ _____RRR____________________ _RRRRRRR_RRR________________ _RRRRRRR_RRR________________ _RRRRRRR_RRR________________ _RRRRRRR_RRR________________ _RRRRRRR_RRR________________ _RRRRRRR_RRR________________ _RRRRRRR_RRR________________ _RRRRRRR_RRR________________ _RRRRRRR_RRR________________ _RRRRRRR_RRR________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ _RRR_RRR____________________ _RRR_RRR____________________ _RRR_RRR____________________ _RRR_RRR____________________ _RRR_RRR____________________ _RRR_RRR____________________ _RRR_RRR____________________ _RRR_RRR____________________ _RRR_RRR____________________ _RRR_RRR____________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ ____________________________ '''), tables_source % {'extra_css': ''' @page { size: 28px 26px } x-table { margin: 0; padding: 0; border: 0 } x-col { background: red } x-td { padding: 0; width: 1px; height: 8px } '''}) @assert_no_logs def test_tables_15(): # Regression test for colspan in last body line with footer # https://github.com/Kozea/WeasyPrint/issues/1250 assert_pixels('colspan_last_row', 22, 36, ''' ______________________ __RRRRRRRRRRRRRRRRRR__ __R_____R____R_____R__ __R_____R____R_____R__ __R_____R____R_____R__ __RRRRRRRRRRRRRRRRRR__ __R_____R____R_____R__ __R_____R____R_____R__ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBB____R____R____BBB_ _BBB____R____R____BBB_ _BBB____R____R____BBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ ______________________ ______________________ __RRRRRRRRRRRRRRRRRR__ __R________________R__ __R________________R__ __R________________R__ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBB____R____R____BBB_ _BBB____R____R____BBB_ _BBB____R____R____BBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ _BBBBBBBBBBBBBBBBBBBB_ ______________________ ______________________ ______________________ ______________________ ''', '''
      ''') @assert_no_logs def test_tables_16(): assert_pixels('table_absolute', 20, 10, ''' ____________________ _RRRRRRRRRRR________ _R____R____R________ _R____R____R________ _R____R_RRRRRRRRRRR_ _RRRRRRRRRRR_R____R_ ________R____R____R_ ________R____R____R_ ________RRRRRRRRRRR_ ____________________ ''', '''
      ''') @assert_no_logs def test_tables_17(): assert_pixels('table_split_collapse', 16, 20, ''' ________________ _RRRRRRRRRRRRRR_ _RRRRRRRRRRRRRR_ _RR____RR____RR_ _RR_BB_RR_BB_RR_ _RR_BB_RR_BB_RR_ _RR_BB_RR____RR_ _RR_BB_RR____RR_ _RR____RR____RR_ ________________ ________________ _RR_BB_RR____RR_ _RR_BB_RR____RR_ _RR_BB_RR____RR_ _RR_BB_RR____RR_ _RR____RR____RR_ _RRRRRRRRRRRRRR_ _RRRRRRRRRRRRRR_ ________________ ________________ ''', '''
      ''') @assert_no_logs def test_tables_18(): assert_pixels('table_split_separate', 12, 22, ''' ____________ _RRRRRRRRRR_ _R________R_ _R_RRRRRR_R_ _R_R____R_R_ _R_R_BB_R_R_ _R_R_BB_R_R_ _R_R_BB_R_R_ _R_R_BB_R_R_ _R_R____R_R_ ____________ ____________ _R_R_BB_R_R_ _R_R_BB_R_R_ _R_R_BB_R_R_ _R_R_BB_R_R_ _R_R____R_R_ _R_RRRRRR_R_ _R________R_ _RRRRRRRRRR_ ____________ ____________ ''', '''
      a a a aa
      ''') @assert_no_logs def test_tables_19(): # Regression test: https://github.com/Kozea/WeasyPrint/issues/1523 assert_pixels('table_split_crash', 2, 8, ''' RR RR RR RR RR RR RR RR ''', '''
      a a a a
      a a a a
      ''') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639848160.8378367 weasyprint-54.1/tests/draw/test_text.py0000644000000000000000000003131200000000000016473 0ustar0000000000000000""" weasyprint.tests.test_draw.test_text ------------------------------------ Test how text is drawn. """ import pytest from . import assert_pixels def test_text_overflow_clip(): assert_pixels('text_overflow', 9, 7, ''' _________ _RRRRRRR_ _RRRRRRR_ _________ _RR__RRR_ _RR__RRR_ _________ ''', '''
      abcde
      a bcde
      ''') def test_text_overflow_ellipsis(): assert_pixels('text_overflow', 9, 16, ''' _________ _RRRRRR__ _RRRRRR__ _________ _RR__RR__ _RR__RR__ _________ _RRRRRR__ _RRRRRR__ _________ _RRRRRRR_ _RRRRRRR_ _________ _RRRRRRR_ _RRRRRRR_ _________ ''', '''
      abcde
      a bcde
      abcde
      abcde
      abcde
      ''') def test_text_align_rtl_trailing_whitespace(): # Test text alignment for rtl text with trailing space. # Test regression: https://github.com/Kozea/WeasyPrint/issues/1111 assert_pixels('text_overflow', 9, 9, ''' _________ _rrrrBBB_ _________ _rrrrBBB_ _________ _BBBrrrr_ _________ _BBBrrrr_ _________ ''', '''

      abc

      ‏abc

      abc

      ‏abc

      ''') def test_max_lines_ellipsis(): assert_pixels('max_lines_ellipsis', 10, 10, ''' BBBBBBBB__ BBBBBBBB__ BBBBBBBBBB BBBBBBBBBB __________ __________ __________ __________ __________ __________ ''', '''

      abcd efgh ijkl

      ''') @pytest.mark.xfail def test_max_lines_nested(): assert_pixels('max_lines_nested', 10, 12, ''' BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB rrrrrrrrrr rrrrrrrrrr rrrrrrrrrr rrrrrrrrrr BBBBBBBBBB BBBBBBBBBB __________ __________ ''', '''
      aaaaa aaaaa
      bbbbb bbbbb bbbbb bbbbb
      aaaaa aaaaa
      ''') def test_line_clamp(): assert_pixels('line_clamp', 10, 10, ''' BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBBBBBBBB BBBBBBBBBB __________ __________ __________ __________ ''', '''

      aa a bb b cc c dddd eeee ffff gggg hhhh

      ''') def test_line_clamp_none(): assert_pixels('line_clamp_none', 10, 10, ''' BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBB__BB__ __________ __________ __________ __________ ''', '''

      aa a bb b cc c

      ''') def test_line_clamp_number(): assert_pixels('line_clamp_number', 10, 10, ''' BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBB__BBBB BBBB__BBBB __________ __________ __________ __________ ''', '''

      aa a bb b cc c dddd eeee

      ''') @pytest.mark.xfail def test_ellipsis_nested(): assert_pixels('ellipsis_nested', 10, 10, ''' BBBBBB____ BBBBBB____ BBBBBB____ BBBBBB____ BBBBBB____ BBBBBB____ BBBBBB____ BBBBBB____ BBBBBBBB__ BBBBBBBB__ ''', '''

      aaa

      aaa

      aaa

      aaa

      aaa

      aaa

      ''') def test_text_align_right(): assert_pixels('text_align_right', 9, 6, ''' _________ __RR__RR_ __RR__RR_ ______RR_ ______RR_ _________ ''', '''
      a c e
      ''') def test_text_align_justify(): assert_pixels('text_align_justify', 9, 6, ''' _________ _RR___RR_ _RR___RR_ _RR______ _RR______ _________ ''', '''
      a c e
      ''') def test_text_word_spacing(): assert_pixels('text_word_spacing', 19, 4, ''' ___________________ _RR____RR____RR____ _RR____RR____RR____ ___________________ ''', '''
      a c e
      ''') def test_text_letter_spacing(): assert_pixels('text_letter_spacing', 19, 4, ''' ___________________ _RR____RR____RR____ _RR____RR____RR____ ___________________ ''', '''
      ace
      ''') def test_text_underline(): assert_pixels('text_underline', 13, 7, ''' _____________ _zzzzzzzzzzz_ _zRRRRRRRRRz_ _zRRRRRRRRRz_ _zBBBBBBBBBz_ _zzzzzzzzzzz_ _____________ ''', '''
      abc
      ''') def test_text_overline(): # Ascent value seems to be a bit random, don’t try to get the exact # position of the line assert_pixels('text_overline', 13, 7, ''' _____________ _zzzzzzzzzzz_ _zzzzzzzzzzz_ _zRRRRRRRRRz_ _zRRRRRRRRRz_ _zzzzzzzzzzz_ _____________ ''', '''
      abc
      ''') def test_text_line_through(): assert_pixels('text_line_through', 13, 7, ''' _____________ _zzzzzzzzzzz_ _zRRRRRRRRRz_ _zBBBBBBBBBz_ _zRRRRRRRRRz_ _zzzzzzzzzzz_ _____________ ''', '''
      abc
      ''') def test_zero_width_character(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1508 assert_pixels('zero_width_character', 6, 4, ''' ______ _RRRR_ _RRRR_ ______ ''', '''
      a‌b
      ''') def test_tabulation_character(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1515 assert_pixels('zero_width_character', 10, 4, ''' __________ _RR____RR_ _RR____RR_ __________ ''', '''
      a	b
      ''') ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/draw/test_transform.py0000644000000000000000000001564000000000000017530 0ustar0000000000000000""" weasyprint.tests.test_draw.test_transform ----------------------------------------- Test transformations. """ from ..testing_utils import assert_no_logs from . import assert_pixels @assert_no_logs def test_2d_transform_1(): assert_pixels('image_rotate90', 8, 8, ''' ________ ________ __BBBr__ __BBBB__ __BBBB__ __BBBB__ ________ ________ ''', '''
      ''') @assert_no_logs def test_2d_transform_2(): assert_pixels('image_translateX_rotate90', 12, 12, ''' ____________ ____________ _____BBBr___ _____BBBB___ _____BBBB___ _____BBBB___ ____________ ____________ ____________ ____________ ____________ ____________ ''', '''
      ''') @assert_no_logs def test_2d_transform_3(): # A translateX after the rotation is actually a translateY assert_pixels('image_rotate90_translateX', 12, 12, ''' ____________ ____________ ____________ ____________ ____________ __BBBr______ __BBBB______ __BBBB______ __BBBB______ ____________ ____________ ____________ ''', '''
      ''') @assert_no_logs def test_2d_transform_4(): assert_pixels('nested_rotate90_translateX', 12, 12, ''' ____________ ____________ ____________ ____________ ____________ __BBBr______ __BBBB______ __BBBB______ __BBBB______ ____________ ____________ ____________ ''', '''
      ''') @assert_no_logs def test_2d_transform_5(): assert_pixels('image_reflection', 8, 8, ''' ________ ________ __BBBr__ __BBBB__ __BBBB__ __BBBB__ ________ ________ ''', '''
      ''') @assert_no_logs def test_2d_transform_6(): assert_pixels('image_translate', 8, 8, ''' ________ ________ ________ ________ ___rBBB_ ___BBBB_ ___BBBB_ ___BBBB_ ''', '''
      ''') @assert_no_logs def test_2d_transform_7(): assert_pixels('image_translate_percentage', 8, 8, ''' ________ ________ ___rBBB_ ___BBBB_ ___BBBB_ ___BBBB_ ________ ________ ''', '''
      ''') @assert_no_logs def test_2d_transform_8(): assert_pixels('image_translateX', 8, 8, ''' ________ ________ _____rBB _____BBB _____BBB _____BBB ________ ________ ''', '''
      ''') @assert_no_logs def test_2d_transform_9(): assert_pixels('image_translateY', 8, 8, ''' ________ __rBBB__ __BBBB__ __BBBB__ __BBBB__ ________ ________ ________ ''', '''
      ''') @assert_no_logs def test_2d_transform_10(): assert_pixels('image_scale', 10, 10, ''' __________ _rrBBBBBB_ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ ''', '''
      ''') @assert_no_logs def test_2d_transform_11(): assert_pixels('image_scale12', 10, 10, ''' __________ __rBBB____ __rBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __________ ''', '''
      ''') @assert_no_logs def test_2d_transform_12(): assert_pixels('image_scaleY', 10, 10, ''' __________ __rBBB____ __rBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __________ ''', '''
      ''') @assert_no_logs def test_2d_transform_13(): assert_pixels('image_scaleX', 10, 10, ''' __________ __________ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ __________ __________ __________ ''', '''
      ''') ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/draw/test_visibility.py0000644000000000000000000000264600000000000017706 0ustar0000000000000000""" weasyprint.tests.test_draw.test_visibility ------------------------------------------ Test visibility. """ from ..testing_utils import assert_no_logs from . import assert_pixels visibility_source = '''
      ''' @assert_no_logs def test_visibility_1(): assert_pixels('visibility_reference', 12, 7, ''' ____________ _rBBB_rBBB__ _BBBB_BBBB__ _BBBB_BBBB__ _BBBB_BBBB__ ____________ ____________ ''', visibility_source % {'extra_css': ''}) @assert_no_logs def test_visibility_2(): assert_pixels('visibility_hidden', 12, 7, ''' ____________ ____________ ____________ ____________ ____________ ____________ ____________ ''', visibility_source % {'extra_css': 'div { visibility: hidden }'}) @assert_no_logs def test_visibility_3(): assert_pixels('visibility_mixed', 12, 7, ''' ____________ ______rBBB__ ______BBBB__ ______BBBB__ ______BBBB__ ____________ ____________ ''', visibility_source % {'extra_css': '''div { visibility: hidden } span { visibility: visible } '''}) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/layout/__init__.py0000644000000000000000000000024100000000000016564 0ustar0000000000000000""" weasyprint.tests.layout ----------------------- Tests for layout, ie. positioning and dimensioning of boxes, line breaks, page breaks. """ ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.024308 weasyprint-54.1/tests/layout/test_block.py0000644000000000000000000006473300000000000017176 0ustar0000000000000000""" weasyprint.tests.layout.block ----------------------------- Tests for blocks layout. """ import pytest from weasyprint.formatting_structure import boxes from ..testing_utils import assert_no_logs, render_pages @assert_no_logs def test_block_widths(): page, = render_pages('''

      ''') html, = page.children assert html.element_tag == 'html' body, = html.children assert body.element_tag == 'body' assert body.width == 120 divs = body.children paragraphs = [] for div in divs: assert isinstance(div, boxes.BlockBox) assert div.element_tag == 'div' assert div.width == 100 for paragraph in div.children: assert isinstance(paragraph, boxes.BlockBox) assert paragraph.element_tag == 'p' assert paragraph.padding_left == 2 assert paragraph.padding_right == 2 assert paragraph.border_left_width == 1 assert paragraph.border_right_width == 1 paragraphs.append(paragraph) assert len(paragraphs) == 15 # width is 'auto' assert paragraphs[0].width == 94 assert paragraphs[0].margin_left == 0 assert paragraphs[0].margin_right == 0 # No 'auto', over-constrained equation with ltr, the initial # 'margin-right: 0' was ignored. assert paragraphs[1].width == 50 assert paragraphs[1].margin_left == 0 # No 'auto', over-constrained equation with rtl, the initial # 'margin-left: 0' was ignored. assert paragraphs[2].width == 50 assert paragraphs[2].margin_right == 0 # width is 'auto' assert paragraphs[3].width == 64 assert paragraphs[3].margin_left == 20 # margin-right is 'auto' assert paragraphs[4].width == 50 assert paragraphs[4].margin_left == 20 # margin-left is 'auto' assert paragraphs[5].width == 50 assert paragraphs[5].margin_left == 24 # Both margins are 'auto', remaining space is split in half assert paragraphs[6].width == 50 assert paragraphs[6].margin_left == 22 # width is 'auto', other 'auto' are set to 0 assert paragraphs[7].width == 74 assert paragraphs[7].margin_left == 20 # width is 'auto', other 'auto' are set to 0 assert paragraphs[8].width == 74 assert paragraphs[8].margin_left == 0 # width is 'auto', other 'auto' are set to 0 assert paragraphs[9].width == 94 assert paragraphs[9].margin_left == 0 # sum of non-auto initially is too wide, set auto values to 0 assert paragraphs[10].width == 200 assert paragraphs[10].margin_left == 0 # Constrained by min-width, same as above assert paragraphs[11].width == 200 assert paragraphs[11].margin_left == 0 # Constrained by max-width, same as paragraphs[6] assert paragraphs[12].width == 50 assert paragraphs[12].margin_left == 22 # NOT constrained by min-width assert paragraphs[13].width == 94 assert paragraphs[13].margin_left == 0 # 70% assert paragraphs[14].width == 70 assert paragraphs[14].margin_left == 0 @assert_no_logs def test_block_heights_p(): page, = render_pages('''

      ''') html, = page.children body, = html.children heights = [div.height for div in body.children] assert heights == [90, 90 * 3, 20, 120, 20, 120, 90, 90] @assert_no_logs def test_block_heights_img(): page, = render_pages('''
      ''') html, = page.children body, = html.children heights = [div.height for div in body.children] assert heights == [40, 20, 20, 20, 20, 20] @assert_no_logs def test_block_heights_img_no_body_height(): # Same but with no height on body: percentage *-height is ignored page, = render_pages('''
      ''') html, = page.children body, = html.children heights = [div.height for div in body.children] assert heights == [40, 40, 20, 40, 20, 0] @assert_no_logs def test_block_percentage_heights_no_html_height(): page, = render_pages(''' ''') html, = page.children assert html.element_tag == 'html' body, = html.children assert body.element_tag == 'body' # Since html’s height depend on body’s, body’s 50% means 'auto' assert body.height == 0 @assert_no_logs def test_block_percentage_heights(): page, = render_pages(''' ''') html, = page.children assert html.element_tag == 'html' body, = html.children assert body.element_tag == 'body' # This time the percentage makes sense assert body.height == 150 @assert_no_logs @pytest.mark.parametrize('size', ( ('width: 10%; height: 1000px',), ('max-width: 10%; max-height: 1000px; height: 2000px',), ('width: 5%; min-width: 10%; min-height: 1000px',), ('width: 10%; height: 1000px; min-width: auto; max-height: none',), )) def test_box_sizing(size): # http://www.w3.org/TR/css3-ui/#box-sizing page, = render_pages('''
      ''' % size) html, = page.children body, = html.children div_1, div_2, div_3, div_4 = body.children for div in div_1, div_2: assert div.style['box_sizing'] == 'content-box' assert div.width == 1000 assert div.height == 1000 assert div.padding_width() == 1020 assert div.padding_height() == 1020 assert div.border_width() == 1022 assert div.border_height() == 1022 assert div.margin_height() == 1222 # margin_width() is the width of the containing block # padding-box assert div_3.style['box_sizing'] == 'padding-box' assert div_3.width == 980 # 1000 - 20 assert div_3.height == 980 assert div_3.padding_width() == 1000 assert div_3.padding_height() == 1000 assert div_3.border_width() == 1002 assert div_3.border_height() == 1002 assert div_3.margin_height() == 1202 # border-box assert div_4.style['box_sizing'] == 'border-box' assert div_4.width == 978 # 1000 - 20 - 2 assert div_4.height == 978 assert div_4.padding_width() == 998 assert div_4.padding_height() == 998 assert div_4.border_width() == 1000 assert div_4.border_height() == 1000 assert div_4.margin_height() == 1200 @assert_no_logs @pytest.mark.parametrize('size', ( ('width: 0; height: 0'), ('max-width: 0; max-height: 0'), ('min-width: 0; min-height: 0; width: 0; height: 0'), )) def test_box_sizing_zero(size): # http://www.w3.org/TR/css3-ui/#box-sizing page, = render_pages('''
      ''' % size) html, = page.children body, = html.children for div in body.children: assert div.width == 0 assert div.height == 0 assert div.padding_width() == 20 assert div.padding_height() == 20 assert div.border_width() == 22 assert div.border_height() == 22 assert div.margin_height() == 222 # margin_width() is the width of the containing block COLLAPSING = ( ('10px', '15px', 15), # not 25 # "The maximum of the absolute values of the negative adjoining margins is # deducted from the maximum of the positive adjoining margins" ('-10px', '15px', 5), ('10px', '-15px', -5), ('-10px', '-15px', -15), ('10px', 'auto', 10), # 'auto' is 0 ) NOT_COLLAPSING = ( ('10px', '15px', 25), ('-10px', '15px', 5), ('10px', '-15px', -5), ('-10px', '-15px', -25), ('10px', 'auto', 10), # 'auto' is 0 ) @pytest.mark.parametrize('margin_1, margin_2, result', COLLAPSING) def test_vertical_space_1(margin_1, margin_2, result): # Siblings page, = render_pages('''

      Lorem ipsum

      dolor sit amet ''' % (margin_1, margin_2)) html, = page.children body, = html.children p1, p2 = body.children p1_bottom = p1.content_box_y() + p1.height p2_top = p2.content_box_y() assert p2_top - p1_bottom == result @pytest.mark.parametrize('margin_1, margin_2, result', COLLAPSING) def test_vertical_space_2(margin_1, margin_2, result): # Not siblings, first is nested page, = render_pages('''

      Lorem ipsum

      dolor sit amet ''' % (margin_1, margin_2)) html, = page.children body, = html.children div, p2 = body.children p1, = div.children p1_bottom = p1.content_box_y() + p1.height p2_top = p2.content_box_y() assert p2_top - p1_bottom == result @pytest.mark.parametrize('margin_1, margin_2, result', COLLAPSING) def test_vertical_space_3(margin_1, margin_2, result): # Not siblings, second is nested page, = render_pages('''

      Lorem ipsum

      dolor sit amet

      ''' % (margin_1, margin_2)) html, = page.children body, = html.children p1, div = body.children p2, = div.children p1_bottom = p1.content_box_y() + p1.height p2_top = p2.content_box_y() assert p2_top - p1_bottom == result @pytest.mark.parametrize('margin_1, margin_2, result', COLLAPSING) def test_vertical_space_4(margin_1, margin_2, result): # Not siblings, second is doubly nested page, = render_pages('''

      Lorem ipsum

      dolor sit amet

      ''' % (margin_1, margin_2)) html, = page.children body, = html.children p1, div1 = body.children div2, = div1.children p2, = div2.children p1_bottom = p1.content_box_y() + p1.height p2_top = p2.content_box_y() assert p2_top - p1_bottom == result @pytest.mark.parametrize('margin_1, margin_2, result', COLLAPSING) def test_vertical_space_5(margin_1, margin_2, result): # Collapsing with children page, = render_pages('''

      Lorem ipsum

      dolor sit amet

      ''' % (margin_1, margin_2)) html, = page.children body, = html.children p1, div1 = body.children div2, = div1.children p2, = div2.children p1_bottom = p1.content_box_y() + p1.height p2_top = p2.content_box_y() # Parent and element edge are the same: assert div1.border_box_y() == p2.border_box_y() assert div2.border_box_y() == p2.border_box_y() assert p2_top - p1_bottom == result @pytest.mark.parametrize('margin_1, margin_2, result', NOT_COLLAPSING) def test_vertical_space_6(margin_1, margin_2, result): # Block formatting context: Not collapsing with children page, = render_pages('''

      Lorem ipsum

      dolor sit amet

      ''' % (margin_1, margin_2)) html, = page.children body, = html.children p1, div1 = body.children div2, = div1.children p2, = div2.children p1_bottom = p1.content_box_y() + p1.height p2_top = p2.content_box_y() assert p2_top - p1_bottom == result @pytest.mark.parametrize('margin_1, margin_2, result', COLLAPSING) def test_vertical_space_7(margin_1, margin_2, result): # Collapsing through an empty div page, = render_pages('''

      Lorem ipsum

      dolor sit amet ''' % (2 * (margin_1, margin_2))) html, = page.children body, = html.children p1, div, p2 = body.children p1_bottom = p1.content_box_y() + p1.height p2_top = p2.content_box_y() assert p2_top - p1_bottom == result @pytest.mark.parametrize('margin_1, margin_2, result', NOT_COLLAPSING) def test_vertical_space_8(margin_1, margin_2, result): # The root element does not collapse page, = render_pages('''

      Lorem ipsum ''' % (margin_1, margin_2)) html, = page.children body, = html.children p1, = body.children p1_top = p1.content_box_y() # Vertical space from y=0 assert p1_top == result @pytest.mark.parametrize('margin_1, margin_2, result', COLLAPSING) def test_vertical_space_9(margin_1, margin_2, result): # DOES collapse page, = render_pages('''

      Lorem ipsum ''' % (margin_1, margin_2)) html, = page.children body, = html.children div, = body.children p1, = div.children p1_top = p1.content_box_y() # Vertical space from y=0 assert p1_top == result @assert_no_logs def test_box_decoration_break_block_slice(): # http://www.w3.org/TR/css3-background/#the-box-decoration-break page_1, page_2 = render_pages('''

      ''') html, = page_1.children body, = html.children paragraph, = body.children img_1, img_2 = paragraph.children assert paragraph.position_y == 0 assert paragraph.margin_top == 5 assert paragraph.border_top_width == 3 assert paragraph.padding_top == 2 assert paragraph.content_box_y() == 10 assert img_1.position_y == 10 assert img_2.position_y == 50 assert paragraph.height == 90 assert paragraph.margin_bottom == 0 assert paragraph.border_bottom_width == 0 assert paragraph.padding_bottom == 0 assert paragraph.margin_height() == 100 html, = page_2.children body, = html.children paragraph, = body.children img_1, img_2 = paragraph.children assert paragraph.position_y == 0 assert paragraph.margin_top == 0 assert paragraph.border_top_width == 0 assert paragraph.padding_top == 0 assert paragraph.content_box_y() == 0 assert img_1.position_y == 0 assert img_2.position_y == 40 assert paragraph.height == 80 assert paragraph.padding_bottom == 2 assert paragraph.border_bottom_width == 3 assert paragraph.margin_bottom == 5 assert paragraph.margin_height() == 90 @assert_no_logs def test_box_decoration_break_block_clone(): # http://www.w3.org/TR/css3-background/#the-box-decoration-break page_1, page_2 = render_pages('''

      ''') html, = page_1.children body, = html.children paragraph, = body.children img_1, img_2 = paragraph.children assert paragraph.position_y == 0 assert paragraph.margin_top == 5 assert paragraph.border_top_width == 3 assert paragraph.padding_top == 2 assert paragraph.content_box_y() == 10 assert img_1.position_y == 10 assert img_2.position_y == 50 assert paragraph.height == 80 # TODO: bottom margin should be 0 # https://www.w3.org/TR/css-break-3/#valdef-box-decoration-break-clone # "Cloned margins are truncated on block-level boxes." # See https://github.com/Kozea/WeasyPrint/issues/115 assert paragraph.margin_bottom == 5 assert paragraph.border_bottom_width == 3 assert paragraph.padding_bottom == 2 assert paragraph.margin_height() == 100 html, = page_2.children body, = html.children paragraph, = body.children img_1, img_2 = paragraph.children assert paragraph.position_y == 0 assert paragraph.margin_top == 0 assert paragraph.border_top_width == 3 assert paragraph.padding_top == 2 assert paragraph.content_box_y() == 5 assert img_1.position_y == 5 assert img_2.position_y == 45 assert paragraph.height == 80 assert paragraph.padding_bottom == 2 assert paragraph.border_bottom_width == 3 assert paragraph.margin_bottom == 5 assert paragraph.margin_height() == 95 @assert_no_logs def test_box_decoration_break_clone_bottom_padding(): page_1, page_2 = render_pages('''

      a
      b
      c
      ''') html, = page_1.children body, = html.children article, = body.children assert article.height == 80 - 2 * 12 div_1, div_2 = article.children assert div_1.position_y == 12 assert div_2.position_y == 12 + 20 html, = page_2.children body, = html.children article, = body.children assert article.height == 20 div, = article.children assert div.position_y == 12 @pytest.mark.xfail @assert_no_logs def test_box_decoration_break_slice_bottom_padding(): # pragma: no cover # Last div fits in first, but not article's padding. As it is impossible to # break between a parent and its last child, put last child on next page. # TODO: at the end of block_container_layout, we should check that the box # with its bottom border/padding doesn't cross the bottom line. If it does, # we should re-render the box with a bottom_space including the bottom # border/padding. page_1, page_2 = render_pages('''
      a
      b
      c
      ''') html, = page_1.children body, = html.children article, = body.children assert article.height == 80 - 12 div_1, div_2 = article.children assert div_1.position_y == 12 assert div_2.position_y == 12 + 20 html, = page_2.children body, = html.children article, = body.children assert article.height == 20 div, = article.children assert div.position_y == 0 @assert_no_logs def test_overflow_auto(): page, = render_pages('''
      bla bla bla
      toto toto''') html, = page.children body, = html.children article, = body.children assert article.height == 50 + 10 + 10 @assert_no_logs def test_box_margin_top_repagination(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/943 page_1, page_2 = render_pages('''

      1
      1
      2
      2

      title

      ''') html, = page_1.children body, = html.children p, div = body.children assert div.margin_top == 20 assert div.padding_box_y() == 10 + 20 html, = page_2.children body, = html.children div, h1 = body.children assert div.margin_top == 0 assert div.padding_box_y() == 0 @assert_no_logs def test_continue_discard(): page_1, = render_pages('''
      a
      b
      c
      d
      e
      f
      ''') html, = page_1.children body, = html.children article, = body.children assert article.height == 3 * 25 div_1, div_2, div_3 = article.children assert div_1.position_y == 1 assert div_2.position_y == 1 + 25 assert div_3.position_y == 1 + 25 * 2 assert article.border_bottom_width == 1 @assert_no_logs def test_continue_discard_children(): page_1, = render_pages('''
      a
      b
      c
      d
      e
      f
      ''') html, = page_1.children body, = html.children article, = body.children assert article.height == 2 + 3 * 25 section, = article.children assert section.height == 3 * 25 div_1, div_2, div_3 = section.children assert div_1.position_y == 2 assert div_2.position_y == 2 + 25 assert div_3.position_y == 2 + 25 * 2 assert article.border_bottom_width == 1 @assert_no_logs def test_block_in_block_with_bottom_padding(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1476 page_1, page_2 = render_pages(''' abc def

      ghi jkl mno pqr

      stu vwx''') html, = page_1.children body, = html.children anon_body, div = body.children line, = anon_body.children assert line.height == 16 assert line.children[0].text == 'abc def' p, = div.children line, = p.children assert line.height == 16 assert line.children[0].text == 'ghi jkl' html, = page_2.children body, = html.children div, anon_body = body.children p, = div.children line, = p.children assert line.height == 16 assert line.children[0].text == 'mno pqr' line, = anon_body.children assert line.height == 16 assert line.content_box_y() == 16 + 16 # p content + div padding assert line.children[0].text == 'stu vwx' ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.025308 weasyprint-54.1/tests/layout/test_column.py0000644000000000000000000003434300000000000017373 0ustar0000000000000000""" weasyprint.tests.layout.column ------------------------------ Tests for multicolumn layout. """ import pytest from ..testing_utils import assert_no_logs, render_pages @assert_no_logs @pytest.mark.parametrize('css', ( 'columns: 4', 'columns: 100px', 'columns: 4 100px', 'columns: 100px 4', 'column-width: 100px', 'column-count: 4', )) def test_columns(css): page, = render_pages('''
      Ipsum dolor sit amet, consectetur adipiscing elit. Sed sollicitudin nibh et turpis molestie tristique.
      ''' % css) html, = page.children body, = html.children div, = body.children columns = div.children assert len(columns) == 4 assert [column.width for column in columns] == [100, 100, 100, 100] assert [column.position_x for column in columns] == [0, 100, 200, 300] assert [column.position_y for column in columns] == [0, 0, 0, 0] @pytest.mark.parametrize('value, width', ( ('normal', 16), # "normal" is 1em = 16px ('unknown', 16), # default value is normal ('15px', 15), ('40%', 16), # percentages are not allowed ('-1em', 16), # negative values are not allowed )) def test_column_gap(value, width): page, = render_pages('''
      Ipsum dolor sit amet, consectetur adipiscing elit. Sed sollicitudin nibh et turpis molestie tristique.
      ''' % value) html, = page.children body, = html.children div, = body.children columns = div.children assert len(columns) == 3 assert [column.width for column in columns] == ( 3 * [100 - 2 * width / 3]) assert [column.position_x for column in columns] == ( [0, 100 + width / 3, 200 + 2 * width / 3]) assert [column.position_y for column in columns] == [0, 0, 0] @assert_no_logs def test_column_span_1(): page, = render_pages('''
      abc def
      test
      test
      ghi jkl
      ''') html, = page.children body, = html.children div, = body.children column1, column2, section1, section2, column3, column4 = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (5 * 16, 0) assert (section1.content_box_x(), section1.content_box_y()) == (0, 32) assert (section2.content_box_x(), section2.content_box_y()) == (0, 64) assert (column3.position_x, column3.position_y) == (0, 96) assert (column4.position_x, column4.position_y) == (5 * 16, 96) assert column1.height == 16 @assert_no_logs def test_column_span_2(): page, = render_pages('''
      test
      abc def ghi jkl mno pqr stu vwx
      ''') html, = page.children body, = html.children div, = body.children section1, column1, column2 = div.children assert (section1.content_box_x(), section1.content_box_y()) == (0, 16) assert (column1.position_x, column1.position_y) == (0, 3 * 16) assert (column2.position_x, column2.position_y) == (5 * 16, 3 * 16) assert column1.height == column2.height == 16 * 4 @assert_no_logs def test_columns_multipage(): page1, page2 = render_pages('''
      a b c d e f g
      ''') html, = page1.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert len(columns[0].children) == 2 assert len(columns[1].children) == 2 columns[0].children[0].children[0].text == 'a' columns[0].children[1].children[0].text == 'b' columns[1].children[0].children[0].text == 'c' columns[1].children[1].children[0].text == 'd' html, = page2.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert len(columns[0].children) == 2 assert len(columns[1].children) == 1 columns[0].children[0].children[0].text == 'e' columns[0].children[1].children[0].text == 'f' columns[1].children[0].children[0].text == 'g' @assert_no_logs def test_columns_not_enough_content(): page, = render_pages('''
      a b c
      ''') html, = page.children body, = html.children div, = body.children assert div.width == 5 columns = div.children assert len(columns) == 3 assert [column.width for column in columns] == [1, 1, 1] assert [column.position_x for column in columns] == [0, 1, 2] assert [column.position_y for column in columns] == [0, 0, 0] @assert_no_logs def test_columns_empty(): page, = render_pages('''
      ''') html, = page.children body, = html.children div, = body.children assert div.width == 3 assert div.height == 0 columns = div.children assert len(columns) == 0 @assert_no_logs @pytest.mark.parametrize('prop', ('height', 'min-height')) def test_columns_fixed_height(prop): # TODO: we should test when the height is too small page, = render_pages('''
      a b c
      ''' % prop) html, = page.children body, = html.children div, = body.children assert div.width == 4 columns = div.children assert len(columns) == 3 assert [column.width for column in columns] == [1, 1, 1] assert [column.height for column in columns] == [10, 10, 10] assert [column.position_x for column in columns] == [0, 1, 2] assert [column.position_y for column in columns] == [0, 0, 0] @assert_no_logs def test_columns_padding(): page, = render_pages('''
      a b c
      ''') html, = page.children body, = html.children div, = body.children assert div.width == 4 assert div.height == 1 assert div.padding_width() == 6 assert div.padding_height() == 3 columns = div.children assert len(columns) == 3 assert [column.width for column in columns] == [1, 1, 1] assert [column.height for column in columns] == [1, 1, 1] assert [column.position_x for column in columns] == [1, 2, 3] assert [column.position_y for column in columns] == [1, 1, 1] @assert_no_logs def test_columns_relative(): page, = render_pages('''
      a b c d
      e
      ''') html, = page.children body, = html.children div, = body.children assert div.width == 4 columns = div.children assert [column.width for column in columns] == [1, 1, 1, 1] assert [column.position_x for column in columns] == [2, 3, 4, 5] assert [column.position_y for column in columns] == [1, 1, 1, 1] column4 = columns[-1] column_line, = column4.children _, absolute_article = column_line.children absolute_line, = absolute_article.children span, = absolute_line.children assert span.position_x == 5 # Default position of the 4th column assert span.position_y == 4 # div's 1px + span's 3px @assert_no_logs def test_columns_regression_1(): # Regression test #1 for https://github.com/Kozea/WeasyPrint/issues/659 page1, page2, page3 = render_pages('''
      A
      B1
      B2
      B3
      C
      ''') html, = page1.children body, = html.children div, = body.children assert div.position_y == 0 assert div.children[0].children[0].text == 'A' html, = page2.children body, = html.children div, = body.children assert div.position_y == 0 column1, column2 = div.children assert column1.position_y == column2.position_y == 0 div1, div2 = column1.children div3, = column2.children assert div1.position_y == div3.position_y == 0 assert div2.position_y == 20 assert div1.children[0].children[0].text == 'B1' assert div2.children[0].children[0].text == 'B2' assert div3.children[0].children[0].text == 'B3' html, = page3.children body, = html.children div, = body.children assert div.position_y == 0 assert div.children[0].children[0].text == 'C' @assert_no_logs def test_columns_regression_2(): # Regression test #2 for https://github.com/Kozea/WeasyPrint/issues/659 page1, page2 = render_pages('''
      B1
      B2
      B3
      B4
      ''') html, = page1.children body, = html.children div, = body.children assert div.position_y == 0 column1, column2 = div.children assert column1.position_y == column2.position_y == 0 div1, div2 = column1.children div3, = column2.children assert div1.position_y == div3.position_y == 0 assert div2.position_y == 20 assert div1.children[0].children[0].text == 'B1' assert div2.children[0].children[0].text == 'B2' assert div3.children[0].children[0].text == 'B3' html, = page2.children body, = html.children div, = body.children assert div.position_y == 0 column1, = div.children assert column1.position_y == 0 div1, = column1.children assert div1.position_y == div3.position_y == 0 assert div1.children[0].children[0].text == 'B4' @assert_no_logs def test_columns_regression_3(): # Regression test #3 for https://github.com/Kozea/WeasyPrint/issues/659 page, = render_pages('''
      B1
      B2
      B3
      ''') html, = page.children body, = html.children div, = body.children assert div.position_y == 0 column1, column2 = div.children assert column1.position_y == column2.position_y == 0 div1, div2 = column1.children div3, = column2.children assert div1.position_y == div3.position_y == 0 assert div2.position_y == 30 assert div.height == 5 + 20 + 5 + 60 assert div1.children[0].children[0].text == 'B1' assert div2.children[0].children[0].text == 'B2' assert div3.children[0].children[0].text == 'B3' @assert_no_logs def test_columns_regression_4(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/897 page, = render_pages('''
      a
      ''') html, = page.children body, = html.children div, = body.children assert div.position_y == 0 column1, = div.children assert column1.position_y == 0 div1, = column1.children assert div1.position_y == 0 @assert_no_logs def test_columns_regression_5(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1191 render_pages('''

      a

      ''') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1642966676.1268482 weasyprint-54.1/tests/layout/test_flex.py0000644000000000000000000004137700000000000017041 0ustar0000000000000000""" weasyprint.tests.layout.flex ---------------------------- Tests for flex layout. """ import pytest from ..testing_utils import assert_no_logs, render_pages @assert_no_logs def test_flex_direction_row(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert ( div_1.position_y == div_2.position_y == div_3.position_y == article.position_y) assert div_1.position_x == article.position_x assert div_1.position_x < div_2.position_x < div_3.position_x @assert_no_logs def test_flex_direction_row_rtl(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert ( div_1.position_y == div_2.position_y == div_3.position_y == article.position_y) assert ( div_1.position_x + div_1.width == article.position_x + article.width) assert div_1.position_x > div_2.position_x > div_3.position_x @assert_no_logs def test_flex_direction_row_reverse(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'C' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'A' assert ( div_1.position_y == div_2.position_y == div_3.position_y == article.position_y) assert ( div_3.position_x + div_3.width == article.position_x + article.width) assert div_1.position_x < div_2.position_x < div_3.position_x @assert_no_logs def test_flex_direction_row_reverse_rtl(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'C' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'A' assert ( div_1.position_y == div_2.position_y == div_3.position_y == article.position_y) assert div_3.position_x == article.position_x assert div_1.position_x > div_2.position_x > div_3.position_x @assert_no_logs def test_flex_direction_column(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert ( div_1.position_x == div_2.position_x == div_3.position_x == article.position_x) assert div_1.position_y == article.position_y assert div_1.position_y < div_2.position_y < div_3.position_y @assert_no_logs def test_flex_direction_column_rtl(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert ( div_1.position_x == div_2.position_x == div_3.position_x == article.position_x) assert div_1.position_y == article.position_y assert div_1.position_y < div_2.position_y < div_3.position_y @assert_no_logs def test_flex_direction_column_reverse(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'C' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'A' assert ( div_1.position_x == div_2.position_x == div_3.position_x == article.position_x) assert ( div_3.position_y + div_3.height == article.position_y + article.height) assert div_1.position_y < div_2.position_y < div_3.position_y @assert_no_logs def test_flex_direction_column_reverse_rtl(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'C' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'A' assert ( div_1.position_x == div_2.position_x == div_3.position_x == article.position_x) assert ( div_3.position_y + div_3.height == article.position_y + article.height) assert div_1.position_y < div_2.position_y < div_3.position_y @assert_no_logs def test_flex_row_wrap(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert div_1.position_y == div_2.position_y == article.position_y assert div_3.position_y == article.position_y + div_2.height assert div_1.position_x == div_3.position_x == article.position_x assert div_1.position_x < div_2.position_x @assert_no_logs def test_flex_column_wrap(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert div_1.position_x == div_2.position_x == article.position_x assert div_3.position_x == article.position_x + div_2.width assert div_1.position_y == div_3.position_y == article.position_y assert div_1.position_y < div_2.position_y @assert_no_logs def test_flex_row_wrap_reverse(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'C' assert div_2.children[0].children[0].text == 'A' assert div_3.children[0].children[0].text == 'B' assert div_1.position_y == article.position_y assert ( div_2.position_y == div_3.position_y == article.position_y + div_1.height) assert div_1.position_x == div_2.position_x == article.position_x assert div_2.position_x < div_3.position_x @assert_no_logs def test_flex_column_wrap_reverse(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'C' assert div_2.children[0].children[0].text == 'A' assert div_3.children[0].children[0].text == 'B' assert div_1.position_x == article.position_x assert ( div_2.position_x == div_3.position_x == article.position_x + div_1.width) assert div_1.position_y == div_2.position_y == article.position_y assert div_2.position_y < div_3.position_y @assert_no_logs def test_flex_direction_column_fixed_height_container(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children section, = body.children article, = section.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert ( div_1.position_x == div_2.position_x == div_3.position_x == article.position_x) assert div_1.position_y == article.position_y assert div_1.position_y < div_2.position_y < div_3.position_y assert section.height == 10 assert article.height > 10 @pytest.mark.xfail @assert_no_logs def test_flex_direction_column_fixed_height(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert ( div_1.position_x == div_2.position_x == div_3.position_x == article.position_x) assert div_1.position_y == article.position_y assert div_1.position_y < div_2.position_y < div_3.position_y assert article.height == 10 assert div_3.position_y > 10 @assert_no_logs def test_flex_direction_column_fixed_height_wrap(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert ( div_1.position_x != div_2.position_x != div_3.position_x) assert div_1.position_y == article.position_y assert ( div_1.position_y == div_2.position_y == div_3.position_y == article.position_y) assert article.height == 10 @assert_no_logs def test_flex_item_min_width(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert div_1.position_x == 0 assert div_1.width == 30 assert div_2.position_x == 30 assert div_2.width == 50 assert div_3.position_x == 80 assert div_3.width > 5 assert ( div_1.position_y == div_2.position_y == div_3.position_y == article.position_y) @assert_no_logs def test_flex_item_min_height(): page, = render_pages('''
      A
      B
      C
      ''') html, = page.children body, = html.children article, = body.children div_1, div_2, div_3 = article.children assert div_1.children[0].children[0].text == 'A' assert div_2.children[0].children[0].text == 'B' assert div_3.children[0].children[0].text == 'C' assert ( div_1.height == div_2.height == div_3.height == article.height == 50) @assert_no_logs def test_flex_auto_margin(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/800 page, = render_pages('
      ') page, = render_pages( '
      ') @assert_no_logs def test_flex_no_baseline(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/765 page, = render_pages('''
      ''') @assert_no_logs @pytest.mark.parametrize('align, height, y1, y2', ( ('flex-start', 50, 0, 10), ('flex-end', 50, 30, 40), ('space-around', 60, 10, 40), ('space-between', 50, 0, 40), ('space-evenly', 50, 10, 30), )) def test_flex_align_content(align, height, y1, y2): # Regression test for https://github.com/Kozea/WeasyPrint/issues/811 page, = render_pages('''
      Lorem
      Lorem
      ''' % (align, height)) html, = page.children body, = html.children article, = body.children section1, section2 = article.children line1, = section1.children line2, = section2.children span1, = line1.children span2, = line2.children assert section1.position_x == span1.position_x == 0 assert section1.position_y == span1.position_y == y1 assert section2.position_x == span2.position_x == 0 assert section2.position_y == span2.position_y == y2 @assert_no_logs def test_flex_item_percentage(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/885 page, = render_pages('''
      a
      ''') html, = page.children body, = html.children flex, = body.children flex_item, = flex.children assert flex_item.height == 15 @assert_no_logs def test_flex_undefined_percentage_height_multiple_lines(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1204 page, = render_pages('''
      a
      b
      ''') @assert_no_logs def test_flex_absolute(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1536 page, = render_pages('''
      a
      ''') ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.025308 weasyprint-54.1/tests/layout/test_footnotes.py0000644000000000000000000004541700000000000020122 0ustar0000000000000000""" weasyprint.tests.layout.footnotes --------------------------------- Tests for footnotes layout. """ import pytest from ..testing_utils import assert_no_logs, render_pages @assert_no_logs def test_inline_footnote(): page, = render_pages('''
      abcde
      ''') html, footnote_area = page.children body, = html.children div, = body.children div_textbox, footnote_call = div.children[0].children assert div_textbox.text == 'abc' assert footnote_call.children[0].text == '1' assert div_textbox.position_y == 0 footnote_marker, footnote_textbox = ( footnote_area.children[0].children[0].children) assert footnote_marker.children[0].text == '1.' assert footnote_textbox.text == 'de' assert footnote_area.position_y == 5 @assert_no_logs def test_block_footnote(): page, = render_pages('''
      abc
      de
      ''') html, footnote_area = page.children body, = html.children div, = body.children div_textbox, footnote_call = div.children[0].children assert div_textbox.text == 'abc' assert footnote_call.children[0].text == '1' assert div_textbox.position_y == 0 footnote_marker, footnote_textbox = ( footnote_area.children[0].children[0].children) assert footnote_marker.children[0].text == '1.' assert footnote_textbox.text == 'de' assert footnote_area.position_y == 5 @assert_no_logs def test_long_footnote(): page, = render_pages('''
      abcde f
      ''') html, footnote_area = page.children body, = html.children div, = body.children div_textbox, footnote_call = div.children[0].children assert div_textbox.text == 'abc' assert footnote_call.children[0].text == '1' assert div_textbox.position_y == 0 footnote_line1, footnote_line2 = footnote_area.children[0].children footnote_marker, footnote_content1 = footnote_line1.children footnote_content2 = footnote_line2.children[0] assert footnote_marker.children[0].text == '1.' assert footnote_content1.text == 'de' assert footnote_area.position_y == 3 assert footnote_content2.text == 'f' assert footnote_content2.position_y == 5 @pytest.mark.xfail @assert_no_logs def test_after_marker_footnote(): # TODO: this syntax is in the specification, but we’re currently limited to # one pseudo element per selector, according to CSS 2.1: # https://drafts.csswg.org/css2/#selector-syntax # and Selectors Level 3: # https://drafts.csswg.org/selectors-3/#selector-syntax # This limitation doesn’t exist anymore in Selectors Level 4: # https://drafts.csswg.org/selectors-4/#typedef-compound-selector page, = render_pages('''
      abcde
      ''') html, footnote_area = page.children footnote_marker, _ = footnote_area.children[0].children[0].children assert footnote_marker.children[0].text == '1.|' @assert_no_logs def test_several_footnote(): page1, page2, = render_pages('''
      abcd efg hijk lmn
      ''') html1, footnote_area1 = page1.children body1, = html1.children div1, = body1.children div1_line1, div1_line2 = div1.children assert div1_line1.children[0].text == 'abcd' div1_line2_text, div1_footnote_call = div1.children[1].children assert div1_line2_text.text == 'e' assert div1_footnote_call.children[0].text == '1' footnote_marker1, footnote_textbox1 = ( footnote_area1.children[0].children[0].children) assert footnote_marker1.children[0].text == '1.' assert footnote_textbox1.text == 'fg' html2, footnote_area2 = page2.children body2, = html2.children div2, = body2.children div2_line1, div2_line2 = div2.children assert div2_line1.children[0].text == 'hijk' div2_line2_text, div2_footnote_call = div2.children[1].children assert div2_line2_text.text == 'l' assert div2_footnote_call.children[0].text == '2' footnote_marker2, footnote_textbox2 = ( footnote_area2.children[0].children[0].children) assert footnote_marker2.children[0].text == '2.' assert footnote_textbox2.text == 'mn' @assert_no_logs def test_reported_footnote_1(): page1, page2, = render_pages('''
      abcf1 hijf2
      ''') html1, footnote_area1 = page1.children body1, = html1.children div1, = body1.children div_line1, div_line2 = div1.children div_line1_text, div_footnote_call1 = div_line1.children assert div_line1_text.text == 'abc' assert div_footnote_call1.children[0].text == '1' div_line2_text, div_footnote_call2 = div_line2.children assert div_line2_text.text == 'hij' assert div_footnote_call2.children[0].text == '2' footnote_marker1, footnote_textbox1 = ( footnote_area1.children[0].children[0].children) assert footnote_marker1.children[0].text == '1.' assert footnote_textbox1.text == 'f1' html2, footnote_area2 = page2.children assert not html2.children footnote_marker2, footnote_textbox2 = ( footnote_area2.children[0].children[0].children) assert footnote_marker2.children[0].text == '2.' assert footnote_textbox2.text == 'f2' @assert_no_logs def test_reported_footnote_2(): page1, page2, = render_pages('''
      abcf1 hijf2 wow
      ''') html1, footnote_area1 = page1.children body1, = html1.children div1, = body1.children div_line1, div_line2 = div1.children div_line1_text, div_footnote_call1 = div_line1.children assert div_line1_text.text == 'abc' assert div_footnote_call1.children[0].text == '1' div_line2_text, div_footnote_call2 = div_line2.children assert div_line2_text.text == 'hij' assert div_footnote_call2.children[0].text == '2' footnote_marker1, footnote_textbox1 = ( footnote_area1.children[0].children[0].children) assert footnote_marker1.children[0].text == '1.' assert footnote_textbox1.text == 'f1' html2, footnote_area2 = page2.children body2, = html2.children div2, = body2.children div2_line, = div2.children assert div2_line.children[0].text == 'wow' footnote_marker2, footnote_textbox2 = ( footnote_area2.children[0].children[0].children) assert footnote_marker2.children[0].text == '2.' assert footnote_textbox2.text == 'f2' @assert_no_logs def test_reported_footnote_3(): page1, page2, = render_pages('''
      abc1 defv long 2 ghi3
      ''') html1, footnote_area1 = page1.children body1, = html1.children div1, = body1.children line1, line2, line3 = div1.children assert line1.children[0].text == 'abc' assert line1.children[1].children[0].text == '1' assert line2.children[0].text == 'def' assert line2.children[1].children[0].text == '2' assert line3.children[0].text == 'ghi' assert line3.children[1].children[0].text == '3' footnote1, = footnote_area1.children assert footnote1.children[0].children[0].children[0].text == '1.' assert footnote1.children[0].children[1].text == '1' html2, footnote_area2 = page2.children footnote2, footnote3 = footnote_area2.children assert footnote2.children[0].children[0].children[0].text == '2.' assert footnote2.children[0].children[1].text == 'v' assert footnote2.children[1].children[0].text == 'long' assert footnote2.children[2].children[0].text == '2' assert footnote3.children[0].children[0].children[0].text == '3.' assert footnote3.children[0].children[1].text == '3' @assert_no_logs def test_footnote_display_inline(): page, = render_pages('''
      abcd fghi
      ''') html, footnote_area = page.children body, = html.children div, = body.children div_line1, div_line2 = div.children div_textbox1, footnote_call1 = div_line1.children div_textbox2, footnote_call2 = div_line2.children assert div_textbox1.text == 'abc' assert div_textbox2.text == 'fgh' assert footnote_call1.children[0].text == '1' assert footnote_call2.children[0].text == '2' line = footnote_area.children[0] footnote_mark1, footnote_textbox1 = line.children[0].children footnote_mark2, footnote_textbox2 = line.children[1].children assert footnote_mark1.children[0].text == '1.' assert footnote_textbox1.text == 'd' assert footnote_mark2.children[0].text == '2.' assert footnote_textbox2.text == 'i' @assert_no_logs def test_footnote_longer_than_space_left(): page1, page2 = render_pages('''
      abcdef ghi jkl
      ''') html1, = page1.children body1, = html1.children div, = body1.children div_textbox, footnote_call = div.children[0].children assert div_textbox.text == 'abc' assert footnote_call.children[0].text == '1' html2, footnote_area = page2.children assert not html2.children footnote_line1, footnote_line2, footnote_line3 = ( footnote_area.children[0].children) footnote_marker, footnote_content1 = footnote_line1.children footnote_content2 = footnote_line2.children[0] footnote_content3 = footnote_line3.children[0] assert footnote_marker.children[0].text == '1.' assert footnote_content1.text == 'def' assert footnote_content2.text == 'ghi' assert footnote_content3.text == 'jkl' @assert_no_logs def test_footnote_longer_than_page(): # Nothing is defined for this use case in the specification. In WeasyPrint, # the content simply overflows. page1, page2 = render_pages('''
      abcdef ghi jkl mno
      ''') html1, = page1.children body1, = html1.children div, = body1.children div_textbox, footnote_call = div.children[0].children assert div_textbox.text == 'abc' assert footnote_call.children[0].text == '1' html2, footnote_area2 = page2.children assert not html2.children footnote_line1, footnote_line2, footnote_line3, footnote_line4 = ( footnote_area2.children[0].children) footnote_marker1, footnote_content1 = footnote_line1.children footnote_content2 = footnote_line2.children[0] footnote_content3 = footnote_line3.children[0] footnote_content4 = footnote_line4.children[0] assert footnote_marker1.children[0].text == '1.' assert footnote_content1.text == 'def' assert footnote_content2.text == 'ghi' assert footnote_content3.text == 'jkl' assert footnote_content4.text == 'mno' @assert_no_logs def test_footnote_policy_line(): page1, page2 = render_pages('''
      abc def ghi jkl1
      ''') html, = page1.children body, = html.children div, = body.children linebox1, linebox2 = div.children assert linebox1.children[0].text == 'abc' assert linebox2.children[0].text == 'def' html, footnote_area = page2.children body, = html.children div, = body.children linebox1, linebox2 = div.children assert linebox1.children[0].text == 'ghi' assert linebox2.children[0].text == 'jkl' assert linebox2.children[1].children[0].text == '1' footnote_marker, footnote_textbox = ( footnote_area.children[0].children[0].children) assert footnote_marker.children[0].text == '1.' assert footnote_textbox.text == '1' @assert_no_logs def test_footnote_policy_block(): page1, page2 = render_pages('''
      abc
      def ghi jkl1
      ''') html, = page1.children body, = html.children div, = body.children linebox1, = div.children assert linebox1.children[0].text == 'abc' html, footnote_area = page2.children body, = html.children div, = body.children linebox1, linebox2, linebox3 = div.children assert linebox1.children[0].text == 'def' assert linebox2.children[0].text == 'ghi' assert linebox3.children[0].text == 'jkl' assert linebox3.children[1].children[0].text == '1' footnote_marker, footnote_textbox = ( footnote_area.children[0].children[0].children) assert footnote_marker.children[0].text == '1.' assert footnote_textbox.text == '1' @assert_no_logs def test_footnote_repagination(): page, = render_pages('''
      abde
      ''') html, footnote_area = page.children body, = html.children div, = body.children div_textbox, footnote_call, div_after = div.children[0].children assert div_textbox.text == 'ab' assert footnote_call.children[0].text == '1' assert div_textbox.position_y == 0 assert div_after.children[0].text == '1' footnote_marker, footnote_textbox = ( footnote_area.children[0].children[0].children) assert footnote_marker.children[0].text == '1.' assert footnote_textbox.text == 'de' assert footnote_area.position_y == 5 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.025308 weasyprint-54.1/tests/layout/test_image.py0000644000000000000000000004517300000000000017163 0ustar0000000000000000""" weasyprint.tests.layout.image ----------------------------- Tests for image layout. """ import pytest from weasyprint.formatting_structure import boxes from ..testing_utils import assert_no_logs, capture_logs, render_pages def get_img(html): page, = render_pages(html) html, = page.children body, = html.children line, = body.children img, = line.children return body, img @pytest.mark.parametrize('html', [ '' % url for url in ( 'pattern.png', 'pattern.gif', 'blue.jpg', 'pattern.svg', "data:image/svg+xml,", "DatA:image/svg+xml,", )] + [ '', '', '', '', '', '', '', '', ] ) @assert_no_logs def test_images_1(html): body, img = get_img(html) assert img.width == 4 assert img.height == 4 @assert_no_logs def test_images_2(): # With physical units url = "data:image/svg+xml," body, img = get_img('' % url) assert img.width == 96 assert img.height == 48 @pytest.mark.parametrize('url', ( 'nonexistent.png', 'unknownprotocol://weasyprint.org/foo.png', 'data:image/unknowntype,Not an image', # Invalid protocol 'datå:image/svg+xml,', # zero-byte images 'data:image/png,', 'data:image/jpeg,', 'data:image/svg+xml,', # Incorrect format 'data:image/png,Not a PNG', 'data:image/jpeg,Not a JPEG', 'data:image/svg+xml,invalid xml', )) @assert_no_logs def test_images_3(url): # Invalid images with capture_logs() as logs: body, img = get_img("invalid image" % url) assert len(logs) == 1 assert 'ERROR: Failed to load image' in logs[0] assert isinstance(img, boxes.InlineBox) # not a replaced box text, = img.children assert text.text == 'invalid image', url @pytest.mark.parametrize('url', ( # GIF with JPEG mimetype 'data:image/jpeg;base64,' 'R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=', # GIF with PNG mimetype 'data:image/png;base64,' 'R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=', # PNG with JPEG mimetype 'data:image/jpeg;base64,' 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC' '0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=', # SVG with PNG mimetype 'data:image/png,', 'really-a-svg.png', # PNG with SVG 'data:image/svg+xml;base64,' 'R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=', 'really-a-png.svg', )) @assert_no_logs def test_images_4(url): # Sniffing, no logs body, img = get_img("" % url) @assert_no_logs def test_images_5(): with capture_logs() as logs: render_pages('') # Failures are cached too: only one error assert len(logs) == 1 assert 'ERROR: Failed to load image' in logs[0] @assert_no_logs def test_images_6(): # Layout rules try to preserve the ratio, so the height should be 40px too: body, img = get_img(''' ''') assert body.height == 40 assert img.position_y == 0 assert img.width == 40 assert img.height == 40 @assert_no_logs def test_images_7(): body, img = get_img(''' ''') assert body.height == 40 assert img.position_y == 0 assert img.width == 40 assert img.height == 40 @assert_no_logs def test_images_8(): # Same with percentages body, img = get_img('''

      ''') assert body.height == 40 assert img.position_y == 0 assert img.width == 40 assert img.height == 40 @assert_no_logs def test_images_9(): body, img = get_img(''' ''') assert body.height == 40 assert img.position_y == 0 assert img.width == 40 assert img.height == 40 @assert_no_logs def test_images_10(): body, img = get_img('') assert img.width == 2 assert img.height == 2 @assert_no_logs def test_images_11(): # display: table-cell is ignored. XXX Should it? page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children img_1, img_2 = line.children assert body.height == 60 assert img_1.width == 40 assert img_1.height == 40 assert img_2.width == 60 assert img_2.height == 60 assert img_1.position_y == 20 assert img_2.position_y == 0 @assert_no_logs def test_images_12(): # Block-level image: page, = render_pages(''' ''') html, = page.children body, = html.children img, = body.children assert img.element_tag == 'img' assert img.position_x == 0 assert img.position_y == 0 assert img.width == 40 assert img.height == 40 assert img.content_box_x() == 30 # (100 - 40) / 2 == 30px for margin-left assert img.content_box_y() == 10 @assert_no_logs def test_images_13(): page, = render_pages(''' ''') html, = page.children body, = html.children img, = body.children assert img.element_tag == 'img' assert img.position_x == 0 assert img.position_y == 0 assert img.width == 40 assert img.height == 40 assert img.content_box_x() == 30 # (100 - 40) / 2 == 30px for margin-left assert img.content_box_y() == 10 @assert_no_logs def test_images_14(): page, = render_pages(''' ''') html, = page.children body, = html.children img, = body.children assert img.element_tag == 'img' assert img.position_x == 0 assert img.position_y == 0 assert img.width == 40 assert img.height == 40 assert img.content_box_x() == 30 # (100 - 40) / 2 == 30px for margin-left assert img.content_box_y() == 10 @assert_no_logs def test_images_15(): page, = render_pages(''' ''') html, = page.children body, = html.children img, = body.children assert img.element_tag == 'img' assert img.position_x == 0 assert img.position_y == 0 assert img.width == 2 assert img.height == 30 assert img.content_box_x() == 49 # (100 - 2) / 2 == 49px for margin-left assert img.content_box_y() == 10 @assert_no_logs def test_images_16(): page, = render_pages(''' ''') html, = page.children body, = html.children img, = body.children assert body.width == 320 assert body.height == 220 assert img.element_tag == 'img' assert img.width == 300 assert img.height == 200 @assert_no_logs def test_images_17(): page, = render_pages('''

      ''') html, = page.children body, = html.children div, = body.children line, = div.children img, = line.children assert div.width == 300 assert div.height == 300 assert img.element_tag == 'img' assert img.width == 300 assert img.height == 150 @assert_no_logs def test_images_18(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1050 page, = render_pages(''' ''') @assert_no_logs def test_linear_gradient(): red = (1, 0, 0, 1) lime = (0, 1, 0, 1) blue = (0, 0, 1, 1) def layout(gradient_css, type_='linear', init=(), positions=[0, 1], colors=[blue, lime]): page, = render_pages('
      ''' % ';'.join( f'{key}: {props.get(key, value)}' for key, value in default.items())) html, = page.children body, = html.children line, = body.children div, = line.children assert div.width == div_width ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1643060388.3957357 weasyprint-54.1/tests/layout/test_inline.py0000644000000000000000000010321500000000000017347 0ustar0000000000000000""" weasyprint.tests.layout.inline ------------------------------ Tests for inline layout. """ import pytest from weasyprint.formatting_structure import boxes from ..testing_utils import SANS_FONTS, assert_no_logs, render_pages @assert_no_logs def test_empty_linebox(): page, = render_pages('

      ') html, = page.children body, = html.children paragraph, = body.children assert len(paragraph.children) == 0 assert paragraph.height == 0 @pytest.mark.xfail @assert_no_logs def test_empty_linebox_removed_space(): # Whitespace removed at the beginning of the line => empty line => no line page, = render_pages('''


      ''') page, = render_pages('

      ') html, = page.children body, = html.children paragraph, = body.children # TODO: The second line should be removed assert len(paragraph.children) == 1 @assert_no_logs def test_breaking_linebox(): page, = render_pages('''

      Lorem Ipsum is verysimply dummytext of the printing and. naaaa naaaa naaaa naaaa naaaa naaaa naaaa naaaa naaaa

      ''' % {'fonts': SANS_FONTS}) html, = page.children body, = html.children paragraph, = body.children assert len(list(paragraph.children)) == 3 lines = paragraph.children for line in lines: assert line.style['font_size'] == 13 assert line.element_tag == 'p' for child in line.children: assert child.element_tag in ('em', 'p') assert child.style['font_size'] == 13 if isinstance(child, boxes.ParentBox): for child_child in child.children: assert child.element_tag in ('em', 'strong', 'span') assert child.style['font_size'] == 13 @assert_no_logs def test_position_x_ltr(): page, = render_pages(''' a
      b
      c
      ''') html, = page.children body, = html.children line1, line2, line3 = body.children span1, = line1.children assert span1.position_x == 0 text1, br1 = span1.children assert text1.position_x == 15 + 3 + 1 span2, = line2.children assert span2.position_x == 0 text2, br2 = span2.children assert text2.position_x == 0 span3, = line3.children assert span3.position_x == 0 text3, = span3.children assert text3.position_x == 0 @assert_no_logs def test_position_x_rtl(): page, = render_pages(''' a
      b
      c
      ''') html, = page.children body, = html.children line1, line2, line3 = body.children span1, = line1.children text1, br1 = span1.children assert span1.position_x == 100 - text1.width - (10 + 2 + 1) assert text1.position_x == 100 - text1.width - (10 + 2 + 1) span2, = line2.children text2, br2 = span2.children assert span2.position_x == 100 - text2.width assert text2.position_x == 100 - text2.width span3, = line3.children text3, = span3.children assert span3.position_x == 100 - text3.width - (15 + 3 + 1) assert text3.position_x == 100 - text3.width @assert_no_logs def test_breaking_linebox_regression_1(): # See http://unicode.org/reports/tr14/ page, = render_pages('
      a\nb\rc\r\nd\u2029e
      ') html, = page.children body, = html.children pre, = body.children lines = pre.children texts = [] for line in lines: text_box, = line.children texts.append(text_box.text) assert texts == ['a', 'b', 'c', 'd', 'e'] @assert_no_logs def test_breaking_linebox_regression_2(): html_sample = '''

      ab c defg hi

      ''' for i in range(16): page, = render_pages(html_sample % i) html, = page.children body, = html.children p, = body.children lines = p.children if i in (0, 1, 2, 3): line_1, line_2, line_3, line_4 = lines textbox_1, = line_1.children assert textbox_1.text == 'ab' span_1, = line_2.children textbox_1, = span_1.children assert textbox_1.text == 'c' span_1, textbox_2 = line_3.children textbox_1, = span_1.children assert textbox_1.text == 'def' assert textbox_2.text == 'g' textbox_1, = line_4.children assert textbox_1.text == 'hi' elif i in (4, 5, 6, 7, 8): line_1, line_2, line_3 = lines textbox_1, span_1 = line_1.children assert textbox_1.text == 'ab ' textbox_2, = span_1.children assert textbox_2.text == 'c' span_1, textbox_2 = line_2.children textbox_1, = span_1.children assert textbox_1.text == 'def' assert textbox_2.text == 'g' textbox_1, = line_3.children assert textbox_1.text == 'hi' elif i in (9, 10): line_1, line_2 = lines textbox_1, span_1 = line_1.children assert textbox_1.text == 'ab ' textbox_2, = span_1.children assert textbox_2.text == 'c' span_1, textbox_2 = line_2.children textbox_1, = span_1.children assert textbox_1.text == 'def' assert textbox_2.text == 'g hi' elif i in (11, 12, 13): line_1, line_2 = lines textbox_1, span_1, textbox_3 = line_1.children assert textbox_1.text == 'ab ' textbox_2, = span_1.children assert textbox_2.text == 'c def' assert textbox_3.text == 'g' textbox_1, = line_2.children assert textbox_1.text == 'hi' else: line_1, = lines textbox_1, span_1, textbox_3 = line_1.children assert textbox_1.text == 'ab ' textbox_2, = span_1.children assert textbox_2.text == 'c def' assert textbox_3.text == 'g hi' @assert_no_logs def test_breaking_linebox_regression_3(): # Regression test #1 for https://github.com/Kozea/WeasyPrint/issues/560 page, = render_pages( '' '
      ' 'aaaa aaaa a [aaa]') html, = page.children body, = html.children div, = body.children line1, line2, line3, line4 = div.children assert line1.children[0].text == line2.children[0].text == 'aaaa' assert line3.children[0].text == 'a' text1, span, text2 = line4.children assert text1.text == '[' assert text2.text == ']' assert span.children[0].text == 'aaa' @assert_no_logs def test_breaking_linebox_regression_4(): # Regression test #2 for https://github.com/Kozea/WeasyPrint/issues/560 page, = render_pages( '' '
      ' 'aaaa a b cd') html, = page.children body, = html.children div, = body.children line1, line2, line3 = div.children assert line1.children[0].text == 'aaaa' assert line2.children[0].text == 'a ' assert line2.children[1].children[0].text == 'b' assert line3.children[0].children[0].text == 'c' assert line3.children[1].text == 'd' @assert_no_logs def test_breaking_linebox_regression_5(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/580 page, = render_pages( '' '
      ' 'aaaa aaaa a a abc') html, = page.children body, = html.children div, = body.children line1, line2, line3, line4 = div.children assert line1.children[0].children[0].text == 'aaaa' assert line2.children[0].children[0].text == 'aaaa' assert line3.children[0].children[0].text == 'a a' assert line4.children[0].children[0].text == 'a' assert line4.children[1].children[0].text == 'bc' @assert_no_logs def test_breaking_linebox_regression_6(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/586 page, = render_pages( '' '
      ' 'a a /ccc') html, = page.children body, = html.children div, = body.children line1, line2 = div.children assert line1.children[0].text == 'a a' assert line2.children[0].children[0].text == '/ccc' @assert_no_logs def test_breaking_linebox_regression_7(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/660 page, = render_pages( '' '
      ' 'abc d ef') html, = page.children body, = html.children div, = body.children line1, line2, line3 = div.children assert line1.children[0].children[0].children[0].text == 'abc' assert line2.children[0].children[0].children[0].text == 'd' assert line3.children[0].children[0].children[0].text == 'e' assert line3.children[1].children[0].text == 'f' @assert_no_logs def test_breaking_linebox_regression_8(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/783 page, = render_pages( '' '

      \n' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' 'bbbbbbbbbbb\n' 'ccccddd

      ') html, = page.children body, = html.children p, = body.children line1, line2 = p.children assert line1.children[0].children[0].text == ( 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbb') assert line2.children[0].children[0].children[0].text == 'cccc' assert line2.children[1].text == 'ddd' @pytest.mark.xfail @assert_no_logs def test_breaking_linebox_regression_9(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/783 # TODO: inlines.can_break_inside return False for span but we can break # before the tag. can_break_inside should be fixed. page, = render_pages( '' '

      \n' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbb\n' 'ccccddd

      ') html, = page.children body, = html.children p, = body.children line1, line2 = p.children assert line1.children[0].children[0].text == ( 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbb') assert line2.children[0].children[0].children[0].text == 'cccc' assert line2.children[1].text == 'ddd' @assert_no_logs def test_breaking_linebox_regression_10(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/923 page, = render_pages( '' '

      ' ' ' ' xxxxxx YYY yyyyyy yyy' ' ZZZZZZ zzzzz' ' )x ' '

      ') html, = page.children body, = html.children p, = body.children line1, line2, line3, line4 = p.children assert line1.children[0].children[0].children[0].text == 'xxxxxx YYY' assert line2.children[0].children[0].children[0].text == 'yyyyyy yyy' assert line3.children[0].children[0].text == 'ZZZZZZ zzzzz' assert line4.children[0].text == ')x' @assert_no_logs def test_breaking_linebox_regression_11(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/953 page, = render_pages( '' '

      ' ' line 1
      123 567 90x' '

      ') html, = page.children body, = html.children p, = body.children line1, line2, line3 = p.children assert line1.children[0].text == 'line 1' assert line2.children[0].children[0].text == '123 567' assert line3.children[0].children[0].text == '90' assert line3.children[1].text == 'x' @assert_no_logs def test_breaking_linebox_regression_12(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/953 page, = render_pages( '' '

      ' '
      123 567 90x' '

      ') html, = page.children body, = html.children p, = body.children line1, line2, line3 = p.children assert line2.children[0].children[0].text == '123 567' assert line3.children[0].children[0].text == '90' assert line3.children[1].text == 'x' @assert_no_logs def test_breaking_linebox_regression_13(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/953 page, = render_pages( '' '

      ' ' 123 567 90 123 567 90x' '

      ') html, = page.children body, = html.children p, = body.children line1, line2, line3 = p.children assert line1.children[0].text == '123 567 90' assert line2.children[0].children[0].text == '123 567' assert line3.children[0].children[0].text == '90' assert line3.children[1].text == 'x' @assert_no_logs def test_linebox_text(): page, = render_pages('''

      Lorem Ipsumis very coool

      ''' % {'fonts': SANS_FONTS}) html, = page.children body, = html.children paragraph, = body.children lines = list(paragraph.children) assert len(lines) == 2 text = ' '.join( (''.join(box.text for box in line.descendants() if isinstance(box, boxes.TextBox))) for line in lines) assert text == 'Lorem Ipsumis very coool' @assert_no_logs def test_linebox_positions(): for width, expected_lines in [(165, 2), (1, 5), (0, 5)]: page = '''

      this is test for Weasyprint

      ''' page, = render_pages(page % {'fonts': SANS_FONTS, 'width': width}) html, = page.children body, = html.children paragraph, = body.children lines = list(paragraph.children) assert len(lines) == expected_lines ref_position_y = lines[0].position_y ref_position_x = lines[0].position_x for line in lines: assert ref_position_y == line.position_y assert ref_position_x == line.position_x for box in line.children: assert ref_position_x == box.position_x ref_position_x += box.width assert ref_position_y == box.position_y assert ref_position_x - line.position_x <= line.width ref_position_x = line.position_x ref_position_y += line.height @assert_no_logs def test_forced_line_breaks_pre(): # These lines should be small enough to fit on the default A4 page # with the default 12pt font-size. page, = render_pages('''
      Lorem ipsum dolor sit amet,
                consectetur adipiscing elit.
      
      
                Sed sollicitudin nibh
      
                et turpis molestie tristique.
      ''') html, = page.children body, = html.children pre, = body.children assert pre.element_tag == 'pre' lines = pre.children assert all(isinstance(line, boxes.LineBox) for line in lines) assert len(lines) == 7 assert [line.height for line in lines] == [42] * 7 @assert_no_logs def test_forced_line_breaks_paragraph(): page, = render_pages('''

      Lorem ipsum dolor sit amet,
      consectetur adipiscing elit.


      Sed sollicitudin nibh

      et turpis molestie tristique.

      ''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.element_tag == 'p' lines = paragraph.children assert all(isinstance(line, boxes.LineBox) for line in lines) assert len(lines) == 7 assert [line.height for line in lines] == [42] * 7 @assert_no_logs def test_inlinebox_splitting(): # The text is strange to test some corner cases # See https://github.com/Kozea/WeasyPrint/issues/389 for width in [10000, 100, 10, 0]: page, = render_pages('''

      WeasyPrint is a frée softwäre ./ visual rendèring enginè for HTML !!! and CSS.

      ''' % {'fonts': SANS_FONTS, 'width': width}) html, = page.children body, = html.children paragraph, = body.children lines = paragraph.children if width == 10000: assert len(lines) == 1 else: assert len(lines) > 1 text_parts = [] for line in lines: strong, = line.children text, = strong.children text_parts.append(text.text) assert ' '.join(text_parts) == ( 'WeasyPrint is a frée softwäre ./ visual ' 'rendèring enginè for HTML !!! and CSS.') @assert_no_logs def test_whitespace_processing(): for source in ['a', ' a ', ' \n \ta', ' a\t ']: page, = render_pages('

      %s

      ' % source) html, = page.children body, = html.children p, = body.children line, = p.children em, = line.children text, = em.children assert text.text == 'a', 'source was %r' % (source,) page, = render_pages( '

      \n\n%s' % source.replace('\n', ' ')) html, = page.children body, = html.children p, = body.children _line1, _line2, line3 = p.children em, = line3.children text, = em.children assert text.text == 'a', 'source was %r' % (source,) @assert_no_logs def test_inline_replaced_auto_margins(): page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children img, = line.children assert img.margin_top == 0 assert img.margin_right == 0 assert img.margin_bottom == 0 assert img.margin_left == 0 @assert_no_logs def test_empty_inline_auto_margins(): page, = render_pages(''' ''') html, = page.children body, = html.children block, = body.children span, = block.children assert span.margin_top != 0 assert span.margin_right == 0 assert span.margin_bottom != 0 assert span.margin_left == 0 @assert_no_logs def test_font_stretch(): page, = render_pages('''

      Hello, world!

      Hello, world!

      ''' % SANS_FONTS) html, = page.children body, = html.children p_1, p_2 = body.children normal = p_1.width condensed = p_2.width assert condensed < normal @assert_no_logs @pytest.mark.parametrize('source, lines_count', ( ('hyphénation', 1), # Default: no hyphenation ('hyphénation', 1), # lang only: no hyphenation ('hyphénation', 1), # hyphens only: no hyph. ('hyphénation', 4), # both: hyph. ('hyp­hénation', 2), # Hyphenation with soft hyphens ('hyp­hénation', 1), # … unless disabled )) def test_line_count(source, lines_count): page, = render_pages( '' '' + source) html, = page.children body, = html.children lines = body.children assert len(lines) == lines_count @assert_no_logs def test_vertical_align_1(): # +-------+ <- position_y = 0 # +-----+ | # 40px | | | 60px # | | | # +-----+-------+ <- baseline page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span, = line.children img_1, img_2 = span.children assert img_1.height == 40 assert img_2.height == 60 assert img_1.position_y == 20 assert img_2.position_y == 0 # 60px + the descent of the font below the baseline assert 60 < line.height < 70 assert body.height == line.height @assert_no_logs def test_vertical_align_2(): # +-------+ <- position_y = 0 # 35px | | # +-----+ | 60px # 40px | | | # | +-------+ <- baseline # +-----+ 15px page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span, = line.children img_1, img_2 = span.children assert img_1.height == 40 assert img_2.height == 60 assert img_1.position_y == 35 assert img_2.position_y == 0 assert line.height == 75 assert body.height == line.height @assert_no_logs def test_vertical_align_3(): # Same as previously, but with percentages page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span, = line.children img_1, img_2 = span.children assert img_1.height == 40 assert img_2.height == 60 assert img_1.position_y == 35 assert img_2.position_y == 0 assert line.height == 75 assert body.height == line.height @assert_no_logs def test_vertical_align_4(): # Same again, but have the vertical-align on an inline box. page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span_1, = line.children span_2, _whitespace, img_2 = span_1.children img_1, = span_2.children assert img_1.height == 40 assert img_2.height == 60 assert img_1.position_y == 35 assert img_2.position_y == 0 assert line.height == 75 assert body.height == line.height @assert_no_logs def test_vertical_align_5(): # Same as previously, but with percentages page, = render_pages( '' '' '' '') html, = page.children body, = html.children line, = body.children span, = line.children img_1, img_2 = span.children assert img_1.height == 40 assert img_2.height == 60 # middle of the image (position_y + 20) is at half the ex-height above # the baseline of the parent. The ex-height of weasyprint.otf is 0.8em # TODO: ex unit doesn't work with @font-face fonts, see computed_values.py # assert img_1.position_y == 35.2 # 60 - 0.5 * 0.8 * font-size - 40/2 assert img_2.position_y == 0 # assert line.height == 75.2 assert body.height == line.height @assert_no_logs def test_vertical_align_6(): # sup and sub currently mean +/- 0.5 em # With the initial 16px font-size, that’s 8px. page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span, = line.children img_1, img_2, img_3 = span.children assert img_1.height == 60 assert img_2.height == 40 assert img_3.height == 40 assert img_1.position_y == 0 assert img_2.position_y == 12 # 20 - 16 * 0.5 assert img_3.position_y == 28 # 20 + 16 * 0.5 assert line.height == 68 assert body.height == line.height @assert_no_logs def test_vertical_align_7(): page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span, = line.children img_1, img_2 = span.children assert img_1.height == 4 assert img_2.height == 4 assert img_1.position_y == 0 assert img_2.position_y == 12 # 16 - 4 assert line.height == 16 assert body.height == line.height @assert_no_logs def test_vertical_align_8(): # This case used to cause an exception: # The second span has no children but should count for line heights # since it has padding. page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span_1, = line.children span_2, = span_1.children assert span_1.height == 16 assert span_2.height == 16 # The line’s strut does not has 'line-height: normal' but the result should # be smaller than 1.5. assert span_1.margin_height() == 24 assert span_2.margin_height() == 24 assert line.height == 24 @assert_no_logs def test_vertical_align_9(): page, = render_pages('''
      foo
      ''') html, = page.children body, = html.children line, = body.children span, div_1 = line.children assert line.height == 178 assert body.height == line.height # Same as earlier img_1, img_2 = span.children assert img_1.height == 40 assert img_2.height == 60 assert img_1.position_y == 138 assert img_2.position_y == 103 div_2, = div_1.children div_3, div_4 = div_2.children div_line, = div_4.children div_img_1, div_img_2 = div_line.children assert div_1.position_y == 0 assert div_1.height == 175 assert div_3.height == 100 assert div_line.height == 75 assert div_img_1.height == 40 assert div_img_2.height == 60 assert div_img_1.position_y == 135 assert div_img_2.position_y == 100 @assert_no_logs def test_vertical_align_10(): # The first two images bring the top of the line box 30px above # the baseline and 10px below. # Each of the inner span page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span_1, = line.children img_1, img_2, span_2, span_4 = span_1.children img_3, span_3 = span_2.children img_4, = span_3.children img_5, = span_4.children assert body.height == line.height assert line.height == 40 assert img_1.position_y == 0 assert img_2.position_y == 36 assert img_3.position_y == 6 assert img_4.position_y == 36 assert img_5.position_y == 30 @assert_no_logs def test_vertical_align_11(): page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span, = line.children img_1, img_2 = span.children assert img_1.position_y == 96 assert img_2.position_y == 0 @assert_no_logs def test_vertical_align_12(): # Reference for the next test page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span, = line.children img_1, = span.children assert img_1.position_y == 0 @assert_no_logs def test_vertical_align_13(): # Should be the same as above page, = render_pages(''' ''') html, = page.children body, = html.children line_1, = body.children span, = line_1.children line_2, = span.children img_1, = line_2.children assert img_1.element_tag == 'img' assert img_1.position_y == 0 @assert_no_logs def test_box_decoration_break_inline_slice(): # http://www.w3.org/TR/css3-background/#the-box-decoration-break page_1, = render_pages(''' a
      b
      c
      ''') html, = page_1.children body, = html.children line_1, line_2, line_3 = body.children span, = line_1.children assert span.width == 16 assert span.margin_width() == 16 + 5 + 1 text, br = span.children assert text.position_x == 5 + 1 span, = line_2.children assert span.width == 16 assert span.margin_width() == 16 text, br = span.children assert text.position_x == 0 span, = line_3.children assert span.width == 16 assert span.margin_width() == 16 + 5 + 1 text, = span.children assert text.position_x == 0 @assert_no_logs def test_box_decoration_break_inline_clone(): # http://www.w3.org/TR/css3-background/#the-box-decoration-break page_1, = render_pages(''' a
      b
      c
      ''') html, = page_1.children body, = html.children line_1, line_2, line_3 = body.children span, = line_1.children assert span.width == 16 assert span.margin_width() == 16 + 2 * (5 + 1) text, br = span.children assert text.position_x == 5 + 1 span, = line_2.children assert span.width == 16 assert span.margin_width() == 16 + 2 * (5 + 1) text, br = span.children assert text.position_x == 5 + 1 span, = line_3.children assert span.width == 16 assert span.margin_width() == 16 + 2 * (5 + 1) text, = span.children assert text.position_x == 5 + 1 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.025308 weasyprint-54.1/tests/layout/test_inline_block.py0000644000000000000000000000737500000000000020533 0ustar0000000000000000""" weasyprint.tests.layout.inline_block ------------------------------------ Tests for inline blocks layout. """ from ..testing_utils import assert_no_logs, render_pages @assert_no_logs def test_inline_block_sizes(): page, = render_pages('''
      a
      Ipsum dolor sit amet, consectetur adipiscing elit. Sed sollicitudin nibh et turpis molestie tristique.
      foo
      Supercalifragilisticexpialidocious
      ''') html, = page.children assert html.element_tag == 'html' body, = html.children assert body.element_tag == 'body' assert body.width == 200 line_1, line_2, line_3, line_4 = body.children # First line: # White space in-between divs ends up preserved in TextBoxes div_1, _, div_2, _, div_3, _, div_4, _ = line_1.children # First div, one ignored space collapsing with next space assert div_1.element_tag == 'div' assert div_1.width == 0 # Second div, one letter assert div_2.element_tag == 'div' assert 0 < div_2.width < 20 # Third div, empty with margin assert div_3.element_tag == 'div' assert div_3.width == 0 assert div_3.margin_width() == 20 assert div_3.height == 100 # Fourth div, empty with margin and padding assert div_4.element_tag == 'div' assert div_4.width == 0 assert div_4.margin_width() == 30 # Second line: div_5, _ = line_2.children # Fifth div, long text, full-width div assert div_5.element_tag == 'div' assert len(div_5.children) > 1 assert div_5.width == 200 # Third line: div_6, _, div_7, _ = line_3.children # Sixth div, empty div with fixed width and height assert div_6.element_tag == 'div' assert div_6.width == 100 assert div_6.margin_width() == 120 assert div_6.height == 100 assert div_6.margin_height() == 140 # Seventh div assert div_7.element_tag == 'div' assert div_7.width == 20 child_line, = div_7.children # Spaces have font-size: 0, they get removed child_div_1, child_div_2 = child_line.children assert child_div_1.element_tag == 'div' assert child_div_1.width == 10 assert child_div_2.element_tag == 'div' assert child_div_2.width == 2 grandchild, = child_div_2.children assert grandchild.element_tag == 'div' assert grandchild.width == 10 div_8, _, div_9 = line_4.children assert div_8.width == 150 assert div_9.width == 10 @assert_no_logs def test_inline_block_with_margin(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1235 page_1, = render_pages(''' a b c d e f g h i j k l''') html, = page_1.children body, = html.children line_1, = body.children span, = line_1.children assert span.width == 40 # 100 - 2 * 30 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.025308 weasyprint-54.1/tests/layout/test_list.py0000644000000000000000000000560100000000000017044 0ustar0000000000000000""" weasyprint.tests.layout.list ---------------------------- Tests for lists layout. """ import pytest from ..testing_utils import assert_no_logs, render_pages @assert_no_logs @pytest.mark.parametrize('inside', ('inside', '',)) @pytest.mark.parametrize('style, character', ( ('circle', '◦ '), ('disc', '• '), ('square', '▪ '), )) def test_lists_style(inside, style, character): page, = render_pages('''
      • abc
      ''' % (inside, style)) html, = page.children body, = html.children unordered_list, = body.children list_item, = unordered_list.children if inside: line, = list_item.children marker, content = line.children marker_text, = marker.children else: marker, line_container, = list_item.children assert marker.position_x == list_item.position_x assert marker.position_y == list_item.position_y line, = line_container.children content, = line.children marker_line, = marker.children marker_text, = marker_line.children assert marker_text.text == character assert content.text == 'abc' def test_lists_empty_item(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/873 page, = render_pages('''
      • a
      • a
      ''') html, = page.children body, = html.children unordered_list, = body.children li1, li2, li3 = unordered_list.children assert li1.position_y != li2.position_y != li3.position_y @pytest.mark.xfail def test_lists_whitespace_item(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/873 page, = render_pages('''
      • a
      • a
      ''') html, = page.children body, = html.children unordered_list, = body.children li1, li2, li3 = unordered_list.children assert li1.position_y != li2.position_y != li3.position_y def test_lists_page_break(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/945 page1, page2 = render_pages('''
      • a
      • a
      • a
      • a
      ''') html, = page1.children body, = html.children ul, = body.children assert len(ul.children) == 3 for li in ul.children: assert len(li.children) == 2 html, = page2.children body, = html.children ul, = body.children assert len(ul.children) == 1 for li in ul.children: assert len(li.children) == 2 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1643120121.609203 weasyprint-54.1/tests/layout/test_page.py0000644000000000000000000011644200000000000017013 0ustar0000000000000000""" weasyprint.tests.layout.page ---------------------------- Tests for pages layout. """ import pytest from weasyprint.formatting_structure import boxes from ..testing_utils import assert_no_logs, render_pages @assert_no_logs @pytest.mark.parametrize('size, width, height', ( ('auto', 793, 1122), ('2in 10in', 192, 960), ('242px', 242, 242), ('letter', 816, 1056), ('letter portrait', 816, 1056), ('letter landscape', 1056, 816), ('portrait', 793, 1122), ('landscape', 1122, 793), )) def test_page_size_basic(size, width, height): """Test the layout for ``@page`` properties.""" page, = render_pages('' % size) assert int(page.margin_width()) == width assert int(page.margin_height()) == height @assert_no_logs def test_page_size_with_margin(): page, = render_pages('''

      ''') assert page.margin_width() == 200 assert page.margin_height() == 300 assert page.position_x == 0 assert page.position_y == 0 assert page.width == 84 # 200px - 10% - 1 inch assert page.height == 230 # 300px - 10px - 20% html, = page.children assert html.element_tag == 'html' assert html.position_x == 96 # 1in assert html.position_y == 10 # root element’s margins do not collapse assert html.width == 84 body, = html.children assert body.element_tag == 'body' assert body.position_x == 96 # 1in assert body.position_y == 10 # body has margins in the UA stylesheet assert body.margin_left == 8 assert body.margin_right == 8 assert body.margin_top == 8 assert body.margin_bottom == 8 assert body.width == 68 paragraph, = body.children assert paragraph.element_tag == 'p' assert paragraph.position_x == 104 # 1in + 8px assert paragraph.position_y == 18 # 10px + 8px assert paragraph.width == 68 @assert_no_logs def test_page_size_with_margin_border_padding(): page, = render_pages('''''') assert page.width == 16 # 100 - 2 * 42 assert page.height == 58 # 100 - 2 * 21 html, = page.children assert html.element_tag == 'html' assert html.position_x == 42 # 2 + 8 + 32 assert html.position_y == 21 # 1 + 4 + 16 @assert_no_logs @pytest.mark.parametrize('margin, top, right, bottom, left', ( ('auto', 15, 10, 15, 10), ('5px 5px auto auto', 5, 5, 25, 15), )) def test_page_size_margins(margin, top, right, bottom, left): page, = render_pages('''''' % margin) assert page.margin_top == top assert page.margin_right == right assert page.margin_bottom == bottom assert page.margin_left == left @assert_no_logs @pytest.mark.parametrize('style, width, height', ( ('size: 4px 10000px; width: 100px; height: 100px;' 'padding: 1px; border: 2px solid; margin: 3px', 112, 112), ('size: 1000px; margin: 100px; max-width: 500px; min-height: 1500px', 700, 1700), ('size: 1000px; margin: 100px; min-width: 1500px; max-height: 500px', 1700, 700), )) def test_page_size_over_constrained(style, width, height): page, = render_pages('' % style) assert page.margin_width() == width assert page.margin_height() == height @assert_no_logs @pytest.mark.parametrize('html', ( '

      1
      ', '
      ', '' )) def test_page_breaks(html): pages = render_pages(''' %s''' % (5 * html)) page_children = [] for page in pages: html, = page.children body, = html.children children = body.children assert all([child.element_tag in ('div', 'img') for child in children]) assert all([child.position_x == 10 for child in children]) page_children.append(children) assert [ [child.position_y for child in page_child] for page_child in page_children] == [[10, 40], [10, 40], [10]] @assert_no_logs def test_page_breaks_complex_1(): page_1, page_2, page_3, page_4 = render_pages('''
      1

      2

      3

      • 4
      ''') # The first page is a right page on rtl, but not here because of # page-break-before on the root element. assert page_1.margin_left == 50 # left page assert page_1.margin_right == 10 html, = page_1.children body, = html.children div, = body.children line, = div.children text, = line.children assert div.element_tag == 'div' assert text.text == '1' html, = page_2.children assert page_2.margin_left == 10 assert page_2.margin_right == 50 # right page assert not html.children # empty page to get to a left page assert page_3.margin_left == 50 # left page assert page_3.margin_right == 10 html, = page_3.children body, = html.children p_1, p_2 = body.children assert p_1.element_tag == 'p' assert p_2.element_tag == 'p' assert page_4.margin_left == 10 assert page_4.margin_right == 50 # right page html, = page_4.children body, = html.children article, = body.children section, = article.children ulist, = section.children assert ulist.element_tag == 'ul' @assert_no_logs def test_page_breaks_complex_2(): # Reference for the following test: # Without any 'avoid', this breaks after the
      page_1, page_2 = render_pages('''



      ''') html, = page_1.children body, = html.children img_1, div = body.children assert img_1.position_y == 0 assert img_1.height == 25 assert div.position_y == 25 assert div.height == 100 html, = page_2.children body, = html.children img_2, = body.children assert img_2.position_y == 0 assert img_2.height == 25 @assert_no_logs def test_page_breaks_complex_3(): # Adding a few page-break-*: avoid, the only legal break is # before the
      page_1, page_2 = render_pages('''



      ''') html, = page_1.children body, = html.children img_1, = body.children assert img_1.position_y == 0 assert img_1.height == 25 html, = page_2.children body, = html.children div, img_2 = body.children assert div.position_y == 0 assert div.height == 100 assert img_2.position_y == 100 assert img_2.height == 25 @assert_no_logs def test_page_breaks_complex_4(): page_1, page_2 = render_pages('''



      ''') html, = page_1.children body, = html.children img_1, = body.children assert img_1.position_y == 0 assert img_1.height == 25 html, = page_2.children body, = html.children outer_div, = body.children inner_div, img_2 = outer_div.children assert inner_div.position_y == 0 assert inner_div.height == 100 assert img_2.position_y == 100 assert img_2.height == 25 @assert_no_logs def test_page_breaks_complex_5(): # Reference for the next test page_1, page_2, page_3 = render_pages('''
      ''') html, = page_1.children body, = html.children div, = body.children assert div.height == 100 html, = page_2.children body, = html.children div, img_4 = body.children assert div.height == 60 assert img_4.height == 30 html, = page_3.children body, = html.children img_5, = body.children assert img_5.height == 30 @assert_no_logs def test_page_breaks_complex_6(): page_1, page_2, page_3 = render_pages('''
      ''') html, = page_1.children body, = html.children div, = body.children assert div.height == 100 html, = page_2.children body, = html.children div, = body.children section, = div.children img_2, = section.children assert img_2.height == 30 # TODO: currently this is 60: we do not increase the used height of blocks # to make them fill the blank space at the end of the age when we remove # children from them for some break-*: avoid. # See TODOs in blocks.block_container_layout # assert div.height == 100 html, = page_3.children body, = html.children div, img_4, img_5, = body.children assert div.height == 30 assert img_4.height == 30 assert img_5.height == 30 @assert_no_logs def test_page_breaks_complex_7(): page_1, page_2, page_3 = render_pages('''

      foo

      bar

      ''') assert len(page_1.children) == 2 # content and @bottom-center assert len(page_2.children) == 1 # content only assert len(page_3.children) == 2 # content and @bottom-center @assert_no_logs def test_page_breaks_complex_8(): page_1, page_2 = render_pages('''
      ''') html, = page_1.children body, _div = html.children div_1, section = body.children div_2, = section.children assert div_1.position_y == 0 assert div_2.position_y == 20 assert div_1.height == 20 assert div_2.height == 20 html, = page_2.children body, = html.children section, div_4 = body.children div_3, = section.children absolute, fixed = div_3.children assert div_3.position_y == 0 assert div_4.position_y == 20 assert div_3.height == 20 assert div_4.height == 20 @assert_no_logs @pytest.mark.parametrize('break_after, margin_break, margin_top', ( ('page', 'auto', 5), ('auto', 'auto', 0), ('page', 'keep', 5), ('auto', 'keep', 5), ('page', 'discard', 0), ('auto', 'discard', 0), )) def test_margin_break(break_after, margin_break, margin_top): page_1, page_2 = render_pages('''
      ''' % (break_after, margin_break)) html, = page_1.children body, = html.children section, = body.children div, = section.children assert div.margin_top == 5 html, = page_2.children body, = html.children section, = body.children div, = section.children assert div.margin_top == margin_top @pytest.mark.xfail @assert_no_logs def test_margin_break_clearance(): page_1, page_2 = render_pages('''
      ''') html, = page_1.children body, = html.children section, = body.children div, = section.children assert div.margin_top == 5 html, = page_2.children body, = html.children section, = body.children div_1, = section.children assert div_1.margin_top == 0 div_2, = div_1.children assert div_2.margin_top == 5 assert div_2.content_box_y() == 5 @assert_no_logs @pytest.mark.parametrize('direction, page_break, pages_number', ( ('ltr', 'recto', 3), ('ltr', 'verso', 2), ('rtl', 'recto', 3), ('rtl', 'verso', 2), ('ltr', 'right', 3), ('ltr', 'left', 2), ('rtl', 'right', 2), ('rtl', 'left', 3), )) def test_recto_verso_break(direction, page_break, pages_number): pages = render_pages(''' abc

      def

      ''' % (direction, page_break)) assert len(pages) == pages_number @assert_no_logs def test_page_names_1(): pages = render_pages('''
      large
      ''') page1, = pages assert (page1.width, page1.height) == (100, 100) @assert_no_logs def test_page_names_2(): pages = render_pages('''
      large
      ''') page1, = pages assert (page1.width, page1.height) == (100, 100) @assert_no_logs def test_page_names_3(): pages = render_pages('''
      large
      large

      narrow

      ''') page1, page2 = pages assert (page1.width, page1.height) == (200, 100) html, = page1.children body, = html.children div, = body.children section1, section2 = div.children assert section1.element_tag == section2.element_tag == 'section' assert (page2.width, page2.height) == (100, 200) html, = page2.children body, = html.children div, = body.children p, = div.children assert p.element_tag == 'p' @assert_no_logs def test_page_names_4(): pages = render_pages('''
      normal
      normal

      small

      small
      ''') page1, page2 = pages assert (page1.width, page1.height) == (200, 200) html, = page1.children body, = html.children section1, section2 = body.children assert section1.element_tag == section2.element_tag == 'section' assert (page2.width, page2.height) == (100, 100) html, = page2.children body, = html.children p, section = body.children assert p.element_tag == 'p' assert section.element_tag == 'section' @assert_no_logs def test_page_names_5(): pages = render_pages('''

      a

      b
      c
      d
      ''') page1, page2 = pages assert (page1.width, page1.height) == (200, 200) html, = page1.children body, = html.children section1, section2 = body.children assert section1.element_tag == section2.element_tag == 'section' p, line = section1.children line, = section2.children assert (page2.width, page2.height) == (100, 100) html, = page2.children body, = html.children section2, = body.children div, = section2.children @assert_no_logs def test_page_names_6(): pages = render_pages('''
      a

      b

      c
      d
      e
      f
      ''') page1, page2, page3 = pages assert (page1.width, page1.height) == (200, 200) html, = page1.children body, = html.children section1, section2 = body.children assert section1.element_tag == section2.element_tag == 'section' line1, p, line2 = section1.children line, = section2.children assert (page2.width, page2.height) == (100, 100) html, = page2.children body, = html.children section2, = body.children div, = section2.children assert (page3.width, page3.height) == (200, 200) html, = page3.children body, = html.children section2, = body.children line, = section2.children @assert_no_logs def test_page_names_7(): pages = render_pages('''
      normal
      normal

      small

      small
      ''') page1, page2, page3 = pages assert (page1.width, page1.height) == (200, 200) html, = page1.children body, = html.children section1, section2 = body.children assert section1.element_tag == section2.element_tag == 'section' assert (page2.width, page2.height) == (200, 200) html, = page2.children assert not html.children assert (page3.width, page3.height) == (100, 100) html, = page3.children body, = html.children p, section = body.children assert p.element_tag == 'p' assert section.element_tag == 'section' @assert_no_logs def test_page_names_8(): pages = render_pages('''

      small

      small

      ''') page1, page2 = pages assert (page1.width, page1.height) == (100, 100) html, = page1.children body, = html.children section, = body.children p, = section.children assert section.element_tag == 'section' assert p.element_tag == 'p' assert (page2.width, page2.height) == (100, 100) html, = page2.children body, = html.children section, = body.children p, = section.children assert section.element_tag == 'section' assert p.element_tag == 'p' @assert_no_logs def test_page_names_9(): pages = render_pages('''
      big
      big
      small
      small
      ''') page1, page2, = pages assert (page1.width, page1.height) == (100, 100) html, = page1.children body, = html.children section, = body.children assert section.element_tag == 'section' assert (page2.width, page2.height) == (100, 100) html, = page2.children body, = html.children article, = body.children assert article.element_tag == 'article' @assert_no_logs @pytest.mark.parametrize('style, line_counts', ( ('orphans: 2; widows: 2', [4, 3]), ('orphans: 5; widows: 2', [0, 7]), ('orphans: 2; widows: 4', [3, 4]), ('orphans: 4; widows: 4', [0, 7]), ('orphans: 2; widows: 2; page-break-inside: avoid', [0, 7]), )) def test_orphans_widows_avoid(style, line_counts): pages = render_pages('''

      Tasty test

      one two three four five six seven

      ''' % style) for i, page in enumerate(pages): html, = page.children body, = html.children body_children = body.children if i else body.children[1:] # skip h1 count = len(body_children[0].children) if body_children else 0 assert line_counts.pop(0) == count assert not line_counts @assert_no_logs def test_page_and_linebox_breaking(): # Empty tests a corner case in skip_first_whitespace() pages = render_pages('''
      1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
      ''') texts = [] for page in pages: html, = page.children body, = html.children div, = body.children lines = div.children for line in lines: line_texts = [] for child in line.descendants(): if isinstance(child, boxes.TextBox): line_texts.append(child.text) texts.append(''.join(line_texts)) assert len(pages) == 4 assert ''.join(texts) == '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15' @assert_no_logs def test_margin_boxes_fixed_dimension_1(): # Corner boxes page, = render_pages(''' ''') html, top_left, top_right, bottom_left, bottom_right = page.children for margin_box, text in zip( [top_left, top_right, bottom_left, bottom_right], ['top_left', 'top_right', 'bottom_left', 'bottom_right']): line, = margin_box.children text, = line.children assert text == text # Check positioning and Rule 1 for fixed dimensions assert top_left.position_x == 0 assert top_left.position_y == 0 assert top_left.margin_width() == 200 # margin-left assert top_left.margin_height() == 100 # margin-top assert top_right.position_x == 700 # size-x - margin-right assert top_right.position_y == 0 assert top_right.margin_width() == 300 # margin-right assert top_right.margin_height() == 100 # margin-top assert bottom_left.position_x == 0 assert bottom_left.position_y == 600 # size-y - margin-bottom assert bottom_left.margin_width() == 200 # margin-left assert bottom_left.margin_height() == 400 # margin-bottom assert bottom_right.position_x == 700 # size-x - margin-right assert bottom_right.position_y == 600 # size-y - margin-bottom assert bottom_right.margin_width() == 300 # margin-right assert bottom_right.margin_height() == 400 # margin-bottom @assert_no_logs def test_margin_boxes_fixed_dimension_2(): # Test rules 2 and 3 page, = render_pages(''' ''') html, margin_box = page.children assert margin_box.margin_width() == 200 assert margin_box.margin_left == 60 assert margin_box.margin_right == 60 assert margin_box.width == 80 # 200 - 60 - 60 assert margin_box.margin_height() == 100 # total was too big, the outside margin was ignored: assert margin_box.margin_top == 60 assert margin_box.margin_bottom == 40 # Not 60 assert margin_box.height == 0 # But not negative @assert_no_logs def test_margin_boxes_fixed_dimension_3(): # Test rule 3 with a non-auto inner dimension page, = render_pages(''' ''') html, margin_box = page.children assert margin_box.margin_width() == 100 assert margin_box.margin_left == -40 # Not 10px assert margin_box.margin_right == 10 assert margin_box.width == 130 # As specified @assert_no_logs def test_margin_boxes_fixed_dimension_4(): # Test rule 4 page, = render_pages(''' ''') html, margin_box = page.children assert margin_box.margin_width() == 100 assert margin_box.margin_left == 10 # 10px this time, no over-constrain assert margin_box.margin_right == 20 assert margin_box.width == 70 # As specified @assert_no_logs def test_margin_boxes_fixed_dimension_5(): # Test rules 2, 3 and 4 page, = render_pages(''' ''') html, margin_box = page.children assert margin_box.margin_width() == 100 assert margin_box.margin_left == 0 # rule 2 assert margin_box.margin_right == -30 # rule 3, after rule 2 assert margin_box.width == 130 # As specified @assert_no_logs def test_margin_boxes_fixed_dimension_6(): # Test rule 5 page, = render_pages(''' ''') html, margin_box = page.children assert margin_box.margin_height() == 100 assert margin_box.margin_top == 10 assert margin_box.margin_bottom == 0 assert margin_box.height == 90 @assert_no_logs def test_margin_boxes_fixed_dimension_7(): # Test rule 5 page, = render_pages(''' ''') html, margin_box = page.children assert margin_box.margin_height() == 100 assert margin_box.margin_top == 0 assert margin_box.margin_bottom == 0 assert margin_box.height == 100 @assert_no_logs def test_margin_boxes_fixed_dimension_8(): # Test rule 6 page, = render_pages(''' ''') html, margin_box = page.children assert margin_box.margin_height() == 100 assert margin_box.margin_top == 15 assert margin_box.margin_bottom == 15 assert margin_box.height == 70 @assert_no_logs def test_margin_boxes_fixed_dimension_9(): # Rule 2 inhibits rule 6 page, = render_pages(''' ''') html, margin_box = page.children assert margin_box.margin_height() == 100 assert margin_box.margin_top == 0 assert margin_box.margin_bottom == -50 # outside assert margin_box.height == 150 def images(*widths): return ' '.join( f'url(\'data:image/svg+xml,\')' for width in widths) @assert_no_logs @pytest.mark.parametrize('css, widths', ( ('''@top-left { content: %s } @top-center { content: %s } @top-right { content: %s } ''' % (images(50, 50), images(50, 50), images(50, 50)), [100, 100, 100]), # Use preferred widths if they fit ('''@top-left { content: %s; margin: auto } @top-center { content: %s } @top-right { content: %s } ''' % (images(50, 50), images(50, 50), images(50, 50)), [100, 100, 100]), # 'auto' margins are set to 0 ('''@top-left { content: %s } @top-center { content: %s } @top-right { content: 'foo'; width: 200px } ''' % (images(100, 50), images(300, 150)), [150, 300, 200]), # Use at least minimum widths, even if boxes overlap ('''@top-left { content: %s } @top-center { content: %s } @top-right { content: %s } ''' % (images(150, 150), images(150, 150), images(150, 150)), [200, 200, 200]), # Distribute remaining space proportionally ('''@top-left { content: %s } @top-center { content: %s } @top-right { content: %s } ''' % (images(100, 100, 100), images(100, 100), images(10)), [220, 160, 10]), ('''@top-left { content: %s; width: 205px } @top-center { content: %s } @top-right { content: %s } ''' % (images(100, 100, 100), images(100, 100), images(10)), [205, 190, 10]), ('''@top-left { width: 1000px; margin: 1000px; padding: 1000px; border: 1000px solid } @top-center { content: %s } @top-right { content: %s } ''' % (images(100, 100), images(10)), [200, 10]), # 'width' and other have no effect without 'content' ('''@top-left { content: ''; width: 200px } @top-center { content: ''; width: 300px } @top-right { content: %s } ''' % images(50, 50), # This leaves 150px for @top-right’s shrink-to-fit [200, 300, 100]), ('''@top-left { content: ''; width: 200px } @top-center { content: ''; width: 300px } @top-right { content: %s } ''' % images(100, 100, 100), [200, 300, 150]), ('''@top-left { content: ''; width: 200px } @top-center { content: ''; width: 300px } @top-right { content: %s } ''' % images(170, 175), [200, 300, 175]), ('''@top-left { content: ''; width: 200px } @top-center { content: ''; width: 300px } @top-right { content: %s } ''' % images(170, 175), [200, 300, 175]), ('''@top-left { content: ''; width: 200px } @top-right { content: ''; width: 500px } ''', [200, 500]), ('''@top-left { content: ''; width: 200px } @top-right { content: %s } ''' % images(150, 50, 150), [200, 350]), ('''@top-left { content: ''; width: 200px } @top-right { content: %s } ''' % images(150, 50, 150, 200), [200, 400]), ('''@top-left { content: %s } @top-right { content: ''; width: 200px } ''' % images(150, 50, 450), [450, 200]), ('''@top-left { content: %s } @top-right { content: %s } ''' % (images(150, 100), images(10, 120)), [250, 130]), ('''@top-left { content: %s } @top-right { content: %s } ''' % (images(550, 100), images(10, 120)), [550, 120]), ('''@top-left { content: %s } @top-right { content: %s } ''' % (images(250, 60), images(250, 180)), [275, 325]), # 250 + (100 * 1 / 4), 250 + (100 * 3 / 4) )) def test_page_style(css, widths): expected_at_keywords = [ at_keyword for at_keyword in [ '@top-left', '@top-center', '@top-right'] if at_keyword + ' { content: ' in css] page, = render_pages(''' ''' % css) assert page.children[0].element_tag == 'html' margin_boxes = page.children[1:] assert [box.at_keyword for box in margin_boxes] == expected_at_keywords offsets = {'@top-left': 0, '@top-center': 0.5, '@top-right': 1} for box in margin_boxes: assert box.position_x == 100 + offsets[box.at_keyword] * ( 600 - box.margin_width()) assert [box.margin_width() for box in margin_boxes] == widths @assert_no_logs def test_margin_boxes_vertical_align(): # 3 px -> +-----+ # | 1 | # +-----+ # # 43 px -> +-----+ # 53 px -> | 2 | # +-----+ # # 83 px -> +-----+ # | 3 | # 103px -> +-----+ page, = render_pages(''' ''') html, top_left, top_center, top_right = page.children line_1, = top_left.children line_2, = top_center.children line_3, = top_right.children assert line_1.position_y == 3 assert line_2.position_y == 43 assert line_3.position_y == 83 @assert_no_logs def test_margin_boxes_element(): pages = render_pages('''
      of

      test1

      test2

      test3

      test4

      test5

      test6

      Static
      ''') footer1_text = ''.join( getattr(node, 'text', '') for node in pages[0].children[1].descendants()) assert footer1_text == '1 of 3' footer2_text = ''.join( getattr(node, 'text', '') for node in pages[1].children[1].descendants()) assert footer2_text == '2 of 3' footer3_text = ''.join( getattr(node, 'text', '') for node in pages[2].children[1].descendants()) assert footer3_text == 'Static' @assert_no_logs @pytest.mark.parametrize('argument, texts', ( # TODO: start doesn’t work because running elements are removed from the # original tree, and the current implentation in # layout.get_running_element_for uses the tree to know if it’s at the # beginning of the page # ('start', ('', '2-first', '2-last', '3-last', '5')), ('first', ('', '2-first', '3-first', '3-last', '5')), ('last', ('', '2-last', '3-last', '3-last', '5')), ('first-except', ('', '', '', '3-last', '')), )) def test_running_elements(argument, texts): pages = render_pages('''
      1

      2-first

      2-last

      3

      3-first

      3-last

      5

      ''' % argument) assert len(pages) == 5 for page, text in zip(pages, texts): html, margin = page.children if margin.children: h1, = margin.children line, = h1.children textbox, = line.children assert textbox.text == text else: assert not text @assert_no_logs def test_running_elements_display(): page, = render_pages(''' text
      table
      block
      inline ''') html, left, center, right = page.children assert ''.join( getattr(node, 'text', '') for node in left.descendants()) == 'inline' assert ''.join( getattr(node, 'text', '') for node in center.descendants()) == 'block' assert ''.join( getattr(node, 'text', '') for node in right.descendants()) == 'table' @assert_no_logs def test_running_img(): # Test regression render_pages(''' ''') @assert_no_logs def test_running_absolute(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1540 render_pages('''
      Hello!

      Bonjour!

      ''') ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.025308 weasyprint-54.1/tests/layout/test_position.py0000644000000000000000000003345700000000000017747 0ustar0000000000000000""" weasyprint.tests.layout.position -------------------------------- Tests for position property. """ from ..testing_utils import assert_no_logs, render_pages @assert_no_logs def test_relative_positioning_1(): page, = render_pages('''

      1

      2

      3

      4

      5

      6

      7

      8

      ''') html, = page.children body, = html.children p1, div, p8 = body.children p2, p3, p4, p5, p6, p7 = div.children assert (p1.position_x, p1.position_y) == (0, 0) assert (div.position_x, div.position_y) == (0, 30) assert (p2.position_x, p2.position_y) == (0, 30) assert (p3.position_x, p3.position_y) == (5, 45) # (0 + 5, 50 - 5) assert (p4.position_x, p4.position_y) == (0, 70) assert (p5.position_x, p5.position_y) == (-5, 85) # (0 - 5, 90 - 5) assert (p6.position_x, p6.position_y) == (0, 110) assert (p7.position_x, p7.position_y) == (0, 130) assert (p8.position_x, p8.position_y) == (0, 140) assert div.height == 120 @assert_no_logs def test_relative_positioning_2(): page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span1, span2, span3 = line.children img1, = span1.children img2, img3, img4, img5, img6, img7 = span2.children img8, = span3.children assert (img1.position_x, img1.position_y) == (0, 0) # Don't test the span2.position_y because it depends on fonts assert span2.position_x == 30 assert (img2.position_x, img2.position_y) == (30, 0) assert (img3.position_x, img3.position_y) == (45, 5) # (50 - 5, y + 5) assert (img4.position_x, img4.position_y) == (70, 0) assert (img5.position_x, img5.position_y) == (85, -5) # (90 - 5, y - 5) assert (img6.position_x, img6.position_y) == (110, 0) assert (img7.position_x, img7.position_y) == (130, 0) assert (img8.position_x, img8.position_y) == (140, 0) assert span2.width == 120 @assert_no_logs def test_absolute_positioning_1(): page, = render_pages('''
      ''') html, = page.children body, = html.children div1, = body.children div2, div3, div4 = div1.children assert div1.height == 0 assert (div1.position_x, div1.position_y) == (0, 0) assert (div2.width, div2.height) == (20, 20) assert (div2.position_x, div2.position_y) == (3, 3) assert (div3.width, div3.height) == (20, 20) assert (div3.position_x, div3.position_y) == (0, 3) assert (div4.width, div4.height) == (20, 20) assert (div4.position_x, div4.position_y) == (3, 0) @assert_no_logs def test_absolute_positioning_2(): page, = render_pages('''
      ''') html, = page.children body, = html.children div1, = body.children div2, div3 = div1.children for div in (div1, div2, div3): assert (div.position_x, div.position_y) == (0, 0) assert (div.width, div.height) == (20, 20) @assert_no_logs def test_absolute_positioning_3(): page, = render_pages(''' 2 3 4 ''') html, = page.children body, = html.children line, = body.children img, span1 = line.children span2, span3, span4 = span1.children assert span1.position_x == 4 assert (span2.position_x, span2.position_y) == (4, 0) assert (span3.position_x, span3.position_y) == (4, 0) assert span4.position_x == 4 @assert_no_logs def test_absolute_positioning_4(): page, = render_pages(''' 2 ''') html, = page.children body, = html.children line, = body.children img1, span, img2 = line.children assert (img1.position_x, img1.position_y) == (0, 0) assert (span.position_x, span.position_y) == (5, 0) assert (img2.position_x, img2.position_y) == (5, 0) @assert_no_logs def test_absolute_positioning_5(): page, = render_pages(''' 2 ''') html, = page.children body, = html.children line, = body.children img1, span, img2 = line.children assert (img1.position_x, img1.position_y) == (0, 0) assert (span.position_x, span.position_y) == (0, 20) assert (img2.position_x, img2.position_y) == (5, 0) @assert_no_logs def test_absolute_positioning_6(): page, = render_pages('''
      ''') html, = page.children body, = html.children div1, = body.children div2, div3 = div1.children assert (div1.position_x, div1.position_y) == (1, 5) assert (div1.width, div1.height) == (20, 60) assert (div1.border_width(), div1.border_height()) == (40, 86) assert (div2.position_x, div2.position_y) == (11, 28) assert (div2.width, div2.height) == (20, 20) assert (div3.position_x, div3.position_y) == (11, 28) assert (div3.width, div3.height) == (20, 20) @assert_no_logs def test_absolute_positioning_7(): page, = render_pages('''

      1

      2

      3

      4

      5

      6

      7

      ''') html, = page.children body, = html.children p1, div, p7 = body.children p2, p3, p4, p5, p6 = div.children line, = p5.children span1, = line.children img, span2, span3 = span1.children assert (p1.position_x, p1.position_y) == (0, 0) assert (div.position_x, div.position_y) == (0, 20) assert (p2.position_x, p2.position_y) == (0, 20) assert (p3.position_x, p3.position_y) == (5, -5) assert (p4.position_x, p4.position_y) == (0, 40) # p5 x = page width - right - margin/padding/border - width # = 1000 - 15 - 2 * 10 - 50 # = 915 # p5 y = page height - bottom - margin/padding/border - height # = 2000 - 5 - 2 * 10 - 200 # = 1775 assert (p5.position_x, p5.position_y) == (915, 1775) assert (img.position_x, img.position_y) == (925, 1785) assert (span2.position_x, span2.position_y) == (929, 1785) # span3 x = p5 right - p5 margin - span width - span right # = 985 - 7 - 20 - 5 # = 953 # span3 y = p5 y + p5 margin top + span top # = 1775 + 7 + -10 # = 1772 assert (span3.position_x, span3.position_y) == (953, 1772) # p6 y = p4 y + p4 margin height - margin collapsing # = 40 + 26 - 3 # = 63 assert (p6.position_x, p6.position_y) == (0, 63) assert div.height == 71 # 20*3 + 2*3 + 8 - 3 assert (p7.position_x, p7.position_y) == (0, 91) @assert_no_logs def test_absolute_positioning_8(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1264 page, = render_pages('''
      ''') html, = page.children body, = html.children div, = body.children assert (div.content_box_x(), div.content_box_y()) == (15, 10) assert (div.width, div.height) == (10, 20) @assert_no_logs def test_absolute_images(): page, = render_pages('''
      ''') html, = page.children body, = html.children div, = body.children img1, img2 = div.children assert div.height == 0 assert (div.position_x, div.position_y) == (0, 0) assert (img1.position_x, img1.position_y) == (10, 10) assert (img1.width, img1.height) == (4, 4) assert (img2.position_x, img2.position_y) == (15, 10) assert (img2.width, img2.height) == (4, 4) # TODO: test the various cases in absolute_replaced() @assert_no_logs def test_fixed_positioning(): # TODO:test page-break-before: left/right page_1, page_2, page_3 = render_pages(''' a

      b

      c ''') html, = page_1.children assert [c.element_tag for c in html.children] == ['body', 'p'] html, = page_2.children body, = html.children div, = body.children assert [c.element_tag for c in div.children] == ['p'] html, = page_3.children assert [c.element_tag for c in html.children] == ['p', 'body'] @assert_no_logs def test_fixed_positioning_regression_1(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/641 page_1, page_2 = render_pages('''
      • a
      b
      page1
      page2
      ''') html, = page_1.children body, = html.children ul, img, div, article = body.children marker = ul.children[0] assert (ul.position_x, ul.position_y) == (80, 10) assert (img.position_x, img.position_y) == (60, 10) assert (div.position_x, div.position_y) == (40, 10) assert (article.position_x, article.position_y) == (0, 0) assert marker.position_x == ul.position_x html, = page_2.children ul, img, div, body = html.children marker = ul.children[0] assert (ul.position_x, ul.position_y) == (180, 10) assert (img.position_x, img.position_y) == (160, 10) assert (div.position_x, div.position_y) == (140, 10) assert (article.position_x, article.position_y) == (0, 0) assert marker.position_x == ul.position_x @assert_no_logs def test_fixed_positioning_regression_2(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/728 page_1, page_2 = render_pages('''
      
          ''')
          html, = page_1.children
          body, = html.children
          div, section = body.children
          assert (div.position_x, div.position_y) == (15, 10)
          article, = div.children
          assert (article.position_x, article.position_y) == (15, 20)
          header, = article.children
          assert (header.position_x, header.position_y) == (5, 10)
      
          html, = page_2.children
          div, body, = html.children
          assert (div.position_x, div.position_y) == (15, 10)
          article, = div.children
          assert (article.position_x, article.position_y) == (15, 20)
          header, = article.children
          assert (header.position_x, header.position_y) == (5, 10)
      ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.025308
      weasyprint-54.1/tests/layout/test_shrink_to_fit.py0000644000000000000000000000337500000000000020741 0ustar0000000000000000"""
          weasyprint.tests.layout.shrink_to_fit
          -------------------------------------
      
          Tests for shrink-to-fit algorithm.
      
      """
      
      import pytest
      
      from ..testing_utils import assert_no_logs, render_pages
      
      
      @assert_no_logs
      @pytest.mark.parametrize('margin_left', range(1, 10))
      @pytest.mark.parametrize('font_size', range(1, 10))
      def test_shrink_to_fit_floating_point_error_1(margin_left, font_size):
          # See bugs #325 and #288, see commit fac5ee9.
          page, = render_pages('''
            
            

      this parrot is dead

      ''' % (margin_left, font_size)) html, = page.children body, = html.children p, = body.children assert len(p.children) == 1 @assert_no_logs @pytest.mark.parametrize('font_size', (1, 5, 10, 50, 100, 1000, 10000)) def test_shrink_to_fit_floating_point_error_2(font_size): letters = 1 while True: page, = render_pages('''

      mmm %s a

      ''' % (font_size, font_size, font_size, 'i' * letters)) html, = page.children body, = html.children p, = body.children assert len(p.children) in (1, 2) assert len(p.children[0].children) == 2 text = p.children[0].children[1].children[0].text assert text if text.endswith('i'): letters = 1 break else: letters += 1 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1643122716.7401023 weasyprint-54.1/tests/layout/test_table.py0000644000000000000000000024062200000000000017164 0ustar0000000000000000""" weasyprint.tests.layout.table ----------------------------- Tests for layout of tables. """ import pytest from ..draw import assert_pixels from ..testing_utils import assert_no_logs, capture_logs, render_pages @assert_no_logs def test_inline_table(): page, = render_pages('''
      foo ''') html, = page.children body, = html.children line, = body.children table_wrapper, text = line.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table.position_x == 5 # 0 + margin-left assert td_1.position_x == 15 # 0 + border-spacing assert td_1.width == 20 assert td_2.position_x == 45 # 15 + 20 + border-spacing assert td_2.width == 30 assert table.width == 80 # 20 + 30 + 3 * border-spacing assert table_wrapper.margin_width() == 90 # 80 + 2 * margin assert text.position_x == 90 @assert_no_logs def test_implicit_width_table_col_percent(): # See https://github.com/Kozea/WeasyPrint/issues/169 page, = render_pages('''
      ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children @assert_no_logs def test_implicit_width_table_td_percent(): page, = render_pages('''
      ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children @assert_no_logs def test_layout_table_fixed_1(): page, = render_pages('''
      a
      ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table.position_x == 5 # 0 + margin-left assert td_1.position_x == 15 # 5 + border-spacing assert td_1.width == 20 assert td_2.position_x == 45 # 15 + 20 + border-spacing assert td_2.width == 40 assert table.width == 90 # 20 + 40 + 3 * border-spacing @assert_no_logs def test_layout_table_fixed_2(): page, = render_pages('''
      a
      ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table.position_x == 5 # 0 + margin-left assert td_1.position_x == 15 # 5 + border-spacing assert td_1.width == 75 # 20 + ((200 - 20 - 40 - 3 * border-spacing) / 2) assert td_2.position_x == 100 # 15 + 75 + border-spacing assert td_2.width == 95 # 40 + ((200 - 20 - 40 - 3 * border-spacing) / 2) assert table.width == 200 @assert_no_logs def test_layout_table_fixed_3(): page, = render_pages('''
      a b
      a b
      ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row_1, row_2 = row_group.children td_1, td_2 = row_1.children td_3, td_4 = row_2.children assert table_wrapper.position_x == 0 assert table.position_x == 5 # 0 + margin-left assert td_1.position_x == 15 # 0 + border-spacing assert td_3.position_x == 15 assert td_1.width == 40 assert td_2.width == 40 assert td_2.position_x == 65 # 15 + 40 + border-spacing assert td_4.position_x == 65 assert td_3.width == 40 assert td_4.width == 40 assert table.width == 110 # 20 + 40 + 3 * border-spacing @assert_no_logs def test_layout_table_fixed_4(): page, = render_pages('''
      a
      ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table.position_x == 10 # 0 + margin-left assert td_1.position_x == 10 assert td_1.width == 80 # 100 - 20 assert td_2.position_x == 90 # 10 + 80 assert td_2.width == 20 assert table.width == 100 @assert_no_logs def test_layout_table_fixed_5(): # With border-collapse page, = render_pages('''
      ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table.position_x == 0 assert table.border_left_width == 5 # half of the collapsed 10px border assert td_1.position_x == 5 # border-spacing is ignored assert td_1.margin_width() == 30 # as
  • ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table_wrapper.width == 38 # Same as table, see below assert table_wrapper.margin_left == 31 # 0 + margin-left = (100 - 38) / 2 assert table_wrapper.margin_right == 31 assert table.position_x == 31 assert td_1.position_x == 41 # 31 + spacing assert td_1.width == 4 assert td_2.position_x == 55 # 31 + 4 + spacing assert td_2.width == 4 assert table.width == 38 # 3 * spacing + 2 * 4 @assert_no_logs def test_layout_table_auto_2(): page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table.position_x == 5 # 0 + margin-left assert td_1.position_x == 6 # 5 + border-spacing assert td_1.width == 4 assert td_2.position_x == 17 # 6 + 4 + spacing + 2 * border assert td_2.width == 8 assert table.width == 27 # 3 * spacing + 4 + 8 + 4 * border @assert_no_logs def test_layout_table_auto_3(): page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row1, row2 = row_group.children td_11, td_12 = row1.children td_21, td_22 = row2.children assert table_wrapper.position_x == 0 assert table.position_x == 5 # 0 + margin-left assert td_11.position_x == td_21.position_x == 6 # 5 + spacing assert td_11.width == td_21.width == 12 assert td_12.position_x == td_22.position_x == 19 # 6 + 12 + spacing assert td_12.width == td_22.width == 8 assert table.width == 23 # 3 * spacing + 12 + 8 @assert_no_logs def test_layout_table_auto_4(): page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row1, row2 = row_group.children td_11, td_12 = row1.children td_21, td_22 = row2.children assert table_wrapper.position_x == 0 assert table.position_x == 5 # 0 + margin-left assert td_11.position_x == td_21.position_x == 6 # 5 + spacing assert td_11.width == 12 # 4 + 2 * 5 - 2 * 1 assert td_21.width == 4 assert td_12.position_x == td_22.position_x == 21 # 6 + 4 + 2 * b1 + sp assert td_12.width == 4 assert td_22.width == 10 # 4 + 2 * 3 assert table.width == 27 # 3 * spacing + 4 + 4 + 2 * b1 + 2 * b2 @assert_no_logs def test_layout_table_auto_5(): page, = render_pages('''
    aa aa aa aa aaaaaaaaaaa This will take the rest of the width
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2, td_3 = row.children assert table.width == 1000 assert td_1.width == 40 assert td_2.width == 11 * 16 assert td_3.width == 1000 - 40 - 11 * 16 @assert_no_logs def test_layout_table_auto_6(): page, = render_pages('''
    ''') # Preferred minimum width is 2 * 4 + 3 * 1 = 11 html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row1, row2 = row_group.children td_11, td_12 = row1.children td_21, = row2.children assert table_wrapper.position_x == 0 assert table.position_x == 0 assert td_11.position_x == td_21.position_x == 1 # spacing assert td_11.width == td_21.width == 4 # minimum width assert td_12.position_x == 6 # 1 + 5 + sp assert td_12.width == 14 # available width assert table.width == 21 @assert_no_logs def test_layout_table_auto_7(): page, = render_pages('''
    a
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table.position_x == 5 # 0 + margin-left assert td_1.position_x == 15 # 0 + border-spacing assert td_1.width == 20 assert td_2.position_x == 45 # 15 + 20 + border-spacing assert td_2.width == 40 assert table.width == 90 # 20 + 40 + 3 * border-spacing @assert_no_logs def test_layout_table_auto_8(): page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table.position_x == 5 # 0 + margin-left assert td_1.position_x == 15 # 5 + border-spacing assert td_1.width == 20 # fixed assert td_2.position_x == 45 # 15 + 20 + border-spacing assert td_2.width == 70 # 120 - 3 * border-spacing - 20 assert table.width == 120 @assert_no_logs def test_layout_table_auto_9(): page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row_1, row_2 = row_group.children td_1, td_2 = row_1.children td_3, td_4 = row_2.children assert table_wrapper.position_x == 0 assert table.position_x == 5 # 0 + margin-left assert td_1.position_x == 15 # 0 + border-spacing assert td_3.position_x == 15 assert td_1.width == 60 assert td_2.width == 30 assert td_2.position_x == 85 # 15 + 60 + border-spacing assert td_4.position_x == 85 assert td_3.width == 60 assert td_4.width == 30 assert table.width == 120 # 60 + 30 + 3 * border-spacing @assert_no_logs def test_layout_table_auto_10(): page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table.position_x == 10 # 0 + margin-left assert td_1.position_x == 10 assert td_1.width == 6 # 14 - 8 assert td_2.position_x == 16 # 10 + 6 assert td_2.width == 8 # maximum of the minimum widths for the column assert table.width == 14 @assert_no_logs def test_layout_table_auto_11(): page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row1, row2, row3 = row_group.children td_11, td_12 = row1.children td_21, td_22, td_23 = row2.children td_31, td_32, td_33 = row3.children assert table_wrapper.position_x == 0 assert table.position_x == 0 assert td_11.width == 10 # fixed assert td_12.width == 28 # 38 - 10 assert td_21.width == 22 # fixed assert td_22.width == 8 # fixed assert td_23.width == 8 # fixed assert td_31.width == 10 # same as first line assert td_32.width == 12 # 22 - 10 assert td_33.width == 16 # 8 + 8 from second line assert table.width == 38 @assert_no_logs def test_layout_table_auto_12(): page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row1, row2, row3 = row_group.children td_11, td_12 = row1.children td_21, td_22, td_23 = row2.children td_31, td_32, td_33 = row3.children assert table_wrapper.position_x == 0 assert table.position_x == 0 assert td_11.width == 10 # fixed assert td_12.width == 48 # 32 - 10 - sp + 2 * 8 + 2 * sp assert td_21.width == 32 # fixed assert td_22.width == 8 # fixed assert td_23.width == 8 # fixed assert td_31.width == 10 # same as first line assert td_32.width == 12 # 32 - 10 - sp assert td_33.width == 26 # 2 * 8 + sp assert table.width == 88 @assert_no_logs def test_layout_table_auto_13(): # Regression tests: these used to crash page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert td_1.width == 20 # 2 / 3 * 30 assert td_2.width == 10 # 1 / 3 * 30 assert table.width == 30 @assert_no_logs def test_layout_table_auto_14(): page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, = row.children assert td_1.width == 20 assert table.width == 20 @assert_no_logs def test_layout_table_auto_15(): page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children column_group, = table.column_groups column_1, column_2 = column_group.children assert column_1.width == 0 assert column_2.width == 0 @assert_no_logs def test_layout_table_auto_16(): # Absolute table page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert td_1.width == 20 # 2 / 3 * 30 assert td_2.width == 10 # 1 / 3 * 30 assert table.width == 30 @assert_no_logs def test_layout_table_auto_17(): # With border-collapse page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert table_wrapper.position_x == 0 assert table.position_x == 0 assert table.border_left_width == 5 # half of the collapsed 10px border assert td_1.position_x == 5 # border-spacing is ignored assert td_1.margin_width() == 30 # as assert td_1.width == 20 # 30 - 5 (border-left) - 1 (border-right) - 2*2 assert td_2.position_x == 35 assert td_2.width == 34 assert td_2.margin_width() == 60 # 34 + 2*10 + 5 + 1 assert table.width == 90 # 30 + 60 assert table.margin_width() == 100 # 90 + 2*5 (border) @assert_no_logs def test_layout_table_auto_18(): # Column widths as percentage page, = render_pages('''
    a abc
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert td_1.width == 140 assert td_2.width == 60 assert table.width == 200 @assert_no_logs def test_layout_table_auto_19(): # Column group width page, = render_pages('''
    a a abc
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2, td_3 = row.children assert td_1.width == 100 assert td_2.width == 100 assert td_3.width == 100 assert table.width == 300 @assert_no_logs def test_layout_table_auto_20(): # Multiple column width page, = render_pages('''
    a a abc
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2, td_3 = row.children assert td_1.width == 50 assert td_2.width == 30 assert td_3.width == 120 assert table.width == 200 @assert_no_logs def test_layout_table_auto_21(): # Fixed-width table with column group with widths as percentages and pixels page, = render_pages('''
    a a abc abc
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2, td_3, td_4 = row.children assert td_1.width == 100 assert td_2.width == 100 assert td_3.width == 150 assert td_4.width == 150 assert table.width == 500 @assert_no_logs def test_layout_table_auto_22(): # Auto-width table with column group with widths as percentages and pixels page, = render_pages('''
    a a a b a c a d
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2, td_3, td_4 = row.children assert td_1.width == 50 assert td_2.width == 50 assert td_3.width == 200 assert td_4.width == 200 assert table.width == 500 @assert_no_logs def test_layout_table_auto_23(): # Wrong column group width page, = render_pages('''
    a a
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert td_1.width == 100 assert td_2.width == 100 assert table.width == 200 @assert_no_logs def test_layout_table_auto_24(): # Column width as percentage and cell width in pixels page, = render_pages('''
    a abc
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert td_1.width == 140 assert td_2.width == 60 assert table.width == 200 @assert_no_logs def test_layout_table_auto_25(): # Column width and cell width as percentage page, = render_pages('''
    a abc
    ''') html, = page.children body, = html.children div, = body.children table_wrapper, = div.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert td_1.width == 140 assert td_2.width == 60 assert table.width == 200 @assert_no_logs def test_layout_table_auto_26(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/307 # Table with a cell larger than the table's max-width page, = render_pages('''
    ''') @assert_no_logs def test_layout_table_auto_27(): # Table with a cell larger than the table's width page, = render_pages('''
    ''') @assert_no_logs def test_layout_table_auto_28(): # Table with a cell larger than the table's width and max-width page, = render_pages('''
    ''') @assert_no_logs def test_layout_table_auto_29(): # Table with a cell larger than the table's width and max-width page, = render_pages('''
    ''') @assert_no_logs def test_layout_table_auto_30(): # Table with a cell larger than the table's max-width page, = render_pages('''
    ''') @assert_no_logs def test_layout_table_auto_31(): # Test a table with column widths < table width < column width + spacing page, = render_pages('''
    ''') @assert_no_logs def test_layout_table_auto_32(): # Table with a cell larger than the table's width page, = render_pages('''
    ''') html, = page.children body, = html.children table_wrapper, = body.children assert table_wrapper.margin_width() == 600 # 400 + 2 * 100 @assert_no_logs def test_layout_table_auto_33(): # Div with auto width containing a table with a min-width page, = render_pages('''
    ''') html, = page.children body, = html.children div, = body.children table_wrapper, = div.children assert div.margin_width() == 600 # 400 + 2 * 100 assert table_wrapper.margin_width() == 600 # 400 + 2 * 100 @assert_no_logs def test_layout_table_auto_34(): # Div with auto width containing an empty table with a min-width page, = render_pages('''
    ''') html, = page.children body, = html.children div, = body.children table_wrapper, = div.children assert div.margin_width() == 600 # 400 + 2 * 100 assert table_wrapper.margin_width() == 600 # 400 + 2 * 100 @assert_no_logs def test_layout_table_auto_35(): # Div with auto width containing a table with a cell larger than the # table's max-width page, = render_pages('''
    ''') html, = page.children body, = html.children div, = body.children table_wrapper, = div.children assert div.margin_width() == 600 # 400 + 2 * 100 assert table_wrapper.margin_width() == 600 # 400 + 2 * 100 @assert_no_logs def test_layout_table_auto_36(): # Test regression on a crash: https://github.com/Kozea/WeasyPrint/pull/152 page, = render_pages('''
    ''') @assert_no_logs def test_layout_table_auto_37(): # Other crashes: https://github.com/Kozea/WeasyPrint/issues/305 page, = render_pages('''
    Test
    ''') @assert_no_logs def test_layout_table_auto_38(): page, = render_pages('''
    ''') @assert_no_logs def test_layout_table_auto_39(): page, = render_pages('''
    ''') @assert_no_logs def test_layout_table_auto_40(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/368 # Check that white-space is used for the shrink-to-fit algorithm page, = render_pages('''
    a a
    ''') html, = page.children body, = html.children div, = body.children table_wrapper, = div.children table, = table_wrapper.children assert table.width == 16 * 3 @assert_no_logs def test_layout_table_auto_41(): # Cell width as percentage in auto-width table page, = render_pages('''
    a a a a a a a a a a a a a a a a
    ''') html, = page.children body, = html.children div, = body.children table_wrapper, = div.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert td_1.width == 70 assert td_2.width == 30 assert table.width == 100 @assert_no_logs def test_layout_table_auto_42(): # Cell width as percentage in auto-width table page, = render_pages('''
    a a a a a a a a a a a a a a a a
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2 = row.children assert td_1.width == 70 assert td_2.width == 30 assert table.width == 100 @assert_no_logs def test_layout_table_auto_43(): # Cell width as percentage on colspan cell in auto-width table page, = render_pages('''
    a a a a a a a a a a a a a a a a a a a a a a a a
    ''') html, = page.children body, = html.children div, = body.children table_wrapper, = div.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2, td_3 = row.children assert td_1.width == 35 assert td_2.width == 30 assert td_3.width == 35 assert table.width == 100 @assert_no_logs def test_layout_table_auto_44(): # Cells widths as percentages on normal and colspan cells page, = render_pages('''
    a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a
    ''') html, = page.children body, = html.children div, = body.children table_wrapper, = div.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td_1, td_2, td_3, td_4, td_5 = row.children assert td_1.width == 10 assert td_2.width == 30 assert td_3.width == 10 assert td_4.width == 40 assert td_5.width == 10 assert table.width == 100 @assert_no_logs def test_layout_table_auto_45(): # Cells widths as percentage on multiple lines page, = render_pages('''
    a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a
    a a a a a a a a a a a a a a a a a a a a a a a a
    ''') html, = page.children body, = html.children div, = body.children table_wrapper, = div.children table, = table_wrapper.children row_group, = table.children row_1, row_2 = row_group.children td_11, td_12, td_13, td_14, td_15 = row_1.children td_21, td_22, td_23 = row_2.children assert td_11.width == 10 # 31% - 30% assert td_12.width == 300 # 30% assert td_13.width == 270 # 1000 - 31% - 42% assert td_14.width == 400 # 40% assert td_15.width == 20 # 42% - 2% assert td_21.width == 310 # 31% assert td_22.width == 270 # 1000 - 31% - 42% assert td_23.width == 420 # 42% assert table.width == 1000 @assert_no_logs def test_layout_table_auto_46(): # Test regression: # http://test.weasyprint.org/suite-css21/chapter8/section2/test56/ page, = render_pages('''
    ''') html, = page.children body, = html.children div, = body.children table_wrapper, = div.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td, = row.children assert td.width == 200 assert table.width == 200 assert div.width == 340 # 200 + 2 * 50 + 2 * 20 @assert_no_logs def test_layout_table_auto_47(): # Test regression: # https://github.com/Kozea/WeasyPrint/issues/666 page, = render_pages('''
    aaa
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td, = row.children assert td.width == 48 # 3 * font-size @assert_no_logs def test_layout_table_auto_48(): # Related to: # https://github.com/Kozea/WeasyPrint/issues/685 page, = render_pages('''
    aaa
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td, = row.children assert td.width == 48 # 3 * font-size @pytest.mark.xfail @assert_no_logs def test_layout_table_auto_49(): # Related to: # https://github.com/Kozea/WeasyPrint/issues/685 # See TODO in table_layout.group_layout page, = render_pages('''
    aaa
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row, = row_group.children td, = row.children assert td.width == 48 # 3 * font-size @assert_no_logs def test_layout_table_auto_50(): # Test regression: # https://github.com/Kozea/WeasyPrint/issues/685 page, = render_pages('''
    aaaaa
    aaa aaa aaa aaa
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row_1, row_2 = row_group.children for td in row_1.children: assert td.width == 44 # (15 * font_size - 4 * sp) / 5 td_21, = row_2.children assert td_21.width == 240 # 15 * font_size @assert_no_logs @pytest.mark.parametrize( 'body_width, table_width, check_width, positions, widths', ( ('500px', '230px', 220, [170, 5], [45, 155]), ('530px', '100%', 520, [395, 5], [120, 380]), ) ) def test_explicit_width_table_percent_rtl(body_width, table_width, check_width, positions, widths): page, = render_pages('''
    الاسم العائلة
    محمد يوسف 29
    ''' % (body_width, table_width)) html, = page.children body, = html.children wrapper, = body.children table, = wrapper.children row_group, = table.children row_1, row_2 = row_group.children assert table.position_x == 0 assert table.width == check_width assert [child.position_x for child in row_1.children] == positions assert [child.position_x for child in row_2.children] == positions assert [child.width for child in row_1.children] == widths assert [child.width for child in row_2.children] == widths @assert_no_logs def test_table_column_width_1(): source = '''
    This cell will be truncated to grid width This cell will be removed as it is beyond the grid width
    ''' with capture_logs() as logs: page, = render_pages(source) assert len(logs) == 1 assert logs[0].startswith('WARNING: This table row has more columns than ' 'the table, ignored 1 cell') html, = page.children body, = html.children wrapper, = body.children table, = wrapper.children row_group, = table.children first_row, second_row, third_row = row_group.children cells = [first_row.children, second_row.children, third_row.children] assert len(first_row.children) == 2 assert len(second_row.children) == 4 # Third cell here is completly removed assert len(third_row.children) == 2 assert body.position_x == 0 assert wrapper.position_x == 0 assert wrapper.margin_left == 5000 assert wrapper.content_box_x() == 5000 # auto margin-left assert wrapper.width == 10000 assert table.position_x == 5000 assert table.width == 10000 assert row_group.position_x == 5100 # 5000 + border_spacing assert row_group.width == 9800 # 10000 - 2*border-spacing assert first_row.position_x == row_group.position_x assert first_row.width == row_group.width # This cell has colspan=3 assert cells[0][0].position_x == 5100 # 5000 + border-spacing # `width` on a cell sets the content width assert cells[0][0].width == 3000 # 30% of 10000px assert cells[0][0].border_width() == 3022 # 3000 + borders + padding # Second cell of the first line, but on the fourth and last column assert cells[0][1].position_x == 8222 # 5100 + 3022 + border-spacing assert cells[0][1].border_width() == 6678 # 10000 - 3022 - 3*100 assert cells[0][1].width == 6656 # 6678 - borders - padding assert cells[1][0].position_x == 5100 # 5000 + border-spacing # `width` on a column sets the border width of cells assert cells[1][0].border_width() == 1000 # 10% of 10000px assert cells[1][0].width == 978 # 1000 - borders - padding assert cells[1][1].position_x == 6200 # 5100 + 1000 + border-spacing assert cells[1][1].border_width() == 911 # (3022 - 1000 - 2*100) / 2 assert cells[1][1].width == 889 # 911 - borders - padding assert cells[1][2].position_x == 7211 # 6200 + 911 + border-spacing assert cells[1][2].border_width() == 911 # (3022 - 1000 - 2*100) / 2 assert cells[1][2].width == 889 # 911 - borders - padding # Same as cells[0][1] assert cells[1][3].position_x == 8222 # Also 7211 + 911 + border-spacing assert cells[1][3].border_width() == 6678 assert cells[1][3].width == 6656 # Same as cells[1][0] assert cells[2][0].position_x == 5100 assert cells[2][0].border_width() == 1000 assert cells[2][0].width == 978 assert cells[2][1].position_x == 6200 # Same as cells[1][1] assert cells[2][1].border_width() == 8700 # 1000 - 1000 - 3*border-spacing assert cells[2][1].width == 8678 # 8700 - borders - padding assert cells[2][1].colspan == 3 # truncated to grid width @assert_no_logs def test_table_column_width_2(): page, = render_pages('''
    ''') html, = page.children body, = html.children wrapper, = body.children table, = wrapper.children row_group, = table.children row, = row_group.children assert row.children[0].width == 500 assert row.children[1].width == 600 assert row.children[2].width == 0 assert table.width == 1500 # 500 + 600 + 4 * border-spacing @assert_no_logs def test_table_column_width_3(): # Sum of columns width larger that the table width: # increase the table width page, = render_pages('''
    ''') html, = page.children body, = html.children wrapper, = body.children table, = wrapper.children row_group, = table.children row, = row_group.children cell_1, cell_2 = row.children assert cell_1.width == 600 # 60% of 1000px assert cell_2.width == 600 assert table.width == 1500 # 600 + 600 + 3*border-spacing assert wrapper.width == table.width @assert_no_logs def test_table_row_height_1(): page, = render_pages('''
    X
    X
    X
    X
    X X X
    ''') html, = page.children body, = html.children wrapper, = body.children table, = wrapper.children row_group, = table.children assert wrapper.position_y == 0 assert table.position_y == 3 # 0 + margin-top assert table.height == 620 # sum of row heigths + 5*border-spacing assert wrapper.height == table.height assert row_group.position_y == 103 # 3 + border-spacing assert row_group.height == 420 # 620 - 2*border-spacing assert [row.height for row in row_group.children] == [ 80, 30, 0, 10] assert [row.position_y for row in row_group.children] == [ # cumulative sum of previous row heights and border-spacings 103, 283, 413, 513] assert [[cell.height for cell in row.children] for row in row_group.children] == [ [420, 60, 40, 20, 20, 20], [0, 10], [], [0] ] assert [[cell.border_height() for cell in row.children] for row in row_group.children] == [ [420, 80, 80, 80, 80, 80], [30, 30], [], [10] ] # The baseline of the first row is at 40px because of the third column. # The second column thus gets a top padding of 20px pushes the bottom # to 80px.The middle is at 40px. assert [[cell.padding_top for cell in row.children] for row in row_group.children] == [ [0, 20, 0, 0, 30, 60], [15, 5], [], [10] ] assert [[cell.padding_bottom for cell in row.children] for row in row_group.children] == [ [0, 0, 40, 60, 30, 0], [15, 15], [], [0] ] assert [[cell.position_y for cell in row.children] for row in row_group.children] == [ [103, 103, 103, 103, 103, 103], [283, 283], [], [513] ] @assert_no_logs def test_table_row_height_2(): # A cell box cannot extend beyond the last row box of a table. page, = render_pages('''
    ''') html, = page.children body, = html.children wrapper, = body.children table, = wrapper.children row_group, = table.children @assert_no_logs def test_table_row_height_3(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/ page, = render_pages('''
    Table
    ''') html, = page.children body, = html.children wrapper, = body.children table, = wrapper.children assert table.height == 20 row_group, = table.children assert row_group.height == 20 row1, row2 = row_group.children assert row1.height == 20 assert row2.height == 0 @assert_no_logs def test_table_vertical_align(): assert_pixels('table_vertical_align', 28, 10, ''' rrrrrrrrrrrrrrrrrrrrrrrrrrrr rBBBBBBBBBBBBBBBBBBBBBBBBBBr rBrBB_BB_BB_BB_BBrrBBrrBB_Br rBrBB_BB_BBrBBrBBrrBBrrBBrBr rB_BBrBB_BBrBBrBBrrBBrrBBrBr rB_BBrBB_BB_BB_BBrrBBrrBB_Br rB_BB_BBrBB_BB_BB__BB__BB_Br rB_BB_BBrBB_BB_BB__BB__BB_Br rBBBBBBBBBBBBBBBBBBBBBBBBBBr rrrrrrrrrrrrrrrrrrrrrrrrrrrr ''', '''
    o o o o o o o o o o o o o o o o
    ''') # noqa @assert_no_logs def test_table_wrapper(): page, = render_pages('''
    ''') html, = page.children body, = html.children wrapper, = body.children table, = wrapper.children assert body.width == 1000 assert wrapper.width == 600 # Not counting borders or padding assert wrapper.margin_left == 100 assert table.margin_width() == 600 assert table.width == 578 # 600 - 2*10 - 2*1, no margin # box-sizing in the UA stylesheet makes `height: 500px` set this assert table.border_height() == 500 assert table.height == 478 # 500 - 2*10 - 2*1 assert table.margin_height() == 500 # no margin assert wrapper.height == 500 assert wrapper.margin_height() == 700 # 500 + 2*100 @assert_no_logs def test_table_html_tag(): # Non-regression test: this used to cause an exception page, = render_pages('') @assert_no_logs @pytest.mark.parametrize('html, rows, positions', ( ('''

    Dummy title

    row 1
    row 2
    row 3
    row 4
    row 5
    row 6
    row 7
    row 8
    ''', [2, 3, 1, 2], [30, 70, 0, 40, 80, 0, 0, 40]), ('''

    Dummy title

    row 1
    row 2
    row 3
    row 4
    ''', [0, 3, 1], [0, 40, 80, 0]), ('''

    Dummy title

    row 1
    row 2
    row 3
    row 4
    ''', [0, 3, 1], [0, 40, 80, 0]), ('''

    Dummy title

    row 1
    row 2
    row 3
    ''', [1, 2], [30, 0, 40]), ('''

    Dummy title

    r1l1
    r2l1
    r2l2
    r3l1
    ''', [1, 2], [30, 0, 80]), ('''

    Dummy title

    r1l1
    r2l1
    r2l2
    r3l1
    ''', [1, 2], [30, 0, 80]), )) def test_table_page_breaks(html, rows, positions): pages = render_pages(html) rows_per_page = [] rows_position_y = [] for i, page in enumerate(pages): html, = page.children body, = html.children if i == 0: body_children = body.children[1:] # skip h1 else: body_children = body.children if not body_children: rows_per_page.append(0) continue table_wrapper, = body_children table, = table_wrapper.children rows_in_this_page = 0 for group in table.children: assert group.children, 'found an empty table group' for row in group.children: rows_in_this_page += 1 rows_position_y.append(row.position_y) cell, = row.children rows_per_page.append(rows_in_this_page) assert rows_per_page == rows assert rows_position_y == positions @assert_no_logs def test_table_page_breaks_complex_1(): pages = render_pages('''

    Lipsum

    Header
    Row 1
    Row 2
    Row 3
    Row 4
    Row 5
    Footer
    ''') rows_per_page = [] for i, page in enumerate(pages): groups = [] html, = page.children body, = html.children table_wrapper, = body.children if i == 0: assert table_wrapper.element_tag == 'h1' else: table, = table_wrapper.children for group in table.children: assert group.children, 'found an empty table group' rows = [] for row in group.children: cell, = row.children line, = cell.children text, = line.children rows.append(text.text) groups.append(rows) rows_per_page.append(groups) assert rows_per_page == [ [], [['Header'], ['Row 1'], ['Footer']], [['Header'], ['Row 2', 'Row 3'], ['Footer']], [['Header'], ['Row 4']], [['Row 5']] ] @assert_no_logs def test_table_page_breaks_complex_2(): pages = render_pages('''
    head 1
    row 1 1
    row 1 2
    row 1 3
    foot 1
    head 2
    row 2 1
    row 2 2
    row 2 3
    foot 2
    ''') rows_per_page = [] for i, page in enumerate(pages): groups = [] html, = page.children body, = html.children for table_wrapper in body.children: table, = table_wrapper.children for group in table.children: assert group.children, 'found an empty table group' rows = [] for row in group.children: cell, = row.children line, = cell.children text, = line.children rows.append(text.text) groups.append(rows) rows_per_page.append(groups) assert rows_per_page == [ [['head 1'], ['row 1 1', 'row 1 2'], ['foot 1']], [['head 1'], ['row 1 3'], ['foot 1'], ['head 2'], ['row 2 1'], ['foot 2']], [['head 2'], ['row 2 2', 'row 2 3'], ['foot 2']], ] # TODO: test positions, the place of footer on the first page is wrong @assert_no_logs def test_table_page_break_after(): page1, page2, page3, page4, page5, page6 = render_pages('''

    Dummy title

    row 1
    row 2
    row 3
    row 1
    row 2
    row 3
    row 1
    row 2
    row 3
    row 1
    row 2
    row 3
    row 1
    row 2
    row 3

    bla bla

    ''') html, = page1.children body, = html.children h1, table_wrapper = body.children table, = table_wrapper.children table_group1, table_group2 = table.children assert len(table_group1.children) == 3 assert len(table_group2.children) == 1 html, = page2.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children table_group1, table_group2 = table.children assert len(table_group1.children) == 2 assert len(table_group2.children) == 3 html, = page3.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children table_group, = table.children assert len(table_group.children) == 3 html, = page4.children assert not html.children html, = page5.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children table_group, = table.children assert len(table_group.children) == 3 html, = page6.children body, = html.children p, = body.children assert p.element_tag == 'p' @assert_no_logs def test_table_page_break_before(): page1, page2, page3, page4, page5, page6 = render_pages('''

    Dummy title

    row 1
    row 2
    row 3
    row 1
    row 2
    row 3
    row 1
    row 2
    row 3
    row 1
    row 2
    row 3
    row 1
    row 2
    row 3

    bla bla

    ''') html, = page1.children body, = html.children h1, = body.children assert h1.element_tag == 'h1' html, = page2.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children table_group1, table_group2 = table.children assert len(table_group1.children) == 3 assert len(table_group2.children) == 1 html, = page3.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children table_group, = table.children assert len(table_group.children) == 2 html, = page4.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children table_group1, table_group2 = table.children assert len(table_group1.children) == 3 assert len(table_group2.children) == 3 html, = page5.children assert not html.children html, = page6.children body, = html.children table_wrapper, p = body.children table, = table_wrapper.children table_group, = table.children assert len(table_group.children) == 3 assert p.element_tag == 'p' @assert_no_logs @pytest.mark.parametrize('html, rows', ( ('''
    row 0
    row 1
    row 2
    row 3
    ''', [1, 3]), ('''
    row 0
    row 1
    row 2
    row 3
    ''', [1, 3]), ('''
    row 0
    row 1
    row 2
    row 3
    ''', [2, 2]), ('''
    row 0
    row 1
    row 2
    row 3
    ''', [3, 1]), ('''
    row 0
    row 1
    row 2
    row 3
    ''', [3, 1]), ('''

    wow p

    row 0
    row 1
    row 2
    row 3
    ''', [1, 3, 1]), ('''
    row 0
    row 1
    row 2
    row 0
    row 1
    row 2
    ''', [2, 3, 1]), ('''
    row 0
    row 1
    row 2
    row 0
    row 1
    row 2
    ''', [2, 3, 1]), ('''
    row 0
    row 1
    row 2
    row 0
    row 1
    row 2
    ''', [2, 3, 1]), ('''
    row 0
    row 1
    row 2
    row 0
    row 1
    row 2
    ''', [2, 3, 1]), ('''
    row 0
    row 1
    row 2
    row 0
    row 1
    row 2
    ''', [3, 3]), )) def test_table_page_break_avoid(html, rows): pages = render_pages(html) assert len(pages) == len(rows) rows_per_page = [] for page in pages: html, = page.children body, = html.children if body.children[0].element_tag == 'p': rows_per_page.append(len(body.children)) continue else: table_wrapper, = body.children table, = table_wrapper.children rows_in_this_page = 0 for group in table.children: for row in group.children: rows_in_this_page += 1 rows_per_page.append(rows_in_this_page) assert rows_per_page == rows @assert_no_logs @pytest.mark.parametrize('vertical_align, table_position_y', ( ('top', 8), ('bottom', 8), ('baseline', 10), )) def test_inline_table_baseline(vertical_align, table_position_y): # Check that inline table's baseline is its first row's baseline. # # Div text's baseline is at 18px from the top (10px because of the # line-height, 8px as it's weasyprint.otf's baseline position). # # When a row has vertical-align: baseline cells, its baseline is its cell's # baseline. The position of the table is thus 10px above the text's # baseline. # # When a row has another value for vertical-align, the baseline is the # bottom of the row. The first cell's text is aligned with the div's text, # and the top of the table is thus 8px above the baseline. page, = render_pages('''
    abc
    a
    a
    abc
    ''' % vertical_align) html, = page.children body, = html.children div, = body.children line, = div.children text1, table_wrapper, text2 = line.children table, = table_wrapper.children assert text1.position_y == text2.position_y == 0 assert table.height == 10 * 2 assert abs(table.position_y - table_position_y) < 0.1 @assert_no_logs def test_table_caption_margin_top(): page, = render_pages('''

    ''') html, = page.children body, = html.children h1, wrapper, h2 = body.children caption, table = wrapper.children tbody, = table.children assert (h1.content_box_x(), h1.content_box_y()) == (20, 20) assert (wrapper.content_box_x(), wrapper.content_box_y()) == (20, 50) assert (caption.content_box_x(), caption.content_box_y()) == (40, 70) assert (tbody.content_box_x(), tbody.content_box_y()) == (20, 100) assert (h2.content_box_x(), h2.content_box_y()) == (20, 130) @assert_no_logs def test_table_caption_margin_bottom(): page, = render_pages('''

    ''') html, = page.children body, = html.children h1, wrapper, h2 = body.children table, caption = wrapper.children tbody, = table.children assert (h1.content_box_x(), h1.content_box_y()) == (20, 20) assert (wrapper.content_box_x(), wrapper.content_box_y()) == (20, 50) assert (tbody.content_box_x(), tbody.content_box_y()) == (20, 50) assert (caption.content_box_x(), caption.content_box_y()) == (40, 80) assert (h2.content_box_x(), h2.content_box_y()) == (20, 130) @assert_no_logs @pytest.mark.parametrize('rows_expected, thead, tfoot, content', ( ([[], ['Header', 'Footer']], 45, 45, '

    content

    '), ([[], ['Header', 'Footer']], 85, 5, '

    content

    '), ([['Header', 'Footer']], 30, 30, '

    content

    '), ([[], ['Header']], 30, 110, '

    content

    '), ([[], ['Header', 'Footer']], 30, 60, '

    content

    '), ([[], ['Footer']], 110, 30, '

    content

    '), # We try to render the header and footer on the same page, but it does not # fit. So we try to render the header or the footer on the next one, but # nothing fit either. ([[], []], 110, 110, '

    content

    '), ([['Header', 'Footer']], 30, 30, ''), ([['Header']], 30, 110, ''), ([['Header', 'Footer']], 30, 60, ''), ([['Footer']], 110, 30, ''), ([[]], 110, 110, ''), )) def test_table_empty_body(rows_expected, thead, tfoot, content): html = ''' %s
    Header
    Footer
    ''' % (thead, tfoot, content) pages = render_pages(html) assert len(pages) == len(rows_expected) for i, page in enumerate(pages): rows = [] html, = page.children body, = html.children table_wrapper = body.children[-1] if not table_wrapper.is_table_wrapper: assert rows == rows_expected[i] continue table, = table_wrapper.children for group in table.children: for row in group.children: cell, = row.children line, = cell.children text, = line.children rows.append(text.text) assert rows == rows_expected[i] def test_table_break_children_margin(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1254 html = '''

    Page1

    Page2

    Page3

    ''' assert len(render_pages(html)) == 3 def test_table_td_break_inside_avoid(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1547 html = '''
    a
    a
    ''' assert len(render_pages(html)) == 2 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/acid2-reference.html0000644000000000000000000000477400000000000020773 0ustar0000000000000000 The Second Acid Test (Reference Rendering)

    Hello World!

    Follow this link to view the reference image, which should be rendered below the text "Hello World!" on the test page in the same way that this paragraph is rendered below that text on this page.

    ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/acid2-test.html0000644000000000000000000003367300000000000020014 0ustar0000000000000000 The Second Acid Test

    Standards compliant?

    Take The Acid2 Test and compare it to the reference rendering.

    Hello World!

                                  
    ERROR
     
    ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/blue.jpg0000644000000000000000000000044100000000000016603 0ustar0000000000000000JFIFHHCC"   ?p+././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/doc1.html0000644000000000000000000000407400000000000016674 0ustar0000000000000000

    WeasyPrint test document (with Ünicōde)

    Hello

    WeasyPrint
    ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/doc1_UTF-16BE.html0000644000000000000000000000144400000000000020043 0ustar0000000000000000<html> <head> <!-- wrong, but overriden: --> <meta http-equiv=Content-Type content=text/html;charset=utf8> </head> <body> <h1>WeasyPrint test document (with nicMde)</h1> <p style="color: black; font-size: 2em">Hello</p> <ul> <li><a href=home.html style="padding: 1px 2px 3px 4px">Home</a> <li> & </ul> <div><span><span>WeasyPrint</span></span></div> </body> ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/icon.png0000644000000000000000000000145600000000000016617 0ustar0000000000000000PNG  IHDRh6 pHYs   cHRMz%u0`:o_FIDATxku;$ub h" ě޼̩E_j<(R ŢYM5fef~ϷUgr[NN щ&oOڤr?UU/^2EWct7V(^S(1qDkiۧG#8GÔCX: s~Tp}6ƞ[VDP-˗ 펉._IWIy,-m _OR'tmmpb{:g_vΔm3!xrj/,ʵ cT8g2:f W>aS^&I[lqTZ*Ie5{W"l8) 87v桕*dnŽ5Pb{{|mbK`/!TV*M-_ b%}?sM|TT]DO0$W¿xdD6L:p;-e93ZN[_}g>οcY'ض.90:̈́@Lµߗ"iCe!~8Mxq$?㑮&sqw&wSܶޗq,;`dQ$$M?w}:z3zft+IENDB`././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/latin1-test.css0000644000000000000000000000012500000000000020030 0ustar0000000000000000h1::before { content: "Ilv Unicode"; background-image: url(pattern.png) } ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/logo_small.png0000644000000000000000000001457300000000000020023 0ustar0000000000000000PNG  IHDRd)$sRGBbKGD pHYs  tIME&'4.IDATxy]Uks[TR!B 2D@@E} (m=b2 3f, HH%JN眽 86?9wokmxg3m{G1ƌ|"r@p@TՂ1FG6+"o7f2#nd_5FġHHW8tmM=AT{ s*" zvU}4Do ykT(A~C7;{0=SS}֒2"c>VVkiwQilTo9y^O[5[e؉ɟjhNKၥ(bDt`7MI|t7:ʍ{mHh$@T"e1qޟTOkv\kq[gA·$+b}zs+oȆj=}gu22r `2oJsR47jt{uKOcdlWBDN,o.9` 5鸹%MU4ƔG@) I" Y|:"v]/m5(Auvuα?ۛi霈dM pUm:Hy=x*0U3Jx使Wα8WZ>qsWȑ|WN?TWJkQ#2Ovws=sYx*/=k6Y6oK4}^M#D_ߛH4RT,8gY l\<5q"cŌRA@ok؊iQ-9qHp&bxsمntAj1bG7];k}σZ-rUs^~C"h`5y]w61[[u鯯~MmI[9`qTtlIrxLsν 7YF"_HA\G?C!:[3Wv՝}\ T9gպsWlaoROlt)z`+bvv\zb_OJ$ hL\SU]#** &* {Q{s_zGmOf;fk1*$H`〒-(.0An owt"S@h%(U$L!!CgHσa/ yD!`*@b7t#|ᾎw^*&Զ]J'_ rܣb՚6/)n2sq~lwn[ElMo(%*t%P"[>~?S?'*$=DLd$bu4S5(Nq~?tc3?8ӜU?~v)_+" f<*];,+H!97Ybu^iָ\!c`Pb?E45;:}5oyH[r簻 6$0%<ؕp$D. D${q.@Q80lx D+v9?O1ehhH(R3@)L6[{e04Ia_U!B4v Lu ?b-WC)",qau_[l689;jsk> ኗDޛ,I1۰?Wl>ļY ږ7ʼp%NOGO37oӻ`$ ئ߰X7KLDlX0(?.21pdxy[" z/;1uM#հ7(Px";}ԍwz{׭'-Hc*m&ctJ|n,.6G(.~IߓRС :%6ò.qK/?#-;fb2I[ecIX[Yhj+S3ம"9k=9i.+=g x3ɢoq|Jv \eKCޖQēQ 62Z"'[bŏ^ߐf-W(,ea=Ӂc껿bPD1X& |'z74M5K*i^pI+֘␽An»$QEoZd,NYHP$)_q-[( S^76hdSk?#3 &3qHD J}{]{y (|Í&O2bt8Q̟p-E谻5GTߛݵk֬٫CsZOe|4[ݫY߮57MVylsfg?UɖRHo%.!;4MEHYq>K:ZјIUU+Rc^n0Ɣ'!fƍu悊@vJD~wl;gCj2xym5;CW}TKO1 Pl 33: Aé^X 8Qfԧ .|½LɞN5]OvA r Zt^~!OXfb?iq#*[)Py3>c(ⴈcRt4ҔV%}T'`u`_Xb̐#q0qI^յt~5=3fx cD*D`Gsw\wya`U_0v,C 9? 'x &q6"EO) )/EK)In15HF۪+!fEо*AV+˝Ԯ2aȈ|5$Gܸ.|eYTd䳽 6N&}QDV[cvqG=cj\S/Qg/0sVA\a 31qw@TiVJi"\EY_ɶ)Bߴ_AH7A@'8 ުJ!OD.J %|x9+[}C2XS#1o"_W"{ƽ2~(_s#"DGk5^9kԟqb HM*Pyɤ^0}CsdDEXk_Ko.kh ͨ&2ĺ Cd[>H90"`"0_ L 7{%ʦ#g8Pt("30fx1G_ G+ڴEAyZzP Jqxߗ,CLՒP#z _OTf bニb*☩1j b& @%ClXrueE9Nw7Rlӌ{rKL81 ckk4վQ7T3WT 삵֩?w9 kZۻ~c3_M8+Җ5}R)Aeď Bg#Y.Ͷ߈ c)i3mTsӫ H\5ڊ/hC1#Jg(A*RO6:K×a$S5&Vktgu$}U^_VUQU#"*"u `Z%~q~*%qT $iWLl 2,JϠ;㐮(fgo3ñg:HوV >īQc_1u &K0V1Ď*)E#jG @)<$g:)Z~[u_}['Fؑ>tkGL{h6 'x˵ڮSS@<_1]թguH"̌/Zew2U%zjLФmkMڵ'xԺNcrĮKTgI߈0@JEXDpq1Y^.b$e@T66pl1 uXQ/;QjNN}{͞^m+ H1/I[9\ܰSZzg=~OaZ(/\?8joXLE8["}‡{>%Wk K@ƧEd`Q#py`#NUq"rjr^5 O3*#]w"2;:":&|hldŸ)?*wt`Um`tO~ULD w}UMa8IENDB`././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/mini_ua.css0000644000000000000000000000023100000000000017302 0ustar0000000000000000/* Minimal user-agent stylesheet */ p { margin: 1em 0px } /* 0px should be translated to 0*/ a { text-decoration: underline } h1 { font-weight: bolder } ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/not-optimized.jpg0000644000000000000000000000130000000000000020451 0ustar0000000000000000JFIFC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222 " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?W ;N+ v" c|#qt^4),g(O5ٗӢ %%\ddrO_@././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/pattern.gif0000644000000000000000000000005500000000000017317 0ustar0000000000000000GIF89a!, ;././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/pattern.palette.png0000644000000000000000000000021400000000000020770 0ustar0000000000000000PNG  IHDR? =sRGBPLTE pHYs  tIME 6 J IDATch`usIENDB`././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/pattern.png0000644000000000000000000000011400000000000017332 0ustar0000000000000000PNG  IHDR& )IDATxcaHIENDB`././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/pattern.svg0000644000000000000000000000031300000000000017346 0ustar0000000000000000 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/really-a-png.svg0000644000000000000000000000011400000000000020160 0ustar0000000000000000PNG  IHDR& )IDATxcaHIENDB`././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/really-a-svg.png0000644000000000000000000000031300000000000020161 0ustar0000000000000000 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/sheet2.css0000644000000000000000000000017000000000000017055 0ustar0000000000000000li { margin-bottom: 3em; /* Should be masked*/ margin: 2em 0; margin-left: 4em; /* Should not be masked*/ } ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/sub_directory/sheet1.css0000644000000000000000000000037100000000000021734 0ustar0000000000000000@import url(../sheet2.css) all; p { background: currentColor; } @media print { ul { /* 1ex == 0.8em for weasyprint.otf. */ margin: 2em 2.5ex; } } @media screen { ul { border-width: 1000px !important; } } ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/user.css0000644000000000000000000000012400000000000016640 0ustar0000000000000000html { /* Reversed contrast */ color: white; background-color: black; } ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/utf8-test.css0000644000000000000000000000013000000000000017522 0ustar0000000000000000h1::before { content: "I løvë Unicode"; background-image: url(pattern.png) } ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/resources/weasyprint.otf0000644000000000000000000003071000000000000020073 0ustar0000000000000000pFFTMkN1GDEF!-\2GPOS 1 GSUBc-OS/2xJx`cmap_Xlrgasp-Tglyf!j headg6hhea 5,4$hmtx׀loca?9@,maxp< X name$HpostWU+PD __< o_Y]g3333@3   HW3C @ 333 @33 lPN@&~1Sx     " & 0 : D!"!&"""""""+"H"`"e"% (1Rx     & 0 9 D!"!&"""""""+"H"`"d"%pL3:&߿ߩ޼ޠމކ"nx  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`qcdhwojui釙rfvk{ϧbml|aԸx݃py""0>LZhv,:HVdr (6DR`n|$2@N\jx .<JXfr (66DR`n|$2@N\jx .<JXft*8FTbp~  & 4 B P ^ l z   " 0 > L Z h v    , : H V d r  ( 6 D R ` n |   $ 2 @ N33!%!!3̀333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!331!!33!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!33!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!333!!3U 3"   ) G   # D;       1WeasyPrint is a font created out of Ahem, with additional OpenType features and 1024 units in 1em to please Pango. Most characters are the em square, except &EAcute and "p", which show ascent/descent from the baseline. Useful for testing composition systems. Ahem is originally produced by Todd Fahrner for the CSS Samurai's browser testing.WeasyPrint is a font created out of Ahem, with additional OpenType features and 1024 units in 1em to please Pango. Most characters are the em square, except &EAcute and "p", which show ascent/descent from the baseline. Useful for testing composition systems. Ahem is originally produced by Todd Fahrner for the CSS Samurai's browser testing.WeasyPrintWeasyPrintRegularRegularFontForge : WeasyPrint : 7-10-2016FontForge : WeasyPrint : 7-10-2016WeasyPrintWeasyPrintVersion 1.2Version 1.2WeasyPrintWeasyPrintAhemAhemRegularRegularWeasyPrintWeasyPrintd  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghjikmlnoqprsutvwxzy{}|~ NULLglyph2glyph204glyph205HTDEL OneAndAHalf ThreeHeights "* LDFLTgrek`탥Nܻ1so{mwVx ' y  Oa7 H0J2sp2pq  KHJI+(*)khji[XZY;8:9{xzyGDFE'0utM>wK,[j5֮߰i۷޵g䔌 fl봲GO_x]}~ΆܧG_˩2዗]|eǛG0ܻpꍻM=}zLe<{ά8ywŒxdP{̜LNfdfNNffr23d&''399LNNfrr2}2AY"+dl-C9"'2Ct.M - mCcdae1gIfYgEVfUVgMGyG<_|om!?B+İSbV,Hȉ(hD ˨ɸLʴʼ,ʲʺlʶU~5B*ղZUjSm]ձ:UZ^=ҳzQ?OsJgMg@\kpn1<' ?75\6Wus6w}<5/k|4_wA;b'총KvŮ ew=GĞ;z\rcn͸p)q9Wp%Wq5p-AD0ab 4f1E,cx=~Ϩ'x10Dѱ7 @{/BȟMUL)7O5VY,=kH4N>iy0 0 lc~ܬ0l&}N 0 0 o6a%_VrͧcxڍTMG-/c  ZEVr$P]cjWLK3Cw/9E ~+y]M,(ؚ"ڣ4 .w% Ƒ?(=-]@5aEc݊ʔ&4@="ZŽKd<3cE+'.9Y'^Ooۗ[PL\RGvK%<?c[X fk/Z\Jd : -.8^#S'Kd7xwy+B)Lg5V ǽP;Dx YTv{g//ƥK =ͅnDSgƳʝVAlvGnyeBŪ(L0U5tXwHdϦn8Xjh>Umi|l=$*T29T~.WGyPb_+6<.v>&ڇv8ٖ[a_Ln:d(;bL:&eW}u 6Dsa\y SM]2[ m֚=W\qSN;9fV[clm72l&jiLKL/󛭶Ad AL Egff&):J$rӲ3R̬{҇Px%ɡ H_ 4!S%dQ4ֱZR;xڭRNQ=+1‚D@B@.f/PX* bEee7 ~ 8;\ڙ͜333;3Px1U2œMcPTV)؁"8EE nz7hjFPAO &>$:5cULD*"L D =R۰at1?6b3zF/!k!DpC!3q*a`1Cc3s<>XG&y$yJ/gnu ([(Y\E IPo%K%MJ^~"L~HVf Y/qm+~m;a[kqmwsT%~A9|'--˪"wKK~~xc`d``bpb``vq aI/JfPI,ca`2R͘Zb1 ` @6H|V+(o_Y]g././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.026308 weasyprint-54.1/tests/test_acid2.py0000644000000000000000000000232700000000000015540 0ustar0000000000000000""" weasyprint.tests.test_draw.test_acid2 ------------------------------------- Check the famous Acid2 test. """ import io import pytest from PIL import Image from weasyprint import HTML from .draw import assert_pixels_equal from .testing_utils import assert_no_logs, capture_logs, resource_filename @pytest.mark.xfail @assert_no_logs def test_acid2(): # TODO: fails because of Ghostscript rendering def render(filename): return HTML(resource_filename(filename)).render() with capture_logs(): # This is a copy of http://www.webstandards.org/files/acid2/test.html document = render('acid2-test.html') intro_page, test_page = document.pages # Ignore the intro page: it is not in the reference test_png = document.copy([test_page]).write_png() test_pixels = Image.open(io.BytesIO(test_png)).getdata() # This is a copy of http://www.webstandards.org/files/acid2/reference.html ref_png = render('acid2-reference.html').write_png() ref_image = Image.open(io.BytesIO(ref_png)) ref_pixels = ref_image.getdata() width, height = ref_image.size assert_pixels_equal( 'acid2', width, height, test_pixels, ref_pixels, tolerance=2) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.027308 weasyprint-54.1/tests/test_api.py0000644000000000000000000010607500000000000015334 0ustar0000000000000000""" weasyprint.tests.test_api ------------------------- Test the public API. """ import gzip import io import os import sys import unicodedata import zlib from pathlib import Path from urllib.parse import urljoin, uses_relative import py import pytest from PIL import Image from weasyprint import CSS, HTML, __main__, default_url_fetcher from weasyprint.document import resolve_links from weasyprint.urls import path2url from .draw import assert_pixels_equal, parse_pixels from .testing_utils import ( FakeHTML, assert_no_logs, capture_logs, http_server, resource_filename) def _test_resource(class_, basename, check, **kwargs): """Common code for testing the HTML and CSS classes.""" absolute_filename = resource_filename(basename) absolute_path = Path(absolute_filename) url = path2url(absolute_filename) check(class_(absolute_filename, **kwargs)) check(class_(absolute_path, **kwargs)) check(class_(guess=absolute_filename, **kwargs)) check(class_(guess=absolute_path, **kwargs)) check(class_(filename=absolute_filename, **kwargs)) check(class_(filename=absolute_path, **kwargs)) check(class_(url, **kwargs)) check(class_(guess=url, **kwargs)) url = path2url(absolute_filename.encode('utf-8')) check(class_(url=url, **kwargs)) with open(absolute_filename, 'rb') as fd: check(class_(fd, **kwargs)) with open(absolute_filename, 'rb') as fd: check(class_(guess=fd, **kwargs)) with open(absolute_filename, 'rb') as fd: check(class_(file_obj=fd, **kwargs)) with open(absolute_filename, 'rb') as fd: content = fd.read() py.path.local(os.path.dirname(__file__)).chdir() relative_filename = os.path.join('resources', basename) relative_path = Path(relative_filename) check(class_(relative_filename, **kwargs)) check(class_(relative_path, **kwargs)) kwargs.pop('base_url', None) check(class_(string=content, base_url=relative_filename, **kwargs)) encoding = kwargs.get('encoding') or 'utf8' check(class_(string=content.decode(encoding), # unicode base_url=relative_filename, **kwargs)) with pytest.raises(TypeError): class_(filename='foo', url='bar') def _check_doc1(html, has_base_url=True): """Check that a parsed HTML document looks like resources/doc1.html""" root = html.etree_element assert root.tag == 'html' assert [child.tag for child in root] == ['head', 'body'] _head, body = root assert [child.tag for child in body] == ['h1', 'p', 'ul', 'div'] h1, p, ul, div = body assert h1.text == 'WeasyPrint test document (with Ünicōde)' if has_base_url: url = urljoin(html.base_url, 'pattern.png') assert url.startswith('file:') assert url.endswith('tests/resources/pattern.png') else: assert html.base_url is None def _run(args, stdin=b''): stdin = io.BytesIO(stdin) stdout = io.BytesIO() try: __main__.HTML = FakeHTML __main__.main(args.split(), stdin=stdin, stdout=stdout) finally: __main__.HTML = HTML return stdout.getvalue() class _fake_file: def __init__(self): self.chunks = [] def write(self, data): self.chunks.append(bytes(data[:])) def getvalue(self): return b''.join(self.chunks) def _png_size(png_bytes): image = Image.open(io.BytesIO(png_bytes)) return image.width, image.height def _round_meta(pages): """Eliminate errors of floating point arithmetic for metadata.""" for page in pages: anchors = page.anchors for anchor_name, (pos_x, pos_y) in anchors.items(): anchors[anchor_name] = round(pos_x, 6), round(pos_y, 6) links = page.links for i, link in enumerate(links): link_type, target, rectangle, download_name = link pos_x, pos_y, width, height = rectangle link = ( link_type, target, (round(pos_x, 6), round(pos_y, 6), round(width, 6), round(height, 6)), download_name) links[i] = link bookmarks = page.bookmarks for i, (level, label, (pos_x, pos_y), state) in enumerate(bookmarks): bookmarks[i] = ( level, label, (round(pos_x, 6), round(pos_y, 6)), state) @assert_no_logs def test_html_parsing(): """Test the constructor for the HTML class.""" _test_resource(FakeHTML, 'doc1.html', _check_doc1) _test_resource(FakeHTML, 'doc1_UTF-16BE.html', _check_doc1, encoding='UTF-16BE') py.path.local(os.path.dirname(__file__)).chdir() filename = os.path.join('resources', 'doc1.html') with open(filename, encoding='utf-8') as fd: string = fd.read() _test_resource(FakeHTML, 'doc1.html', _check_doc1, base_url=filename) _check_doc1(FakeHTML(string=string, base_url=filename)) _check_doc1(FakeHTML(string=string), has_base_url=False) string_with_meta = string.replace( '{css_string}{html_string}', base_url=base_url, media_type='screen' ).write_png() == rotated_png_bytes @assert_no_logs def test_command_line_render(tmpdir): css = b''' @page { margin: 2px; size: 8px; background: #fff } @media screen { img { transform: rotate(-90deg) } } body { margin: 0; font-size: 0 } ''' html = b'' combined = b'' + html linked = b'' + html not_optimized = b'a' tmpdir.chdir() for name in ('pattern.png', 'not-optimized.jpg'): pattern_bytes = Path(resource_filename(name)).read_bytes() tmpdir.join(name).write_binary(pattern_bytes) # Reference html_obj = FakeHTML(string=combined, base_url='dummy.html') pdf_bytes = html_obj.write_pdf() rotated_pdf_bytes = FakeHTML( string=combined, base_url='dummy.html', media_type='screen').write_pdf() tmpdir.join('no_css.html').write_binary(html) tmpdir.join('combined.html').write_binary(combined) tmpdir.join('combined-UTF-16BE.html').write_binary( combined.decode('ascii').encode('UTF-16BE')) tmpdir.join('linked.html').write_binary(linked) tmpdir.join('not_optimized.html').write_binary(not_optimized) tmpdir.join('style.css').write_binary(css) _run('combined.html out2.pdf') assert tmpdir.join('out2.pdf').read_binary() == pdf_bytes _run('combined-UTF-16BE.html out3.pdf --encoding UTF-16BE') assert tmpdir.join('out3.pdf').read_binary() == pdf_bytes _run(tmpdir.join('combined.html').strpath + ' out4.pdf') assert tmpdir.join('out4.pdf').read_binary() == pdf_bytes _run(path2url(tmpdir.join('combined.html').strpath) + ' out5.pdf') assert tmpdir.join('out5.pdf').read_binary() == pdf_bytes _run('linked.html --debug out6.pdf') # test relative URLs assert tmpdir.join('out6.pdf').read_binary() == pdf_bytes _run('combined.html --verbose out7') _run('combined.html --quiet out8') assert tmpdir.join('out7').read_binary() == pdf_bytes assert tmpdir.join('out8').read_binary() == pdf_bytes _run('no_css.html out9.pdf') _run('no_css.html out10.pdf -s style.css') assert tmpdir.join('out9.pdf').read_binary() != pdf_bytes assert tmpdir.join('out10.pdf').read_binary() == pdf_bytes stdout = _run('combined.html -') assert stdout == pdf_bytes _run('- out11.pdf', stdin=combined) assert tmpdir.join('out11.pdf').read_binary() == pdf_bytes stdout = _run('- -', stdin=combined) assert stdout == pdf_bytes _run('combined.html out13.pdf --media-type screen') _run('combined.html out12.pdf -m screen') _run('linked.html out14.pdf -m screen') assert tmpdir.join('out12.pdf').read_binary() == rotated_pdf_bytes assert tmpdir.join('out13.pdf').read_binary() == rotated_pdf_bytes assert tmpdir.join('out14.pdf').read_binary() == rotated_pdf_bytes _run('not_optimized.html out15.pdf') _run('not_optimized.html out16.pdf -O images') _run('not_optimized.html out17.pdf -O fonts') _run('not_optimized.html out18.pdf -O fonts -O images') _run('not_optimized.html out19.pdf -O all') _run('not_optimized.html out20.pdf -O none') _run('not_optimized.html out21.pdf -O none -O all') _run('not_optimized.html out22.pdf -O all -O none') # TODO: test that equivalent CLI options give equivalent PDF sizes, # unfortunately font optimization makes PDF generation not reproducible assert ( len(tmpdir.join('out16.pdf').read_binary()) < len(tmpdir.join('out15.pdf').read_binary()) < len(tmpdir.join('out20.pdf').read_binary())) stdout = _run('combined.html -') assert stdout.count(b'attachment') == 0 stdout = _run('combined.html -') assert stdout.count(b'attachment') == 0 stdout = _run('-a pattern.png combined.html -') assert stdout.count(b'attachment') == 1 stdout = _run('-a style.css -a pattern.png combined.html -') assert stdout.count(b'attachment') == 2 os.mkdir('subdirectory') py.path.local('subdirectory').chdir() with capture_logs() as logs: stdout = _run('- -', stdin=combined) assert len(logs) == 1 assert logs[0].startswith('ERROR: Failed to load image') assert stdout.startswith(b'%PDF') with capture_logs() as logs: stdout = _run('--base-url= - -', stdin=combined) assert len(logs) == 1 assert logs[0].startswith( 'ERROR: Relative URI reference without a base URI') assert stdout.startswith(b'%PDF') stdout = _run('--base-url .. - -', stdin=combined) assert stdout == pdf_bytes with pytest.raises(SystemExit): _run('--info') with pytest.raises(SystemExit): _run('--version') @assert_no_logs def test_unicode_filenames(tmpdir): """Test non-ASCII filenames both in Unicode or bytes form.""" # Replicate pattern.png in CSS so that base_url does not matter. html = b''' ''' png_bytes = FakeHTML(string=html).write_png() check_png_pattern(png_bytes) unicode_filename = 'Unicödé' if sys.platform.startswith('darwin'): # pragma: no cover unicode_filename = unicodedata.normalize('NFD', unicode_filename) tmpdir.chdir() tmpdir.join(unicode_filename).write(html) bytes_file, = tmpdir.listdir() assert bytes_file.basename == unicode_filename assert FakeHTML(unicode_filename).write_png() == png_bytes assert FakeHTML(bytes_file.strpath).write_png() == png_bytes os.remove(unicode_filename) assert tmpdir.listdir() == [] FakeHTML(string=html).write_png(unicode_filename) assert bytes_file.read_binary() == png_bytes @assert_no_logs def test_low_level_api(): html = FakeHTML(string='') css = CSS(string=''' @page { margin: 2px; size: 8px; background: #fff } html { background: #00f; } body { background: #f00; width: 1px; height: 1px } ''') pdf_bytes = html.write_pdf(stylesheets=[css]) assert pdf_bytes.startswith(b'%PDF') png_bytes = html.write_png(stylesheets=[css]) document = html.render([css]) page, = document.pages assert page.width == 8 assert page.height == 8 assert document.write_png() == png_bytes assert document.copy([page]).write_png() == png_bytes document = html.render([css]) page, = document.pages assert (page.width, page.height) == (8, 8) png_bytes = document.write_png(resolution=192) check_png_pattern(png_bytes, x2=True) document = html.render([css]) page, = document.pages assert (page.width, page.height) == (8, 8) # A resolution that is not multiple of 96: assert _png_size(document.write_png(resolution=145.2)) == (12, 12) document = FakeHTML(string='''

    ''').render() page_1, page_2 = document.pages assert (page_1.width, page_1.height) == (5, 10) assert (page_2.width, page_2.height) == (6, 4) result = document.write_png() # (Max of both widths, Sum of both heights) assert _png_size(result) == (6, 14) assert document.copy([page_1, page_2]).write_png() == result assert _png_size(document.copy([page_1]).write_png()) == (5, 10) assert _png_size(document.copy([page_2]).write_png()) == (6, 4) @pytest.mark.parametrize('html, expected_by_page, expected_tree, round', ( ('''

    a

    b

    c

    d

    e

    ''', [ [(1, 'a', (0, 0), 'open'), (4, 'b', (0, 10), 'open')], [(3, 'c', (3, 2), 'open'), (2, 'd', (0, 10), 'open'), (1, 'e', (0, 20), 'open')], ], [ ('a', (0, 0, 0), [ ('b', (0, 0, 10), [], 'open'), ('c', (1, 3, 2), [], 'open'), ('d', (1, 0, 10), [], 'open')], 'open'), ('e', (1, 0, 20), [], 'open'), ], False), ('''

    Title 1

    Title 2

    Title 3

    Title 4

    Title 5

    Title 6

    Title 7

    Title 8

    Title 9

    Title 10

    Title 11

    ''', [ [ (1, 'Title 1', (0, 0), 'open'), (1, 'Title 2', (0, 100), 'open'), (2, 'Title 3', (20, 200), 'open'), (2, 'Title 4', (0, 300), 'open'), (3, 'Title 5', (0, 400), 'open') ], [ (2, 'Title 6', (0, 100), 'open'), (1, 'Title 7', (0, 200), 'open'), (2, 'Title 8', (0, 300), 'open'), (3, 'Title 9', (0, 400), 'open'), (1, 'Title 10', (0, 500), 'open'), (2, 'Title 11', (0, 600), 'open') ], ], [ ('Title 1', (0, 0, 0), [], 'open'), ('Title 2', (0, 0, 100), [ ('Title 3', (0, 20, 200), [], 'open'), ('Title 4', (0, 0, 300), [ ('Title 5', (0, 0, 400), [], 'open')], 'open'), ('Title 6', (1, 0, 100), [], 'open')], 'open'), ('Title 7', (1, 0, 200), [ ('Title 8', (1, 0, 300), [ ('Title 9', (1, 0, 400), [], 'open')], 'open')], 'open'), ('Title 10', (1, 0, 500), [ ('Title 11', (1, 0, 600), [], 'open')], 'open'), ], False), ('''

    A

    depth 1

    B

    depth 2

    C

    depth 1

    D

    depth 2

    E

    depth 3

    ''', [[ (2, 'A', (0, 0), 'open'), (4, 'B', (0, 20), 'open'), (2, 'C', (0, 40), 'open'), (3, 'D', (0, 60), 'open'), (4, 'E', (0, 80), 'open'), ]], [ ('A', (0, 0, 0), [ ('B', (0, 0, 20), [], 'open')], 'open'), ('C', (0, 0, 40), [ ('D', (0, 0, 60), [ ('E', (0, 0, 80), [], 'open')], 'open')], 'open'), ], False), ('''

    A

    h2 depth 1

    B

    h4 depth 2

    C

    h3 depth 2

    D

    h5 depth 3

    E

    h1 depth 1

    F

    h2 depth 2

    G

    h2 depth 2

    H

    h4 depth 3

    I

    h1 depth 1

    ''', [[ (2, 'A', (0, 0), 'open'), (4, 'B', (0, 20), 'open'), (3, 'C', (0, 40), 'open'), (5, 'D', (0, 60), 'open'), (1, 'E', (0, 70), 'open'), (2, 'F', (0, 90), 'open'), (2, 'G', (0, 110), 'open'), (4, 'H', (0, 130), 'open'), (1, 'I', (0, 150), 'open'), ]], [ ('A', (0, 0, 0), [ ('B', (0, 0, 20), [], 'open'), ('C', (0, 0, 40), [ ('D', (0, 0, 60), [], 'open')], 'open')], 'open'), ('E', (0, 0, 70), [ ('F', (0, 0, 90), [], 'open'), ('G', (0, 0, 110), [ ('H', (0, 0, 130), [], 'open')], 'open')], 'open'), ('I', (0, 0, 150), [], 'open'), ], False), ('

    é', [ [(1, 'é', (0, 0), 'open')] ], [ ('é', (0, 0, 0), [], 'open') ], False), ('''

    ! ''', [ [(1, '!', (50, 0), 'open')] ], [ ('!', (0, 50, 0), [], 'open') ], False), (''' Chocolate ''' % path2url(resource_filename('pattern.png')), [[(1, 'Chocolate', (0, 0), 'open')]], [('Chocolate', (0, 0, 0), [], 'open')], False), ('''

    ! ''', [[(1, '!', (0, 50), 'open')]], [('!', (0, 0, 50), [], 'open')], True), ('''

    ! ''', [[(1, '!', (0, 50), 'open')]], [('!', (0, 0, 50), [], 'open')], True), )) @assert_no_logs def test_assert_bookmarks(html, expected_by_page, expected_tree, round): document = FakeHTML(string=html).render() if round: _round_meta(document.pages) assert [page.bookmarks for page in document.pages] == expected_by_page assert document.make_bookmark_tree() == expected_tree @assert_no_logs def test_links(): def assert_links(html, expected_links_by_page, expected_anchors_by_page, expected_resolved_links, base_url=resource_filename(''), warnings=(), round=False): with capture_logs() as logs: document = FakeHTML(string=html, base_url=base_url).render() if round: _round_meta(document.pages) resolved_links = list(resolve_links(document.pages)) assert len(logs) == len(warnings) for message, expected in zip(logs, warnings): assert expected in message assert [p.links for p in document.pages] == expected_links_by_page assert [p.anchors for p in document.pages] == expected_anchors_by_page assert resolved_links == expected_resolved_links assert_links('''

    Hello, World

    ''', [ [ ('external', 'http://weasyprint.org', (0, 0, 30, 20), None), ('external', 'http://weasyprint.org', (0, 0, 30, 30), None), ('internal', 'lipsum', (10, 100, 42, 120), None), ('internal', 'lipsum', (10, 100, 42, 132), None) ], [('internal', 'hello', (0, 0, 200, 30), None)], ], [ {'hello': (0, 200)}, {'lipsum': (0, 0)} ], [ ( [ ('external', 'http://weasyprint.org', (0, 0, 30, 20), None), ('external', 'http://weasyprint.org', (0, 0, 30, 30), None), ('internal', 'lipsum', (10, 100, 42, 120), None), ('internal', 'lipsum', (10, 100, 42, 132), None) ], [('hello', 0, 200)], ), ( [ ('internal', 'hello', (0, 0, 200, 30), None) ], [('lipsum', 0, 0)]), ]) assert_links( ''' ''', [[('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9', (5, 10, 195, 10), None)]], [{}], [([('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9', (5, 10, 195, 10), None)], [])], base_url='http://weasyprint.org/foo/bar/') assert_links( '''
    ''', [[('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9', (5, 10, 195, 10), None)]], [{}], [([('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9', (5, 10, 195, 10), None)], [])], base_url='http://weasyprint.org/foo/bar/') # Relative URI reference without a base URI: allowed for links assert_links( ''' ''', [[('external', '../lipsum', (5, 10, 195, 10), None)]], [{}], [([('external', '../lipsum', (5, 10, 195, 10), None)], [])], base_url=None) # Relative URI reference without a base URI: not supported for -weasy-link assert_links( '''
    ''', [[]], [{}], [([], [])], base_url=None, warnings=[ 'WARNING: Ignored `-weasy-link: url(../lipsum)` at 1:1, ' 'Relative URI reference without a base URI']) # Internal or absolute URI reference without a base URI: OK assert_links( ''' ''', [[ ('internal', 'lipsum', (5, 10, 195, 10), None), ('external', 'http://weasyprint.org/', (0, 10, 200, 10), None)]], [{'lipsum': (5, 10)}], [([('internal', 'lipsum', (5, 10, 195, 10), None), ('external', 'http://weasyprint.org/', (0, 10, 200, 10), None)], [('lipsum', 5, 10)])], base_url=None) assert_links( '''
    ''', [[('internal', 'lipsum', (5, 10, 195, 10), None)]], [{'lipsum': (5, 10)}], [([('internal', 'lipsum', (5, 10, 195, 10), None)], [('lipsum', 5, 10)])], base_url=None) assert_links( ''' ''', [[('internal', 'lipsum', (0, 0, 200, 15), None), ('internal', 'missing', (0, 15, 200, 30), None)]], [{'lipsum': (0, 15)}], [([('internal', 'lipsum', (0, 0, 200, 15), None)], [('lipsum', 0, 15)])], base_url=None, warnings=[ 'ERROR: No anchor #missing for internal URI reference']) assert_links( ''' ''', [[('internal', 'lipsum', (30, 10, 70, 210), None)]], [{'lipsum': (70, 10)}], [([('internal', 'lipsum', (30, 10, 70, 210), None)], [('lipsum', 70, 10)])], round=True) # Download for attachment assert_links( ''' ''', [[('attachment', 'pattern.png', (5, 10, 195, 10), 'wow.png')]], [{}], [([('attachment', 'pattern.png', (5, 10, 195, 10), 'wow.png')], [])], base_url=None) # Make relative URL references work with our custom URL scheme. uses_relative.append('weasyprint-custom') @assert_no_logs def test_url_fetcher(): filename = resource_filename('pattern.png') with open(filename, 'rb') as pattern_fd: pattern_png = pattern_fd.read() def fetcher(url): if url == 'weasyprint-custom:foo/%C3%A9_%e9_pattern': return {'string': pattern_png, 'mime_type': 'image/png'} elif url == 'weasyprint-custom:foo/bar.css': return { 'string': 'body { background: url(é_%e9_pattern)', 'mime_type': 'text/css'} elif url == 'weasyprint-custom:foo/bar.no': return { 'string': 'body { background: red }', 'mime_type': 'text/no'} else: return default_url_fetcher(url) base_url = resource_filename('dummy.html') css = CSS(string=''' @page { size: 8px; margin: 2px; background: #fff } body { margin: 0; font-size: 0 } ''', base_url=base_url) def test(html, blank=False): html = FakeHTML(string=html, url_fetcher=fetcher, base_url=base_url) check_png_pattern(html.write_png(stylesheets=[css]), blank=blank) test('') # Test a "normal" URL test(f'') test(f'') test('') test('') test('
  • ') test('') test('') test('') test('') test('') with capture_logs() as logs: test('', blank=True) assert len(logs) == 1 assert logs[0].startswith( "ERROR: Failed to load image at 'custom:foo/bar'") with capture_logs() as logs: test( '' '') assert len(logs) == 1 assert logs[0].startswith('ERROR: Unsupported stylesheet type text/no') def fetcher_2(url): assert url == 'weasyprint-custom:%C3%A9_%e9.css' return {'string': '', 'mime_type': 'text/css'} FakeHTML( string='') assert_meta( ''' Test document

    Another title

    ''', authors=['I Me & Myself', 'Smith, John'], title='Test document', generator='Human after all', keywords=['html', 'css', 'pdf', 'Python; pydyf'], description="Blah… ", created='2011-04', modified='2013') assert_meta( ''' One Two Three ''', title='One', authors=['', 'Me']) @assert_no_logs def test_http(): def gzip_compress(data): file_obj = io.BytesIO() gzip_file = gzip.GzipFile(fileobj=file_obj, mode='wb') gzip_file.write(data) gzip_file.close() return file_obj.getvalue() with http_server({ '/gzip': lambda env: ( (gzip_compress(b''), [('Content-Encoding', 'gzip')]) if 'gzip' in env.get('HTTP_ACCEPT_ENCODING', '') else (b'', []) ), '/deflate': lambda env: ( (zlib.compress(b''), [('Content-Encoding', 'deflate')]) if 'deflate' in env.get('HTTP_ACCEPT_ENCODING', '') else (b'', []) ), '/raw-deflate': lambda env: ( # Remove zlib header and checksum (zlib.compress(b'')[2:-4], [('Content-Encoding', 'deflate')]) if 'deflate' in env.get('HTTP_ACCEPT_ENCODING', '') else (b'', []) ), }) as root_url: assert HTML(root_url + '/gzip').etree_element.get('test') == 'ok' assert HTML(root_url + '/deflate').etree_element.get('test') == 'ok' assert HTML( root_url + '/raw-deflate').etree_element.get('test') == 'ok' @assert_no_logs def test_page_copy_relative(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1473 document = FakeHTML(string='
    a').render() duplicated_pages = document.copy([*document.pages, *document.pages]) pngs = duplicated_pages.write_png(split_images=True) assert pngs[0] == pngs[1] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.027308 weasyprint-54.1/tests/test_boxes.py0000644000000000000000000011422500000000000015677 0ustar0000000000000000""" weasyprint.tests.test_boxes --------------------------- Test that the "before layout" box tree is correctly constructed. """ import pytest from weasyprint.css import PageType, get_all_computed_styles from weasyprint.formatting_structure import boxes, build from weasyprint.layout.page import set_page_type_computed_styles from .testing_utils import ( FakeHTML, assert_no_logs, assert_tree, capture_logs, parse, parse_all, render_pages) def _get_grid(html): html = parse_all(html) body, = html.children table_wrapper, = body.children table, = table_wrapper.children return tuple( [[(style, width, color) if width else None for _score, (style, width, color) in column] for column in grid] for grid in table.collapsed_border_grid) @assert_no_logs def test_box_tree(): assert_tree(parse('

    '), [('p', 'Block', [])]) assert_tree(parse('''

    Hello World L!

    '''), [ ('p', 'Block', [ ('p', 'Text', 'Hello '), ('em', 'Inline', [ ('em', 'Text', 'World '), ('img', 'InlineReplaced', ''), ('span', 'InlineBlock', [ ('span', 'Text', 'L')])]), ('p', 'Text', '!')])]) @assert_no_logs def test_html_entities(): for quote in ['"', '"', '"', '"']: assert_tree(parse('

    {0}abc{1}'.format(quote, quote)), [ ('p', 'Block', [ ('p', 'Text', '"abc"')])]) @assert_no_logs def test_inline_in_block_1(): source = '

    Hello, World!\n

    Lipsum.

    ' expected = [ ('div', 'Block', [ ('div', 'Block', [ ('div', 'Line', [ ('div', 'Text', 'Hello, '), ('em', 'Inline', [ ('em', 'Text', 'World')]), ('div', 'Text', '! ')])]), ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Lipsum.')])])])] box = parse(source) box = build.inline_in_block(box) assert_tree(box, expected) @assert_no_logs def test_inline_in_block_2(): source = '

    Lipsum.

    Hello, World!\n
    ' expected = [ ('div', 'Block', [ ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Lipsum.')])]), ('div', 'Block', [ ('div', 'Line', [ ('div', 'Text', 'Hello, '), ('em', 'Inline', [ ('em', 'Text', 'World')]), ('div', 'Text', '! ')])])])] box = parse(source) box = build.inline_in_block(box) assert_tree(box, expected) @assert_no_logs def test_inline_in_block_3(): # Absolutes are left in the lines to get their static position later. source = '''

    Hello World!

    ''' expected = [ ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Hello '), ('em', 'Block', [ ('em', 'Line', [ ('em', 'Text', 'World')])]), ('p', 'Text', '!')])])] box = parse(source) box = build.inline_in_block(box) assert_tree(box, expected) box = build.block_in_inline(box) assert_tree(box, expected) @assert_no_logs def test_inline_in_block_4(): # Floats are pull to the top of their containing blocks source = '

    Hello World!

    ' box = parse(source) box = build.inline_in_block(box) box = build.block_in_inline(box) assert_tree(box, [ ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Hello '), ('em', 'Block', [ ('em', 'Line', [ ('em', 'Text', 'World')])]), ('p', 'Text', '!')])])]) @assert_no_logs def test_block_in_inline(): box = parse('''

    Lorem ipsum dolor sit amet,conse''') box = build.inline_in_block(box) assert_tree(box, [ ('body', 'Line', [ ('p', 'InlineBlock', [ ('p', 'Line', [ ('p', 'Text', 'Lorem '), ('em', 'Inline', [ ('em', 'Text', 'ipsum '), ('strong', 'Inline', [ ('strong', 'Text', 'dolor '), ('span', 'Block', [ # This block is "pulled up" ('span', 'Line', [ ('span', 'Text', 'sit')])]), ('strong', 'Text', ' '), ('span', 'Block', [ # This block is "pulled up" ('span', 'Line', [ ('span', 'Text', 'amet,')])])]), ('span', 'Block', [ # This block is "pulled up" ('span', 'Line', [ ('em', 'Inline', [ ('em', 'Text', 'conse'), ('i', 'Block', [])])])])])])])])]) box = build.block_in_inline(box) assert_tree(box, [ ('body', 'Line', [ ('p', 'InlineBlock', [ ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Lorem '), ('em', 'Inline', [ ('em', 'Text', 'ipsum '), ('strong', 'Inline', [ ('strong', 'Text', 'dolor ')])])])]), ('span', 'Block', [ ('span', 'Line', [ ('span', 'Text', 'sit')])]), ('p', 'Block', [ ('p', 'Line', [ ('em', 'Inline', [ ('strong', 'Inline', [ ('strong', 'Text', ' ')])])])]), ('span', 'Block', [ ('span', 'Line', [ ('span', 'Text', 'amet,')])]), ('p', 'Block', [ ('p', 'Line', [ ('em', 'Inline', [ ('strong', 'Inline', [])])])]), ('span', 'Block', [ ('span', 'Block', [ ('span', 'Line', [ ('em', 'Inline', [ ('em', 'Text', 'conse')])])]), ('i', 'Block', []), ('span', 'Block', [ ('span', 'Line', [ ('em', 'Inline', [])])])]), ('p', 'Block', [ ('p', 'Line', [ ('em', 'Inline', [])])])])])]) @assert_no_logs def test_styles(): box = parse('''

    Lorem ipsum dolor sit amet,consectetur

    ''') box = build.inline_in_block(box) box = build.block_in_inline(box) descendants = list(box.descendants()) assert len(descendants) == 31 assert descendants[0] == box for child in descendants: # All boxes inherit the color assert child.style['color'] == (0, 0, 1, 1) # blue # Only non-anonymous boxes have margins assert child.style['margin_top'] in ((0, 'px'), (42, 'px')) @assert_no_logs def test_whitespace(): # TODO: test more cases # http://www.w3.org/TR/CSS21/text.html#white-space-model assert_tree(parse_all('''

    Lorem \t\r\n ipsum\t dolor sit amet consectetur.

    \t  foo\n
    \t  foo\n
    \t  foo\n
    '''), [ ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Lorem ipsum '), ('strong', 'Inline', [ ('strong', 'Text', 'dolor '), ('img', 'InlineReplaced', ''), ('strong', 'Text', ' sit '), ('span', 'Block', []), ('em', 'Inline', [ ('em', 'Text', 'amet ')]), ('strong', 'Text', 'consectetur')]), ('p', 'Text', '.')])]), ('pre', 'Block', [ ('pre', 'Line', [ # pre ('pre', 'Text', '\t foo\n')])]), ('pre', 'Block', [ ('pre', 'Line', [ # pre-wrap ('pre', 'Text', '\t foo\n')])]), ('pre', 'Block', [ ('pre', 'Line', [ # pre-line ('pre', 'Text', ' foo\n')])])]) @assert_no_logs @pytest.mark.parametrize('page_type, top, right, bottom, left', ( (PageType(side='left', first=True, index=0, blank=None, name=None), 20, 3, 3, 10), (PageType(side='right', first=True, index=0, blank=None, name=None), 20, 10, 3, 3), (PageType(side='left', first=None, index=1, blank=None, name=None), 10, 3, 3, 10), (PageType(side='right', first=None, index=1, blank=None, name=None), 10, 10, 3, 3), (PageType(side='right', first=None, index=1, blank=None, name='name'), 5, 10, 3, 15), (PageType(side='right', first=None, index=2, blank=None, name='name'), 5, 10, 1, 15), (PageType(side='right', first=None, index=8, blank=None, name='name'), 5, 10, 2, 15), )) def test_page_style(page_type, top, right, bottom, left): document = FakeHTML(string=''' ''') style_for = get_all_computed_styles(document) # Force the generation of the style for this page type as it's generally # only done during the rendering. set_page_type_computed_styles(page_type, document, style_for) style = style_for(page_type) assert style['margin_top'] == (top, 'px') assert style['margin_right'] == (right, 'px') assert style['margin_bottom'] == (bottom, 'px') assert style['margin_left'] == (left, 'px') @assert_no_logs def test_images_1(): with capture_logs() as logs: result = parse_all('''

    No srcInexistent src

    ''') assert len(logs) == 1 assert 'ERROR: Failed to load image' in logs[0] assert 'inexistent.jpg' in logs[0] assert_tree(result, [ ('p', 'Block', [ ('p', 'Line', [ ('img', 'InlineReplaced', ''), ('img', 'Inline', [ ('img', 'Text', 'No src')]), ('img', 'Inline', [ ('img', 'Text', 'Inexistent src')])])])]) @assert_no_logs def test_images_2(): with capture_logs() as logs: result = parse_all('

    No base_url', base_url=None) assert len(logs) == 1 assert 'ERROR: Relative URI reference without a base URI' in logs[0] assert_tree(result, [ ('p', 'Block', [ ('p', 'Line', [ ('img', 'Inline', [ ('img', 'Text', 'No base_url')])])])]) @assert_no_logs def test_tables_1(): # Rules in http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes # Rule 1.3 # Also table model: http://www.w3.org/TR/CSS21/tables.html#model assert_tree(parse_all(''' foo bar top caption baz '''), [ ('x-table', 'Block', [ ('x-caption', 'TableCaption', [ ('x-caption', 'Line', [ ('x-caption', 'Text', 'top caption')])]), ('x-table', 'Table', [ ('x-table', 'TableColumnGroup', [ ('x-col', 'TableColumn', [])]), ('x-thead', 'TableRowGroup', [ ('x-thead', 'TableRow', [ ('x-th', 'TableCell', [])])]), ('x-table', 'TableRowGroup', [ ('x-tr', 'TableRow', [ ('x-th', 'TableCell', [ ('x-th', 'Line', [ ('x-th', 'Text', 'foo')])]), ('x-th', 'TableCell', [ ('x-th', 'Line', [ ('x-th', 'Text', 'bar')])])])]), ('x-thead', 'TableRowGroup', []), ('x-table', 'TableRowGroup', [ ('x-tr', 'TableRow', [ ('x-td', 'TableCell', [ ('x-td', 'Line', [ ('x-td', 'Text', 'baz')])])])]), ('x-tfoot', 'TableRowGroup', [])]), ('x-caption', 'TableCaption', [])])]) @assert_no_logs def test_tables_2(): # Rules 1.4 and 3.1 assert_tree(parse_all(''' foo bar '''), [ ('body', 'Block', [ ('body', 'Table', [ ('body', 'TableRowGroup', [ ('body', 'TableRow', [ ('span', 'TableCell', [ ('span', 'Line', [ ('span', 'Text', 'foo')])]), ('span', 'TableCell', [ ('span', 'Line', [ ('span', 'Text', 'bar')])])])])])])]) @assert_no_logs def test_tables_3(): # http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes # Rules 1.1 and 1.2 # Rule XXX (not in the spec): column groups have at least one column child assert_tree(parse_all(''' 1 2 3 4 '''), [ ('body', 'Block', [ ('body', 'Table', [ ('span', 'TableColumnGroup', [ ('em', 'TableColumn', [])]), ('ins', 'TableColumnGroup', [ ('ins', 'TableColumn', [])])])])]) @assert_no_logs def test_tables_4(): # Rules 2.1 then 2.3 assert_tree(parse_all('foo

    '), [ ('x-table', 'Block', [ ('x-table', 'Table', [ ('x-table', 'TableRowGroup', [ ('x-table', 'TableRow', [ ('x-table', 'TableCell', [ ('x-table', 'Block', [ ('x-table', 'Line', [ ('x-table', 'Text', 'foo ')])]), ('div', 'Block', [])])])])])])]) @assert_no_logs def test_tables_5(): # Rule 2.2 assert_tree(parse_all('' '
    '), [ ('body', 'Block', [ ('body', 'Table', [ ('x-thead', 'TableRowGroup', [ ('x-thead', 'TableRow', [ ('x-thead', 'TableCell', [ ('div', 'Block', [])]), ('x-td', 'TableCell', [])])])])])]) @assert_no_logs def test_tables_6(): # Rule 3.2 assert_tree(parse_all(''), [ ('body', 'Line', [ ('span', 'Inline', [ ('span', 'InlineBlock', [ ('span', 'InlineTable', [ ('span', 'TableRowGroup', [ ('x-tr', 'TableRow', [])])])])])])]) @assert_no_logs def test_tables_7(): # Rule 3.1 # Also, rule 1.3 does not apply: whitespace before and after is preserved assert_tree(parse_all(''' '''), [ ('body', 'Line', [ ('span', 'Inline', [ # Whitespace is preserved in table handling, then collapsed # into a single space. ('span', 'Text', ' '), ('span', 'InlineBlock', [ ('span', 'InlineTable', [ ('span', 'TableRowGroup', [ ('span', 'TableRow', [ ('em', 'TableCell', []), ('em', 'TableCell', [])])])])]), ('span', 'Text', ' ')])])]) @assert_no_logs def test_tables_8(): # Rule 3.2 assert_tree(parse_all('\t'), [ ('body', 'Block', [ ('body', 'Table', [ ('body', 'TableRowGroup', [ ('x-tr', 'TableRow', []), ('x-tr', 'TableRow', [])])])])]) @assert_no_logs def test_tables_9(): assert_tree(parse_all('\n'), [ ('body', 'Block', [ ('body', 'Table', [ ('body', 'TableColumnGroup', [ ('x-col', 'TableColumn', [])]), ('x-colgroup', 'TableColumnGroup', [ ('x-colgroup', 'TableColumn', [])])])])]) @assert_no_logs def test_table_style(): html = parse_all('
    ') body, = html.children wrapper, = body.children table, = wrapper.children assert isinstance(wrapper, boxes.BlockBox) assert isinstance(table, boxes.TableBox) assert wrapper.style['margin_top'] == (1, 'px') assert wrapper.style['padding_top'] == (0, 'px') assert table.style['margin_top'] == (0, 'px') assert table.style['padding_top'] == (2, 'px') @assert_no_logs def test_column_style(): html = parse_all('''
    ''') body, = html.children wrapper, = body.children table, = wrapper.children colgroup, = table.column_groups widths = [col.style['width'] for col in colgroup.children] assert widths == [(10, 'px'), (10, 'px'), (10, 'px'), 'auto', 'auto'] assert [col.grid_x for col in colgroup.children] == [0, 1, 2, 3, 4] # copies, not the same box object assert colgroup.children[0] is not colgroup.children[1] @assert_no_logs def test_nested_grid_x(): html = parse_all('''
    ''') body, = html.children wrapper, = body.children table, = wrapper.children grid = [(colgroup.grid_x, [col.grid_x for col in colgroup.children]) for colgroup in table.column_groups] assert grid == [(0, [0, 1]), (2, [2, 3]), (4, [4, 5, 6]), (7, [7])] @assert_no_logs def test_colspan_rowspan_1(): # +---+---+---+ # | A | B | C | X # +---+---+---+ # | D | E | X # +---+---+ +---+ # | F ...| | | <-- overlap # +---+---+---+ + # | H | X X | G | # +---+---+ + + # | I | J | X | | # +---+---+ +---+ # X: empty cells html = parse_all('''
    A B C
    D E
    F G
    H
    I J
    ''') body, = html.children wrapper, = body.children table, = wrapper.children group, = table.children assert [[c.grid_x for c in row.children] for row in group.children] == [ [0, 1, 2], [0, 1], [0, 3], [0], [0, 1], ] assert [[c.colspan for c in row.children] for row in group.children] == [ [1, 1, 1], [1, 2], [2, 1], [1], [1, 1], ] assert [[c.rowspan for c in row.children] for row in group.children] == [ [1, 1, 1], [1, 2], [1, 3], [1], [1, 1], ] @assert_no_logs def test_colspan_rowspan_2(): # A cell box cannot extend beyond the last row box of a table. html = parse_all('''
    ''') body, = html.children wrapper, = body.children table, = wrapper.children group, = table.children assert [[c.grid_x for c in row.children] for row in group.children] == [ [0, 1], [1], ] assert [[c.colspan for c in row.children] for row in group.children] == [ [1, 1], [1], ] assert [[c.rowspan for c in row.children] for row in group.children] == [ [2, 1], # Not 5 [1], ] @assert_no_logs def test_before_after_1(): assert_tree(parse_all('''

    '''), [ # No content in pseudo-element, no box generated ('p', 'Block', []), ('div', 'Block', []), ('section', 'Block', [])]) @assert_no_logs def test_before_after_2(): assert_tree(parse_all('''

    c

    '''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', 'ab')]), ('p', 'Text', ' c '), ('p::after', 'Inline', [ ('p::after', 'Text', 'de')])])])]) @assert_no_logs def test_before_after_3(): assert_tree(parse_all('''

    some text

    '''), [ ('p', 'Block', [ ('p', 'Line', [ ('a', 'Inline', [ ('a::before', 'Inline', [ ('a::before', 'Text', '[some url] ')]), ('a', 'Text', 'some text')])])])]) @assert_no_logs def test_before_after_4(): assert_tree(parse_all('''

    Lorem ipsum dolor sit amet

    '''), [ ('p', 'Block', [ ('p', 'Line', [ ('q', 'Inline', [ ('q::before', 'Inline', [ ('q::before', 'Text', '« ')]), ('q', 'Text', 'Lorem ipsum '), ('q', 'Inline', [ ('q::before', 'Inline', [ ('q::before', 'Text', '“ ')]), ('q', 'Text', 'dolor'), ('q::after', 'Inline', [ ('q::after', 'Text', ' ”')])]), ('q', 'Text', ' sit amet'), ('q::after', 'Inline', [ ('q::after', 'Text', ' »')])])])])]) @assert_no_logs def test_before_after_5(): with capture_logs() as logs: assert_tree(parse_all('''

    c

    '''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', 'a'), ('p::before', 'InlineReplaced', ''), ('p::before', 'Text', 'b')]), ('p', 'Text', 'c')])])]) assert len(logs) == 1 assert 'nested-function(' in logs[0] assert 'invalid value' in logs[0] @assert_no_logs def test_margin_boxes(): page_1, page_2 = render_pages('''

    lorem ipsum ''') assert page_1.children[0].element_tag == 'html' assert page_2.children[0].element_tag == 'html' margin_boxes_1 = [box.at_keyword for box in page_1.children[1:]] margin_boxes_2 = [box.at_keyword for box in page_2.children[1:]] assert margin_boxes_1 == ['@top-center', '@bottom-left', '@bottom-left-corner'] assert margin_boxes_2 == ['@top-center'] html, top_center = page_2.children line_box, = top_center.children text_box, = line_box.children assert text_box.text == 'Title' @assert_no_logs def test_margin_box_string_set_1(): # Test that both pages get string in the `bottom-center` margin box page_1, page_2 = render_pages('''

    first assignment

    ''') html, bottom_center = page_2.children line_box, = bottom_center.children text_box, = line_box.children assert text_box.text == 'first assignment' html, bottom_center = page_1.children line_box, = bottom_center.children text_box, = line_box.children assert text_box.text == 'first assignment' @assert_no_logs def test_margin_box_string_set_2(): def simple_string_set_test(content_val, extra_style=""): page_1, = render_pages('''

    first assignment

    ''' % dict(content_val=content_val, extra_style=extra_style)) html, top_center = page_1.children line_box, = top_center.children text_box, = line_box.children if content_val in ('before', 'after'): assert text_box.text == 'pseudo' else: assert text_box.text == 'first assignment' # Test each accepted value of `content()` as an arguemnt to `string-set` for value in ('', 'text', 'before', 'after'): if value in ('before', 'after'): extra_style = 'p:%s{content: "pseudo"}' % value simple_string_set_test(value, extra_style) else: simple_string_set_test(value) @assert_no_logs def test_margin_box_string_set_3(): # Test `first` (default value) ie. use the first assignment on the page page_1, = render_pages('''

    first assignment

    Second assignment

    ''') html, top_center = page_1.children line_box, = top_center.children text_box, = line_box.children assert text_box.text == 'first assignment' @assert_no_logs def test_margin_box_string_set_4(): # test `first-except` ie. exclude from page on which value is assigned page_1, page_2 = render_pages('''

    first_excepted

    ''') html, top_center = page_1.children assert len(top_center.children) == 0 html, top_center = page_2.children line_box, = top_center.children text_box, = line_box.children assert text_box.text == 'first_excepted' @assert_no_logs def test_margin_box_string_set_5(): # Test `last` ie. use the most-recent assignment page_1, = render_pages('''

    String set

    Second assignment

    ''') html, top_center = page_1.children[:2] line_box, = top_center.children text_box, = line_box.children assert text_box.text == 'Second assignment' @assert_no_logs def test_margin_box_string_set_6(): # Test multiple complex string-set values page_1, = render_pages('''
    • first
      • second ''') html, top_center, bottom_center = page_1.children top_line_box, = top_center.children top_text_box, = top_line_box.children assert top_text_box.text == 'before!-first-after!I.1' bottom_line_box, = bottom_center.children bottom_text_box, = bottom_line_box.children assert bottom_text_box.text == 'before!last-secondclass2|1/I' def test_margin_box_string_set_7(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/722 page_1, = render_pages(''' Chocolate Cake ''') html, top_left, top_right = page_1.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[Chocolate]' right_line_box, = top_right.children right_text_box, = right_line_box.children assert right_text_box.text == '{Cake}' @assert_no_logs def test_margin_box_string_set_8(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/726 page_1, page_2, page_3 = render_pages('''

        Initial

        Empty

        Space

        ''') html, top_left = page_1.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[initial]' html, top_left = page_2.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[]' html, top_left = page_3.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[ ]' @assert_no_logs def test_margin_box_string_set_9(): # Test that named strings are case-sensitive # See https://github.com/Kozea/WeasyPrint/pull/827 page_1, = render_pages('''

        first assignment

        second assignment
        ''') html, top_center = page_1.children line_box, = top_center.children text_box, = line_box.children assert text_box.text == 'first assignment second assignment' @assert_no_logs def test_page_counters(): """Test page-based counters.""" pages = render_pages('''

        lorem ipsum dolor ''') for page_number, page in enumerate(pages, 1): html, bottom_center = page.children line_box, = bottom_center.children text_box, = line_box.children assert text_box.text == 'Page {0} of 3.'.format(page_number) black = (0, 0, 0, 1) red = (1, 0, 0, 1) green = (0, 1, 0, 1) # lime in CSS blue = (0, 0, 1, 1) yellow = (1, 1, 0, 1) black_3 = ('solid', 3, black) red_1 = ('solid', 1, red) yellow_5 = ('solid', 5, yellow) green_5 = ('solid', 5, green) dashed_blue_5 = ('dashed', 5, blue) @assert_no_logs def test_border_collapse_1(): html = parse_all('
        ') body, = html.children table_wrapper, = body.children table, = table_wrapper.children assert isinstance(table, boxes.TableBox) assert not hasattr(table, 'collapsed_border_grid') grid = _get_grid('
        ') assert grid == ([], []) @assert_no_logs def test_border_collapse_2(): vertical_borders, horizontal_borders = _get_grid('''
        A B
        C D
        ''') assert vertical_borders == [ [black_3, red_1, black_3], [black_3, red_1, black_3], ] assert horizontal_borders == [ [black_3, black_3], [red_1, red_1], [black_3, black_3], ] @assert_no_logs def test_border_collapse_3(): # hidden vs. none vertical_borders, horizontal_borders = _get_grid('''
        A B
        C D
        ''') assert vertical_borders == [ [black_3, None, None], [black_3, black_3, black_3], ] assert horizontal_borders == [ [black_3, None], [black_3, None], [black_3, black_3], ] @assert_no_logs def test_border_collapse_4(): vertical_borders, horizontal_borders = _get_grid('''
        ''') assert vertical_borders == [ [yellow_5, black_3, red_1, yellow_5], [yellow_5, dashed_blue_5, green_5, green_5], [yellow_5, black_3, red_1, yellow_5], [yellow_5, black_3, red_1, yellow_5], ] assert horizontal_borders == [ [yellow_5, yellow_5, yellow_5], [red_1, dashed_blue_5, green_5], [red_1, dashed_blue_5, green_5], [red_1, red_1, red_1], [yellow_5, yellow_5, yellow_5], ] @assert_no_logs def test_border_collapse_5(): # rowspan and colspan vertical_borders, horizontal_borders = _get_grid('''
        ''') assert vertical_borders == [ [black_3, black_3, black_3, black_3], [black_3, black_3, None, black_3], ] assert horizontal_borders == [ [black_3, black_3, black_3], [None, black_3, black_3], [black_3, black_3, black_3], ] @assert_no_logs @pytest.mark.parametrize('html', ( '', 'abc', '

        abc', '

        abc', )) def test_display_none_root(html): box = parse_all(html) assert box.style['display'] == ('block', 'flow') assert not box.children ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.027308 weasyprint-54.1/tests/test_counters.py0000644000000000000000000004553000000000000016423 0ustar0000000000000000""" weasyprint.tests.test_counters ------------------------------ Test CSS counters. """ import pytest from weasyprint import HTML from .testing_utils import assert_no_logs, assert_tree, parse_all, render_pages @assert_no_logs def test_counters_1(): assert_tree(parse_all('''

        '''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', counter)])])]) for counter in '0 1 3 2 4 6 -11 -9 -7 44 46 48'.split()]) @assert_no_logs def test_counters_2(): assert_tree(parse_all('''
        '''), [ ('ol', 'Block', [ ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '1. ')])])]), ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '2. ')])])]), ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '3. ')])])]), ('li', 'Block', [ ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '4. ')])])]), ('ol', 'Block', [ ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '1. ')])])]), ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '1. ')])])]), ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '2. ')])])])])]), ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '5. ')])])])])]) @assert_no_logs def test_counters_3(): assert_tree(parse_all('''

        '''), [ ('div', 'Block', [ ('p', 'Block', [ ('p', 'Line', [ ('p::marker', 'Inline', [ ('p::marker', 'Text', '1. ')])])]), ('p', 'Block', [ ('p', 'Line', [ ('p::marker', 'Inline', [ ('p::marker', 'Text', '2. ')])])]), ('p', 'Block', [ ('p', 'Line', [ ('p::marker', 'Inline', [ ('p::marker', 'Text', '-55. ')])])])]), ('p', 'Block', [ ('p', 'Line', [ ('p::marker', 'Inline', [ ('p::marker', 'Text', '1. ')])])])]) @assert_no_logs def test_counters_4(): assert_tree(parse_all('''

        '''), [ ('section', 'Block', [ ('section', 'Block', [ ('section', 'Line', [ ('section::before', 'Inline', [])])]), ('h1', 'Block', [ ('h1', 'Line', [ ('h1::before', 'Inline', [ ('h1::before', 'Text', '1')])])]), ('h1', 'Block', [ ('h1', 'Line', [ ('h1::before', 'Inline', [ ('h1::before', 'Text', '2')])])]), ('section', 'Block', [ ('section', 'Block', [ ('section', 'Line', [ ('section::before', 'Inline', [])])]), ('h1', 'Block', [ ('h1', 'Line', [ ('h1::before', 'Inline', [ ('h1::before', 'Text', '2.1')])])]), ('h1', 'Block', [ ('h1', 'Line', [ ('h1::before', 'Inline', [ ('h1::before', 'Text', '2.2')])])])]), ('h1', 'Block', [ ('h1', 'Line', [ ('h1::before', 'Inline', [ ('h1::before', 'Text', '3')])])])])]) @assert_no_logs def test_counters_5(): assert_tree(parse_all('''
        Scope created now, deleted after the div

        '''), [ ('div', 'Block', [ ('div', 'Line', [ ('span', 'Inline', [ ('span', 'Text', 'Scope created now, deleted after the div ')])])]), ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', '0')])])])]) @assert_no_logs def test_counters_6(): # counter-increment may interfere with display: list-item assert_tree(parse_all('''

        '''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::marker', 'Inline', [ ('p::marker', 'Text', '0. ')])])])]) @assert_no_logs def test_counters_7(): # Test that counters are case-sensitive # See https://github.com/Kozea/WeasyPrint/pull/827 assert_tree(parse_all('''

        '''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', counter)])])]) for counter in '2.0 2.3 4.3'.split()]) @assert_no_logs def test_counters_8(): assert_tree(parse_all('''

        '''), 2 * [ ('p', 'Block', [ ('p::before', 'Block', [ ('p::marker', 'Block', [ ('p::marker', 'Line', [ ('p::marker', 'Text', '• ')])]), ('p::before', 'Block', [ ('p::before', 'Line', [ ('p::before', 'Text', 'a')])])])])]) @assert_no_logs def test_counter_styles_1(): assert_tree(parse_all('''

        '''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', counter)])])]) for counter in '-- • ◦ ▪ -7 Counter:-6 -5:Counter'.split()]) @assert_no_logs def test_counter_styles_2(): assert_tree(parse_all('''

        '''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', counter)])])]) for counter in '''-1986 -1985 -11 -10 -9 -8 -1 00 01 02 09 10 11 99 100 101 4135 4136'''.split()]) @assert_no_logs def test_counter_styles_3(): render = HTML(string='')._ua_counter_style()[0].render_value assert [render(value, 'decimal-leading-zero') for value in [ -1986, -1985, -11, -10, -9, -8, -1, 0, 1, 2, 9, 10, 11, 99, 100, 101, 4135, 4136 ]] == ''' -1986 -1985 -11 -10 -9 -8 -1 00 01 02 09 10 11 99 100 101 4135 4136 '''.split() @assert_no_logs def test_counter_styles_4(): render = HTML(string='')._ua_counter_style()[0].render_value assert [render(value, 'lower-roman') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 49, 50, 389, 390, 3489, 3490, 3491, 4999, 5000, 5001 ]] == ''' -1986 -1985 -1 0 i ii iii iv v vi vii viii ix x xi xii xlix l ccclxxxix cccxc mmmcdlxxxix mmmcdxc mmmcdxci 4999 5000 5001 '''.split() @assert_no_logs def test_counter_styles_5(): render = HTML(string='')._ua_counter_style()[0].render_value assert [render(value, 'upper-roman') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 49, 50, 389, 390, 3489, 3490, 3491, 4999, 5000, 5001 ]] == ''' -1986 -1985 -1 0 I II III IV V VI VII VIII IX X XI XII XLIX L CCCLXXXIX CCCXC MMMCDLXXXIX MMMCDXC MMMCDXCI 4999 5000 5001 '''.split() @assert_no_logs def test_counter_styles_6(): render = HTML(string='')._ua_counter_style()[0].render_value assert [render(value, 'lower-alpha') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 25, 26, 27, 28, 29, 2002, 2003 ]] == ''' -1986 -1985 -1 0 a b c d y z aa ab ac bxz bya '''.split() @assert_no_logs def test_counter_styles_7(): render = HTML(string='')._ua_counter_style()[0].render_value assert [render(value, 'upper-alpha') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 25, 26, 27, 28, 29, 2002, 2003 ]] == ''' -1986 -1985 -1 0 A B C D Y Z AA AB AC BXZ BYA '''.split() @assert_no_logs def test_counter_styles_8(): render = HTML(string='')._ua_counter_style()[0].render_value assert [render(value, 'lower-latin') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 25, 26, 27, 28, 29, 2002, 2003 ]] == ''' -1986 -1985 -1 0 a b c d y z aa ab ac bxz bya '''.split() @assert_no_logs def test_counter_styles_9(): render = HTML(string='')._ua_counter_style()[0].render_value assert [render(value, 'upper-latin') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 25, 26, 27, 28, 29, 2002, 2003 ]] == ''' -1986 -1985 -1 0 A B C D Y Z AA AB AC BXZ BYA '''.split() @assert_no_logs def test_counter_styles_10(): render = HTML(string='')._ua_counter_style()[0].render_value assert [render(value, 'georgian') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 19999, 20000, 20001 ]] == ''' -1986 -1985 -1 0 ა ბ გ დ ე ვ ზ ჱ თ ი ია იბ კ ლ მ ნ ჲ ო პ ჟ რ ს ტ ჳ ფ ქ ღ ყ შ ჩ ც ძ წ ჭ ხ ჴ ჯ ჰ ჵ ჵჰშჟთ 20000 20001 '''.split() @assert_no_logs def test_counter_styles_11(): render = HTML(string='')._ua_counter_style()[0].render_value assert [render(value, 'armenian') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 9999, 10000, 10001 ]] == ''' -1986 -1985 -1 0 Ա Բ Գ Դ Ե Զ Է Ը Թ Ժ ԺԱ ԺԲ Ի Լ Խ Ծ Կ Հ Ձ Ղ Ճ Մ Յ Ն Շ Ո Չ Պ Ջ Ռ Ս Վ Տ Ր Ց Ւ Փ Ք ՔՋՂԹ 10000 10001 '''.split() @assert_no_logs @pytest.mark.parametrize('arguments, values', ( ('cyclic "a" "b" "c"', ('a ', 'b ', 'c ', 'a ')), ('symbolic "a" "b"', ('a ', 'b ', 'aa ', 'bb ')), ('"a" "b"', ('a ', 'b ', 'aa ', 'bb ')), ('alphabetic "a" "b"', ('a ', 'b ', 'aa ', 'ab ')), ('fixed "a" "b"', ('a ', 'b ', '3 ', '4 ')), ('numeric "0" "1" "2"', ('1 ', '2 ', '10 ', '11 ')), )) def test_counter_symbols(arguments, values): page, = render_pages('''
        1. abc
        2. abc
        3. abc
        4. abc
        ''' % arguments) html, = page.children body, = html.children ol, = body.children li_1, li_2, li_3, li_4 = ol.children assert li_1.children[0].children[0].children[0].text == values[0] assert li_2.children[0].children[0].children[0].text == values[1] assert li_3.children[0].children[0].children[0].text == values[2] assert li_4.children[0].children[0].children[0].text == values[3] @assert_no_logs @pytest.mark.parametrize('style_type, values', ( ('decimal', ('1. ', '2. ', '3. ', '4. ')), ('"/"', ('/', '/', '/', '/')), )) def test_list_style_types(style_type, values): page, = render_pages('''
        1. abc
        2. abc
        3. abc
        4. abc
        ''' % style_type) html, = page.children body, = html.children ol, = body.children li_1, li_2, li_3, li_4 = ol.children assert li_1.children[0].children[0].children[0].text == values[0] assert li_2.children[0].children[0].children[0].text == values[1] assert li_3.children[0].children[0].children[0].text == values[2] assert li_4.children[0].children[0].children[0].text == values[3] def test_counter_set(): page, = render_pages('''

        ''') html, = page.children body, = html.children art_1, art_2, art_3, art_4, art_5, art_6 = body.children h1, = art_1.children assert h1.children[0].children[0].children[0].text == '1' h2, h3, = art_2.children assert h2.children[0].children[0].children[0].text == '3' assert h3.children[0].children[0].children[0].text == '4' h3, = art_3.children assert h3.children[0].children[0].children[0].text == '5' h2, = art_4.children assert h2.children[0].children[0].children[0].text == '3' h3_1, h3_2 = art_5.children assert h3_1.children[0].children[0].children[0].text == '4' assert h3_2.children[0].children[0].children[0].text == '5' h1, h2, h3 = art_6.children assert h1.children[0].children[0].children[0].text == '1' assert h2.children[0].children[0].children[0].text == '3' assert h3.children[0].children[0].children[0].text == '4' def test_counter_multiple_extends(): # Inspired by W3C failing test system-extends-invalid page, = render_pages('''
        ''') html, = page.children body, = html.children ol, = body.children li_1, li_2, li_3, li_4, li_5, li_6, li_7, li_8 = ol.children assert li_1.children[0].children[0].children[0].text == 'a1b' assert li_2.children[0].children[0].children[0].text == '2b' assert li_3.children[0].children[0].children[0].text == 'c3. ' assert li_4.children[0].children[0].children[0].text == 'd4. ' assert li_5.children[0].children[0].children[0].text == 'e5. ' assert li_6.children[0].children[0].children[0].text == '6. ' assert li_7.children[0].children[0].children[0].text == '7. ' assert li_8.children[0].children[0].children[0].text == '8. ' ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.027308 weasyprint-54.1/tests/test_css.py0000644000000000000000000004240600000000000015350 0ustar0000000000000000""" weasyprint.tests.test_css ------------------------- Test the CSS parsing, cascade, inherited and computed values. """ from math import isclose import pytest import tinycss2 from weasyprint import CSS, css, default_url_fetcher from weasyprint.css import ( PageType, get_all_computed_styles, parse_page_selectors) from weasyprint.css.computed_values import strut_layout from weasyprint.layout.page import set_page_type_computed_styles from weasyprint.urls import path2url from .testing_utils import ( BASE_URL, FakeHTML, assert_no_logs, capture_logs, resource_filename) @assert_no_logs def test_find_stylesheets(): html = FakeHTML(resource_filename('doc1.html')) sheets = list(css.find_stylesheets( html.wrapper_element, 'print', default_url_fetcher, html.base_url, font_config=None, counter_style=None, page_rules=None)) assert len(sheets) == 2 # Also test that stylesheets are in tree order assert [s.base_url.rsplit('/', 1)[-1].rsplit(',', 1)[-1] for s in sheets] \ == ['a%7Bcolor%3AcurrentColor%7D', 'doc1.html'] rules = [] for sheet in sheets: for sheet_rules in sheet.matcher.lower_local_name_selectors.values(): for rule in sheet_rules: rules.append(rule) for rule in sheet.page_rules: rules.append(rule) assert len(rules) == 10 # TODO: test that the values are correct too @assert_no_logs def test_expand_shorthands(): sheet = CSS(resource_filename('sheet2.css')) assert list(sheet.matcher.lower_local_name_selectors) == ['li'] rules = sheet.matcher.lower_local_name_selectors['li'][0][4] assert rules[0][0] == 'margin_bottom' assert rules[0][1] == (3, 'em') assert rules[1][0] == 'margin_top' assert rules[1][1] == (2, 'em') assert rules[2][0] == 'margin_right' assert rules[2][1] == (0, None) assert rules[3][0] == 'margin_bottom' assert rules[3][1] == (2, 'em') assert rules[4][0] == 'margin_left' assert rules[4][1] == (0, None) assert rules[5][0] == 'margin_left' assert rules[5][1] == (4, 'em') # TODO: test that the values are correct too @assert_no_logs def test_annotate_document(): document = FakeHTML(resource_filename('doc1.html')) document._ua_stylesheets = lambda: [CSS(resource_filename('mini_ua.css'))] style_for = get_all_computed_styles( document, user_stylesheets=[CSS(resource_filename('user.css'))]) # Element objects behave as lists of their children _head, body = document.etree_element h1, p, ul, div = body li_0, _li_1 = ul a, = li_0 span1, = div span2, = span1 h1 = style_for(h1) p = style_for(p) ul = style_for(ul) li_0 = style_for(li_0) div = style_for(div) after = style_for(a, 'after') a = style_for(a) span1 = style_for(span1) span2 = style_for(span2) assert h1['background_image'] == ( ('url', path2url(resource_filename('logo_small.png'))),) assert h1['font_weight'] == 700 assert h1['font_size'] == 40 # 2em # x-large * initial = 3/2 * 16 = 24 assert p['margin_top'] == (24, 'px') assert p['margin_right'] == (0, 'px') assert p['margin_bottom'] == (24, 'px') assert p['margin_left'] == (0, 'px') assert p['background_color'] == 'currentColor' # 2em * 1.25ex = 2 * 20 * 1.25 * 0.8 = 40 # 2.5ex * 1.25ex = 2.5 * 0.8 * 20 * 1.25 * 0.8 = 40 # TODO: ex unit doesn't work with @font-face fonts, see computed_values.py # assert ul['margin_top'] == (40, 'px') # assert ul['margin_right'] == (40, 'px') # assert ul['margin_bottom'] == (40, 'px') # assert ul['margin_left'] == (40, 'px') assert ul['font_weight'] == 400 # thick = 5px, 0.25 inches = 96*.25 = 24px assert ul['border_top_width'] == 0 assert ul['border_right_width'] == 5 assert ul['border_bottom_width'] == 0 assert ul['border_left_width'] == 24 assert li_0['font_weight'] == 700 assert li_0['font_size'] == 8 # 6pt assert li_0['margin_top'] == (16, 'px') # 2em assert li_0['margin_right'] == (0, 'px') assert li_0['margin_bottom'] == (16, 'px') assert li_0['margin_left'] == (32, 'px') # 4em assert a['text_decoration_line'] == {'underline'} assert a['font_weight'] == 900 assert a['font_size'] == 24 # 300% of 8px assert a['padding_top'] == (1, 'px') assert a['padding_right'] == (2, 'px') assert a['padding_bottom'] == (3, 'px') assert a['padding_left'] == (4, 'px') assert a['border_top_width'] == 42 assert a['border_bottom_width'] == 42 assert a['color'] == (1, 0, 0, 1) assert a['border_top_color'] == 'currentColor' assert div['font_size'] == 40 # 2 * 20px assert span1['width'] == (160, 'px') # 10 * 16px (root default is 16px) assert span1['height'] == (400, 'px') # 10 * (2 * 20px) assert span2['font_size'] == 32 # The href attr should be as in the source, not made absolute. assert after['content'] == ( ('string', ' ['), ('string', 'home.html'), ('string', ']')) assert after['background_color'] == (1, 0, 0, 1) assert after['border_top_width'] == 42 assert after['border_bottom_width'] == 3 # TODO: much more tests here: test that origin and selector precedence # and inheritance are correct… @assert_no_logs def test_page(): document = FakeHTML(resource_filename('doc1.html')) style_for = get_all_computed_styles( document, user_stylesheets=[CSS(string=''' html { color: red } @page { margin: 10px } @page :right { color: blue; margin-bottom: 12pt; font-size: 20px; @top-left { width: 10em } @top-right { font-size: 10px} } ''')]) page_type = PageType( side='left', first=True, blank=False, index=0, name='') set_page_type_computed_styles(page_type, document, style_for) style = style_for(page_type) assert style['margin_top'] == (5, 'px') assert style['margin_left'] == (10, 'px') assert style['margin_bottom'] == (10, 'px') assert style['color'] == (1, 0, 0, 1) # red, inherited from html page_type = PageType( side='right', first=True, blank=False, index=0, name='') set_page_type_computed_styles(page_type, document, style_for) style = style_for(page_type) assert style['margin_top'] == (5, 'px') assert style['margin_left'] == (10, 'px') assert style['margin_bottom'] == (16, 'px') assert style['color'] == (0, 0, 1, 1) # blue page_type = PageType( side='left', first=False, blank=False, index=1, name='') set_page_type_computed_styles(page_type, document, style_for) style = style_for(page_type) assert style['margin_top'] == (10, 'px') assert style['margin_left'] == (10, 'px') assert style['margin_bottom'] == (10, 'px') assert style['color'] == (1, 0, 0, 1) # red, inherited from html page_type = PageType( side='right', first=False, blank=False, index=1, name='') set_page_type_computed_styles(page_type, document, style_for) style = style_for(page_type) assert style['margin_top'] == (10, 'px') assert style['margin_left'] == (10, 'px') assert style['margin_bottom'] == (16, 'px') assert style['color'] == (0, 0, 1, 1) # blue page_type = PageType( side='left', first=True, blank=False, index=0, name='') set_page_type_computed_styles(page_type, document, style_for) style = style_for(page_type, '@top-left') assert style is None page_type = PageType( side='right', first=True, blank=False, index=0, name='') set_page_type_computed_styles(page_type, document, style_for) style = style_for(page_type, '@top-left') assert style['font_size'] == 20 # inherited from @page assert style['width'] == (200, 'px') page_type = PageType( side='right', first=True, blank=False, index=0, name='') set_page_type_computed_styles(page_type, document, style_for) style = style_for(page_type, '@top-right') assert style['font_size'] == 10 @assert_no_logs @pytest.mark.parametrize('style, selectors', ( ('@page {}', [{ 'side': None, 'blank': None, 'first': None, 'name': None, 'index': None, 'specificity': [0, 0, 0]}]), ('@page :left {}', [{ 'side': 'left', 'blank': None, 'first': None, 'name': None, 'index': None, 'specificity': [0, 0, 1]}]), ('@page:first:left {}', [{ 'side': 'left', 'blank': None, 'first': True, 'name': None, 'index': None, 'specificity': [0, 1, 1]}]), ('@page pagename {}', [{ 'side': None, 'blank': None, 'first': None, 'name': 'pagename', 'index': None, 'specificity': [1, 0, 0]}]), ('@page pagename:first:right:blank {}', [{ 'side': 'right', 'blank': True, 'first': True, 'name': 'pagename', 'index': None, 'specificity': [1, 2, 1]}]), ('@page pagename, :first {}', [ {'side': None, 'blank': None, 'first': None, 'name': 'pagename', 'index': None, 'specificity': [1, 0, 0]}, {'side': None, 'blank': None, 'first': True, 'name': None, 'index': None, 'specificity': [0, 1, 0]}]), ('@page :first:first {}', [{ 'side': None, 'blank': None, 'first': True, 'name': None, 'index': None, 'specificity': [0, 2, 0]}]), ('@page :left:left {}', [{ 'side': 'left', 'blank': None, 'first': None, 'name': None, 'index': None, 'specificity': [0, 0, 2]}]), ('@page :nth(2) {}', [{ 'side': None, 'blank': None, 'first': None, 'name': None, 'index': (0, 2, None), 'specificity': [0, 1, 0]}]), ('@page :nth(2n + 4) {}', [{ 'side': None, 'blank': None, 'first': None, 'name': None, 'index': (2, 4, None), 'specificity': [0, 1, 0]}]), ('@page :nth(3n) {}', [{ 'side': None, 'blank': None, 'first': None, 'name': None, 'index': (3, 0, None), 'specificity': [0, 1, 0]}]), ('@page :nth( n+2 ) {}', [{ 'side': None, 'blank': None, 'first': None, 'name': None, 'index': (1, 2, None), 'specificity': [0, 1, 0]}]), ('@page :nth(even) {}', [{ 'side': None, 'blank': None, 'first': None, 'name': None, 'index': (2, 0, None), 'specificity': [0, 1, 0]}]), ('@page pagename:nth(2) {}', [{ 'side': None, 'blank': None, 'first': None, 'name': 'pagename', 'index': (0, 2, None), 'specificity': [1, 1, 0]}]), ('@page page page {}', None), ('@page :left page {}', None), ('@page :left, {}', None), ('@page , {}', None), ('@page :left, test, {}', None), ('@page :wrong {}', None), ('@page :left:wrong {}', None), ('@page :left:right {}', None), )) def test_page_selectors(style, selectors): at_rule, = tinycss2.parse_stylesheet(style) assert parse_page_selectors(at_rule) == selectors @assert_no_logs @pytest.mark.parametrize('source, messages', ( (':lipsum { margin: 2cm', ['WARNING: Invalid or unsupported selector']), ('::lipsum { margin: 2cm', ['WARNING: Invalid or unsupported selector']), ('foo { margin-color: red', ['WARNING: Ignored', 'unknown property']), ('foo { margin-top: red', ['WARNING: Ignored', 'invalid value']), ('@import "relative-uri.css"', ['ERROR: Relative URI reference without a base URI']), ('@import "invalid-protocol://absolute-URL"', ['ERROR: Failed to load stylesheet at']), )) def test_warnings(source, messages): """Check that appropriate warnings are logged.""" with capture_logs() as logs: CSS(string=source) assert len(logs) == 1, source for message in messages: assert message in logs[0] @assert_no_logs def test_warnings_stylesheet(): html = '' with capture_logs() as logs: FakeHTML(string=html).render() assert len(logs) == 1 assert 'ERROR: Failed to load stylesheet at' in logs[0] @assert_no_logs @pytest.mark.parametrize('style', ( '

        ''') page, = document.render().pages html, = page._page_box.children body, = html.children div, = body.children section, = div.children paragraph, = section.children assert html.style['font_size'] == 10 assert div.style['font_size'] == 20 # 140% of 10px = 14px is inherited from html assert strut_layout(div.style)[0] == 14 assert div.style['vertical_align'] == 7 # 50 % of 14px assert paragraph.style['font_size'] == 20 # 1.4 is inherited from p, 1.4 * 20px on em = 28px assert strut_layout(paragraph.style)[0] == 28 assert paragraph.style['vertical_align'] == 14 # 50% of 28px @assert_no_logs def test_important(): document = FakeHTML(string='''

        ''') page, = document.render(stylesheets=[CSS(string=''' body p:nth-child(1) { color: red } p:nth-child(2) { color: lime !important } p:nth-child(4) { color: lime !important } body p:nth-child(4) { color: red } ''')]).pages html, = page._page_box.children body, = html.children for paragraph in body.children: assert paragraph.style['color'] == (0, 1, 0, 1) # lime (light green) @assert_no_logs def test_named_pages(): document = FakeHTML(string='''

        a

        ''') page, = document.render().pages html, = page._page_box.children body, = html.children div, = body.children p, = div.children span, = p.children assert html.style['page'] == '' assert body.style['page'] == '' assert div.style['page'] == '' assert p.style['page'] == 'NARRow' assert span.style['page'] == 'NARRow' @assert_no_logs @pytest.mark.parametrize('value, width', ( ('96px', 96), ('1in', 96), ('72pt', 96), ('6pc', 96), ('2.54cm', 96), ('25.4mm', 96), ('101.6q', 96), ('1.1em', 11), ('1.1rem', 17.6), # TODO: ch and ex units don't work with font-face, see computed_values.py # ('1.1ch', 11), # ('1.5ex', 12), )) def test_units(value, width): document = FakeHTML(base_url=BASE_URL, string='''

        ''' % value) page, = document.render().pages html, = page._page_box.children body, = html.children p, = body.children assert p.margin_left == width @assert_no_logs @pytest.mark.parametrize('parent_css, parent_size, child_css, child_size', ( ('10px', 10, '10px', 10), ('x-small', 12, 'xx-large', 32), ('x-large', 24, '2em', 48), ('1em', 16, '1em', 16), ('1em', 16, 'larger', 6 / 5 * 16), ('medium', 16, 'larger', 6 / 5 * 16), ('x-large', 24, 'larger', 32), ('xx-large', 32, 'larger', 1.2 * 32), ('1px', 1, 'larger', 3 / 5 * 16), ('28px', 28, 'larger', 32), ('100px', 100, 'larger', 120), ('xx-small', 3 / 5 * 16, 'larger', 12), ('1em', 16, 'smaller', 8 / 9 * 16), ('medium', 16, 'smaller', 8 / 9 * 16), ('x-large', 24, 'smaller', 6 / 5 * 16), ('xx-large', 32, 'smaller', 24), ('xx-small', 3 / 5 * 16, 'smaller', 0.8 * 3 / 5 * 16), ('1px', 1, 'smaller', 0.8), ('28px', 28, 'smaller', 24), ('100px', 100, 'smaller', 32), )) def test_font_size(parent_css, parent_size, child_css, child_size): document = FakeHTML(string='

        ab') style_for = get_all_computed_styles(document, user_stylesheets=[CSS( string='p{font-size:%s}span{font-size:%s}' % (parent_css, child_css))]) _head, body = document.etree_element p, = body span, = p assert isclose(style_for(p)['font_size'], parent_size) assert isclose(style_for(span)['font_size'], child_size) @pytest.mark.parametrize('media, width, warning', ( ('@media screen { @page { size: 10px } }', 20, False), ('@media print { @page { size: 10px } }', 10, False), ('@media ("unknown content") { @page { size: 10px } }', 20, True), )) def test_media_queries(media, width, warning): document = FakeHTML(string='

        ab') with capture_logs() as logs: page, = document.render( stylesheets=[CSS(string='@page{size:20px}%s' % media)]).pages html, = page._page_box.children assert html.width == width assert (logs if warning else not logs) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.027308 weasyprint-54.1/tests/test_css_descriptors.py0000644000000000000000000001665200000000000017775 0ustar0000000000000000""" weasyprint.tests.test_css_descriptors ------------------------------------- Test CSS descriptors. """ import pytest import tinycss2 from weasyprint.css import preprocess_stylesheet from weasyprint.css.validation.descriptors import preprocess_descriptors from .testing_utils import assert_no_logs, capture_logs @assert_no_logs def test_font_face_1(): stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: Gentium Hard;' ' src: url(http://example.com/fonts/Gentium.woff);' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src = list(preprocess_descriptors( 'font-face', 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ( 'src', (('external', 'http://example.com/fonts/Gentium.woff'),)) @assert_no_logs def test_font_face_2(): stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: "Fonty Smiley";' ' src: url(Fonty-Smiley.woff);' ' font-style: italic;' ' font-weight: 200;' ' font-stretch: condensed;' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src, font_style, font_weight, font_stretch = list( preprocess_descriptors( 'font-face', 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Fonty Smiley') assert src == ( 'src', (('external', 'http://weasyprint.org/foo/Fonty-Smiley.woff'),)) assert font_style == ('font_style', 'italic') assert font_weight == ('font_weight', 200) assert font_stretch == ('font_stretch', 'condensed') @assert_no_logs def test_font_face_3(): stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: Gentium Hard;' ' src: local();' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src = list(preprocess_descriptors( 'font-face', 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', None),)) @assert_no_logs def test_font_face_4(): # See bug #487 stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: Gentium Hard;' ' src: local(Gentium Hard);' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src = list(preprocess_descriptors( 'font-face', 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', 'Gentium Hard'),)) def test_font_face_bad_1(): stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: "Bad Font";' ' src: url(BadFont.woff);' ' font-stretch: expanded;' ' font-style: wrong;' ' font-weight: bolder;' ' font-stretch: wrong;' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' with capture_logs() as logs: font_family, src, font_stretch = list( preprocess_descriptors( 'font-face', 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Bad Font') assert src == ( 'src', (('external', 'http://weasyprint.org/foo/BadFont.woff'),)) assert font_stretch == ('font_stretch', 'expanded') assert logs == [ 'WARNING: Ignored `font-style: wrong` at 1:91, invalid value.', 'WARNING: Ignored `font-weight: bolder` at 1:111, invalid value.', 'WARNING: Ignored `font-stretch: wrong` at 1:133, invalid value.'] def test_font_face_bad_2(): stylesheet = tinycss2.parse_stylesheet('@font-face{}') with capture_logs() as logs: descriptors = [] preprocess_stylesheet( 'print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None, None) assert not descriptors assert logs == [ "WARNING: Missing src descriptor in '@font-face' rule at 1:1"] def test_font_face_bad_3(): stylesheet = tinycss2.parse_stylesheet('@font-face{src: url(test.woff)}') with capture_logs() as logs: descriptors = [] preprocess_stylesheet( 'print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None, None) assert not descriptors assert logs == [ "WARNING: Missing font-family descriptor in '@font-face' rule at 1:1"] def test_font_face_bad_4(): stylesheet = tinycss2.parse_stylesheet('@font-face{font-family: test}') with capture_logs() as logs: descriptors = [] preprocess_stylesheet( 'print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None, None) assert not descriptors assert logs == [ "WARNING: Missing src descriptor in '@font-face' rule at 1:1"] def test_font_face_bad_5(): stylesheet = tinycss2.parse_stylesheet( '@font-face { font-family: test; src: wrong }') with capture_logs() as logs: descriptors = [] preprocess_stylesheet( 'print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None, None) assert not descriptors assert logs == [ 'WARNING: Ignored `src: wrong ` at 1:33, invalid value.', "WARNING: Missing src descriptor in '@font-face' rule at 1:1"] def test_font_face_bad_6(): stylesheet = tinycss2.parse_stylesheet( '@font-face { font-family: good, bad; src: url(test.woff) }') with capture_logs() as logs: descriptors = [] preprocess_stylesheet( 'print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None, None) assert not descriptors assert logs == [ 'WARNING: Ignored `font-family: good, bad` at 1:14, invalid value.', "WARNING: Missing font-family descriptor in '@font-face' rule at 1:1"] def test_font_face_bad_7(): stylesheet = tinycss2.parse_stylesheet( '@font-face { font-family: good, bad; src: really bad }') with capture_logs() as logs: descriptors = [] preprocess_stylesheet( 'print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None, None) assert not descriptors assert logs == [ 'WARNING: Ignored `font-family: good, bad` at 1:14, invalid value.', 'WARNING: Ignored `src: really bad ` at 1:38, invalid value.', "WARNING: Missing src descriptor in '@font-face' rule at 1:1"] @pytest.mark.parametrize('rule', ( '@counter-style test {system: alphabetic; symbols: a}', '@counter-style test {system: cyclic}', '@counter-style test {system: additive; additive-symbols: a 1}', '@counter-style test {system: additive; additive-symbols: 10 x, 1 i, 5 v}', )) def test_counter_style_invalid(rule): stylesheet = tinycss2.parse_stylesheet(rule) with capture_logs() as logs: descriptors = [] preprocess_stylesheet( 'print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None, {}) assert not descriptors assert len(logs) >= 1 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1643655079.3595529 weasyprint-54.1/tests/test_css_validation.py0000644000000000000000000011662300000000000017565 0ustar0000000000000000""" weasyprint.tests.test_css_properties ------------------------------------ Test expanders for shorthand properties. """ import math import pytest import tinycss2 from weasyprint.css import preprocess_declarations from weasyprint.css.computed_values import ZERO_PIXELS from weasyprint.css.properties import INITIAL_VALUES from weasyprint.images import LinearGradient, RadialGradient from .testing_utils import assert_no_logs, capture_logs def expand_to_dict(css, expected_error=None): """Helper to test shorthand properties expander functions.""" declarations = tinycss2.parse_declaration_list(css) with capture_logs() as logs: base_url = 'http://weasyprint.org/foo/' declarations = list(preprocess_declarations(base_url, declarations)) if expected_error: assert len(logs) == 1 assert expected_error in logs[0] else: assert not logs return dict( (name, value) for name, value, _priority in declarations if value != 'initial') def assert_invalid(css, message='invalid'): assert expand_to_dict(css, message) == {} @assert_no_logs def test_not_print(): assert expand_to_dict( 'volume: 42', 'the property does not apply for the print media') == {} @assert_no_logs @pytest.mark.parametrize('rule, values', ( ('1px, 3em, auto, auto', ((1, 'px'), (3, 'em'), 'auto', 'auto')), ('1px, 3em, auto auto', ((1, 'px'), (3, 'em'), 'auto', 'auto')), ('1px 3em auto 1px', ((1, 'px'), (3, 'em'), 'auto', (1, 'px'))), )) def test_function(rule, values): assert expand_to_dict('clip: rect(%s)' % rule) == {'clip': values} @assert_no_logs @pytest.mark.parametrize('rule', ( 'clip: square(1px, 3em, auto, auto)', 'clip: rect(1px, 3em, auto)', 'clip: rect(1px, 3em / auto)', )) def test_function_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('counter-reset: foo bar 2 baz', { 'counter_reset': (('foo', 0), ('bar', 2), ('baz', 0))}), ('counter-increment: foo bar 2 baz', { 'counter_increment': (('foo', 1), ('bar', 2), ('baz', 1))}), ('counter-reset: foo', {'counter_reset': (('foo', 0),)}), ('counter-set: FoO', {'counter_set': (('FoO', 0),)}), ('counter-increment: foo bAr 2 Bar', { 'counter_increment': (('foo', 1), ('bAr', 2), ('Bar', 1))}), ('counter-reset: none', {'counter_reset': ()}), )) def test_counters(rule, result): assert expand_to_dict(rule) == result @pytest.mark.parametrize('rule, warning, result', ( ('counter-reset: foo initial', 'Invalid counter name: initial.', {}), ('counter-reset: foo none', 'Invalid counter name: none.', {}), )) def test_counters_warning(rule, warning, result): assert expand_to_dict(rule, warning) == result @assert_no_logs @pytest.mark.parametrize('rule', ( 'counter-reset: foo 3px', 'counter-reset: 3', )) def test_counters_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('letter-spacing: normal', {'letter_spacing': 'normal'}), ('letter-spacing: 3px', {'letter_spacing': (3, 'px')}), ('word-spacing: normal', {'word_spacing': 'normal'}), ('word-spacing: 3px', {'word_spacing': (3, 'px')}), )) def test_spacing(rule, result): assert expand_to_dict(rule) == result @assert_no_logs def test_spacing_warning(): assert expand_to_dict( 'letter_spacing: normal', 'you probably mean "letter-spacing"') == {} @assert_no_logs @pytest.mark.parametrize('rule', ( 'letter-spacing: 3', 'word-spacing: 3', )) def test_spacing_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('text-decoration-line: none', {'text_decoration_line': 'none'}), ('text-decoration-line: overline', {'text_decoration_line': {'overline'}}), ('text-decoration-line: overline blink line-through', { 'text_decoration_line': {'blink', 'line-through', 'overline'}}), )) def test_decoration_line(rule, result): assert expand_to_dict(rule) == result @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('text-decoration-style: solid', {'text_decoration_style': 'solid'}), ('text-decoration-style: double', {'text_decoration_style': 'double'}), ('text-decoration-style: dotted', {'text_decoration_style': 'dotted'}), ('text-decoration-style: dashed', {'text_decoration_style': 'dashed'}), )) def test_decoration_style(rule, result): assert expand_to_dict(rule) == result @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('text-decoration: none', {'text_decoration_line': 'none'}), ('text-decoration: overline', {'text_decoration_line': {'overline'}}), ('text-decoration: overline blink line-through', { 'text_decoration_line': {'blink', 'line-through', 'overline'}}), ('text-decoration: red', {'text_decoration_color': (1, 0, 0, 1)}), )) def test_decoration(rule, result): default = { key: value for key, value in INITIAL_VALUES.items() if key.startswith('text_decoration_')} real_result = {**default, **result} assert expand_to_dict(rule) == real_result @assert_no_logs @pytest.mark.parametrize('rule', ( 'text-decoration: solid solid', 'text-decoration: red red', 'text-decoration: 1px', 'text-decoration: underline none', )) def test_decoration_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('size: 200px', {'size': ((200, 'px'), (200, 'px'))}), ('size: 200px 300pt', {'size': ((200, 'px'), (300, 'pt'))}), ('size: auto', {'size': ((210, 'mm'), (297, 'mm'))}), ('size: portrait', {'size': ((210, 'mm'), (297, 'mm'))}), ('size: landscape', {'size': ((297, 'mm'), (210, 'mm'))}), ('size: A3 portrait', {'size': ((297, 'mm'), (420, 'mm'))}), ('size: A3 landscape', {'size': ((420, 'mm'), (297, 'mm'))}), ('size: portrait A3', {'size': ((297, 'mm'), (420, 'mm'))}), ('size: landscape A3', {'size': ((420, 'mm'), (297, 'mm'))}), )) def test_size(rule, result): assert expand_to_dict(rule) == result @pytest.mark.parametrize('rule', ( 'size: A3 landscape A3', 'size: A12', 'size: foo', 'size: foo bar', 'size: 20%', )) def test_size_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('transform: none', {'transform': ()}), ('transform: translate(6px) rotate(90deg)', { 'transform': ( ('translate', ((6, 'px'), (0, 'px'))), ('rotate', math.pi / 2))}), ('transform: translate(-4px, 0)', { 'transform': (('translate', ((-4, 'px'), (0, None))),)}), ('transform: translate(6px, 20%)', { 'transform': (('translate', ((6, 'px'), (20, '%'))),)}), ('transform: scale(2)', {'transform': (('scale', (2, 2)),)}), ('transform: translate(6px 20%)', { 'transform': (('translate', ((6, 'px'), (20, '%'))),)}), )) def test_transforms(rule, result): assert expand_to_dict(rule) == result @assert_no_logs @pytest.mark.parametrize('rule', ( 'transform: lipsumize(6px)', 'transform: foo', 'transform: scale(2) foo', 'transform: 6px', )) def test_transforms_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('margin: inherit', { 'margin_top': 'inherit', 'margin_right': 'inherit', 'margin_bottom': 'inherit', 'margin_left': 'inherit', }), ('margin: 1em', { 'margin_top': (1, 'em'), 'margin_right': (1, 'em'), 'margin_bottom': (1, 'em'), 'margin_left': (1, 'em'), }), ('margin: -1em auto 20%', { 'margin_top': (-1, 'em'), 'margin_right': 'auto', 'margin_bottom': (20, '%'), 'margin_left': 'auto', }), ('padding: 1em 0', { 'padding_top': (1, 'em'), 'padding_right': (0, None), 'padding_bottom': (1, 'em'), 'padding_left': (0, None), }), ('padding: 1em 0 2%', { 'padding_top': (1, 'em'), 'padding_right': (0, None), 'padding_bottom': (2, '%'), 'padding_left': (0, None), }), ('padding: 1em 0 2em 5px', { 'padding_top': (1, 'em'), 'padding_right': (0, None), 'padding_bottom': (2, 'em'), 'padding_left': (5, 'px'), }), )) def test_expand_four_sides(rule, result): assert expand_to_dict(rule) == result @assert_no_logs def test_expand_four_sides_warning(): assert expand_to_dict( 'padding: 1 2 3 4 5', 'Expected 1 to 4 token components got 5') == {} @assert_no_logs @pytest.mark.parametrize('rule', ( 'margin: rgb(0, 0, 0)', 'padding: auto', 'padding: -12px', 'border-width: -3em', 'border-width: 12%', )) def test_expand_four_sides_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('border-top: 3px dotted red', { 'border_top_width': (3, 'px'), 'border_top_style': 'dotted', 'border_top_color': (1, 0, 0, 1), # red }), ('border-top: 3px dotted', { 'border_top_width': (3, 'px'), 'border_top_style': 'dotted', }), ('border-top: 3px red', { 'border_top_width': (3, 'px'), 'border_top_color': (1, 0, 0, 1), # red }), ('border-top: solid', {'border_top_style': 'solid'}), ('border: 6px dashed lime', { 'border_top_width': (6, 'px'), 'border_top_style': 'dashed', 'border_top_color': (0, 1, 0, 1), # lime 'border_left_width': (6, 'px'), 'border_left_style': 'dashed', 'border_left_color': (0, 1, 0, 1), # lime 'border_bottom_width': (6, 'px'), 'border_bottom_style': 'dashed', 'border_bottom_color': (0, 1, 0, 1), # lime 'border_right_width': (6, 'px'), 'border_right_style': 'dashed', 'border_right_color': (0, 1, 0, 1), # lime }), )) def test_expand_borders(rule, result): assert expand_to_dict(rule) == result @assert_no_logs def test_expand_borders_invalid(): assert_invalid('border: 6px dashed left') @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('list-style: inherit', { 'list_style_position': 'inherit', 'list_style_image': 'inherit', 'list_style_type': 'inherit', }), ('list-style: url(../bar/lipsum.png)', { 'list_style_image': ('url', 'http://weasyprint.org/bar/lipsum.png'), }), ('list-style: square', { 'list_style_type': 'square', }), ('list-style: circle inside', { 'list_style_position': 'inside', 'list_style_type': 'circle', }), ('list-style: none circle inside', { 'list_style_position': 'inside', 'list_style_image': ('none', None), 'list_style_type': 'circle', }), ('list-style: none inside none', { 'list_style_position': 'inside', 'list_style_image': ('none', None), 'list_style_type': 'none', }), ('list-style: inside special none', { 'list_style_position': 'inside', 'list_style_image': ('none', None), 'list_style_type': 'special', }), )) def test_expand_list_style(rule, result): assert expand_to_dict(rule) == result @assert_no_logs def test_expand_list_style_warning(): assert_invalid( 'list-style: circle disc', 'got multiple type values in a list-style shorthand') @assert_no_logs @pytest.mark.parametrize('rule', ( 'list-style: none inside none none', 'list-style: 1px', )) def test_expand_list_style_invalid(rule): assert_invalid(rule) def assert_background(css, **expected): """Helper checking the background properties.""" expanded = expand_to_dict(f'background: {css}') assert expanded.pop('background_color') == expected.pop( 'background_color', INITIAL_VALUES['background_color']) nb_layers = len(expanded['background_image']) for name, value in expected.items(): assert expanded.pop(name) == value for name, value in expanded.items(): assert tuple(value) == INITIAL_VALUES[name] * nb_layers @assert_no_logs def test_expand_background(): assert_background('red', background_color=(1, 0, 0, 1)) assert_background( 'url(lipsum.png)', background_image=[('url', 'http://weasyprint.org/foo/lipsum.png')]) assert_background( 'no-repeat', background_repeat=[('no-repeat', 'no-repeat')]) assert_background('fixed', background_attachment=['fixed']) assert_background( 'repeat no-repeat fixed', background_repeat=[('repeat', 'no-repeat')], background_attachment=['fixed']) assert_background( 'inherit', background_repeat='inherit', background_attachment='inherit', background_image='inherit', background_position='inherit', background_size='inherit', background_clip='inherit', background_origin='inherit', background_color='inherit') assert_background( 'top', background_position=[('left', (50, '%'), 'top', (0, '%'))]) assert_background( 'top right', background_position=[('left', (100, '%'), 'top', (0, '%'))]) assert_background( 'top right 20px', background_position=[('right', (20, 'px'), 'top', (0, '%'))]) assert_background( 'top 1% right 20px', background_position=[('right', (20, 'px'), 'top', (1, '%'))]) assert_background( 'top no-repeat', background_repeat=[('no-repeat', 'no-repeat')], background_position=[('left', (50, '%'), 'top', (0, '%'))]) assert_background( 'top right no-repeat', background_repeat=[('no-repeat', 'no-repeat')], background_position=[('left', (100, '%'), 'top', (0, '%'))]) assert_background( 'top right 20px no-repeat', background_repeat=[('no-repeat', 'no-repeat')], background_position=[('right', (20, 'px'), 'top', (0, '%'))]) assert_background( 'top 1% right 20px no-repeat', background_repeat=[('no-repeat', 'no-repeat')], background_position=[('right', (20, 'px'), 'top', (1, '%'))]) assert_background( 'url(bar) #f00 repeat-y center left fixed', background_color=(1, 0, 0, 1), background_image=[('url', 'http://weasyprint.org/foo/bar')], background_repeat=[('no-repeat', 'repeat')], background_attachment=['fixed'], background_position=[('left', (0, '%'), 'top', (50, '%'))]) assert_background( '#00f 10% 200px', background_color=(0, 0, 1, 1), background_position=[('left', (10, '%'), 'top', (200, 'px'))]) assert_background( 'right 78px fixed', background_attachment=['fixed'], background_position=[('left', (100, '%'), 'top', (78, 'px'))]) assert_background( 'center / cover red', background_size=['cover'], background_position=[('left', (50, '%'), 'top', (50, '%'))], background_color=(1, 0, 0, 1)) assert_background( 'center / auto red', background_size=[('auto', 'auto')], background_position=[('left', (50, '%'), 'top', (50, '%'))], background_color=(1, 0, 0, 1)) assert_background( 'center / 42px', background_size=[((42, 'px'), 'auto')], background_position=[('left', (50, '%'), 'top', (50, '%'))]) assert_background( 'center / 7% 4em', background_size=[((7, '%'), (4, 'em'))], background_position=[('left', (50, '%'), 'top', (50, '%'))]) assert_background( 'red content-box', background_color=(1, 0, 0, 1), background_origin=['content-box'], background_clip=['content-box']) assert_background( 'red border-box content-box', background_color=(1, 0, 0, 1), background_origin=['border-box'], background_clip=['content-box']) assert_background( 'border-box red', background_color=(1, 0, 0, 1), background_origin=['border-box']) assert_background( 'url(bar) center, no-repeat', background_color=(0, 0, 0, 0), background_image=[('url', 'http://weasyprint.org/foo/bar'), ('none', None)], background_position=[('left', (50, '%'), 'top', (50, '%')), ('left', (0, '%'), 'top', (0, '%'))], background_repeat=[('repeat', 'repeat'), ('no-repeat', 'no-repeat')]) # Color must be in the last layer: assert_invalid('background: red, url(foo)') @assert_no_logs @pytest.mark.parametrize('rule', ( 'background: 10px lipsum', 'background-position: 10px lipsum', 'background: content-box red content-box', 'background-image: inexistent-gradient(blue, green)', )) def test_expand_background_invalid(rule): assert_invalid(rule) @assert_no_logs def test_expand_background_position(): """Test the ``background-position`` property.""" def position(css, *expected): [(name, [value])] = expand_to_dict( f'background-position: {css}').items() assert name == 'background_position' assert value == expected for css_x, val_x in [ ('left', (0, '%')), ('center', (50, '%')), ('right', (100, '%')), ('4.5%', (4.5, '%')), ('12px', (12, 'px')) ]: for css_y, val_y in [ ('top', (0, '%')), ('center', (50, '%')), ('bottom', (100, '%')), ('7%', (7, '%')), ('1.5px', (1.5, 'px')) ]: # Two tokens: position('%s %s' % (css_x, css_y), 'left', val_x, 'top', val_y) # One token: position(css_x, 'left', val_x, 'top', (50, '%')) # One token, vertical position('top', 'left', (50, '%'), 'top', (0, '%')) position('bottom', 'left', (50, '%'), 'top', (100, '%')) # Three tokens: position('center top 10%', 'left', (50, '%'), 'top', (10, '%')) position('top 10% center', 'left', (50, '%'), 'top', (10, '%')) position('center bottom 10%', 'left', (50, '%'), 'bottom', (10, '%')) position('bottom 10% center', 'left', (50, '%'), 'bottom', (10, '%')) position('right top 10%', 'right', (0, '%'), 'top', (10, '%')) position('top 10% right', 'right', (0, '%'), 'top', (10, '%')) position('right bottom 10%', 'right', (0, '%'), 'bottom', (10, '%')) position('bottom 10% right', 'right', (0, '%'), 'bottom', (10, '%')) position('center left 10%', 'left', (10, '%'), 'top', (50, '%')) position('left 10% center', 'left', (10, '%'), 'top', (50, '%')) position('center right 10%', 'right', (10, '%'), 'top', (50, '%')) position('right 10% center', 'right', (10, '%'), 'top', (50, '%')) position('bottom left 10%', 'left', (10, '%'), 'bottom', (0, '%')) position('left 10% bottom', 'left', (10, '%'), 'bottom', (0, '%')) position('bottom right 10%', 'right', (10, '%'), 'bottom', (0, '%')) position('right 10% bottom', 'right', (10, '%'), 'bottom', (0, '%')) # Four tokens: position('left 10% bottom 3px', 'left', (10, '%'), 'bottom', (3, 'px')) position('bottom 3px left 10%', 'left', (10, '%'), 'bottom', (3, 'px')) position('right 10% top 3px', 'right', (10, '%'), 'top', (3, 'px')) position('top 3px right 10%', 'right', (10, '%'), 'top', (3, 'px')) assert_invalid('background-position: left center 3px') assert_invalid('background-position: 3px left') assert_invalid('background-position: bottom 4%') assert_invalid('background-position: bottom top') @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('border-radius: 1px', { 'border_top_left_radius': ((1, 'px'), (1, 'px')), 'border_top_right_radius': ((1, 'px'), (1, 'px')), 'border_bottom_right_radius': ((1, 'px'), (1, 'px')), 'border_bottom_left_radius': ((1, 'px'), (1, 'px')), }), ('border-radius: 1px 2em', { 'border_top_left_radius': ((1, 'px'), (1, 'px')), 'border_top_right_radius': ((2, 'em'), (2, 'em')), 'border_bottom_right_radius': ((1, 'px'), (1, 'px')), 'border_bottom_left_radius': ((2, 'em'), (2, 'em')), }), ('border-radius: 1px / 2em', { 'border_top_left_radius': ((1, 'px'), (2, 'em')), 'border_top_right_radius': ((1, 'px'), (2, 'em')), 'border_bottom_right_radius': ((1, 'px'), (2, 'em')), 'border_bottom_left_radius': ((1, 'px'), (2, 'em')), }), ('border-radius: 1px 3px / 2em 4%', { 'border_top_left_radius': ((1, 'px'), (2, 'em')), 'border_top_right_radius': ((3, 'px'), (4, '%')), 'border_bottom_right_radius': ((1, 'px'), (2, 'em')), 'border_bottom_left_radius': ((3, 'px'), (4, '%')), }), ('border-radius: 1px 2em 3%', { 'border_top_left_radius': ((1, 'px'), (1, 'px')), 'border_top_right_radius': ((2, 'em'), (2, 'em')), 'border_bottom_right_radius': ((3, '%'), (3, '%')), 'border_bottom_left_radius': ((2, 'em'), (2, 'em')), }), ('border-radius: 1px 2em 3% 4rem', { 'border_top_left_radius': ((1, 'px'), (1, 'px')), 'border_top_right_radius': ((2, 'em'), (2, 'em')), 'border_bottom_right_radius': ((3, '%'), (3, '%')), 'border_bottom_left_radius': ((4, 'rem'), (4, 'rem')), }), )) def test_expand_border_radius(rule, result): assert expand_to_dict(rule) == result @assert_no_logs @pytest.mark.parametrize('rule, reason', ( ('border-radius: 1px 1px 1px 1px 1px', '1 to 4 token'), ('border-radius: 1px 1px 1px 1px 1px / 1px', '1 to 4 token'), ('border-radius: 1px / 1px / 1px', 'only one "/"'), ('border-radius: 1px, 1px', 'invalid'), ('border-radius: 1px /', 'value after "/"'), )) def test_expand_border_radius_invalid(rule, reason): assert_invalid(rule, reason) @assert_no_logs def test_font(): """Test the ``font`` property.""" assert expand_to_dict('font: 12px My Fancy Font, serif') == { 'font_size': (12, 'px'), 'font_family': ('My Fancy Font', 'serif'), } assert expand_to_dict('font: small/1.2 "Some Font", serif') == { 'font_size': 'small', 'line_height': (1.2, None), 'font_family': ('Some Font', 'serif'), } assert expand_to_dict('font: small-caps italic 700 large serif') == { 'font_style': 'italic', 'font_variant_caps': 'small-caps', 'font_weight': 700, 'font_size': 'large', 'font_family': ('serif',), } assert expand_to_dict( 'font: small-caps condensed normal 700 large serif' ) == { 'font_stretch': 'condensed', 'font_variant_caps': 'small-caps', 'font_weight': 700, 'font_size': 'large', 'font_family': ('serif',), } assert_invalid('font-family: "My" Font, serif') assert_invalid('font-family: "My" "Font", serif') assert_invalid('font-family: "My", 12pt, serif') assert_invalid('font: menu', 'System fonts are not supported') assert_invalid('font: 12deg My Fancy Font, serif') assert_invalid('font: 12px') assert_invalid('font: 12px/foo serif') assert_invalid('font: 12px "Invalid" family') assert_invalid('font: normal normal normal normal normal large serif') assert_invalid('font: normal small-caps italic 700 condensed large serif') assert_invalid('font: small-caps italic 700 normal condensed large serif') assert_invalid('font: small-caps italic 700 condensed normal large serif') assert_invalid('font: normal normal normal normal') assert_invalid('font: normal normal normal italic') assert_invalid('font: caption', 'System fonts') @assert_no_logs def test_font_variant(): """Test the ``font-variant`` property.""" assert expand_to_dict('font-variant: normal') == { 'font_variant_alternates': 'normal', 'font_variant_caps': 'normal', 'font_variant_east_asian': 'normal', 'font_variant_ligatures': 'normal', 'font_variant_numeric': 'normal', 'font_variant_position': 'normal', } assert expand_to_dict('font-variant: none') == { 'font_variant_alternates': 'normal', 'font_variant_caps': 'normal', 'font_variant_east_asian': 'normal', 'font_variant_ligatures': 'none', 'font_variant_numeric': 'normal', 'font_variant_position': 'normal', } assert expand_to_dict('font-variant: historical-forms petite-caps') == { 'font_variant_alternates': 'historical-forms', 'font_variant_caps': 'petite-caps', } assert expand_to_dict( 'font-variant: lining-nums contextual small-caps common-ligatures' ) == { 'font_variant_ligatures': ('contextual', 'common-ligatures'), 'font_variant_numeric': ('lining-nums',), 'font_variant_caps': 'small-caps', } assert expand_to_dict('font-variant: jis78 ruby proportional-width') == { 'font_variant_east_asian': ('jis78', 'ruby', 'proportional-width'), } # CSS2-style font-variant assert expand_to_dict('font-variant: small-caps') == { 'font_variant_caps': 'small-caps', } assert_invalid('font-variant: normal normal') assert_invalid('font-variant: 2') assert_invalid('font-variant: ""') assert_invalid('font-variant: extra') assert_invalid('font-variant: jis78 jis04') assert_invalid('font-variant: full-width lining-nums ordinal normal') assert_invalid('font-variant: diagonal-fractions stacked-fractions') assert_invalid( 'font-variant: common-ligatures contextual no-common-ligatures') assert_invalid('font-variant: sub super') assert_invalid('font-variant: slashed-zero slashed-zero') @assert_no_logs def test_line_height(): """Test the ``line-height`` property.""" assert expand_to_dict('line-height: 1px') == {'line_height': (1, 'px')} assert expand_to_dict('line-height: 1.1%') == {'line_height': (1.1, '%')} assert expand_to_dict('line-height: 1em') == {'line_height': (1, 'em')} assert expand_to_dict('line-height: 1') == {'line_height': (1, None)} assert expand_to_dict('line-height: 1.3') == {'line_height': (1.3, None)} assert expand_to_dict('line-height: -0') == {'line_height': (0, None)} assert expand_to_dict('line-height: 0px') == {'line_height': (0, 'px')} assert_invalid('line-height: 1deg') assert_invalid('line-height: -1px') assert_invalid('line-height: -1') assert_invalid('line-height: -0.5%') assert_invalid('line-height: 1px 1px') @assert_no_logs def test_string_set(): """Test the ``string-set`` property.""" assert expand_to_dict('string-set: test content(text)') == { 'string_set': (('test', (('content()', 'text'),)),)} assert expand_to_dict('string-set: test content(before)') == { 'string_set': (('test', (('content()', 'before'),)),)} assert expand_to_dict('string-set: test "string"') == { 'string_set': (('test', (('string', 'string'),)),)} assert expand_to_dict( 'string-set: test1 "string", test2 "string"') == { 'string_set': ( ('test1', (('string', 'string'),)), ('test2', (('string', 'string'),)))} assert expand_to_dict('string-set: test attr(class)') == { 'string_set': (('test', (('attr()', ('class', 'string', '')),)),)} assert expand_to_dict('string-set: test counter(count)') == { 'string_set': (('test', (('counter()', ('count', 'decimal')),)),)} assert expand_to_dict( 'string-set: test counter(count, upper-roman)') == { 'string_set': ( ('test', (('counter()', ('count', 'upper-roman')),)),)} assert expand_to_dict('string-set: test counters(count, ".")') == { 'string_set': ( ('test', (('counters()', ('count', '.', 'decimal')),)),)} assert expand_to_dict( 'string-set: test counters(count, ".", upper-roman)') == { 'string_set': ( ('test', (('counters()', ('count', '.', 'upper-roman')),)),)} assert expand_to_dict( 'string-set: test content(text) "string" ' 'attr(title) attr(title) counter(count)') == { 'string_set': (('test', ( ('content()', 'text'), ('string', 'string'), ('attr()', ('title', 'string', '')), ('attr()', ('title', 'string', '')), ('counter()', ('count', 'decimal')))),)} assert_invalid('string-set: test') assert_invalid('string-set: test test1') assert_invalid('string-set: test content(test)') assert_invalid('string-set: test unknown()') assert_invalid('string-set: test attr(id, class)') @assert_no_logs def test_linear_gradient(): red = (1, 0, 0, 1) lime = (0, 1, 0, 1) blue = (0, 0, 1, 1) pi = math.pi def gradient(css, direction, colors=(blue,), stop_positions=(None,)): for repeating, prefix in ((False, ''), (True, 'repeating-')): expanded = expand_to_dict( 'background-image: %slinear-gradient(%s)' % (prefix, css)) [(_, [(type_, image)])] = expanded.items() assert type_ == 'linear-gradient' assert isinstance(image, LinearGradient) assert image.repeating == repeating assert image.direction_type == direction[0] if isinstance(image.direction, str): image.direction == direction[1] else: assert image.direction == pytest.approx(direction[1]) assert image.colors == tuple(colors) assert image.stop_positions == tuple(stop_positions) def invalid(css): assert_invalid('background-image: linear-gradient(%s)' % css) assert_invalid('background-image: repeating-linear-gradient(%s)' % css) invalid(' ') invalid('1% blue') invalid('blue 10deg') invalid('blue 4') invalid('soylent-green 4px') invalid('red 4px 2px') gradient('blue', ('angle', pi)) gradient('red', ('angle', pi), [red], [None]) gradient('blue 1%, lime,red 2em ', ('angle', pi), [blue, lime, red], [(1, '%'), None, (2, 'em')]) invalid('18deg') gradient('18deg, blue', ('angle', pi / 10)) gradient('4rad, blue', ('angle', 4)) gradient('.25turn, blue', ('angle', pi / 2)) gradient('100grad, blue', ('angle', pi / 2)) gradient('12rad, blue 1%, lime,red 2em ', ('angle', 12), [blue, lime, red], [(1, '%'), None, (2, 'em')]) invalid('10arc-minutes, blue') invalid('10px, blue') invalid('to 90deg, blue') gradient('to top, blue', ('angle', 0)) gradient('to right, blue', ('angle', pi / 2)) gradient('to bottom, blue', ('angle', pi)) gradient('to left, blue', ('angle', pi * 3 / 2)) gradient('to right, blue 1%, lime,red 2em ', ('angle', pi / 2), [blue, lime, red], [(1, '%'), None, (2, 'em')]) invalid('to the top, blue') invalid('to up, blue') invalid('into top, blue') invalid('top, blue') gradient('to top left, blue', ('corner', 'top_left')) gradient('to left top, blue', ('corner', 'top_left')) gradient('to top right, blue', ('corner', 'top_right')) gradient('to right top, blue', ('corner', 'top_right')) gradient('to bottom left, blue', ('corner', 'bottom_left')) gradient('to left bottom, blue', ('corner', 'bottom_left')) gradient('to bottom right, blue', ('corner', 'bottom_right')) gradient('to right bottom, blue', ('corner', 'bottom_right')) invalid('to bottom up, blue') invalid('bottom left, blue') @assert_no_logs def test_overflow_wrap(): assert expand_to_dict('overflow-wrap: normal') == { 'overflow_wrap': 'normal'} assert expand_to_dict('overflow-wrap: break-word') == { 'overflow_wrap': 'break-word'} assert_invalid('overflow-wrap: none') assert_invalid('overflow-wrap: normal, break-word') @assert_no_logs def test_expand_word_wrap(): assert expand_to_dict('word-wrap: normal') == { 'overflow_wrap': 'normal'} assert expand_to_dict('word-wrap: break-word') == { 'overflow_wrap': 'break-word'} assert_invalid('word-wrap: none') assert_invalid('word-wrap: normal, break-word') @assert_no_logs def test_radial_gradient(): red = (1, 0, 0, 1) lime = (0, 1, 0, 1) blue = (0, 0, 1, 1) def gradient(css, shape='ellipse', size=('keyword', 'farthest-corner'), center=('left', (50, '%'), 'top', (50, '%')), colors=(blue,), stop_positions=(None,)): for repeating, prefix in ((False, ''), (True, 'repeating-')): expanded = expand_to_dict( 'background-image: %sradial-gradient(%s)' % (prefix, css)) [(_, [(type_, image)])] = expanded.items() assert type_ == 'radial-gradient' assert isinstance(image, RadialGradient) assert image.repeating == repeating assert image.shape == shape assert image.size_type == size[0] assert image.size == size[1] assert image.center == center assert image.colors == tuple(colors) assert image.stop_positions == tuple(stop_positions) def invalid(css): assert_invalid('background-image: radial-gradient(%s)' % css) assert_invalid('background-image: repeating-radial-gradient(%s)' % css) invalid(' ') invalid('1% blue') invalid('blue 10deg') invalid('blue 4') invalid('soylent-green 4px') invalid('red 4px 2px') gradient('blue') gradient('red', colors=[red]) gradient('blue 1%, lime,red 2em ', colors=[blue, lime, red], stop_positions=[(1, '%'), None, (2, 'em')]) gradient('circle, blue', 'circle') gradient('ellipse, blue', 'ellipse') invalid('circle') invalid('square, blue') invalid('closest-triangle, blue') invalid('center, blue') gradient('ellipse closest-corner, blue', 'ellipse', ('keyword', 'closest-corner')) gradient('circle closest-side, blue', 'circle', ('keyword', 'closest-side')) gradient('farthest-corner circle, blue', 'circle', ('keyword', 'farthest-corner')) gradient('farthest-side, blue', 'ellipse', ('keyword', 'farthest-side')) gradient('5ch, blue', 'circle', ('explicit', ((5, 'ch'), (5, 'ch')))) gradient('5ch circle, blue', 'circle', ('explicit', ((5, 'ch'), (5, 'ch')))) gradient('circle 5ch, blue', 'circle', ('explicit', ((5, 'ch'), (5, 'ch')))) invalid('ellipse 5ch') invalid('5ch ellipse') gradient('10px 50px, blue', 'ellipse', ('explicit', ((10, 'px'), (50, 'px')))) gradient('10px 50px ellipse, blue', 'ellipse', ('explicit', ((10, 'px'), (50, 'px')))) gradient('ellipse 10px 50px, blue', 'ellipse', ('explicit', ((10, 'px'), (50, 'px')))) invalid('circle 10px 50px, blue') invalid('10px 50px circle, blue') invalid('10%, blue') invalid('10% circle, blue') invalid('circle 10%, blue') gradient('10px 50px, blue', 'ellipse', ('explicit', ((10, 'px'), (50, 'px')))) invalid('at appex, blue') gradient('at top 10% right, blue', center=('right', (0, '%'), 'top', (10, '%'))) gradient('circle at bottom, blue', shape='circle', center=('left', (50, '%'), 'top', (100, '%'))) gradient('circle at 10px, blue', shape='circle', center=('left', (10, 'px'), 'top', (50, '%'))) gradient('closest-side circle at right 5em, blue', shape='circle', size=('keyword', 'closest-side'), center=('left', (100, '%'), 'top', (5, 'em'))) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('flex: auto', { 'flex_grow': 1, 'flex_shrink': 1, 'flex_basis': 'auto', }), ('flex: none', { 'flex_grow': 0, 'flex_shrink': 0, 'flex_basis': 'auto', }), ('flex: 10', { 'flex_grow': 10, 'flex_shrink': 1, 'flex_basis': ZERO_PIXELS, }), ('flex: 2 2', { 'flex_grow': 2, 'flex_shrink': 2, 'flex_basis': ZERO_PIXELS, }), ('flex: 2 2 1px', { 'flex_grow': 2, 'flex_shrink': 2, 'flex_basis': (1, 'px'), }), ('flex: 2 2 auto', { 'flex_grow': 2, 'flex_shrink': 2, 'flex_basis': 'auto', }), ('flex: 2 auto', { 'flex_grow': 2, 'flex_shrink': 1, 'flex_basis': 'auto', }), ('flex: 0 auto', { 'flex_grow': 0, 'flex_shrink': 1, 'flex_basis': 'auto', }), )) def test_flex(rule, result): assert expand_to_dict(rule) == result @assert_no_logs @pytest.mark.parametrize('rule', ( 'flex: auto 0 0 0', 'flex: 1px 2px', 'flex: auto auto', 'flex: auto 1 auto', )) def test_flex_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('flex-flow: column', { 'flex_direction': 'column', }), ('flex-flow: wrap', { 'flex_wrap': 'wrap', }), ('flex-flow: wrap column', { 'flex_direction': 'column', 'flex_wrap': 'wrap', }), ('flex-flow: row wrap', { 'flex_direction': 'row', 'flex_wrap': 'wrap', }), )) def test_flex_flow(rule, result): assert expand_to_dict(rule) == result @assert_no_logs @pytest.mark.parametrize('rule', ( 'flex-flow: 1px', 'flex-flow: wrap 1px', 'flex-flow: row row', 'flex-flow: wrap nowrap', 'flex-flow: column wrap nowrap row', )) def test_flex_flow_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule', ( 'list-style-type: symbols()', 'list-style-type: symbols(cyclic)', 'list-style-type: symbols(symbolic)', 'list-style-type: symbols(fixed)', 'list-style-type: symbols(alphabetic "a")', 'list-style-type: symbols(numeric "1")', 'list-style-type: symbols(test "a" "b")', 'list-style-type: symbols(fixed symbolic "a" "b")', )) def test_function_symbols(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('page-break-after: left', {'break_after': 'left'}), ('page-break-before: always', {'break_before': 'page'}), )) def test_page_break(rule, result): assert expand_to_dict(rule) == result @assert_no_logs @pytest.mark.parametrize('rule', ( 'page-break-after: top', 'page-break-before: 1px', )) def test_page_break_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('page-break-inside: avoid', {'break_inside': 'avoid'}), )) def test_page_break_inside(rule, result): assert expand_to_dict(rule) == result @assert_no_logs @pytest.mark.parametrize('rule', ( 'page-break-inside: top', )) def test_page_break_inside_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('columns: 1em', {'column_width': (1, 'em'), 'column_count': 'auto'}), ('columns: auto', {'column_width': 'auto', 'column_count': 'auto'}), ('columns: auto auto', {'column_width': 'auto', 'column_count': 'auto'}), )) def test_columns(rule, result): assert expand_to_dict(rule) == result @assert_no_logs @pytest.mark.parametrize('rule, reason', ( ('columns: 1px 2px', 'invalid'), ('columns: auto auto auto', 'multiple'), )) def test_columns_invalid(rule, reason): assert_invalid(rule, reason) @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('line-clamp: none', { 'max_lines': 'none', 'continue': 'auto', 'block_ellipsis': 'none'}), ('line-clamp: 2', { 'max_lines': 2, 'continue': 'discard', 'block_ellipsis': 'auto'}), ('line-clamp: 3 "…"', { 'max_lines': 3, 'continue': 'discard', 'block_ellipsis': ('string', '…')}), )) def test_line_clamp(rule, result): assert expand_to_dict(rule) == result @assert_no_logs @pytest.mark.parametrize('rule, reason', ( ('line-clamp: none none none', 'invalid'), ('line-clamp: 1px', 'invalid'), ('line-clamp: 0 "…"', 'invalid'), ('line-clamp: 1px 2px', 'invalid'), )) def test_line_clamp_invalid(rule, reason): assert_invalid(rule, reason) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.027308 weasyprint-54.1/tests/test_float.py0000644000000000000000000005623700000000000015674 0ustar0000000000000000""" weasyprint.tests.layout ----------------------- Tests for floating boxes layout. """ import pytest from weasyprint.formatting_structure import boxes from .testing_utils import assert_no_logs, render_pages def outer_area(box): """Return the (x, y, w, h) rectangle for the outer area of a box.""" return (box.position_x, box.position_y, box.margin_width(), box.margin_height()) @assert_no_logs def test_floats_1(): # adjacent-floats-001 page, = render_pages('''

        ''') html, = page.children body, = html.children div_1, div_2 = body.children assert outer_area(div_1) == (0, 0, 100, 100) assert outer_area(div_2) == (100, 0, 100, 100) @assert_no_logs def test_floats_2(): # c414-flt-fit-000 page, = render_pages('''
        ''') html, = page.children body, = html.children div_1, div_2, div_4, anon_block = body.children line_3, line_5 = anon_block.children img_3, = line_3.children img_5, = line_5.children assert outer_area(div_1) == (0, 0, 100, 60) assert outer_area(div_2) == (100, 0, 100, 60) assert outer_area(img_3) == (200, 0, 60, 60) assert outer_area(div_4) == (0, 60, 100, 60) assert outer_area(img_5) == (100, 60, 60, 60) @assert_no_logs def test_floats_3(): # c414-flt-fit-002 page, = render_pages('''

        ⇦ A 1

        ⇦ B 2

        ⇦ A 3

        B 4 ⇨

        ⇦ A 5

        B 6 ⇨

        B 8 ⇨

        ⇦ A 7

        ⇦ A 9

        ⇦ B 10

        ''') html, = page.children body, = html.children positions = [(paragraph.position_x, paragraph.position_y) for paragraph in body.children] assert positions == [ (0, 0), (70, 0), (0, 20), (130, 20), (0, 40), (130, 40), (130, 60), (0, 60), (0, 80), (70, 80), ] @assert_no_logs def test_floats_4(): # c414-flt-wrap-000 ... more or less page, = render_pages('''

        ''') html, = page.children body, = html.children p_1, p_2, anon_block = body.children line_1, line_2 = anon_block.children assert anon_block.position_y == 0 assert (line_1.position_x, line_1.position_y) == (20, 0) assert (line_2.position_x, line_2.position_y) == (0, 200) @assert_no_logs def test_floats_5(): # c414-flt-wrap-000 with text ... more or less page, = render_pages('''

        A B ''') html, = page.children body, = html.children p_1, p_2, anon_block = body.children line_1, line_2 = anon_block.children assert anon_block.position_y == 0 assert (line_1.position_x, line_1.position_y) == (20, 0) assert (line_2.position_x, line_2.position_y) == (0, 200) @assert_no_logs def test_floats_6(): # floats-placement-vertical-001b page, = render_pages(''' ''') html, = page.children body, = html.children line_1, line_2 = body.children span_1, = line_1.children span_2, = line_2.children img_1, = span_1.children img_2, img_3 = span_2.children assert outer_area(img_1) == (0, 0, 50, 50) assert outer_area(img_2) == (30, 50, 50, 50) assert outer_area(img_3) == (0, 50, 30, 30) @assert_no_logs def test_floats_7(): # Variant of the above: no page, = render_pages(''' ''') html, = page.children body, = html.children line_1, line_2 = body.children img_1, = line_1.children img_2, img_3 = line_2.children assert outer_area(img_1) == (0, 0, 50, 50) assert outer_area(img_2) == (30, 50, 50, 50) assert outer_area(img_3) == (0, 50, 30, 30) @assert_no_logs def test_floats_8(): # Floats do no affect other pages page_1, page_2 = render_pages('''
        ''') html, = page_1.children body, = html.children float_img, anon_block, = body.children line, = anon_block.children img_1, = line.children assert outer_area(float_img) == (0, 0, 30, 30) assert outer_area(img_1) == (30, 0, 50, 50) html, = page_2.children body, = html.children div, anon_block = body.children line, = anon_block.children img_2, = line.children @assert_no_logs def test_floats_9(): # Regression test # https://github.com/Kozea/WeasyPrint/issues/263 page, = render_pages('''
        ''') @assert_no_logs def test_floats_page_breaks_1(): # Tests floated images shorter than the page pages = render_pages(''' ''') assert len(pages) == 2 page_images = [] for page in pages: images = [d for d in page.descendants() if d.element_tag == 'img'] assert all([img.element_tag == 'img' for img in images]) assert all([img.position_x == 10 for img in images]) page_images.append(images) del images positions_y = [[img.position_y for img in images] for images in page_images] assert positions_y == [[10], [10]] @assert_no_logs def test_floats_page_breaks_2(): # Tests floated images taller than the page pages = render_pages(''' ''') assert len(pages) == 2 page_images = [] for page in pages: images = [d for d in page.descendants() if d.element_tag == 'img'] assert all([img.element_tag == 'img' for img in images]) assert all([img.position_x == 10 for img in images]) page_images.append(images) del images positions_y = [[img.position_y for img in images] for images in page_images] assert positions_y == [[10], [10]] @assert_no_logs def test_floats_page_breaks_3(): # Tests floated images shorter than the page pages = render_pages(''' ''') assert len(pages) == 3 page_images = [] for page in pages: images = [d for d in page.descendants() if d.element_tag == 'img'] assert all([img.element_tag == 'img' for img in images]) assert all([img.position_x == 10 for img in images]) page_images.append(images) del images positions_y = [[img.position_y for img in images] for images in page_images] assert positions_y == [[10, 40], [10, 40], [10]] @assert_no_logs def test_floats_page_breaks_4(): # last float does not fit, pushed to next page pages = render_pages('''
        ''') assert len(pages) == 2 page_divs = [] for page in pages: divs = [div for div in page.descendants() if div.element_tag == 'div'] assert all([div.element_tag == 'div' for div in divs]) page_divs.append(divs) del divs positions_y = [[div.position_y for div in divs] for divs in page_divs] assert positions_y == [[10, 70], [10]] @assert_no_logs def test_floats_page_breaks_5(): # last float does not fit, pushed to next page # center div must not pages = render_pages('''
        ''') assert len(pages) == 2 page_divs = [] for page in pages: divs = [div for div in page.descendants() if div.element_tag == 'div'] assert all([div.element_tag == 'div' for div in divs]) page_divs.append(divs) del divs positions_y = [[div.position_y for div in divs] for divs in page_divs] assert positions_y == [[10], [10, 30]] @assert_no_logs def test_floats_page_breaks_6(): # center div must be the last element, # but float won't fit and will get pushed anyway pages = render_pages('''
        ''') assert len(pages) == 3 page_divs = [] for page in pages: divs = [div for div in page.descendants() if div.element_tag == 'div'] assert all([div.element_tag == 'div' for div in divs]) page_divs.append(divs) del divs positions_y = [[div.position_y for div in divs] for divs in page_divs] assert positions_y == [[10], [10], [10]] @assert_no_logs def test_preferred_widths_1(): def get_float_width(body_width): page, = render_pages('''

        Lorem ipsum dolor sit amet, consectetur elit

        ''' % body_width) html, = page.children body, = html.children paragraph, = body.children return paragraph.width # Preferred minimum width: assert get_float_width(10) == len('consectetur elit') * 16 # Preferred width: assert get_float_width(1000000) == len('Lorem ipsum dolor sit amet,') * 16 @assert_no_logs def test_preferred_widths_2(): # Non-regression test: # Incorrect whitespace handling in preferred width used to cause # unnecessary line break. page, = render_pages('''

        Lorem ipsum dolor.

        ''') html, = page.children body, = html.children paragraph, = body.children assert len(paragraph.children) == 1 assert isinstance(paragraph.children[0], boxes.LineBox) @assert_no_logs def test_preferred_widths_3(): page, = render_pages('''


        ''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == 40 @assert_no_logs def test_preferred_widths_4(): page, = render_pages( '' '

        XX
        XX
        X

        ') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == 40 @assert_no_logs def test_preferred_widths_5(): # The space is the start of the line is collapsed. page, = render_pages( '' '

        XX
        XX
        X

        ') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == 40 @assert_no_logs def test_float_in_inline_1(): page, = render_pages('''

        aa bb cc ddd ee ff

        ''') html, = page.children body, = html.children paragraph, = body.children line1, line2 = paragraph.children p1, a, p2 = line1.children assert p1.width == 6 * 20 assert p1.text == 'aa bb ' assert p1.position_x == 0 * 20 assert p2.width == 3 * 20 assert p2.text == ' ee' assert p2.position_x == 9 * 20 span, a_text = a.children assert a_text.width == 3 * 20 # leading space collapse assert a_text.text == 'ddd' assert a_text.position_x == 6 * 20 assert span.width == 2 * 20 assert span.children[0].children[0].text == 'cc' assert span.position_x == 12 * 20 p3, = line2.children assert p3.width == 2 * 20 @assert_no_logs def test_float_in_inline_2(): page, = render_pages('''
        a b c
        1 2 3 4 5 6
        ''') html, = page.children body, = html.children article, = body.children line1, line2 = article.children span1, = line1.children div, text = span1.children assert div.children[0].children[0].text.strip() == 'a b c' assert text.text.strip() == '1 2 3' span2, = line2.children text, = span2.children assert text.text.strip() == '4 5 6' @assert_no_logs def test_float_in_inline_3(): page, = render_pages('''
        1 2 3
        a b c
        4 5 6
        ''') html, = page.children body, = html.children article, = body.children line1, line2 = article.children span1, = line1.children text, div = span1.children assert text.text.strip() == '1 2 3' assert div.children[0].children[0].text.strip() == 'a b c' span2, = line2.children text, = span2.children assert text.text.strip() == '4 5 6' @assert_no_logs def test_float_in_inline_4(): page, = render_pages('''
        1 2 3 4
        a b c
        5 6
        ''') html, = page.children body, = html.children article, = body.children line1, line2 = article.children span1, div = line1.children text1, text2 = span1.children assert text1.text.strip() == '1 2 3 4' assert text2.text.strip() == '5' assert div.position_y == 16 assert div.children[0].children[0].text.strip() == 'a b c' span2, = line2.children text, = span2.children assert text.text.strip() == '6' @assert_no_logs def test_float_next_line(): page, = render_pages('''

        pp pp pp pp ppppp aa pp pp pp pp pp

        ''') html, = page.children body, = html.children paragraph, = body.children line1, line2, line3 = paragraph.children assert len(line1.children) == 1 assert len(line3.children) == 1 a, p = line2.children span, a_text = a.children assert span.position_x == 0 assert span.width == 5 * 20 assert a_text.position_x == a.position_x == 5 * 20 assert a_text.width == a.width == 2 * 20 assert p.position_x == 7 * 20 @assert_no_logs def test_float_text_indent_1(): page, = render_pages('''

        aa float aa

        ''') html, = page.children body, = html.children paragraph, = body.children line1, = paragraph.children a, = line1.children a1, span, a2 = a.children span_text, = span.children assert span.position_x == span_text.position_x == 0 assert span.width == span_text.width == ( (1 + 5) * 20) # text-indent + span text assert a1.width == 3 * 20 assert a1.position_x == (1 + 5 + 1) * 20 # span + a1 text-indent assert a2.width == 2 * 20 # leading space collapse assert a2.position_x == (1 + 5 + 1 + 3) * 20 # span + a1 t-i + a1 @assert_no_logs def test_float_text_indent_2(): page, = render_pages('''

        oooooooooooo aa float aa

        ''') html, = page.children body, = html.children paragraph, = body.children line1, line2 = paragraph.children p1, = line1.children assert p1.position_x == 1 * 20 # text-indent assert p1.width == 12 * 20 # p text a, = line2.children a1, span, a2 = a.children span_text, = span.children assert span.position_x == span_text.position_x == 0 assert span.width == span_text.width == ( (1 + 5) * 20) # text-indent + span text assert a1.width == 3 * 20 assert a1.position_x == (1 + 5) * 20 # span assert a2.width == 2 * 20 # leading space collapse assert a2.position_x == (1 + 5 + 3) * 20 # span + a1 @assert_no_logs def test_float_text_indent_3(): page, = render_pages('''

        oooooooooooo aa float aa oooooooooooo

        ''') html, = page.children body, = html.children paragraph, = body.children line1, line2, line3 = paragraph.children p1, = line1.children assert p1.position_x == 1 * 20 # text-indent assert p1.width == 12 * 20 # p text a, = line2.children a1, span, a2 = a.children span_text, = span.children assert span.position_x == span_text.position_x == (14 - 5 - 1) * 20 assert span.width == span_text.width == ( (1 + 5) * 20) # text-indent + span text assert a1.position_x == 0 # span assert a2.width == 2 * 20 # leading space collapse assert a2.position_x == (14 - 5 - 1 - 2) * 20 p2, = line3.children assert p2.position_x == 0 assert p2.width == 12 * 20 # p text @pytest.mark.xfail @assert_no_logs def test_float_fail(): page, = render_pages('''

        bb bb pp bb pp pb pp pp apa bb bb

        ''') html, = page.children body, = html.children paragraph, = body.children line1, line2, line3 = paragraph.children ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639575171.410225 weasyprint-54.1/tests/test_fonts.py0000644000000000000000000001165600000000000015714 0ustar0000000000000000""" weasyprint.tests.test_fonts --------------------------- Test the fonts features. """ from .testing_utils import assert_no_logs, render_pages @assert_no_logs def test_font_face(): page, = render_pages(''' abc''') html, = page.children body, = html.children line, = body.children assert line.width == 3 * 16 @assert_no_logs def test_kerning_default(): # Kerning and ligatures are on by default page, = render_pages(''' kkliga''') html, = page.children body, = html.children line, = body.children span1, span2 = line.children assert span1.width == 1.5 * 16 assert span2.width == 1.5 * 16 @assert_no_logs def test_ligatures_word_space(): # Kerning and ligatures are on for text with increased word spacing # https://github.com/Kozea/WeasyPrint/issues/1469 page, = render_pages(''' aa liga aa''') html, = page.children body, = html.children assert len(body.children) == 1 @assert_no_logs def test_kerning_deactivate(): # Deactivate kerning page, = render_pages(''' kkkk''') html, = page.children body, = html.children line, = body.children span1, span2 = line.children assert span1.width == 1.5 * 16 assert span2.width == 2 * 16 @assert_no_logs def test_kerning_ligature_deactivate(): # Deactivate kerning and ligatures page, = render_pages(''' kk ligakk liga''') html, = page.children body, = html.children line, = body.children span1, span2 = line.children assert span1.width == (1.5 + 1 + 1.5) * 16 assert span2.width == (2 + 1 + 4) * 16 @assert_no_logs def test_font_face_descriptors(): page, = render_pages( ''' ''' 'kk' 'subs' 'dlig' 'onum' 'zero') html, = page.children body, = html.children line, = body.children kern, subs, dlig, onum, zero = line.children assert kern.width == 1.5 * 16 assert subs.width == 1.5 * 16 assert dlig.width == 1.5 * 16 assert onum.width == 1.5 * 16 assert zero.width == 1.5 * 16 @assert_no_logs def test_woff_simple(): page, = render_pages(( ''' ''' 'woff font' 'woff font' 'woff font' 'woff font')) html, = page.children body, = html.children line, = body.children span1, span2, span3, span4 = line.children # otf font matches woff font assert span1.width == span2.width # otf font matches woff font loaded from cache assert span1.width == span3.width # the default font does not match the loaded fonts assert span1.width != span4.width ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1639487799.8764234 weasyprint-54.1/tests/test_pdf.py0000644000000000000000000005025600000000000015333 0ustar0000000000000000""" weasyprint.tests.test_pdf ------------------------- Test PDF-related code, including metadata, bookmarks and hyperlinks. """ import hashlib import io import os import re from codecs import BOM_UTF16_BE import pytest from weasyprint import Attachment from weasyprint.document import Document, DocumentMetadata from weasyprint.text.fonts import FontConfiguration from weasyprint.urls import path2url from .testing_utils import ( FakeHTML, assert_no_logs, capture_logs, resource_filename) # Top and right positions in points, rounded to the default float precision of # 6 digits, a rendered by pydyf TOP = round(297 * 72 / 25.4, 6) RIGHT = round(210 * 72 / 25.4, 6) @assert_no_logs @pytest.mark.parametrize('zoom', (1, 1.5, 0.5)) def test_page_size_zoom(zoom): pdf = FakeHTML(string='

        1

        2

        3

        4

        5

        6

        7

        8

        9

        10

        11

        ''').write_pdf() # 1 # 2 # |_ 3 # |_ 4 # | L_ 5 # L_ 6 # 7 # L_ 8 # L_ 9 # 10 # L_ 11 assert re.findall(b'/Title \\((.*)\\)', pdf) == [ str(i).encode('ascii') for i in range(1, 12)] counts = re.findall(b'/Count ([0-9-]*)', pdf) counts.pop(0) # Page count outlines = counts.pop() assert outlines == b'11' assert counts == [ b'0', b'4', b'0', b'1', b'0', b'0', b'2', b'1', b'0', b'1', b'0'] @assert_no_logs def test_bookmarks_5(): pdf = FakeHTML(string='''

        1

        level 1

        2

        level 2

        3

        level 1

        4

        level 2

        5

        level 3 ''').write_pdf() # 1 # L_ 2 # 3 # L_ 4 # L_ 5 assert re.findall(b'/Title \\((.*)\\)', pdf) == [ str(i).encode('ascii') for i in range(1, 6)] counts = re.findall(b'/Count ([0-9-]*)', pdf) counts.pop(0) # Page count outlines = counts.pop() assert outlines == b'5' assert counts == [b'1', b'0', b'2', b'1', b'0'] @assert_no_logs def test_bookmarks_6(): pdf = FakeHTML(string='''

        1

        h2 level 1

        2

        h4 level 2

        3

        h3 level 2
        4
        h5 level 3

        5

        h1 level 1

        6

        h2 level 2

        7

        h2 level 2

        8

        h4 level 3

        9

        h1 level 1 ''').write_pdf() # 1 # |_ 2 # L_ 3 # L_ 4 # 5 # |_ 6 # L_ 7 # L_ 8 # 9 assert re.findall(b'/Title \\((.*)\\)', pdf) == [ str(i).encode('ascii') for i in range(1, 10)] counts = re.findall(b'/Count ([0-9-]*)', pdf) counts.pop(0) # Page count outlines = counts.pop() assert outlines == b'9' assert counts == [b'3', b'0', b'1', b'0', b'3', b'0', b'1', b'0', b'0'] @assert_no_logs def test_bookmarks_7(): # Reference for the next test. zoom=1 pdf = FakeHTML(string='

        a

        ').write_pdf() assert re.findall(b'/Title \\((.*)\\)', pdf) == [b'a'] dest, = re.findall(b'/Dest \\[(.*)\\]', pdf) y = round(float(dest.strip().split()[-2])) pdf = FakeHTML(string='

        a

        ').write_pdf(zoom=1.5) assert re.findall(b'/Title \\((.*)\\)', pdf) == [b'a'] dest, = re.findall(b'/Dest \\[(.*)\\]', pdf) assert round(float(dest.strip().split()[-2])) == 1.5 * y @assert_no_logs def test_bookmarks_8(): pdf = FakeHTML(string='''

        a

        b

        c

        d

        e

        f

        g

        ''').write_pdf() # a # |_ b # | |_ c # |_ d (closed) # | |_ e # | |_ f # g assert re.findall(b'/Title \\((.*)\\)', pdf) == [ b'a', b'b', b'c', b'd', b'e', b'f', b'g'] counts = re.findall(b'/Count ([0-9-]*)', pdf) counts.pop(0) # Page count outlines = counts.pop() assert outlines == b'5' assert counts == [b'3', b'1', b'0', b'-2', b'1', b'0', b'0'] @assert_no_logs def test_bookmarks_9(): pdf = FakeHTML(string='''

        a

        ''').write_pdf() counts = re.findall(b'/Count ([0-9-]*)', pdf) outlines = counts.pop() assert outlines == b'1' assert re.findall(b'/Title \\((.*)\\)', pdf) == [b'h1 on page 1'] @assert_no_logs def test_bookmarks_10(): pdf = FakeHTML(string='''
        a
        ''').write_pdf() # x # x counts = re.findall(b'/Count ([0-9-]*)', pdf) outlines = counts.pop() assert outlines == b'2' assert re.findall(b'/Title \\((.*)\\)', pdf) == [b'x', b'x'] @assert_no_logs def test_bookmarks_11(): pdf = FakeHTML(string='''
        a a a
        b
        c
        ''').write_pdf() # a # b counts = re.findall(b'/Count ([0-9-]*)', pdf) outlines = counts.pop() assert outlines == b'2' assert re.findall(b'/Title \\((.*)\\)', pdf) == [b'a', b'b'] @assert_no_logs def test_bookmarks_12(): pdf = FakeHTML(string='''
        a
        ''').write_pdf() # a counts = re.findall(b'/Count ([0-9-]*)', pdf) outlines = counts.pop() assert outlines == b'1' assert re.findall(b'/Title \\((.*)\\)', pdf) == [b'a'] @assert_no_logs def test_bookmarks_13(): pdf = FakeHTML(string='''
        a
        ''').write_pdf() # a counts = re.findall(b'/Count ([0-9-]*)', pdf) outlines = counts.pop() assert outlines == b'1' assert re.findall(b'/Title \\((.*)\\)', pdf) == [b'a'] @assert_no_logs def test_bookmarks_14(): pdf = FakeHTML(string='''

        a

        b c d

        e f

        g h i

        ''').write_pdf() assert re.findall(b'/Count ([0-9-]*)', pdf)[-1] == b'4' assert re.findall(b'/Title \\((.*)\\)', pdf) == [ b'a', b'b c d', b'e f', b'g h i'] @assert_no_logs def test_links_none(): pdf = FakeHTML(string='').write_pdf() assert b'Annots' not in pdf @assert_no_logs def test_links(): pdf = FakeHTML(string='''

        Hello, World

        a

        ''', base_url=resource_filename('')).write_pdf() uris = re.findall(b'/URI \\((.*)\\)', pdf) types = re.findall(b'/S (.*)', pdf) subtypes = re.findall(b'/Subtype (.*)', pdf) rects = [ [float(number) for number in match.split()] for match in re.findall( b'/Rect \\[ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+ [\\d\\.]+) \\]', pdf)] # 30pt wide (like the image), 20pt high (like line-height) assert uris.pop(0) == b'http://weasyprint.org' assert subtypes.pop(0) == b'/Link' assert types.pop(0) == b'/URI' assert rects.pop(0) == [0, TOP, 30, TOP - 20] # The image itself: 30*30pt assert uris.pop(0) == b'http://weasyprint.org' assert subtypes.pop(0) == b'/Link' assert types.pop(0) == b'/URI' assert rects.pop(0) == [0, TOP, 30, TOP - 30] # 32pt wide (image + 2 * 1pt of border), 20pt high assert subtypes.pop(0) == b'/Link' assert b'/Dest (lipsum)' in pdf link = re.search( b'\\(lipsum\\) \\[ \\d+ 0 R /XYZ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+) ]', pdf).group(1) assert [float(number) for number in link.split()] == [0, TOP, 0] assert rects.pop(0) == [10, TOP - 100, 10 + 32, TOP - 100 - 20] # The image itself: 32*32pt assert subtypes.pop(0) == b'/Link' assert rects.pop(0) == [10, TOP - 100, 10 + 32, TOP - 100 - 32] # 100% wide (block), 30pt high assert subtypes.pop(0) == b'/Link' assert b'/Dest (hello)' in pdf link = re.search( b'\\(hello\\) \\[ \\d+ 0 R /XYZ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+) ]', pdf).group(1) assert [float(number) for number in link.split()] == [0, TOP - 200, 0] assert rects.pop(0) == [0, TOP, RIGHT, TOP - 30] @assert_no_logs def test_sorted_links(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1352 pdf = FakeHTML(string='''

        zzz

        aaa

        z a ''', base_url=resource_filename('')).write_pdf() assert b'(zzz) [' in pdf.split(b'(aaa) [')[-1] @assert_no_logs def test_relative_links_no_height(): # 100% wide (block), 0pt high pdf = FakeHTML( string='a', base_url='http://weasyprint.org/foo/bar/').write_pdf() assert b'/S /URI\n/URI (http://weasyprint.org/foo/lipsum)' assert f'/Rect [ 0 {TOP} {RIGHT} {TOP} ]'.encode('ascii') in pdf @assert_no_logs def test_relative_links_missing_base(): # Relative URI reference without a base URI pdf = FakeHTML( string='a', base_url=None).write_pdf() assert b'/S /URI\n/URI (../lipsum)' assert f'/Rect [ 0 {TOP} {RIGHT} {TOP} ]'.encode('ascii') in pdf @assert_no_logs def test_relative_links_missing_base_link(): # Relative URI reference without a base URI: not supported for -weasy-link with capture_logs() as logs: pdf = FakeHTML( string='
        ', base_url=None).write_pdf() assert b'/Annots' not in pdf assert len(logs) == 1 assert 'WARNING: Ignored `-weasy-link: url(../lipsum)`' in logs[0] assert 'Relative URI reference without a base URI' in logs[0] @assert_no_logs def test_relative_links_internal(): # Internal URI reference without a base URI: OK pdf = FakeHTML( string='a', base_url=None).write_pdf() assert b'/Dest (lipsum)' in pdf link = re.search( b'\\(lipsum\\) \\[ \\d+ 0 R /XYZ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+) ]', pdf).group(1) assert [float(number) for number in link.split()] == [0, TOP, 0] rect = re.search( b'/Rect \\[ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+ [\\d\\.]+) \\]', pdf).group(1) assert [float(number) for number in rect.split()] == [0, TOP, RIGHT, TOP] @assert_no_logs def test_relative_links_anchors(): pdf = FakeHTML( string='
        a', base_url=None).write_pdf() assert b'/Dest (lipsum)' in pdf link = re.search( b'\\(lipsum\\) \\[ \\d+ 0 R /XYZ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+) ]', pdf).group(1) assert [float(number) for number in link.split()] == [0, TOP, 0] rect = re.search( b'/Rect \\[ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+ [\\d\\.]+) \\]', pdf).group(1) assert [float(number) for number in rect.split()] == [0, TOP, RIGHT, TOP] @assert_no_logs def test_relative_links_different_base(): pdf = FakeHTML( string='a', base_url='http://weasyprint.org/foo/bar/').write_pdf() assert b'http://weasyprint.org/test/lipsum' in pdf @assert_no_logs def test_relative_links_same_base(): pdf = FakeHTML( string='a', base_url='http://weasyprint.org/foo/bar/').write_pdf() assert b'/Dest (test)' in pdf @assert_no_logs def test_missing_links(): with capture_logs() as logs: pdf = FakeHTML(string=''' a ''', base_url=None).write_pdf() assert b'/Dest (lipsum)' in pdf assert len(logs) == 1 link = re.search( b'\\(lipsum\\) \\[ \\d+ 0 R /XYZ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+) ]', pdf).group(1) assert [float(number) for number in link.split()] == [0, TOP - 15, 0] rect = re.search( b'/Rect \\[ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+ [\\d\\.]+) \\]', pdf).group(1) assert [float(number) for number in rect.split()] == [ 0, TOP, RIGHT, TOP - 15] assert 'ERROR: No anchor #missing for internal URI reference' in logs[0] @assert_no_logs def test_embed_gif(): assert b'/Filter /DCTDecode' not in FakeHTML( base_url=resource_filename('dummy.html'), string='').write_pdf() @assert_no_logs def test_embed_jpeg(): # JPEG-encoded image, embedded in PDF: assert b'/Filter /DCTDecode' in FakeHTML( base_url=resource_filename('dummy.html'), string='').write_pdf() @assert_no_logs def test_embed_image_once(): # Image repeated multiple times, embedded once assert FakeHTML( base_url=resource_filename('dummy.html'), string='''
        ''').write_pdf().count(b'/Filter /DCTDecode') == 1 @assert_no_logs def test_embed_images_from_pages(): page1, = FakeHTML( base_url=resource_filename('dummy.html'), string='').render().pages page2, = FakeHTML( base_url=resource_filename('dummy.html'), string='').render().pages document = Document( (page1, page2), metadata=DocumentMetadata(), font_config=FontConfiguration(), url_fetcher=None, optimize_size=()).write_pdf() assert document.count(b'/Filter /DCTDecode') == 2 @assert_no_logs def test_document_info(): pdf = FakeHTML(string=''' Test document

        Another title

        ''').write_pdf() assert b'/Author (I Me & Myself)' in pdf assert b'/Title (Test document)' in pdf assert ( b'/Creator ') in pdf assert b'/Keywords (html, css, pdf)' in pdf assert b'/Subject ' in pdf assert b'/CreationDate (20110421230000Z)' in pdf assert b"/ModDate (20130721234600+01'00)" in pdf @assert_no_logs def test_embedded_files_attachments(tmpdir): absolute_tmp_file = tmpdir.join('some_file.txt').strpath adata = b'12345678' with open(absolute_tmp_file, 'wb') as afile: afile.write(adata) absolute_url = path2url(absolute_tmp_file) assert absolute_url.startswith('file://') relative_tmp_file = tmpdir.join('äöü.txt').strpath rdata = b'abcdefgh' with open(relative_tmp_file, 'wb') as rfile: rfile.write(rdata) pdf = FakeHTML( string=''' Test document

        Heading 1

        Heading 2

        '''.format(absolute_url, os.path.basename(relative_tmp_file)), base_url=tmpdir.strpath, ).write_pdf( attachments=[ Attachment('data:,oob attachment', description='Hello'), 'data:,raw URL', io.BytesIO(b'file like obj') ] ) assert ( '<{}>'.format(hashlib.md5(b'hi there').hexdigest()).encode('ascii') in pdf) assert b'/F ()' in pdf assert b'/UF (attachment.bin)' in pdf name = BOM_UTF16_BE + 'some file attachment äöü'.encode('utf-16-be') assert b'/Desc <' + name.hex().encode('ascii') + b'>' in pdf assert hashlib.md5(adata).hexdigest().encode('ascii') in pdf assert os.path.basename(absolute_tmp_file).encode('ascii') in pdf assert hashlib.md5(rdata).hexdigest().encode('ascii') in pdf name = BOM_UTF16_BE + 'some file attachment äöü'.encode('utf-16-be') assert b'/Desc <' + name.hex().encode('ascii') + b'>' in pdf assert hashlib.md5(b'oob attachment').hexdigest().encode('ascii') in pdf assert b'/Desc (Hello)' in pdf assert hashlib.md5(b'raw URL').hexdigest().encode('ascii') in pdf assert hashlib.md5(b'file like obj').hexdigest().encode('ascii') in pdf assert b'/EmbeddedFiles' in pdf assert b'/Outlines' in pdf @assert_no_logs def test_attachments_data(): pdf = FakeHTML(string=''' Test document 2 ''').write_pdf() md5 = '<{}>'.format(hashlib.md5(b'some data').hexdigest()).encode('ascii') assert md5 in pdf @assert_no_logs def test_attachments_none(): pdf = FakeHTML(string=''' Test document 3

        Heading

        ''').write_pdf() assert b'Names' not in pdf assert b'Outlines' in pdf @assert_no_logs def test_attachments_none_empty(): pdf = FakeHTML(string=''' Test document 3 ''').write_pdf() assert b'Names' not in pdf assert b'Outlines' not in pdf @assert_no_logs def test_annotations(): pdf = FakeHTML(string=''' Test document A link that lets you download an attachment ''').write_pdf() assert hashlib.md5(b'some data').hexdigest().encode('ascii') in pdf assert b'/FileAttachment' in pdf assert b'/EmbeddedFiles' not in pdf @pytest.mark.parametrize('style, media, bleed, trim', ( ('bleed: 30pt; size: 10pt', [-30, -30, 40, 40], [-10, -10, 20, 20], [0, 0, 10, 10]), ('bleed: 15pt 3pt 6pt 18pt; size: 12pt 15pt', [-18, -15, 15, 21], [-10, -10, 15, 21], [0, 0, 12, 15]), )) @assert_no_logs def test_bleed(style, media, bleed, trim): pdf = FakeHTML(string=''' Test document test ''' % style).write_pdf() assert '/MediaBox [ {} {} {} {} ]'.format(*media).encode('ascii') in pdf assert '/BleedBox [ {} {} {} {} ]'.format(*bleed).encode('ascii') in pdf assert '/TrimBox [ {} {} {} {} ]'.format(*trim).encode('ascii') in pdf ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.027308 weasyprint-54.1/tests/test_presentational_hints.py0000644000000000000000000002255600000000000021021 0ustar0000000000000000""" weasyprint.tests.test_presentational_hints ------------------------------------------ Test the HTML presentational hints. """ from weasyprint import CSS, HTML from .testing_utils import BASE_URL, assert_no_logs PH_TESTING_CSS = CSS(string=''' @page {margin: 0; size: 1000px 1000px} body {margin: 0} ''') @assert_no_logs def test_no_ph(): # Test both CSS and non-CSS rules document = HTML(string='''
        0
        ''').render(stylesheets=[PH_TESTING_CSS]) page, = document.pages html, = page._page_box.children body, = html.children hr, table = body.children assert hr.border_height() != 100 assert table.position_x == 0 @assert_no_logs def test_ph_page(): document = HTML(string=''' ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True) page, = document.pages html, = page._page_box.children body, = html.children assert body.margin_top == 2 assert body.margin_bottom == 2 assert body.margin_left == 5 assert body.margin_right == 0 assert body.style['background_color'] == (1, 0, 0, 1) assert body.style['color'] == (0, 0, 1, 1) @assert_no_logs def test_ph_flow(): document = HTML(string='''
        
              
        ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True) page, = document.pages html, = page._page_box.children body, = html.children pre, center, div1, div2, div3, div4, div5 = body.children assert pre.style['white_space'] == 'pre-wrap' assert center.style['text_align_all'] == 'center' assert div1.style['text_align_all'] == 'center' assert div2.style['text_align_all'] == 'center' assert div3.style['text_align_all'] == 'left' assert div4.style['text_align_all'] == 'right' assert div5.style['text_align_all'] == 'justify' @assert_no_logs def test_ph_phrasing(): document = HTML(string='''



        ''', base_url=BASE_URL).render( stylesheets=[PH_TESTING_CSS], presentational_hints=True) page, = document.pages html, = page._page_box.children body, = html.children line1, line2, line3, line4, line5 = body.children br1, = line1.children br2, = line2.children br3, = line3.children br4, = line4.children font1, font2, font3, font4 = line5.children assert br1.style['clear'] == 'left' assert br2.style['clear'] == 'right' assert br3.style['clear'] == 'both' assert br4.style['clear'] == 'both' assert font1.style['color'] == (1, 0, 0, 1) assert font1.style['font_family'] == ('weasyprint',) assert font1.style['font_size'] == 1.5 * 2 * 16 assert font2.style['font_size'] == 6 / 5 * 16 assert font3.style['font_size'] == 1.5 * 2 * 16 assert font4.style['font_size'] == 8 / 9 * 16 @assert_no_logs def test_ph_lists(): document = HTML(string='''
        ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True) page, = document.pages html, = page._page_box.children body, = html.children ol, ul = body.children oli1, oli2, oli3, oli4, oli5 = ol.children uli1, uli2, uli3 = ul.children assert oli1.style['list_style_type'] == 'upper-alpha' assert oli2.style['list_style_type'] == 'decimal' assert oli3.style['list_style_type'] == 'lower-alpha' assert oli4.style['list_style_type'] == 'lower-roman' assert oli5.style['list_style_type'] == 'upper-roman' assert uli1.style['list_style_type'] == 'circle' assert uli2.style['list_style_type'] == 'disc' assert uli3.style['list_style_type'] == 'square' @assert_no_logs def test_ph_lists_types(): document = HTML(string='''
                        ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True) page, = document.pages html, = page._page_box.children body, = html.children ol1, ol2, ol3, ol4, ol5, ul1, ul2, ul3 = body.children assert ol1.style['list_style_type'] == 'upper-alpha' assert ol2.style['list_style_type'] == 'decimal' assert ol3.style['list_style_type'] == 'lower-alpha' assert ol4.style['list_style_type'] == 'lower-roman' assert ol5.style['list_style_type'] == 'upper-roman' assert ul1.style['list_style_type'] == 'circle' assert ul2.style['list_style_type'] == 'disc' assert ul3.style['list_style_type'] == 'square' @assert_no_logs def test_ph_tables(): document = HTML(string='''

                        ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True) page, = document.pages html, = page._page_box.children body, = html.children wrapper1, wrapper2, wrapper3, wrapper4, = body.children assert wrapper1.style['float'] == 'left' assert wrapper2.style['float'] == 'right' assert wrapper3.style['margin_left'] == 'auto' assert wrapper3.style['margin_right'] == 'auto' assert wrapper1.children[0].style['border_left_style'] == 'hidden' assert wrapper1.style['border_collapse'] == 'collapse' assert wrapper2.children[0].style['border_left_style'] == 'hidden' assert wrapper2.style['border_collapse'] == 'collapse' assert wrapper3.children[0].style['border_left_style'] == 'hidden' assert wrapper3.style['border_collapse'] == 'collapse' table4, = wrapper4.children assert table4.style['border_top_style'] == 'outset' assert table4.style['border_top_width'] == 10 assert table4.style['border_spacing'] == (3, 3) r, g, b, a = table4.style['border_left_color'] assert g > r and g > b head_group, rows_group, foot_group = table4.children head, = head_group.children th, = head.children assert th.style['vertical_align'] == 'top' line1, line2 = rows_group.children td, = line1.children assert td.style['white_space'] == 'nowrap' assert td.style['border_top_width'] == 1 assert td.style['border_top_style'] == 'inset' h1, p = td.children assert h1.style['text_align_all'] == 'right' assert p.style['text_align_all'] == 'center' foot, = foot_group.children tr, = foot.children assert tr.style['text_align_all'] == 'justify' @assert_no_logs def test_ph_hr(): document = HTML(string='''




                        ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True) page, = document.pages html, = page._page_box.children body, = html.children hr1, hr2, hr3, hr4, hr5 = body.children assert hr1.margin_left == 0 assert hr1.style['margin_right'] == 'auto' assert hr2.style['margin_left'] == 'auto' assert hr2.margin_right == 0 assert hr3.style['margin_left'] == 'auto' assert hr3.style['margin_right'] == 'auto' assert hr3.style['color'] == (1, 0, 0, 1) assert hr4.style['margin_left'] == 'auto' assert hr4.style['margin_right'] == 'auto' assert hr4.border_height() == 10 assert hr4.style['border_top_width'] == 5 assert hr5.border_height() == 8 assert hr5.height == 6 assert hr5.width == 100 assert hr5.style['border_top_width'] == 1 @assert_no_logs def test_ph_embedded(): document = HTML(string=''' text ''').render(stylesheets=[PH_TESTING_CSS], presentational_hints=True) page, = document.pages html, = page._page_box.children body, = html.children line, = body.children object_, text1, img, embed, text2 = line.children assert embed.style['vertical_align'] == 'text-top' assert object_.style['vertical_align'] == 'top' assert object_.margin_top == 20 assert object_.margin_left == 10 assert img.style['float'] == 'right' assert img.width == 10 assert img.height == 20 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.027308 weasyprint-54.1/tests/test_stacking.py0000644000000000000000000000454000000000000016360 0ustar0000000000000000""" weasyprint.tests.stacking ------------------------- Test CSS stacking contexts. """ import pytest from weasyprint.stacking import StackingContext from .draw import assert_pixels from .testing_utils import assert_no_logs, render_pages, serialize z_index_source = '''
                        ''' def serialize_stacking(context): return ( context.box.element_tag, [b.element_tag for b in context.blocks_and_cells], [serialize_stacking(c) for c in context.zero_z_contexts]) @assert_no_logs @pytest.mark.parametrize('source, contexts', ( ('''

                        ''', ('html', ['body', 'p'], [('div', ['p'], [])])), ('''

                        ''', ('html', ['body'], [('div', [], []), ('p', [], [])])), )) def test_nested(source, contexts): page, = render_pages(source) html, = page.children assert serialize_stacking(StackingContext.from_box(html, page)) == contexts @assert_no_logs def test_image_contexts(): page, = render_pages(''' Some text: ''') html, = page.children context = StackingContext.from_box(html, page) # The image is *not* in this context: assert serialize([context.box]) == [ ('html', 'Block', [ ('body', 'Block', [ ('body', 'Line', [ ('body', 'Text', 'Some text: ')])])])] # ... but in a sub-context: assert serialize(c.box for c in context.zero_z_contexts) == [ ('img', 'InlineReplaced', '')] @assert_no_logs @pytest.mark.parametrize('z_indexes, color', ( ((3, 2, 1), 'R'), ((1, 2, 3), 'G'), ((1, 2, -3), 'B'), ((1, 2, 'auto'), 'B'), ((-1, 'auto', -2), 'B'), )) def test_z_index(z_indexes, color): assert_pixels( 'z_index_%s_%s_%s' % z_indexes, 10, 10, '\n'.join([color * 10] * 10), z_index_source % z_indexes) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.027308 weasyprint-54.1/tests/test_target.py0000644000000000000000000001302700000000000016043 0ustar0000000000000000""" weasyprint.tests.test_target ---------------------------- Test the CSS cross references using target-*() functions. """ from .testing_utils import FakeHTML, assert_no_logs @assert_no_logs def test_target_counter(): document = FakeHTML(string='''
                        ''') page, = document.render().pages html, = page._page_box.children body, = html.children div1, div2, div3, div4 = body.children before = div1.children[0].children[0].children[0] assert before.text == '4' before = div2.children[0].children[0].children[0] assert before.text == 'test 1' before = div3.children[0].children[0].children[0] assert before.text == 'iv' before = div4.children[0].children[0].children[0] assert before.text == '3' @assert_no_logs def test_target_counter_attr(): document = FakeHTML(string='''
                        ''') page, = document.render().pages html, = page._page_box.children body, = html.children div1, div2, div3, div4 = body.children before = div1.children[0].children[0].children[0] assert before.text == '4' before = div2.children[0].children[0].children[0] assert before.text == '1' before = div3.children[0].children[0].children[0] assert before.text == '2' before = div4.children[0].children[0].children[0] assert before.text == 'c' @assert_no_logs def test_target_counters(): document = FakeHTML(string='''
                        ''') page, = document.render().pages html, = page._page_box.children body, = html.children div1, div2, div3, div4 = body.children before = div1.children[1].children[0].children[0].children[0] assert before.text == '4.2' before = div2.children[0].children[0].children[0].children[0] assert before.text == '3' before = div3.children[0].children[0].children[0] assert before.text == 'b.a' before = div4.children[1].children[0].children[0].children[0] assert before.text == '12' @assert_no_logs def test_target_text(): document = FakeHTML(string='''
                        1 Chapter 1
                        2 Chapter 2
                        3 Chapter 3
                        4 Chapter 4
                        ''') page, = document.render().pages html, = page._page_box.children body, = html.children a1, div1, a2, div2, div3, a3, div4, a4 = body.children before = a1.children[0].children[0].children[0] assert before.text == 'test 4 Chapter 4' before = a2.children[0].children[0].children[0] assert before.text == 'wow' assert len(a3.children[0].children[0].children) == 0 before = a4.children[0].children[0].children[0] assert before.text == '1' @assert_no_logs def test_target_float(): document = FakeHTML(string='''

                        abc

                        ''') page, = document.render().pages html, = page._page_box.children body, = html.children div, h1 = body.children line, = div.children inline, = line.children text_box, after = inline.children assert text_box.text == 'link' assert after.children[0].children[0].text == '1' ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1643655079.360553 weasyprint-54.1/tests/test_text.py0000644000000000000000000011546000000000000015545 0ustar0000000000000000""" weasyprint.tests.test_text -------------------------- Test the text layout. """ import pytest from weasyprint.css.properties import INITIAL_VALUES from weasyprint.text.line_break import split_first_line from .testing_utils import MONO_FONTS, SANS_FONTS, assert_no_logs, render_pages def make_text(text, width=None, **style): """Wrapper for split_first_line() creating a style dict.""" new_style = dict(INITIAL_VALUES) new_style['font_family'] = MONO_FONTS.split(',') new_style.update(style) return split_first_line( text, new_style, context=None, max_width=width, justification_spacing=0) @assert_no_logs def test_line_content(): for width, remaining in [(100, 'text for test'), (45, 'is a text for test')]: text = 'This is a text for test' _, length, resume_index, _, _, _ = make_text( text, width, font_family=SANS_FONTS.split(','), font_size=19) assert text[resume_index:] == remaining assert length + 1 == resume_index # +1 for the removed trailing space @assert_no_logs def test_line_with_any_width(): _, _, _, width_1, _, _ = make_text('some text') _, _, _, width_2, _, _ = make_text('some text some text') assert width_1 < width_2 @assert_no_logs def test_line_breaking(): string = 'Thïs is a text for test' # These two tests do not really rely on installed fonts _, _, resume_index, _, _, _ = make_text(string, 90, font_size=1) assert resume_index is None _, _, resume_index, _, _, _ = make_text(string, 90, font_size=100) assert string.encode('utf-8')[resume_index:].decode('utf-8') == ( 'is a text for test') _, _, resume_index, _, _, _ = make_text( string, 100, font_family=SANS_FONTS.split(','), font_size=19) assert string.encode('utf-8')[resume_index:].decode('utf-8') == ( 'text for test') @assert_no_logs def test_line_breaking_rtl(): string = 'لوريم ايبسوم دولا' # These two tests do not really rely on installed fonts _, _, resume_index, _, _, _ = make_text(string, 90, font_size=1) assert resume_index is None _, _, resume_index, _, _, _ = make_text(string, 90, font_size=100) assert string.encode('utf-8')[resume_index:].decode('utf-8') == ( 'ايبسوم دولا') @assert_no_logs def test_text_dimension(): string = 'This is a text for test. This is a test for text.py' _, _, _, width_1, height_1, _ = make_text(string, 200, font_size=12) _, _, _, width_2, height_2, _ = make_text(string, 200, font_size=20) assert width_1 * height_1 < width_2 * height_2 @assert_no_logs def test_text_font_size_zero(): page, = render_pages('''

                        test font size zero

                        ''') html, = page.children body, = html.children paragraph, = body.children line, = paragraph.children # zero-sized text boxes are removed assert not line.children assert line.height == 0 assert paragraph.height == 0 @assert_no_logs def test_text_font_size_very_small(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1499 page, = render_pages('''

                        test font size zero

                        ''') html, = page.children body, = html.children paragraph, = body.children line, = paragraph.children assert line.height < 0.001 assert paragraph.height < 0.001 @assert_no_logs def test_text_spaced_inlines(): page, = render_pages('''

                        start bi1 bi2 b1 end

                        ''') html, = page.children body, = html.children paragraph, = body.children line, = paragraph.children start, i, space, b, end = line.children assert start.text == 'start ' assert space.text == ' ' assert space.width > 0 assert end.text == ' end' bi1, space, bi2 = i.children bi1, = bi1.children bi2, = bi2.children assert bi1.text == 'bi1' assert space.text == ' ' assert space.width > 0 assert bi2.text == 'bi2' b1, = b.children assert b1.text == 'b1' @assert_no_logs def test_text_align_left(): # <--------------------> page, body # +-----+ # +---+ | # | | | # +---+-----+ # ^ ^ ^ ^ # x=0 x=40 x=100 x=200 page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children img_1, img_2 = line.children # initial value for text-align: left (in ltr text) assert img_1.position_x == 0 assert img_2.position_x == 40 @assert_no_logs def test_text_align_right(): # <--------------------> page, body # +-----+ # +---+ | # | | | # +---+-----+ # ^ ^ ^ ^ # x=0 x=100 x=200 # x=140 page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children img_1, img_2 = line.children assert img_1.position_x == 100 # 200 - 60 - 40 assert img_2.position_x == 140 # 200 - 60 @assert_no_logs def test_text_align_center(): # <--------------------> page, body # +-----+ # +---+ | # | | | # +---+-----+ # ^ ^ ^ ^ # x= x=50 x=150 # x=90 page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children img_1, img_2 = line.children assert img_1.position_x == 50 assert img_2.position_x == 90 @assert_no_logs def test_text_align_justify(): page, = render_pages('''

                        ''') html, = page.children body, = html.children paragraph, = body.children line_1, line_2 = paragraph.children image_1, space_1, strong = line_1.children image_2, space_2, image_3, space_3, image_4 = strong.children image_5, = line_2.children assert space_1.text == ' ' assert space_2.text == ' ' assert space_3.text == ' ' assert image_1.position_x == 0 assert space_1.position_x == 40 assert strong.position_x == 70 assert image_2.position_x == 70 assert space_2.position_x == 130 assert image_3.position_x == 160 assert space_3.position_x == 170 assert image_4.position_x == 200 assert strong.width == 230 assert image_5.position_x == 0 @assert_no_logs def test_text_align_justify_all(): page, = render_pages('''

                        ''') html, = page.children body, = html.children paragraph, = body.children line_1, line_2 = paragraph.children image_1, space_1, strong = line_1.children image_2, space_2, image_3, space_3, image_4 = strong.children image_5, space_4, image_6 = line_2.children assert space_1.text == ' ' assert space_2.text == ' ' assert space_3.text == ' ' assert space_4.text == ' ' assert image_1.position_x == 0 assert space_1.position_x == 40 assert strong.position_x == 70 assert image_2.position_x == 70 assert space_2.position_x == 130 assert image_3.position_x == 160 assert space_3.position_x == 170 assert image_4.position_x == 200 assert strong.width == 230 assert image_5.position_x == 0 assert space_4.position_x == 200 assert image_6.position_x == 290 @assert_no_logs def test_text_align_all_last(): page, = render_pages('''

                        ''') html, = page.children body, = html.children paragraph, = body.children line_1, line_2 = paragraph.children image_1, space_1, strong = line_1.children image_2, space_2, image_3, space_3, image_4 = strong.children image_5, image_6 = line_2.children assert space_1.text == ' ' assert space_2.text == ' ' assert space_3.text == ' ' assert image_1.position_x == 0 assert space_1.position_x == 40 assert strong.position_x == 70 assert image_2.position_x == 70 assert space_2.position_x == 130 assert image_3.position_x == 160 assert space_3.position_x == 170 assert image_4.position_x == 200 assert strong.width == 230 assert image_5.position_x == 90 assert image_6.position_x == 290 @assert_no_logs def test_text_align_not_enough_space(): page, = render_pages('''

                        aaaaaaaaaaaaaaaaaaaaaaaaaa

                        ''') html, = page.children body, = html.children paragraph, = body.children span, = paragraph.children assert span.position_x == 0 @assert_no_logs def test_text_align_justify_no_space(): # single-word line (zero spaces) page, = render_pages('''

                        Supercalifragilisticexpialidocious bar

                        ''') html, = page.children body, = html.children paragraph, = body.children line_1, line_2 = paragraph.children text, = line_1.children assert text.position_x == 0 @assert_no_logs def test_text_align_justify_text_indent(): # text-indent page, = render_pages('''

                        ''') html, = page.children body, = html.children paragraph, = body.children line_1, line_2 = paragraph.children image_1, space_1, strong = line_1.children image_2, space_2, image_3, space_3, image_4 = strong.children image_5, = line_2.children assert space_1.text == ' ' assert space_2.text == ' ' assert space_3.text == ' ' assert image_1.position_x == 3 assert space_1.position_x == 43 assert strong.position_x == 72 assert image_2.position_x == 72 assert space_2.position_x == 132 assert image_3.position_x == 161 assert space_3.position_x == 171 assert image_4.position_x == 200 assert strong.width == 228 assert image_5.position_x == 0 @assert_no_logs def test_text_align_justify_no_break_between_children(): # Test justification when line break happens between two inline children # that must stay together. # Test regression: https://github.com/Kozea/WeasyPrint/issues/637 page, = render_pages('''

                        a b bla, b

                        ''') html, = page.children body, = html.children paragraph, = body.children line_1, line_2 = paragraph.children span_1, space_1, span_2, space_2 = line_1.children assert span_1.position_x == 0 assert span_2.position_x == 6 * 16 # 1 character + 5 spaces assert line_1.width == 7 * 16 # 7em span_1, span_2, space_1, span_3, space_2 = line_2.children assert span_1.position_x == 0 assert span_2.position_x == 3 * 16 # 3 characters assert span_3.position_x == 5 * 16 # (3 + 1) characters + 1 space @assert_no_logs def test_word_spacing(): # keep the empty Lorem ipsum dolorsit amet''') html, = page.children body, = html.children line, = body.children strong_1, = line.children # TODO: Pango gives only half of word-spacing to a space at the end # of a TextBox. Is this what we want? page, = render_pages(''' Lorem ipsum dolorsit amet''') html, = page.children body, = html.children line, = body.children strong_2, = line.children assert strong_2.width - strong_1.width == 33 @assert_no_logs def test_letter_spacing_1(): page, = render_pages(''' Supercalifragilisticexpialidocious''') html, = page.children body, = html.children line, = body.children strong_1, = line.children page, = render_pages(''' Supercalifragilisticexpialidocious''') html, = page.children body, = html.children line, = body.children strong_2, = line.children assert strong_2.width - strong_1.width == 34 * 11 # an embedded tag should not affect the single-line letter spacing page, = render_pages( '' 'Supercalifragilisticexpialidocious' '') html, = page.children body, = html.children line, = body.children strong_3, = line.children assert strong_3.width == strong_2.width # duplicate wrapped lines should also have same overall width # Note work-around for word-wrap bug (issue #163) by marking word # as an inline-block page, = render_pages( '' '' ' Supercalifragilisticexpialidocious ' ' Supercalifragilisticexpialidocious' '') html, = page.children body, = html.children line1, line2 = body.children assert line1.children[0].width == line2.children[0].width assert line1.children[0].width == strong_2.width @pytest.mark.parametrize('spacing', ('word-spacing', 'letter-spacing')) @assert_no_logs def test_spacing_ex(spacing): # Test regression on ex units in spacing properties render_pages(f'
                        abc def') @pytest.mark.parametrize('indent', ('12px', '6%')) @assert_no_logs def test_text_indent(indent): page, = render_pages('''

                        Some text that is long enough that it take at least three line, but maybe more. ''' % {'indent': indent}) html, = page.children body, = html.children paragraph, = body.children lines = paragraph.children text_1, = lines[0].children text_2, = lines[1].children text_3, = lines[2].children assert text_1.position_x == 22 # 10px margin-left + 12px indent assert text_2.position_x == 10 # No indent assert text_3.position_x == 10 # No indent @assert_no_logs def test_text_indent_inline(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1000 page, = render_pages('''

                        text ''') html, = page.children body, = html.children paragraph, = body.children line, = paragraph.children assert line.width == (4 + 1) * 16 @pytest.mark.parametrize('indent', ('12px', '6%')) @assert_no_logs def test_text_indent_multipage(indent): # Test regression: https://github.com/Kozea/WeasyPrint/issues/706 pages = render_pages('''

                        Some text that is long enough that it take at least three line, but maybe more. ''' % {'indent': indent}) page = pages.pop(0) html, = page.children body, = html.children paragraph, = body.children line, = paragraph.children text, = line.children assert text.position_x == 22 # 10px margin-left + 12px indent page = pages.pop(0) html, = page.children body, = html.children paragraph, = body.children line, = paragraph.children text, = line.children assert text.position_x == 10 # No indent @assert_no_logs def test_hyphenate_character_1(): page, = render_pages( '' '' '' 'hyphénation') html, = page.children body, = html.children lines = body.children assert len(lines) > 1 assert lines[0].children[0].text.endswith('!') full_text = ''.join(line.children[0].text for line in lines) assert full_text.replace('!', '') == 'hyphénation' @assert_no_logs def test_hyphenate_character_2(): page, = render_pages( '' '' '' 'hyphénation') html, = page.children body, = html.children lines = body.children assert len(lines) > 1 assert lines[0].children[0].text.endswith('à') full_text = ''.join(line.children[0].text for line in lines) assert full_text.replace('à', '') == 'hyphénation' @assert_no_logs def test_hyphenate_character_3(): page, = render_pages( '' '' '' 'hyphénation') html, = page.children body, = html.children lines = body.children assert len(lines) > 1 assert lines[0].children[0].text.endswith('ù ù') full_text = ''.join(line.children[0].text for line in lines) assert full_text.replace(' ', '').replace('ù', '') == 'hyphénation' @assert_no_logs def test_hyphenate_character_4(): page, = render_pages( '' '' '' 'hyphénation') html, = page.children body, = html.children lines = body.children assert len(lines) > 1 full_text = ''.join(line.children[0].text for line in lines) assert full_text == 'hyphénation' @assert_no_logs def test_hyphenate_character_5(): page, = render_pages( '' '' '' 'hyphénation') html, = page.children body, = html.children lines = body.children assert len(lines) > 1 assert lines[0].children[0].text.endswith('———') full_text = ''.join(line.children[0].text for line in lines) assert full_text.replace('—', '') == 'hyphénation' @assert_no_logs def test_hyphenate_manual_1(): for i in range(1, len('hyphénation')): for hyphenate_character in ('!', 'ù ù'): word = 'hyphénation'[:i] + '\u00ad' + 'hyphénation'[i:] page, = render_pages( '' '' '{word}') html, = page.children body, = html.children lines = body.children assert len(lines) == 2 assert lines[0].children[0].text.endswith(hyphenate_character) full_text = ''.join( child.text for line in lines for child in line.children) assert full_text.replace(hyphenate_character, '') == word @assert_no_logs def test_hyphenate_manual_2(): for i in range(1, len('hy phénation')): for hyphenate_character in ('!', 'ù ù'): word = 'hy phénation'[:i] + '\u00ad' + 'hy phénation'[i:] page, = render_pages( '' '' '{word}') html, = page.children body, = html.children lines = body.children assert len(lines) in (2, 3) full_text = ''.join( child.text for line in lines for child in line.children) full_text = full_text.replace(hyphenate_character, '') if lines[0].children[0].text.endswith(hyphenate_character): assert full_text == word else: assert lines[0].children[0].text.endswith('y') if len(lines) == 3: assert lines[1].children[0].text.endswith( hyphenate_character) @assert_no_logs def test_hyphenate_manual_3(): # Automatic hyphenation opportunities within a word must be ignored if the # word contains a conditional hyphen, in favor of the conditional # hyphen(s). page, = render_pages( '' 'in­lighten­lighten­in') html, = page.children body, = html.children line_1, line_2, line_3, line_4 = body.children assert line_1.children[0].text == 'in\xad‐' assert line_2.children[0].text == 'lighten\xad‐' assert line_3.children[0].text == 'lighten\xad‐' assert line_4.children[0].text == 'in' @assert_no_logs def test_hyphenate_limit_zone_1(): page, = render_pages( '' '' '' 'mmmmm hyphénation') html, = page.children body, = html.children lines = body.children assert len(lines) == 2 assert lines[0].children[0].text.endswith('‐') full_text = ''.join(line.children[0].text for line in lines) assert full_text.replace('‐', '') == 'mmmmm hyphénation' @assert_no_logs def test_hyphenate_limit_zone_2(): page, = render_pages( '' '' '' 'mmmmm hyphénation') html, = page.children body, = html.children lines = body.children assert len(lines) > 1 assert lines[0].children[0].text.endswith('mm') full_text = ''.join(line.children[0].text for line in lines) assert full_text == 'mmmmmhyphénation' @assert_no_logs def test_hyphenate_limit_zone_3(): page, = render_pages( '' '' '' 'mmmmm hyphénation') html, = page.children body, = html.children lines = body.children assert len(lines) == 2 assert lines[0].children[0].text.endswith('‐') full_text = ''.join(line.children[0].text for line in lines) assert full_text.replace('‐', '') == 'mmmmm hyphénation' @assert_no_logs def test_hyphenate_limit_zone_4(): page, = render_pages( '' '' '' 'mmmmm hyphénation') html, = page.children body, = html.children lines = body.children assert len(lines) > 1 assert lines[0].children[0].text.endswith('mm') full_text = ''.join(line.children[0].text for line in lines) assert full_text == 'mmmmmhyphénation' @assert_no_logs @pytest.mark.parametrize('css, result', ( ('auto', 2), ('auto auto 0', 2), ('0 0 0', 2), ('4 4 auto', 1), ('6 2 4', 2), ('auto 1 auto', 2), ('7 auto auto', 1), ('6 auto auto', 2), ('5 2', 2), ('3', 2), ('2 4 6', 1), ('auto 4', 1), ('auto 2', 2), )) def test_hyphenate_limit_chars(css, result): page, = render_pages( '' '' '' 'hyphen') html, = page.children body, = html.children lines = body.children assert len(lines) == result @assert_no_logs @pytest.mark.parametrize('css', ( # light·en '3 3 3', # 'en' is shorter than 3 '3 6 2', # 'light' is shorter than 6 '8', # 'lighten' is shorter than 8 )) def test_hyphenate_limit_chars_punctuation(css): # See https://github.com/Kozea/WeasyPrint/issues/109 page, = render_pages( '' '' '' '..lighten..') html, = page.children body, = html.children lines = body.children assert len(lines) == 1 @assert_no_logs @pytest.mark.parametrize('wrap, text, test, full_text', ( ('anywhere', 'aaaaaaaa', lambda a: a > 1, 'aaaaaaaa'), ('break-word', 'aaaaaaaa', lambda a: a > 1, 'aaaaaaaa'), ('normal', 'aaaaaaaa', lambda a: a == 1, 'aaaaaaaa'), ('break-word', 'hyphenations', lambda a: a > 3, 'hy\u2010phen\u2010ations'), ('break-word', "A splitted word. An hyphenated word.", lambda a: a > 8, "Asplittedword.Anhy\u2010phen\u2010atedword."), )) def test_overflow_wrap(wrap, text, test, full_text): page, = render_pages(''' %s ''' % (wrap, text)) html, = page.children body, = html.children lines = [] for line in body.children: box, = line.children textBox, = box.children lines.append(textBox.text) lines_full_text = ''.join(line for line in lines) assert test(len(lines)) assert full_text == lines_full_text @assert_no_logs @pytest.mark.parametrize('wrap, text, body_width, expected_width', ( ('anywhere', 'aaaaaa', 10, 20), ('anywhere', 'aaaaaa', 40, 40), ('break-word', 'aaaaaa', 40, 120), ('normal', 'aaaaaa', 40, 120), )) def test_overflow_wrap_2(wrap, text, body_width, expected_width): page, = render_pages('''
                        %s''' % (body_width, wrap, text)) html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children tr, = row_group.children td, = tr.children assert td.width == expected_width @assert_no_logs @pytest.mark.parametrize('wrap, text, body_width, expected_width', ( # TODO: broken with Pango 1.50, # see https://gitlab.gnome.org/GNOME/pango/-/issues/646 # ('anywhere', 'aaaaaa', 10, 20), ('anywhere', 'aaaaaa', 40, 40), ('break-word', 'aaaaaa', 40, 120), ('normal', 'abcdef', 40, 120), )) def test_overflow_wrap_trailing_space(wrap, text, body_width, expected_width): page, = render_pages('''
                        %s ''' % (body_width, wrap, text)) html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children tr, = row_group.children td, = tr.children assert td.width == expected_width def white_space_lines(width, space): page, = render_pages(''' This + \n is text''' % (width, space)) html, = page.children body, = html.children return body.children @assert_no_logs def test_white_space_1(): line1, line2, line3, line4 = white_space_lines(1, 'normal') box1, = line1.children text1, = box1.children assert text1.text == 'This' box2, = line2.children text2, = box2.children assert text2.text == '+' box3, = line3.children text3, = box3.children assert text3.text == 'is' box4, = line4.children text4, = box4.children assert text4.text == 'text' @assert_no_logs def test_white_space_2(): line1, line2 = white_space_lines(1, 'pre') box1, = line1.children text1, = box1.children assert text1.text == 'This + ' box2, = line2.children text2, = box2.children assert text2.text == ' is text' @assert_no_logs def test_white_space_3(): line1, = white_space_lines(1, 'nowrap') box1, = line1.children text1, = box1.children assert text1.text == 'This + is text' @assert_no_logs def test_white_space_4(): line1, line2, line3, line4, line5 = white_space_lines(1, 'pre-wrap') box1, = line1.children text1, = box1.children assert text1.text == 'This ' box2, = line2.children text2, = box2.children assert text2.text == '+ ' box3, = line3.children text3, = box3.children assert text3.text == ' ' box4, = line4.children text4, = box4.children assert text4.text == 'is ' box5, = line5.children text5, = box5.children assert text5.text == 'text' @assert_no_logs def test_white_space_5(): line1, line2, line3, line4 = white_space_lines(1, 'pre-line') box1, = line1.children text1, = box1.children assert text1.text == 'This' box2, = line2.children text2, = box2.children assert text2.text == '+' box3, = line3.children text3, = box3.children assert text3.text == 'is' box4, = line4.children text4, = box4.children assert text4.text == 'text' @assert_no_logs def test_white_space_6(): line1, = white_space_lines(1000000, 'normal') box1, = line1.children text1, = box1.children assert text1.text == 'This + is text' @assert_no_logs def test_white_space_7(): line1, line2 = white_space_lines(1000000, 'pre') box1, = line1.children text1, = box1.children assert text1.text == 'This + ' box2, = line2.children text2, = box2.children assert text2.text == ' is text' @assert_no_logs def test_white_space_8(): line1, = white_space_lines(1000000, 'nowrap') box1, = line1.children text1, = box1.children assert text1.text == 'This + is text' @assert_no_logs def test_white_space_9(): line1, line2 = white_space_lines(1000000, 'pre-wrap') box1, = line1.children text1, = box1.children assert text1.text == 'This + ' box2, = line2.children text2, = box2.children assert text2.text == ' is text' @assert_no_logs def test_white_space_10(): line1, line2 = white_space_lines(1000000, 'pre-line') box1, = line1.children text1, = box1.children assert text1.text == 'This +' box2, = line2.children text2, = box2.children assert text2.text == 'is text' @assert_no_logs def test_white_space_11(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/813 page, = render_pages('''
                        This
                        is text''') html, = page.children body, = html.children pre, = body.children line1, line2 = pre.children text1, box = line1.children assert text1.text == 'This' assert box.element_tag == 'br' text2, = line2.children assert text2.text == 'is text' @assert_no_logs def test_white_space_12(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/813 page, = render_pages('''
                        This is lol text''')
                            html, = page.children
                            body, = html.children
                            pre, = body.children
                            line1, = pre.children
                            text1, span, text2 = line1.children
                            assert text1.text == 'This is '
                            assert span.element_tag == 'span'
                            assert text2.text == ' text'
                        
                        
                        @assert_no_logs
                        @pytest.mark.parametrize('value, width', (
                            (8, 144),  # (2 + (8 - 1)) * 16
                            (4, 80),  # (2 + (4 - 1)) * 16
                            ('3em', 64),  # (2 + (3 - 1)) * 16
                            ('25px', 41),  # 2 * 16 + 25 - 1 * 16
                            # (0, 32),  # See Layout.set_tabs
                        ))
                        def test_tab_size(value, width):
                            page, = render_pages('''
                              
                              
                        a	a
                        ''' % value) html, = page.children body, = html.children paragraph, = body.children line, = paragraph.children assert line.width == width @assert_no_logs def test_text_transform(): page, = render_pages('''

                        hé lO1

                        hé lO1

                        hé lO1

                        hé lO1

                        hé lO1

                        ''') html, = page.children body, = html.children p1, p2, p3, p4, p5 = body.children line1, = p1.children text1, = line1.children assert text1.text == 'Hé Lo1' line2, = p2.children text2, = line2.children assert text2.text == 'HÉ LO1' line3, = p3.children text3, = line3.children assert text3.text == 'hé lo1' line4, = p4.children text4, = line4.children assert text4.text == '\uff48é\u3000\uff4c\uff2f\uff11' line5, = p5.children text5, = line5.children assert text5.text == 'hé lO1' @assert_no_logs def test_text_floating_pre_line(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/610 page, = render_pages('''
                        This is oh this end
                        ''') @assert_no_logs @pytest.mark.parametrize( 'leader, content', ( ('dotted', '.'), ('solid', '_'), ('space', ' '), ('" .-"', ' .-'), ) ) def test_leader_content(leader, content): page, = render_pages('''
                        ''' % leader) html, = page.children body, = html.children div, = body.children line, = div.children after, = line.children inline, = after.children assert inline.children[0].text == content @pytest.mark.xfail @assert_no_logs def test_max_lines(): page, = render_pages('''

                        abcd efgh ijkl

                        ''') html, = page.children body, = html.children p1, p2 = body.children line1, line2 = p1.children line3, = p2.children text1, = line1.children text2, = line2.children text3, = line3.children assert text1.text == 'abcd' assert text2.text == 'efgh' assert text3.text == 'ijkl' @assert_no_logs def test_continue(): page, = render_pages('''
                        abcd efgh ijkl
                        ''') html, = page.children body, = html.children p, = body.children line1, line2 = p.children text1, = line1.children text2, = line2.children assert text1.text == 'abcd' assert text2.text == 'efgh' ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.028308 weasyprint-54.1/tests/test_unicode.py0000644000000000000000000000326500000000000016206 0ustar0000000000000000""" weasyprint.tests.test_unicode ----------------------------- Test various unicode texts and filenames. """ import os.path import shutil import tempfile from weasyprint.urls import ensure_url from .draw import assert_pixels_equal, document_to_pixels, html_to_pixels from .testing_utils import FakeHTML, assert_no_logs, resource_filename @assert_no_logs def test_unicode(): text = 'I løvë Unicode' style = ''' @page { background: #fff; size: 200px 50px; } p { color: blue } ''' expected_lines = html_to_pixels('unicode_reference', 200, 50, '''

                        {1}

                        '''.format(style, text)) temp = tempfile.mkdtemp(prefix=text + '-') try: stylesheet = os.path.join(temp, 'style.css') image = os.path.join(temp, 'pattern.png') html = os.path.join(temp, 'doc.html') with open(stylesheet, 'wb') as fd: fd.write(style.encode('utf8')) with open(resource_filename('pattern.png'), 'rb') as fd: image_content = fd.read() with open(image, 'wb') as fd: fd.write(image_content) with open(html, 'wb') as fd: html_content = '''

                        {2}

                        '''.format( ensure_url(stylesheet), ensure_url(image), text ) fd.write(html_content.encode('utf8')) document = FakeHTML(html, encoding='utf8') lines = document_to_pixels(document, 'unicode', 200, 50) assert_pixels_equal('unicode', 200, 50, lines, expected_lines) finally: shutil.rmtree(temp) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.028308 weasyprint-54.1/tests/test_variables.py0000644000000000000000000000525000000000000016524 0ustar0000000000000000""" weasyprint.tests.test_variables ------------------------------- Test CSS custom proproperties, also known as CSS variables. """ import pytest from weasyprint.css.properties import KNOWN_PROPERTIES from .testing_utils import render_pages as parse SIDES = ('top', 'right', 'bottom', 'left') def test_variable_simple(): page, = parse('''

                        ''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == 10 def test_variable_inherit(): page, = parse('''

                        ''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == 10 def test_variable_inherit_override(): page, = parse('''

                        ''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == 10 def test_variable_case_sensitive(): page, = parse('''

                        ''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == 10 def test_variable_chain(): page, = parse('''

                        ''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == 10 def test_variable_partial_1(): page, = parse('''
                        ''') html, = page.children body, = html.children div, = body.children assert div.margin_top == 0 assert div.margin_right == 0 assert div.margin_bottom == 0 assert div.margin_left == 10 def test_variable_initial(): page, = parse('''

                        ''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == 10 @pytest.mark.parametrize('prop', sorted(KNOWN_PROPERTIES)) def test_variable_fallback(prop): parse('''
                        ''' % prop) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.028308 weasyprint-54.1/tests/testing_utils.py0000644000000000000000000001753300000000000016421 0ustar0000000000000000""" weasyprint.tests.testing_utils ------------------------------ Helpers for tests. """ import contextlib import functools import logging import os.path import sys import threading import wsgiref.simple_server from weasyprint import CSS, HTML, images from weasyprint.css import get_all_computed_styles from weasyprint.css.counters import CounterStyle from weasyprint.css.targets import TargetCollector from weasyprint.formatting_structure import boxes, build from weasyprint.logger import LOGGER from weasyprint.urls import path2url # Lists of fonts with many variants (including condensed) if sys.platform.startswith('win'): # pragma: no cover SANS_FONTS = 'DejaVu Sans, Arial Nova, Arial, sans' MONO_FONTS = 'Courier New, Courier, monospace' else: # pragma: no cover SANS_FONTS = 'DejaVu Sans, sans' MONO_FONTS = 'DejaVu Sans Mono, monospace' TEST_UA_STYLESHEET = CSS(filename=os.path.join( os.path.dirname(__file__), '..', 'weasyprint', 'css', 'tests_ua.css' )) PROPER_CHILDREN = dict((key, tuple(map(tuple, value))) for key, value in { # Children can be of *any* type in *one* of the lists. boxes.BlockContainerBox: [[boxes.BlockLevelBox], [boxes.LineBox]], boxes.LineBox: [[boxes.InlineLevelBox]], boxes.InlineBox: [[boxes.InlineLevelBox]], boxes.TableBox: [[boxes.TableCaptionBox, boxes.TableColumnGroupBox, boxes.TableColumnBox, boxes.TableRowGroupBox, boxes.TableRowBox]], boxes.InlineTableBox: [[boxes.TableCaptionBox, boxes.TableColumnGroupBox, boxes.TableColumnBox, boxes.TableRowGroupBox, boxes.TableRowBox]], boxes.TableColumnGroupBox: [[boxes.TableColumnBox]], boxes.TableRowGroupBox: [[boxes.TableRowBox]], boxes.TableRowBox: [[boxes.TableCellBox]], }.items()) class FakeHTML(HTML): """Like weasyprint.HTML, but with a lighter UA stylesheet.""" def _ua_stylesheets(self): return [TEST_UA_STYLESHEET] def resource_filename(basename): """Return the absolute path of the resource called ``basename``.""" return os.path.join(os.path.dirname(__file__), 'resources', basename) # Dummy filename, but in the right directory. BASE_URL = path2url(resource_filename('')) class CallbackHandler(logging.Handler): """A logging handler that calls a function for every message.""" def __init__(self, callback): logging.Handler.__init__(self) self.emit = callback @contextlib.contextmanager def capture_logs(): """Return a context manager that captures all logged messages.""" logger = LOGGER messages = [] def emit(record): if record.name == 'weasyprint.progress': return messages.append(f'{record.levelname.upper()}: {record.getMessage()}') previous_handlers = logger.handlers previous_level = logger.level logger.handlers = [] logger.addHandler(CallbackHandler(emit)) logger.setLevel(logging.DEBUG) try: yield messages finally: logger.handlers = previous_handlers logger.level = previous_level def assert_no_logs(function): """Decorator that asserts that nothing is logged in a function.""" @functools.wraps(function) def wrapper(*args, **kwargs): with capture_logs() as logs: try: function(*args, **kwargs) except Exception: # pragma: no cover if logs: print(f'{len(logs)} errors logged:', file=sys.stderr) for message in logs: print(message, file=sys.stderr) raise else: if logs: # pragma: no cover for message in logs: print(message, file=sys.stderr) raise AssertionError(f'{len(logs)} errors logged') return wrapper @contextlib.contextmanager def http_server(handlers): def wsgi_app(environ, start_response): handler = handlers.get(environ['PATH_INFO']) if handler: status = str('200 OK') response, headers = handler(environ) headers = [(str(name), str(value)) for name, value in headers] else: # pragma: no cover status = str('404 Not Found') response = b'' headers = [] start_response(status, headers) return [response] # Port 0: let the OS pick an available port number # http://stackoverflow.com/a/1365284/1162888 server = wsgiref.simple_server.make_server('127.0.0.1', 0, wsgi_app) _host, port = server.socket.getsockname() thread = threading.Thread(target=server.serve_forever) thread.start() try: yield f'http://127.0.0.1:{port}' finally: server.shutdown() thread.join() def serialize(box_list): """Transform a box list into a structure easier to compare for testing.""" return [( box.element_tag, type(box).__name__[:-3], # All concrete boxes are either text, replaced, column or parent. (box.text if isinstance(box, boxes.TextBox) else '' if isinstance(box, boxes.ReplacedBox) else serialize( getattr(box, 'column_groups', ()) + tuple(box.children)))) for box in box_list] def _parse_base(html_content, base_url=BASE_URL): document = FakeHTML(string=html_content, base_url=base_url) counter_style = CounterStyle() style_for = get_all_computed_styles(document, counter_style=counter_style) get_image_from_uri = functools.partial( images.get_image_from_uri, cache={}, url_fetcher=document.url_fetcher, optimize_size=()) target_collector = TargetCollector() footnotes = [] return ( document.etree_element, style_for, get_image_from_uri, base_url, target_collector, counter_style, footnotes) def parse(html_content): """Parse some HTML, apply stylesheets and transform to boxes.""" box, = build.element_to_box(*_parse_base(html_content)) return box def parse_all(html_content, base_url=BASE_URL): """Like parse() but also run all corrections on boxes.""" box = build.build_formatting_structure(*_parse_base( html_content, base_url)) _sanity_checks(box) return box def render_pages(html_content): """Lay out a document and return a list of PageBox objects.""" return [ page._page_box for page in FakeHTML(string=html_content, base_url=BASE_URL).render().pages] def assert_tree(box, expected): """Check the box tree equality. The obtained result is prettified in the message in case of failure. box: a Box object, starting with and blocks. expected: a list of serialized children as returned by to_lists(). """ assert box.element_tag == 'html' assert isinstance(box, boxes.BlockBox) assert len(box.children) == 1 box = box.children[0] assert isinstance(box, boxes.BlockBox) assert box.element_tag == 'body' assert serialize(box.children) == expected def _sanity_checks(box): """Check that the rules regarding boxes are met. This is not required and only helps debugging. - A block container can contain either only block-level boxes or only line boxes; - Line boxes and inline boxes can only contain inline-level boxes. """ if not isinstance(box, boxes.ParentBox): return acceptable_types_lists = None # raises when iterated for class_ in type(box).mro(): # pragma: no cover if class_ in PROPER_CHILDREN: acceptable_types_lists = PROPER_CHILDREN[class_] break assert any( all(isinstance(child, acceptable_types) or not child.is_in_normal_flow() for child in box.children) for acceptable_types in acceptable_types_lists ), (box, box.children) for child in box.children: _sanity_checks(child) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1643654890.8982391 weasyprint-54.1/weasyprint/__init__.py0000644000000000000000000003336200000000000016324 0ustar0000000000000000""" WeasyPrint ========== WeasyPrint converts web documents to PDF. The public API is what is accessible from this "root" packages without importing sub-modules. """ import contextlib from pathlib import Path import cssselect2 import html5lib import tinycss2 VERSION = __version__ = '54.1' __all__ = [ 'HTML', 'CSS', 'Attachment', 'Document', 'Page', 'default_url_fetcher', 'VERSION', '__version__'] # Import after setting the version, as the version is used in other modules from .urls import ( # noqa isort:skip fetch, default_url_fetcher, path2url, ensure_url, url_is_absolute) from .logger import LOGGER, PROGRESS_LOGGER # noqa isort:skip # Some imports are at the end of the file (after the CSS class) # to work around circular imports. class HTML: """HTML document parsed by html5lib. You can just create an instance with a positional argument: ``doc = HTML(something)`` The class will try to guess if the input is a filename, an absolute URL, or a :term:`file object`. Alternatively, use **one** named argument so that no guessing is involved: :type filename: str or pathlib.Path :param filename: A filename, relative to the current directory, or absolute. :param str url: An absolute, fully qualified URL. :type file_obj: :term:`file object` :param file_obj: Any object with a ``read`` method. :param str string: A string of HTML source. Specifying multiple inputs is an error: ``HTML(filename="foo.html", url="localhost://bar.html")`` will raise a :obj:`TypeError`. You can also pass optional named arguments: :param str encoding: Force the source character encoding. :param str base_url: The base used to resolve relative URLs (e.g. in ````). If not provided, try to use the input filename, URL, or ``name`` attribute of :term:`file objects `. :type url_fetcher: :term:`function` :param url_fetcher: A function or other callable with the same signature as :func:`default_url_fetcher` called to fetch external resources such as stylesheets and images. (See :ref:`URL Fetchers`.) :param str media_type: The media type to use for ``@media``. Defaults to ``'print'``. **Note:** In some cases like ``HTML(string=foo)`` relative URLs will be invalid if ``base_url`` is not provided. """ def __init__(self, guess=None, filename=None, url=None, file_obj=None, string=None, encoding=None, base_url=None, url_fetcher=default_url_fetcher, media_type='print'): PROGRESS_LOGGER.info( 'Step 1 - Fetching and parsing HTML - %s', guess or filename or url or getattr(file_obj, 'name', 'HTML string')) result = _select_source( guess, filename, url, file_obj, string, base_url, url_fetcher) with result as (source_type, source, base_url, protocol_encoding): if isinstance(source, str): result = html5lib.parse(source, namespaceHTMLElements=False) else: result = html5lib.parse( source, override_encoding=encoding, transport_encoding=protocol_encoding, namespaceHTMLElements=False) self.base_url = find_base_url(result, base_url) self.url_fetcher = url_fetcher self.media_type = media_type self.wrapper_element = cssselect2.ElementWrapper.from_html_root( result, content_language=None) self.etree_element = self.wrapper_element.etree_element def _ua_stylesheets(self): return [HTML5_UA_STYLESHEET] def _ua_counter_style(self): return [HTML5_UA_COUNTER_STYLE.copy()] def _ph_stylesheets(self): return [HTML5_PH_STYLESHEET] def render(self, stylesheets=None, presentational_hints=False, optimize_size=('fonts',), font_config=None, counter_style=None, image_cache=None): """Lay out and paginate the document, but do not (yet) export it. This returns a :class:`document.Document` object which provides access to individual pages and various meta-data. See :meth:`write_pdf` to get a PDF directly. .. versionadded:: 0.15 :param list stylesheets: An optional list of user stylesheets. List elements are :class:`CSS` objects, filenames, URLs, or file objects. (See :ref:`Stylesheet Origins`.) :param bool presentational_hints: Whether HTML presentational hints are followed. :param tuple optimize_size: Optimize size of generated PDF. Can contain "images" and "fonts". :type font_config: :class:`text.fonts.FontConfiguration` :param font_config: A font configuration handling ``@font-face`` rules. :type counter_style: :class:`css.counters.CounterStyle` :param counter_style: A dictionary storing ``@counter-style`` rules. :param dict image_cache: A dictionary used to cache images. :returns: A :class:`document.Document` object. """ return Document._render( self, stylesheets, presentational_hints, optimize_size, font_config, counter_style, image_cache) def write_pdf(self, target=None, stylesheets=None, zoom=1, attachments=None, finisher=None, presentational_hints=False, optimize_size=('fonts',), font_config=None, counter_style=None, image_cache=None): """Render the document to a PDF file. This is a shortcut for calling :meth:`render`, then :meth:`Document.write_pdf() `. :type target: :class:`str`, :class:`pathlib.Path` or :term:`file object` :param target: A filename where the PDF file is generated, a file object, or :obj:`None`. :param list stylesheets: An optional list of user stylesheets. The list's elements are :class:`CSS` objects, filenames, URLs, or file-like objects. (See :ref:`Stylesheet Origins`.) :param float zoom: The zoom factor in PDF units per CSS units. **Warning**: All CSS units are affected, including physical units like ``cm`` and named sizes like ``A4``. For values other than 1, the physical CSS units will thus be "wrong". :param list attachments: A list of additional file attachments for the generated PDF document or :obj:`None`. The list's elements are :class:`Attachment` objects, filenames, URLs or file-like objects. :param finisher: A finisher function, that accepts the document and a :class:`pydyf.PDF` object as parameters, can be passed to perform post-processing on the PDF right before the trailer is written. :param bool presentational_hints: Whether HTML presentational hints are followed. :param tuple optimize_size: Optimize size of generated PDF. Can contain "images" and "fonts". :type font_config: :class:`text.fonts.FontConfiguration` :param font_config: A font configuration handling ``@font-face`` rules. :type counter_style: :class:`css.counters.CounterStyle` :param counter_style: A dictionary storing ``@counter-style`` rules. :param dict image_cache: A dictionary used to cache images. :returns: The PDF as :obj:`bytes` if ``target`` is not provided or :obj:`None`, otherwise :obj:`None` (the PDF is written to ``target``). """ return ( self.render( stylesheets, presentational_hints=presentational_hints, optimize_size=optimize_size, font_config=font_config, counter_style=counter_style, image_cache=image_cache) .write_pdf(target, zoom, attachments, finisher)) class CSS: """CSS stylesheet parsed by tinycss2. An instance is created in the same way as :class:`HTML`, with the same arguments. An additional argument called ``font_config`` must be provided to handle ``@font-face`` rules. The same ``text.fonts.FontConfiguration`` object must be used for different ``CSS`` objects applied to the same document. ``CSS`` objects have no public attributes or methods. They are only meant to be used in the :meth:`HTML.write_pdf` and :meth:`HTML.render` methods of :class:`HTML` objects. """ def __init__(self, guess=None, filename=None, url=None, file_obj=None, string=None, encoding=None, base_url=None, url_fetcher=default_url_fetcher, _check_mime_type=False, media_type='print', font_config=None, counter_style=None, matcher=None, page_rules=None): PROGRESS_LOGGER.info( 'Step 2 - Fetching and parsing CSS - %s', filename or url or getattr(file_obj, 'name', 'CSS string')) result = _select_source( guess, filename, url, file_obj, string, base_url=base_url, url_fetcher=url_fetcher, check_css_mime_type=_check_mime_type) with result as (source_type, source, base_url, protocol_encoding): if source_type == 'string' and not isinstance(source, bytes): # unicode, no encoding stylesheet = tinycss2.parse_stylesheet(source) else: if source_type == 'file_obj': source = source.read() stylesheet, encoding = tinycss2.parse_stylesheet_bytes( source, environment_encoding=encoding, protocol_encoding=protocol_encoding) self.base_url = base_url self.matcher = matcher or cssselect2.Matcher() self.page_rules = [] if page_rules is None else page_rules self.fonts = [] preprocess_stylesheet( media_type, base_url, stylesheet, url_fetcher, self.matcher, self.page_rules, self.fonts, font_config, counter_style) class Attachment: """File attachment for a PDF document. .. versionadded:: 0.22 An instance is created in the same way as :class:`HTML`, except that the HTML specific arguments (``encoding`` and ``media_type``) are not supported. An optional description can be provided with the ``description`` argument. :param description: A description of the attachment to be included in the PDF document. May be :obj:`None`. """ def __init__(self, guess=None, filename=None, url=None, file_obj=None, string=None, base_url=None, url_fetcher=default_url_fetcher, description=None): self.source = _select_source( guess, filename, url, file_obj, string, base_url=base_url, url_fetcher=url_fetcher) self.description = description @contextlib.contextmanager def _select_source(guess=None, filename=None, url=None, file_obj=None, string=None, base_url=None, url_fetcher=default_url_fetcher, check_css_mime_type=False): """If only one input is given, return it with normalized ``base_url``.""" if base_url is not None: base_url = ensure_url(base_url) selected_params = [ param for param in (guess, filename, url, file_obj, string) if param is not None] if len(selected_params) != 1: raise TypeError('Expected exactly one source, got ' + ( ', '.join(selected_params) or 'nothing')) elif guess is not None: if hasattr(guess, 'read'): type_ = 'file_obj' elif isinstance(guess, Path): type_ = 'filename' elif url_is_absolute(guess): type_ = 'url' else: type_ = 'filename' result = _select_source( base_url=base_url, url_fetcher=url_fetcher, check_css_mime_type=check_css_mime_type, **{type_: guess}) with result as result: yield result elif filename is not None: if isinstance(filename, Path): filename = str(filename) if base_url is None: base_url = path2url(filename) with open(filename, 'rb') as file_obj: yield 'file_obj', file_obj, base_url, None elif url is not None: with fetch(url_fetcher, url) as result: if check_css_mime_type and result['mime_type'] != 'text/css': LOGGER.error( 'Unsupported stylesheet type %s for %s', result['mime_type'], result['redirected_url']) yield 'string', '', base_url, None else: proto_encoding = result.get('encoding') if base_url is None: base_url = result.get('redirected_url', url) if 'string' in result: yield 'string', result['string'], base_url, proto_encoding else: yield ( 'file_obj', result['file_obj'], base_url, proto_encoding) elif file_obj is not None: if base_url is None: # filesystem file-like objects have a 'name' attribute. name = getattr(file_obj, 'name', None) # Some streams have a .name like '', not a filename. if name and not name.startswith('<'): base_url = ensure_url(name) yield 'file_obj', file_obj, base_url, None else: assert string is not None yield 'string', string, base_url, None # Work around circular imports. from .css import preprocess_stylesheet # noqa isort:skip from .html import ( # noqa isort:skip HTML5_UA_COUNTER_STYLE, HTML5_UA_STYLESHEET, HTML5_PH_STYLESHEET, find_base_url) from .document import Document, Page # noqa isort:skip ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1639425971.028308 weasyprint-54.1/weasyprint/__main__.py0000644000000000000000000001510000000000000016273 0ustar0000000000000000""" weasyprint.__main__ ------------------- Command-line interface to WeasyPrint. """ import argparse import logging import platform import sys import pydyf from . import HTML, LOGGER, __version__ from .text.ffi import pango class PrintInfo(argparse.Action): def __call__(*_, **__): uname = platform.uname() print('System:', uname.system) print('Machine:', uname.machine) print('Version:', uname.version) print('Release:', uname.release) print() print('WeasyPrint version:', __version__) print('Python version:', sys.version.split()[0]) print('Pydyf version:', pydyf.__version__) print('Pango version:', pango.pango_version()) sys.exit() def main(argv=None, stdout=None, stdin=None): """The ``weasyprint`` program takes at least two arguments: .. code-block:: sh weasyprint [options] The input is a filename or URL to an HTML document, or ``-`` to read HTML from stdin. The output is a filename, or ``-`` to write to stdout. Options can be mixed anywhere before, between, or after the input and output. .. option:: -e , --encoding Force the input character encoding (e.g. ``-e utf8``). .. option:: -s , --stylesheet Filename or URL of a user cascading stylesheet (see :ref:`Stylesheet Origins`) to add to the document (e.g. ``-s print.css``). Multiple stylesheets are allowed. .. option:: -m , --media-type Set the media type to use for ``@media``. Defaults to ``print``. .. option:: -u , --base-url Set the base for relative URLs in the HTML input. Defaults to the input’s own URL, or the current directory for stdin. .. option:: -a , --attachment Adds an attachment to the document. The attachment is included in the PDF output. This option can be used multiple times. .. option:: -p, --presentational-hints Follow `HTML presentational hints `_. .. option:: -O , --optimize-size Optimize the size of generated documents. Supported types are ``images``, ``fonts``, ``all`` and ``none``. This option can be used multiple times, ``all`` adds all allowed values, ``none`` removes all previously set values. .. option:: -v, --verbose Show warnings and information messages. .. option:: -d, --debug Show debugging messages. .. option:: -q, --quiet Hide logging messages. .. option:: --version Show the version number. Other options and arguments are ignored. .. option:: -h, --help Show the command-line usage. Other options and arguments are ignored. """ parser = argparse.ArgumentParser( prog='weasyprint', description='Renders web pages to PDF.') parser.add_argument('--version', action='version', version=f'WeasyPrint version {__version__}', help="Print WeasyPrint's version number and exit.") parser.add_argument('-i', '--info', action=PrintInfo, nargs=0, help='Print system information and exit.') parser.add_argument('-e', '--encoding', help='Character encoding of the input') parser.add_argument('-s', '--stylesheet', action='append', help='URL or filename for a user CSS stylesheet. ' 'May be given multiple times.') parser.add_argument('-m', '--media-type', default='print', help='Media type to use for @media, defaults to print') parser.add_argument('-u', '--base-url', help='Base for relative URLs in the HTML input. ' "Defaults to the input's own filename or URL " 'or the current directory for stdin.') parser.add_argument('-a', '--attachment', action='append', help='URL or filename of a file ' 'to attach to the PDF document') parser.add_argument('-p', '--presentational-hints', action='store_true', help='Follow HTML presentational hints.') parser.add_argument('-O', '--optimize-size', action='append', help='Optimize output size for specified features.', choices=('images', 'fonts', 'all', 'none'), default=['fonts']) parser.add_argument('-v', '--verbose', action='store_true', help='Show warnings and information messages.') parser.add_argument('-d', '--debug', action='store_true', help='Show debugging messages.') parser.add_argument('-q', '--quiet', action='store_true', help='Hide logging messages.') parser.add_argument( 'input', help='URL or filename of the HTML input, or - for stdin') parser.add_argument( 'output', help='Filename where output is written, or - for stdout') args = parser.parse_args(argv) if args.input == '-': source = stdin or sys.stdin.buffer if args.base_url is None: args.base_url = '.' # current directory elif args.base_url == '': args.base_url = None # no base URL else: source = args.input if args.output == '-': output = stdout or sys.stdout.buffer else: output = args.output optimize_size = set() for arg in args.optimize_size: if arg == 'none': optimize_size.clear() elif arg == 'all': optimize_size |= {'images', 'fonts'} else: optimize_size.add(arg) kwargs = { 'stylesheets': args.stylesheet, 'presentational_hints': args.presentational_hints, 'optimize_size': tuple(optimize_size), 'attachments': args.attachment} # Default to logging to stderr. if args.debug: LOGGER.setLevel(logging.DEBUG) elif args.verbose: LOGGER.setLevel(logging.INFO) if not args.quiet: handler = logging.StreamHandler() handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) LOGGER.addHandler(handler) html = HTML(source, base_url=args.base_url, encoding=args.encoding, media_type=args.media_type) html.write_pdf(output, **kwargs) if __name__ == '__main__': # pragma: no cover main() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1643655079.361553 weasyprint-54.1/weasyprint/css/__init__.py0000644000000000000000000013653600000000000017123 0ustar0000000000000000""" weasyprint.css -------------- This module takes care of steps 3 and 4 of “CSS 2.1 processing model”: Retrieve stylesheets associated with a document and annotate every element with a value for every CSS property. http://www.w3.org/TR/CSS21/intro.html#processing-model This module does this in more than two steps. The :func:`get_all_computed_styles` function does everything, but it is itsef based on other functions in this module. """ from collections import namedtuple from logging import DEBUG, WARNING import cssselect2 import tinycss2 import tinycss2.nth from .. import CSS from ..logger import LOGGER, PROGRESS_LOGGER from ..urls import URLFetchingError, get_url_attribute, url_join from . import computed_values, counters, media_queries from .properties import INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES from .utils import get_url, remove_whitespace from .validation import preprocess_declarations from .validation.descriptors import preprocess_descriptors # Reject anything not in here: PSEUDO_ELEMENTS = ( None, 'before', 'after', 'marker', 'first-line', 'first-letter', 'footnote-call', 'footnote-marker') PageType = namedtuple('PageType', ['side', 'blank', 'first', 'index', 'name']) class StyleFor: """Convenience function to get the computed styles for an element.""" def __init__(self, html, sheets, presentational_hints, target_collector): # keys: (element, pseudo_element_type) # element: an ElementTree Element or the '@page' string # pseudo_element_type: a string such as 'first' (for @page) or # 'after', or None for normal elements # values: dicts of # keys: property name as a string # values: (values, weight) # values: a PropertyValue-like object # weight: values with a greater weight take precedence, see # http://www.w3.org/TR/CSS21/cascade.html#cascading-order self._cascaded_styles = cascaded_styles = {} # keys: (element, pseudo_element_type), like cascaded_styles # values: style dict objects: # keys: property name as a string # values: a PropertyValue-like object self._computed_styles = {} self._sheets = sheets PROGRESS_LOGGER.info('Step 3 - Applying CSS') for specificity, attributes in find_style_attributes( html.etree_element, presentational_hints, html.base_url): element, declarations, base_url = attributes style = cascaded_styles.setdefault((element, None), {}) for name, values, importance in preprocess_declarations( base_url, declarations): precedence = declaration_precedence('author', importance) weight = (precedence, specificity) old_weight = style.get(name, (None, None))[1] if old_weight is None or old_weight <= weight: style[name] = values, weight # First, add declarations and set computed styles for "real" elements # *in tree order*. Tree order is important so that parents have # computed styles before their children, for inheritance. # Iterate on all elements, even if there is no cascaded style for them. for element in html.wrapper_element.iter_subtree(): for sheet, origin, sheet_specificity in sheets: # Add declarations for matched elements for selector in sheet.matcher.match(element): specificity, order, pseudo_type, declarations = selector specificity = sheet_specificity or specificity style = cascaded_styles.setdefault( (element.etree_element, pseudo_type), {}) for name, values, importance in declarations: precedence = declaration_precedence(origin, importance) weight = (precedence, specificity) old_weight = style.get(name, (None, None))[1] if old_weight is None or old_weight <= weight: style[name] = values, weight parent = element.parent.etree_element if element.parent else None self.set_computed_styles( element.etree_element, root=html.etree_element, parent=parent, base_url=html.base_url, target_collector=target_collector) # Then computed styles for pseudo elements, in any order. # Pseudo-elements inherit from their associated element so they come # last. Do them in a second pass as there is no easy way to iterate # on the pseudo-elements for a given element with the current structure # of cascaded_styles. (Keys are (element, pseudo_type) tuples.) # Only iterate on pseudo-elements that have cascaded styles. (Others # might as well not exist.) for element, pseudo_type in cascaded_styles: if pseudo_type and not isinstance(element, PageType): self.set_computed_styles( element, pseudo_type=pseudo_type, # The pseudo-element inherits from the element. root=html.etree_element, parent=element, base_url=html.base_url, target_collector=target_collector) # Clear the cascaded styles, we don't need them anymore. Keep the # dictionary, it is used later for page margins. self._cascaded_styles.clear() def __call__(self, element, pseudo_type=None): style = self._computed_styles.get((element, pseudo_type)) if style: if ('table' in style['display'] and style['border_collapse'] == 'collapse'): # Padding do not apply for side in ['top', 'bottom', 'left', 'right']: style[f'padding_{side}'] = computed_values.ZERO_PIXELS if (len(style['display']) == 1 and style['display'][0].startswith('table-') and style['display'][0] != 'table-caption'): # Margins do not apply for side in ['top', 'bottom', 'left', 'right']: style[f'margin_{side}'] = computed_values.ZERO_PIXELS return style def set_computed_styles(self, element, parent, root=None, pseudo_type=None, base_url=None, target_collector=None): """Set the computed values of styles to ``element``. Take the properties left by ``apply_style_rule`` on an element or pseudo-element and assign computed values with respect to the cascade, declaration priority (ie. ``!important``) and selector specificity. """ cascaded_styles = self.get_cascaded_styles() computed_styles = self.get_computed_styles() if element == root and pseudo_type is None: assert parent is None parent_style = None root_style = { # When specified on the font-size property of the root element, # the rem units refer to the property’s initial value. 'font_size': INITIAL_VALUES['font_size'], } else: assert parent is not None parent_style = computed_styles[parent, None] root_style = computed_styles[root, None] cascaded = cascaded_styles.get((element, pseudo_type), {}) computed_styles[element, pseudo_type] = computed_from_cascaded( element, cascaded, parent_style, pseudo_type, root_style, base_url, target_collector) # The style of marker is deleted when display is different from # list-item. if pseudo_type is None: for pseudo in (None, 'before', 'after'): pseudo_style = cascaded_styles.get((element, pseudo), {}) if 'display' in pseudo_style: if 'list-item' in pseudo_style['display'][0]: break else: if (element, 'marker') in cascaded_styles: del cascaded_styles[element, 'marker'] def add_page_declarations(self, page_type): for sheet, origin, sheet_specificity in self._sheets: for _rule, selector_list, declarations in sheet.page_rules: for selector in selector_list: specificity, pseudo_type, selector_page_type = selector if self._page_type_match(selector_page_type, page_type): specificity = sheet_specificity or specificity style = self._cascaded_styles.setdefault( (page_type, pseudo_type), {}) for name, values, importance in declarations: precedence = declaration_precedence( origin, importance) weight = (precedence, specificity) old_weight = style.get(name, (None, None))[1] if old_weight is None or old_weight <= weight: style[name] = values, weight def get_cascaded_styles(self): return self._cascaded_styles def get_computed_styles(self): return self._computed_styles @staticmethod def _page_type_match(selector_page_type, page_type): if selector_page_type.side not in (None, page_type.side): return False if selector_page_type.blank not in (None, page_type.blank): return False if selector_page_type.first not in (None, page_type.first): return False if selector_page_type.name not in (None, page_type.name): return False if selector_page_type.index is not None: a, b, group = selector_page_type.index # TODO: handle group if a: if (page_type.index + 1 - b) % a: return False else: if page_type.index + 1 != b: return False return True def get_child_text(element): """Return the text directly in the element, not descendants.""" content = [element.text] if element.text else [] for child in element: if child.tail: content.append(child.tail) return ''.join(content) def find_stylesheets(wrapper_element, device_media_type, url_fetcher, base_url, font_config, counter_style, page_rules): """Yield the stylesheets in ``element_tree``. The output order is the same as the source order. """ from ..html import element_has_link_type for wrapper in wrapper_element.query_all('style', 'link'): element = wrapper.etree_element mime_type = element.get('type', 'text/css').split(';', 1)[0].strip() # Only keep 'type/subtype' from 'type/subtype ; param1; param2'. if mime_type != 'text/css': continue media_attr = element.get('media', '').strip() or 'all' media = [media_type.strip() for media_type in media_attr.split(',')] if not media_queries.evaluate_media_query(media, device_media_type): continue if element.tag == 'style': # Content is text that is directly in the