././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1708289864.164647 weasyprint-62.3/LICENSE0000644000000000000000000000277614564467510011634 0ustar00BSD 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. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3263621 weasyprint-62.3/README.rst0000644000000000000000000000314414634254702012277 0ustar00**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.9+, 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. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1718704578.327362 weasyprint-62.3/docs/api_reference.rst0000644000000000000000000007716114634254702015073 0ustar00API 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 .. autodata:: DEFAULT_OPTIONS .. 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: https://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`` property; - the ``match-parent`` value of the ``text-align`` property; - 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. .. _#1153: https://github.com/Kozea/WeasyPrint/issues/1153 .. _supported by Pyphen: https://github.com/Kozea/Pyphen/tree/main/pyphen/dictionaries .. _hyphenation: https://www.w3.org/TR/css-text-3/#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 / 4 ++++++++++++++++++++++++++++ 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. 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. From `CSS Fonts Module Level 4`_ we only support the ``font-variation-settings`` property enabling specific font variations. .. _CSS Fonts Module Level 3: https://www.w3.org/TR/css-fonts-3/ .. _CSS Fonts Module Level 4: https://www.w3.org/TR/css-fonts-4/ 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: https://drafts.csswg.org/css-page-3/ .. _#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: https://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 ``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 } `Leaders`_ are also supported: .. code-block:: css li a::after { content: ' ' leader(dotted) ' ' target-counter(attr(href), page); } The other features of this module are **not** implemented: - quotes (``content: *-quote``); .. _CSS Generated Content Module Level 3: https://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 .. _user agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/main/weasyprint/css/html5_ua.css .. _Leaders: https://www.w3.org/TR/css-content-3/#leaders 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: https://www.w3.org/TR/css-color-3/ 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``, ``translateX``, ``translateY``, ``scale``, ``scaleX``, ``scaleY``, ``skew``, ``skewX``, ``skewY``). WeasyPrint does **not** support the ``transform-style``, ``perspective``, ``perspective-origin`` and ``backface-visibility`` properties, and all the 3D transformations (``matrix3d``, ``rotate3d``, ``rotateX``, ``rotateY``, ``rotateZ``, ``translate3d``, ``translateZ``, ``scale3d``, ``scaleZ``). .. _CSS Transforms Module Level 1: https://drafts.csswg.org/css-transforms-1/ 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 also supports 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: https://www.w3.org/TR/css-backgrounds-3/ .. _border part: https://www.w3.org/TR/css-backgrounds-3/#borders .. _background part: https://www.w3.org/TR/css-backgrounds-3/#backgrounds .. _rounded corners part: https://www.w3.org/TR/css-backgrounds-3/#corners .. _border images part: https://www.w3.org/TR/css-backgrounds-3/#border-images .. _box shadow part: https://www.w3.org/TR/css-backgrounds-3/#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`` and ``image-orientation`` properties are supported. .. _Image Values and Replaced Content Module Level 3: https://www.w3.org/TR/css-images-3/ .. _Image Values and Replaced Content Module Level 4: https://www.w3.org/TR/css-images-4/ 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. The ``calc()`` function is **not** supported. 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 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/css-multicol-1/ 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/ CSS Grid Layout Module Level 2 ++++++++++++++++++++++++++++++ The `CSS Grid Layout Module Level 2`_ "defines a two-dimensional grid-based layout system, optimized for user interface design". This module works for simple cases, but has some limitations. Here are non-exhaustive lists of supported/unsupported features. Supported: - ``display: grid``, - ``grid-auto-*``, ``grid-template-*`` and other ``grid-*`` properties, - ``grid`` and other ``grid-*`` shorthands, - flexible lengths (``fr`` unit), - line names, - grid areas, - auto rows and auto columns, - ``z-index``, - ``repeat(X, *)``, - ``minmax()``, - ``align-*`` and ``justify-*`` alignment properties, - ``gap`` and ``*-gap`` properties for gutters, - dense auto flow, - ``order``, - margins, borders, padding on grid containers and grid items, - fragmentation between rows. Unsupported or untested: - ``display: inline-grid``, - auto content size for grid containers, - ``grid-auto-flow: column``, - subgrids, - ``repeat(auto-fill, *)`` and ``repeat(auto-fit, *)``, - auto margins for grid items, - ``span`` with line names, - ``span`` for flexible tracks, - ``safe`` and ``unsafe`` alignments, - baseline alignment, - grid items with intrinsic size (images), - distribute space beyond limits, - grid items larger than grid containers, - ``min-width``, ``max-width``, ``min-height``, ``max-height`` on grid items, - complex ``min-content`` and ``max-content`` cases, - absolutely positioned and floating grid items, - fragmentation in rows. .. _CSS Grid Layout Module Level 2: https://www.w3.org/TR/css-grid-2/ CSS Basic User Interface Module Level 3/4 +++++++++++++++++++++++++++++++++++++++++ The `CSS Basic User Interface Module Level 3/4`_ "enables authors to style user interface related properties and values." The ``outline-width``, ``outline-style``, ``outline-color`` properties and the ``outline`` shorthand are supported. The ``outline-offset`` property is **not** supported. The ``resize``, ``cursor``, ``caret-*`` and ``nav-*`` properties are **not** supported. The ``appearance`` property is supported. When set to ``auto``, it displays form fields as PDF form fields (supported for text inputs, check boxes, text areas, and select only). The ``accent-color`` property is **not** supported. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984725.6887183 weasyprint-62.3/docs/changelog.rst0000644000000000000000000032665714635320026014234 0ustar00Changelog ========= Version 62.3 ------------ Released on 2024-06-21. Bug fixes: * `#2174 `_: Fix extra width distribution for auto table layout * `#2175 `_: Don’t compress PDF metadata for PDF/A-1 * `61f8bb3 `_: Set default PDF variant values in options before generating PDF * `2c4351e `_: Avoid PDF artifacts when drawing 0-width borders * `d9d7f62 `_: Don’t duplicate column when container is split on multiple pages * `4617b94 `_: Don’t set default Fontconfig values for unset properties * `4c81663 `_: Fix layout when all footnotes are removed from the footnote area * `#2184 `_: Make items overflowing grid wrap to the next row/column * `#2187 `_: Don’t append useless tracks when grid elements are positioned Contributors: * Guillaume Ayoub Backers and sponsors: * Spacinov * Kobalt * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * Simon Sapin * René Fritz * TrainingSparkle * Healthchecks.io * Hammerbacher * Docraptor * Yanal-Yvez Fargialla * Douwe van Loenen * Morntag * Xavid Version 62.2 ------------ Released on 2024-06-04. Features: * `#2142 `_, `#2162 `_: Support grid-auto-flow: column, with financial support from Menutech Bug fixes: * `#2167 `_: Fix space added by CSS gap at the end * `#2134 `_: Remove absolute placeholders from discarded content * `#2154 `_: Don’t crash when grid items have auto margins * `8cdd66f `_: Fix CSS nesting for nested selectors with comma * `3359db5 `_: Fix and test grid shorthand * `82deda4 `_: Fix wrong resume_at for split floats * `ff2acf1 `_: Ensure that gradient size is positive to please some PDF readers Contributors: * Guillaume Ayoub Backers and sponsors: * Spacinov * Kobalt * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * Simon Sapin * René Fritz * TrainingSparkle * Healthchecks.io * Hammerbacher * Docraptor * Yanal-Yvez Fargialla * Douwe van Loenen * Morntag * Xavid Version 62.1 ------------ Released on 2024-05-06. Bug fixes: * `#2144 `_, `#2149 `_: Avoid broken fonts when generating multiple documents * `c10c6892 `_: Display at least one grid row on empty pages * `#2146 `_: Don’t crash when flex container’s parent’s height is auto Contributors: * Guillaume Ayoub * Claudius Ellsel Backers and sponsors: * Spacinov * Kobalt * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * Simon Sapin * René Fritz * TrainingSparkle * Healthchecks.io * Docraptor * Yanal-Yvez Fargialla * Douwe van Loenen * Morntag * Xavid Version 62.0 ------------ Released on 2024-04-30. Dependencies: * Python 3.9+ is now needed, Python 3.7 and 3.8 are not supported anymore * pydyf 0.10.0+ is now needed * tinycss2 1.3.0+ is now needed Features: * `#543 `_, `#2121 `_: Support CSS Grid layout * `#2124 `_, `#2125 `_: Support border-image-* properties * `#2084 `_, `#2077 `_: Support CSS nesting * `#2101 `_: Support HTML maxlength attribute for form fields * `#2095 `_: Apply overflow to replaced boxes * `245e4f5 `_: Add support of PDF/A-?u Bug fixes: * `#2136 `_: Don’t clip aligned text in SVG * `#2135 `_: Allow column-direction flex containers to use percentage-based heights * `#2128 `_: Don’t crash when a FontConfig object is destroyed early * `#2079 `_: Fix executable file for some Windows versions * `#2131 `_: Fix alpha for images before/after transparent text * `#2111 `_: Handle auto and none values for CSS quotes property * `#2103 `_: Don’t crash with overconstrained columns * `#2100 `_: Fix rounding error when detecting overflows * `#2093 `_, `#2097 `_, `#2094 `_: Mark use of md5() and sha1() as not for security * `#1956 `_, `#2087 `_: Use CSS table module level 3 to compute widths * `#2086 `_: Fix selects with empty values displaying None * `#1112 `_, `#2082 `_, `#2085 `_: Fix computation for outer min-content width for table cells * `016bd81 `_: Fix many different bugs with SVG markers Performance: * `#2130 `_: Cache font key instead of whole font content Documentation: * `#2108 `_: Update documentation about CSS leader() function Contributors: * Guillaume Ayoub * Lucie Anglade * Xavid Pretzer * kygoh * Germain Gueutier * Vagner José Nicolodi Backers and sponsors: * Spacinov * Kobalt * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * Simon Sapin * René Fritz * TrainingSparkle * Healthchecks.io * Docraptor * Yanal-Yvez Fargialla * Douwe van Loenen * Morntag * Xavid Version 61.2 ------------ Released on 2024-03-08. **This is a security update.** We strongly recommend to upgrade WeasyPrint to the latest version if you use WeasyPrint 61.0 or 61.1. Older versions are not impacted. Security: - Always use URL fetcher for attachments Contributors: * Guillaume Ayoub * Ilia Novoselov Backers and sponsors: * Spacinov * Kobalt * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * René Fritz * Simon Sapin * Arcanite * TrainingSparkle * Healthchecks.io * Hammerbacher * Docraptor * Yanal-Yvez Fargialla * Morntag * NBCO Version 61.1 ------------ Released on 2024-02-26. Bug fixes: - `#2075 `_: Use default value when variable is not defined - `#2070 `_: Don’t crash when rendering SVGs with non-text a children - Don’t crash when SVG file can’t be rendered Documentation: - `#2067 `_: Suggest "dnf" instead of "yum" to install Fedora packages - Improve documentation for Windows - Fix required version of TinyCSS2 Contributors: * Guillaume Ayoub * Felix Schwarz * Lucie Anglade Backers and sponsors: * Spacinov * Kobalt * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * René Fritz * Simon Sapin * Arcanite * TrainingSparkle * Healthchecks.io * Hammerbacher * Docraptor * Yanal-Yvez Fargialla * Morntag * NBCO Version 61.0 ------------ Released on 2024-02-12. Python API: * ``DocumentMetadata.attachments`` is now a list of ``Attachment`` objects, not a list of ``(url, description)`` tuples. New features: * `#1219 `_, `#2017 `_: Support var() in shorthand and multiple-value functions * `#1986 `_: Support percentages for opacity * `#2050 `_: Build executable file for Windows * `#2000 `_: Support select fields * `#1993 `_: Handle background-attachment: fixed to cover the whole page * `#2023 `_, `#2022 `_: Allow text-based file objects for HTML and CSS classes * `#2014 `_: Remove warnings for PDF/A and PDF/UA compatibility Bug fixes: * `#2052 `_, `#1869 `_: Handle attachments for PDF/A documents * `#2013 `_, `#2051 `_: Apply margin to running tables * `#1278 `_, `#1884 `_: Draw collapsed borders of running tables * `#2029 `_: Fix page counter in non-root absolute boxes * `#2043 `_: Fix text-anchor on SVG tspan elements * `#1968 `_, `#2039 `_: Use cell's border-height to calculate table row height * `#2030 `_: Ensure that bounding box is set to invisible text tags * `#2040 `_, `#2041 `_: Don’t crash on malformed URLs * `#2026 `_: Don’t break pages when fixed-height elements don’t overflow page * `#2038 `_: Don’t mix original streams when drawing transparent text * `#2016 `_: Avoid duplication when breaking out-of-flow boxes * `#2012 `_: Don’t crash when CSS properties have no value * `#2010 `_, `#1287 `_: Fix many corner cases with CSS variables * `#1996 `_: Don’t crash when drawing groove/ridge collapsed borders * `#1982 `_: Fix SVG markers size, position and drawing Documentation: * `#2021 `_, `#2048 `_: Replace non-virtualenv installation instructions with distribution packages Contributors: * Guillaume Ayoub * kygoh * Lucie Anglade * Timo Ramsauer * Alexander Gitter * Michael Lisitsa * Vagner José Nicolodi * Manolis Stamatogiannakis * Pascal de Bruijn * Viktor Shevtsov * Eduardo Gonzalez * Kesara Rathnayake Backers and sponsors: * Spacinov * Kobalt * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * René Fritz * Simon Sapin * Arcanite * TrainingSparkle * Healthchecks.io * Hammerbacher * Docraptor * Yanal-Yvez Fargialla * Morntag * NBCO Version 60.2 ------------ Released on 2023-12-11. Bug fixes: * `#1982 `_: Fix SVG markers size, position and drawing * `23cfc775 `_: Draw background behind absolutely positioned replaced boxes * `fe2f0c69 `_: Don’t crash with bitmap fonts with no "glyf" table * `14605225 `_: Improve SVG text-anchor attribute Contributors: * Guillaume Ayoub Backers and sponsors: * Spacinov * Kobalt * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * NCC Group * René Fritz * Nicola Auchmuty * Syslifters * Hammerbacher * TrainingSparkle * Daniel Kucharski * Healthchecks.io * Yanal-Yvez Fargialla * WakaTime * Paheko * Synapsium * DocRaptor Version 60.1 ------------ Released on 2023-09-29. Bug fixes: * `#1973 `_: Fix crash caused by wrong UTF-8 indices Contributors: * Guillaume Ayoub Backers and sponsors: * Spacinov * Kobalt * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * NCC Group * René Fritz * Nicola Auchmuty * Syslifters * Hammerbacher * TrainingSparkle * Daniel Kucharski * Healthchecks.io * Yanal-Yvez Fargialla * WakaTime * Paheko * Synapsium * DocRaptor Version 60.0 ------------ Released on 2023-09-25. New features: * `#1903 `_: Print form fields * `#1922 `_: Add support for textLength and lengthAdjust in SVG text elements * `#1965 `_: Handle tag * `#1970 `_: Handle y offset of glyphs * `#1909 `_: Add a --timeout option Bug fixes: * `#1887 `_: Fix footnote-call displayed incorrectly for some fonts * `#1890 `_: Fix page-margin boxes layout algorithm * `#1908 `_: Fix IndexError when rendering PDF version 1.4 * `#1906 `_: Apply text transformations to first-letter pseudo elements * `#1915 `_: Avoid footnote appearing before its call * `#1934 `_: Fix balance before "column-span: all" * `#1935 `_: Only draw required glyph with OpenType-SVG fonts * `#1595 `_: Don’t draw clipPath when defined after reference * `#1895 `_: Don’t ignore min-width when computing cell size * `#1899 `_: Fix named pages inheritance * `#1936 `_: Avoid page breaks caused by children of overflow hidden boxes * `#1943 `_: Use bleed area for page’s painting area * `#1946 `_: Use margin box of children to define available width for leaders Contributors: * Guillaume Ayoub * Sahil Rohilla * Azharuddin Syed * kygoh * Andy Lenards * Gaurav Samudra * Michael Wedl * Lucie Anglade * Obeida Shamoun * Evgeniy Krysanov Backers and sponsors: * Spacinov * Kobalt * Grip Angebotssoftware * Manuel Barkhau * SimonSoft * Menutech * KontextWork * NCC Group * René Fritz * Nicola Auchmuty * Syslifters * Hammerbacher * TrainingSparkle * Daniel Kucharski * Healthchecks.io * Yanal-Yvez Fargialla * WakaTime * Paheko * Synapsium * DocRaptor Version 59.0 ------------ Released on 2023-05-11. This version also includes the changes from unstable b1 version listed below. Bug fixes: * `#1864 `_: Handle overflow for svg and symbol tags in SVG images * `#1867 `_: Remove duplicate compression of attachments * `d0ad5c1 `_: Override use tag children instead of drawing their references * `93df1a5 `_: Don’t resize the same image twice when the --dpi option is set * `#1874 `_: Drawn underline and overline behind text Contributors: * Guillaume Ayoub * Timo Ramsauer * Alexander Mankuta Backers and sponsors: * Castedo Ellerman * Kobalt * Spacinov * Grip Angebotssoftware * Crisp BV * Manuel Barkhau * SimonSoft * Menutech * KontextWork * NCC Group * René Fritz * Moritz Mahringer * Yanal-Yvez Fargialla * Piotr Horzycki * Healthchecks.io * TrainingSparkle * Hammerbacher * Synapsium Version 59.0b1 -------------- Released on 2023-04-14. **This version is experimental, don't use it in production. If you find bugs, please report them!** Command-line API: * The ``--optimize-size`` option and its short equivalent ``-O`` have been deprecated. To activate or deactivate different size optimizations, you can now use: * ``--uncompressed-pdf``, * ``--optimize-images``, * ``--full-fonts``, * ``--hinting``, * ``--dpi ``, and * ``--jpeg-quality ``. * A new ``--cache-folder `` option has been added to store temporary data in the given folder on the disk instead of keeping them in memory. Python API: * Global rendering options are now given in ``**options`` instead of dedicated parameters, with slightly different names. It means that the signature of the ``HTML.render()``, ``HTML.write_pdf()`` and ``Document.write_pdf()`` has changed. Here are the steps to port your Python code to v59.0: 1. Use named parameters for these functions, not positioned parameters. 2. Rename some the parameters: * ``image_cache`` becomes ``cache`` (see below), * ``identifier`` becomes ``pdf_identifier``, * ``variant`` becomes ``pdf_variant``, * ``version`` becomes ``pdf_version``, * ``forms`` becomes ``pdf_forms``. * The ``optimize_size`` parameter of ``HTML.render()``, ``HTML.write_pdf()`` and ``Document()`` has been removed and will be ignored. You can now use the ``uncompressed_pdf``, ``full_fonts``, ``hinting``, ``dpi`` and ``jpeg_quality`` parameters that are included in ``**options``. * The ``cache`` parameter can be included in ``**options`` to replace ``image_cache``. If it is a dictionary, this dictionary will be used to store temporary data in memory, and can be even shared between multiple documents. If it’s a folder Path or string, WeasyPrint stores temporary data in the given temporary folder on disk instead of keeping them in memory. New features: * `#1853 `_, `#1854 `_: Reduce PDF size, with financial support from Code & Co. * `#1824 `_, `#1829 `_: Reduce memory use for images * `#1858 `_: Add an option to keep hinting information in embedded fonts Bug fixes: * `#1855 `_: Fix position of emojis in justified text * `#1852 `_: Don’t crash when line can be split before trailing spaces * `#1843 `_: Fix syntax of dates in metadata * `#1827 `_, `#1832 `_: Fix word-spacing problems with nested tags Documentation: * `#1841 `_: Add a paragraph about unsupported calc() function Contributors: * Guillaume Ayoub * Lucie Anglade * Alex Ch * whi_ne * Jonas Castro Backers and sponsors: * Castedo Ellerman * Kobalt * Spacinov * Grip Angebotssoftware * Crisp BV * Manuel Barkhau * SimonSoft * Menutech * KontextWork * NCC Group * René Fritz * Moritz Mahringer * Yanal-Yvez Fargialla * Piotr Horzycki * Healthchecks.io * TrainingSparkle * Hammerbacher * Synapsium Version 58.1 ------------ Released on 2023-03-07. Bug fixes: * `#1815 `_: Fix bookmarks coordinates * `#1822 `_, `#1823 `_: Fix vertical positioning for absolute replaced elements Documentation: * `#1814 `_: Fix broken link pointing to samples Contributors: * Guillaume Ayoub * Jonas Castro * Lucie Anglade * Menelaos Kotoglou Backers and sponsors: * Kobalt * Grip Angebotssoftware * Spacinov * Crisp BV * Castedo Ellerman * Manuel Barkhau * SimonSoft * Menutech * KontextWork * NCC Group * René Fritz * Moritz Mahringer * Yanal-Yvez Fargialla * Piotr Horzycki * Healthchecks.io * Hammerbacher * TrainingSparkle * Synapsium Version 58.0 ------------ Released on 2023-02-17. This version also includes the changes from unstable b1 version listed below. Bug fixes: * `#1807 `_: Don’t crash when out-of-flow box is split in out-of-flow parent * `#1806 `_: Don’t crash when fixed elements aren’t displayed yet in aborted line * `#1809 `_: Fix background drawing for out-of-the-page transformed boxes Contributors: * Guillaume Ayoub Backers and sponsors: * Kobalt * Grip Angebotssoftware * Crisp BV * Spacinov * Castedo Ellerman * Manuel Barkhau * SimonSoft * Menutech * KontextWork * NCC Group * René Fritz * Moritz Mahringer * Yanal-Yvez Fargialla * Piotr Horzycki * Healthchecks.io Version 58.0b1 -------------- Released on 2023-02-03. **This version is experimental, don't use it in production. If you find bugs, please report them!** New features: * `#61 `_, `#1796 `_: Support PDF forms, with financial support from Personalkollen * `#1173 `_: Add style for form fields Bug fixes: * `#1777 `_: Detect JPEG/MPO images as normal JPEG files * `#1771 `_: Improve SVG gradients Contributors: * Guillaume Ayoub * Lucie Anglade Backers and sponsors: * Kobalt * Grip Angebotssoftware * Crisp BV * Spacinov * Castedo Ellerman * Manuel Barkhau * SimonSoft * Menutech * KontextWork * NCC Group * René Fritz * Moritz Mahringer * Yanal-Yvez Fargialla * Piotr Horzycki * Healthchecks.io Version 57.2 ------------ Released on 2022-12-23. Bug fixes: * `0f2e377 `_: Print annotations with PDF/A * `0e9426f `_: Hide annotations with PDF/UA * `#1764 `_: Use reference instead of stream for annotation appearance stream * `#1783 `_: Fix multiple font weights for @font-face declarations Contributors: * Guillaume Ayoub Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * Spacinov * KontextWork * René Fritz * NCC Group * Kobalt * Tom Pohl * Castedo Ellerman * Moritz Mahringer * Piotr Horzycki * Gábor Nyers * Sidharth Kapur Version 57.1 ------------ Released on 2022-11-04. Dependencies: * `#1754 `_: Pillow 9.1.0 is now needed Bug fixes: * `#1756 `_: Fix rem font size for SVG images * `#1755 `_: Keep format when transposing images * `#1753 `_: Don’t use deprecated ``read_text`` function when ``files`` is available * `#1741 `_: Generate better manpage * `#1747 `_: Correctly set target counters in pages’ absolute elements * `#1748 `_: Always set font size when font is changed in line * `2b05137 `_: Fix stability of font identifiers Documentation: * `#1750 `_: Fix documentation spelling Contributors: * Guillaume Ayoub * Eli Schwartz * Mikhail Anikin * Scott Kitterman Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * Spacinov * KontextWork * René Fritz * NCC Group * Kobalt * Tom Pohl * John R Ellis * Castedo Ellerman * Moritz Mahringer * Gábor * Piotr Horzycki Version 57.0 ------------ Released on 2022-10-18. This version also includes the changes from unstable b1 version listed below. New features: * `a4fc7a1 `_: Support image-orientation Bug fixes: * `#1739 `_: Set baseline on all flex containers * `#1740 `_: Don’t crash when currentColor is set on root svg tag * `#1718 `_: Don’t crash with empty bitmap glyphs * `#1736 `_: Always use the font’s vector variant when possible * `eef8b4d `_: Always set color and state before drawing * `#1662 `_: Use a stable key to store stream fonts * `#1733 `_: Don’t remove attachments when adding internal anchors * `3c4fa50 `_, `c215697 `_, `d275dac `_, `b04bfff `_: Fix many bugs related to PDF/UA structure Performance: * `dfccf1b `_: Use faces as fonts dictionary keys * `0dc12b6 `_: Cache add_font to avoid calling get_face too often * `75e17bf `_: Don’t call process_whitespace twice on many children * `498d3e1 `_: Optimize __missing__ functions Documentation: * `863b3d6 `_: Update documentation of installation on macOS with Homebrew Contributors: * Guillaume Ayoub Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * Spacinov * KontextWork * René Fritz * NCC Group * Kobalt * Tom Pohl * John R Ellis * Castedo Ellerman * Moritz Mahringer * Gábor * Piotr Horzycki Version 57.0b1 -------------- Released on 2022-09-22. **This version is experimental, don't use it in production. If you find bugs, please report them!** New features: * `#1704 `_: Support PDF/UA, with financial support from Novareto * `#1454 `_: Support variable fonts Bug fixes: * `#1058 `_: Fix bullet position after page break, with financial support from OpenZeppelin * `#1707 `_: Fix footnote positioning in multicolumn layout, with financial support from Code & Co. * `#1722 `_: Handle skew transformation with only one parameter * `#1715 `_: Don’t crash when images are truncated * `#1697 `_: Don’t crash when attr() is used in text-decoration-color * `#1695 `_: Include language information in PDF metadata * `#1612 `_: Don’t lowercase letters when capitalizing text * `#1700 `_: Fix crash when rendering footnote with repagination * `#1667 `_: Follow EXIF metadata for image rotation * `#1669 `_: Take care of floats when remvoving placeholders * `#1638 `_: Use the original box when breaking waiting children Contributors: * Guillaume Ayoub * Konstantin Weddige * VeteraNovis * Lucie Anglade Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * Spacinov * KontextWork * René Fritz * NCC Group * Kobalt * Tom Pohl * John R Ellis * Moritz Mahringer * Gábor * Piotr Horzycki * Andrew Ittner Version 56.1 ------------ Released on 2022-07-24. Bug fixes: * `#1674 `_: Follow max-height on footnot area, with financial support from Code & Co. * `#1678 `_: Fix gradients with opacity set Contributors: * Guillaume Ayoub * Lucie Anglade Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * Spacinov * KontextWork * René Fritz * NCC Group * Kobalt * Tom Pohl * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * Gábor * Piotr Horzycki * Andrew Ittner Version 56.0 ------------ Released on 2022-07-07. This version also includes the changes from unstable b1 version listed below. New features: * `70f9b62 `_: Support format 5 for bitmap glyphs Bug fixes: * `#1666 `_ Fix reproducible PDF generation with embedded images * `#1668 `_: Fix @page:nth() selector * `3bd9a8e `_: Don’t limit the opacity groups to the original box size * `cb9540b `_, `76d174f `_, `9ce6547 `_: Minor bugfixes for split table rows Contributors: * Guillaume Ayoub Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * Spacinov * KontextWork * René Fritz * NCC Group * Kobalt * Des images et des mots * Andreas Zettl * Tom Pohl * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * Gábor * Piotr Horzycki Version 56.0b1 -------------- Released on 2022-06-17. **This version is experimental, don't use it in production. If you find bugs, please report them!** Dependencies: * pydyf 0.2.0+ is now needed New features: * `#1660 `_: Support nested line-clamp, with financial support from Expert Germany * `#1644 `_, `#1645 `_: Support bitmap fonts, with financial support from Expert Germany * `#1651 `_, `#630 `_: Support PDF/A, with financial support from Blueshoe Bug fixes: * `#1656 `_: Fix chained variables in the same selector block * `#1028 `_: Fix font weight management in @font-face rules * `#1653 `_: Don’t crash when @font-face’s src ends with a comma * `#1650 `_: Don’t check origin when URL only contains fragment * `e38bff8 `_: Don’t crash when inherited SVG attributes are not set on the parent Performance: * `e6021da `_: Launch tests in parallel by default Contributors: * Guillaume Ayoub * aschmitz * Lucie Anglade Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * Spacinov * KontextWork * René Fritz * NCC Group * Kobalt * Des images et des mots * Andreas Zettl * Tom Pohl * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * Gábor * Piotr Horzycki Version 55.0 ------------ Released on 2022-05-12. This version also includes the changes from unstable b1 version listed below. Bug fixes: * `#1626 `_, `3802f88 `_: Fix the vertical position and available height of absolute boxes * `9641098 `_, `e5e6b88 `_: Minor fixes for multi-column layout * `0fcc7de `_: Don’t stop rendering SVG when CSS parsing fails * `#1636 `_: Fix sequential footnotes that could disappear when overflowing * `#1637 `_: Fix position of absolute boxes with right-to-left direction * `#1641 `_: Fix relative paths for SVG files stored as data URLs Contributors: * Guillaume Ayoub * aschmitz Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * Spacinov * KontextWork * René Fritz * NCC Group * Kobalt * Nathalie Gutton * Andreas Zettl * Tom Pohl * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * Gábor * Piotr Horzycki Version 55.0b1 -------------- Released on 2022-04-15. **This version is experimental, don't use it in production. If you find bugs, please report them!** Dependencies: * Python 3.7+ is now needed, Python 3.6 is not supported anymore New features: * `#1534 `_: Support ``word-break: break-all`` * `#489 `_, `#1619 `_: Support column breaks * `#1553 `_: Allow reproducible PDF generation Bug fixes: * `#1007 `_, `#1524 `_: Handle ``inherit`` in shorthand properties * `#1539 `_, `#1541 `_: Space out no-repeat patterns * `#1554 `_: Avoid invalid PDF operators when drawing SVG text * `#1564 `_, `#1566 `_, `#1570 `_: Don’t output footnotes before their call sites * `#1020 `_, `#1597 `_: Prevent infinite loops in multi-column layout * `#1512 `_, `#1613 `_: Fix position of absolute boxes in right-to-left contexts * `#1093 `_: Draw borders around absolute replaced boxes * `#984 `_, `#1604 `_: Fix skip stacks for columns * `#1621 `_: Better support of nested ``text-decoration`` properties * `fe1f3d9 `_: Fix absolute blocks in lines * `4650b70 `_: Clear adjoining margins when a container’s child doesn’t fit Performance: * `#1548 `_: Improve tests speed * `3b0ae92 `_, `#1457 `_: Improve fonts management * `#1597 `_: Improve column layout speed * `#1587 `_, `#1607 `_, `#1608 `_: Cache ``ch`` and ``ex`` units calculations Contributors: * Guillaume Ayoub * aschmitz * Lucie Anglade * Christoph Kepper * Jack Lin * Rian McGuire Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * KontextWork * Maykin Media * René Fritz * NCC Group * Spacinov * Nathalie Gutton * Andreas Zettl * Tom Pohl * Kobalt * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * Gábor * Piotr Horzycki * DeivGuerrero Version 54.3 ------------ Released on 2022-04-04. Bug fixes: * `#1588 `_: Support position: absolute in footnotes * `#1586 `_: Fix discarded text-align values Contributors: * aschmitz * Guillaume Ayoub Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * KontextWork * Maykin Media * René Fritz * NCC Group * Spacinov * Nathalie Gutton * Andreas Zettl * Tom Pohl * Kobalt * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * Gábor * Piotr Horzycki * DeivGuerrero Version 54.2 ------------ Released on 2022-02-27. Bug fixes: * `#1575 `_: Always store parent blocks children as lists * `#1574 `_, `#1559 `_: Fix float rounding errors * `#1571 `_: Ignore unknown glyphs * `#1561 `_, `#1562 `_: Fix line break when breaks occur between a nbsp and an inline block * `#1560 `_: Always set the child index * `#1558 `_: Fix patterns with use tags Contributors: * Guillaume Ayoub * Lucie Anglade * Jack Lin * aschmitz Backers and sponsors: * Grip Angebotssoftware * Manuel Barkhau * Crisp BV * SimonSoft * Menutech * KontextWork * Maykin Media * René Fritz * NCC Group * Spacinov * Nathalie Gutton * Andreas Zettl * Tom Pohl * Kobalt * Moritz Mahringer * Florian Demmer * Yanal-Yvez Fargialla * Gábor * Piotr Horzycki * DeivGuerrero Version 54.1 ------------ Released on 2022-01-31. New 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 alignment 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 recommended 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](https://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 dictionaries 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: https://packages.python.org/tinycss/ .. _cssselect: https://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. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703402.7972314 weasyprint-62.3/docs/common_use_cases.rst0000644000000000000000000001122414634252453015613 0ustar00Common 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: https://flask.palletsprojects.com/ .. _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/ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1718704578.328362 weasyprint-62.3/docs/conf.py0000644000000000000000000000535114634254702013041 0ustar00# 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, } # Favicon URL html_favicon = 'https://www.courtbouillon.org/static/images/favicon.png' # 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 = [ ('manpage', 'weasyprint', 'The Awesome Document Factory', ['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 and contributors', '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), } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1718704578.328362 weasyprint-62.3/docs/contribute.rst0000644000000000000000000000405214634254702014447 0ustar00Contribute ========== 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. Tests require Ghostscript_ to be installed and available on the local path. You should also install all the `DejaVu fonts`_ if you’re on Linux. You can launch tests using the following command:: venv/bin/python -m pytest WeasyPrint also uses ruff_ to check the coding style:: venv/bin/python -m ruff check .. _pytest: https://docs.pytest.org/ .. _Ghostscript: https://www.ghostscript.com/ .. _DejaVu fonts: https://dejavu-fonts.github.io/ .. _ruff: https://docs.astral.sh/ruff/ 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/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4844034 weasyprint-62.3/docs/first_steps.rst0000644000000000000000000005645114635317053014650 0ustar00First Steps =========== .. currentmodule:: weasyprint Installation ------------ WeasyPrint |version| depends on: * Python_ ≥ 3.9.0 * Pango_ ≥ 1.44.0 * pydyf_ ≥ 0.10.0 * CFFI_ ≥ 0.6 * html5lib_ ≥ 1.1 * tinycss2_ ≥ 1.3.0 * cssselect2_ ≥ 0.1 * Pyphen_ ≥ 0.9.1 * Pillow_ ≥ 9.1.0 * fontTools_ ≥ 4.0.0 .. _Python: https://www.python.org/ .. _Pango: https://pango.gnome.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: https://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.7.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.14 +++++++++++++ To install WeasyPrint using your distribution’s package:: apk add weasyprint 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 using your distribution’s package:: pacman -S python-weasyprint 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 using your distribution’s package:: apt install weasyprint 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 using your distribution’s package:: dnf install weasyprint To install WeasyPrint inside a virtualenv using wheels (if possible), you need the following packages:: dnf install python-pip pango To install WeasyPrint inside a virtualenv without using wheels, you need the following packages:: dnf install python3-pip pango gcc python3-devel gcc-c++ zlib-devel libjpeg-devel openjpeg2-devel libffi-devel Ubuntu ≥ 20.04 ++++++++++++++ To install WeasyPrint using your distribution’s package:: apt install weasyprint 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_:: brew install weasyprint .. _Homebrew: https://brew.sh/ Windows ~~~~~~~ To use WeasyPrint on Windows, the easiest way is to use the `executable`_ of the latest release. If you want to use WeasyPrint as a Python library, you’ll have to follow a few extra steps. Please read this chapter carefully. 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. You can then launch a command prompt by clicking on the Start menu, typing "cmd" and clicking the "Command Prompt" icon. 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 .. _executable: https://github.com/Kozea/WeasyPrint/releases .. _Microsoft Store: https://apps.microsoft.com/store/search/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 https://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 https://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('https://weasyprint.org/').write_pdf('/tmp/weasyprint-website.pdf') … or with the inline stylesheet: .. code-block:: python from weasyprint import HTML, CSS HTML('https://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('https://weasyprint.org') # Same as … HTML(url='https://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(https://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 https://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(',')) string = generate_graph(graph_data) return {'string': string, '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 retrieve data. .. _Flask-Weasyprint: https://github.com/Kozea/Flask-WeasyPrint .. _Flask: https://flask.pocoo.org/ .. _Django-WeasyPrint: https://github.com/fdemmer/django-weasyprint .. _Django: https://www.djangoproject.com/ Image Cache and Optimization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ WeasyPrint provides many options to deal with images: ``optimize_images``, ``jpeg_quality``, ``dpi`` and ``image_cache``. ``optimize_images`` can enable size optimization for images. When enabled, the generated PDF will include smaller images with no quality penalty, but the rendering time may be slightly increased. The ``jpeg_quality`` option can be set to decrease the quality of JPEG images included in the PDF. You can set a value between 95 (best quality) to 0 (smaller image size), depending on your needs. The ``dpi`` option offers the possibility to reduce the size (in pixels, and thus in bytes) of all included raster images. The resolution, set in dots per inch, indicates the maximum number of pixels included in one inch on the generated PDF. .. code-block:: python # Original high-quality images, faster, but generated PDF is larger HTML('https://weasyprint.org/').write_pdf('weasyprint.pdf') # Optimized lower-quality images, a bit slower, but generated PDF is smaller HTML('https://weasyprint.org/').write_pdf( 'weasyprint.pdf', optimize_images=True, jpeg_quality=60, dpi=150) ``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'https://weasyprint.org/').write_pdf( f'example-{i}.pdf', image_cache=cache) It’s also possible to cache images on disk instead of keeping them in memory. The ``--cache-folder`` CLI option can be used to define the folder used to store temporary images. You can also provide this folder path as a string for ``image_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 ``https://`` 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 ``https://`` 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. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3293622 weasyprint-62.3/docs/going_further.rst0000644000000000000000000002674714634254702015152 0ustar00Going 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: https://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: https://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: https://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/main/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: https://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: https://www.w3.org/TR/CSS21/cascade.html#used-value .. _CSS transform: https://www.w3.org/TR/css-transforms-1/ 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: https://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. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1736472 weasyprint-62.3/docs/index.rst0000644000000000000000000000046414564467510013410 0ustar00WeasyPrint ========== .. 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3293622 weasyprint-62.3/docs/manpage.rst0000644000000000000000000000026414634254702013702 0ustar00:orphan: .. currentmodule:: weasyprint Description ----------- .. autofunction:: weasyprint.__main__.main(argv=sys.argv) :noindex: About ----- .. include:: ../README.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1746473 weasyprint-62.3/docs/support.rst0000644000000000000000000000156214564467510014015 0ustar00Support ======= 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3293622 weasyprint-62.3/pyproject.toml0000644000000000000000000000435714634254702013533 0ustar00[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.9' readme = {file = 'README.rst', content-type = 'text/x-rst'} license = {file = 'LICENSE'} dependencies = [ 'pydyf >=0.10.0', 'cffi >=0.6', 'html5lib >=1.1', 'tinycss2 >=1.3.0', 'cssselect2 >=0.1', 'Pyphen >=0.9.1', 'Pillow >=9.1.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.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', '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', 'ruff'] [project.scripts] weasyprint = 'weasyprint.__main__:main' [tool.flit.sdist] exclude = ['.*'] [tool.coverage.run] branch = true include = ['tests/*', 'weasyprint/*'] [tool.coverage.report] exclude_lines = ['pragma: no cover', 'def __repr__', 'raise NotImplementedError'] omit = ['.*'] [tool.ruff.lint] select = ['E', 'W', 'F', 'I', 'N', 'RUF'] ignore = ['RUF001', 'RUF002', 'RUF003'] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703377.2049472 weasyprint-62.3/tests/__init__.py0000644000000000000000000000004114634252421014050 0ustar00"""The Weasyprint test suite.""" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1718704578.330362 weasyprint-62.3/tests/conftest.py0000644000000000000000000000676214634254702014162 0ustar00"""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 from . import draw MAGIC_NUMBER = b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a' def document_write_png(document, 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: document.write_pdf(pdf, zoom=zoom) command = ( 'gs', '-q', '-sDEVICE=png16m', f'-dTextAlphaBits={antialiasing}', f'-dGraphicsAlphaBits={antialiasing}', '-dBATCH', '-dNOPAUSE', '-dPDFSTOPONERROR', f'-r{resolution / zoom}', '-sOutputFile=-', pdf.name) pngs = run(command, stdout=PIPE).stdout os.remove(pdf.name) error = pngs.split(MAGIC_NUMBER)[0].decode().strip() or 'no output' assert pngs.startswith(MAGIC_NUMBER), f'Ghostscript error: {error}' 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(document, target=None, font_config=None, counter_style=None, resolution=96, **options): document = document.render(font_config, counter_style, **options) return document.write_png(target, resolution) Document.write_png = document_write_png HTML.write_png = html_write_png def test_filename(filename): return ''.join( character if character.isalnum() else '_' for character in filename[5:50]).rstrip('_') @pytest.fixture def assert_pixels(request, *args, **kwargs): return lambda *args, **kwargs: draw.assert_pixels( test_filename(request.node.name), *args, **kwargs) @pytest.fixture def assert_same_renderings(request, *args, **kwargs): return lambda *args, **kwargs: draw.assert_same_renderings( test_filename(request.node.name), *args, **kwargs) @pytest.fixture def assert_different_renderings(request, *args, **kwargs): return lambda *args, **kwargs: draw.assert_different_renderings( test_filename(request.node.name), *args, **kwargs) @pytest.fixture def assert_pixels_equal(request, *args, **kwargs): return lambda *args, **kwargs: draw.assert_pixels_equal( test_filename(request.node.name), *args, **kwargs) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1718704578.330362 weasyprint-62.3/tests/css/__init__.py0000644000000000000000000000003114634254702014643 0ustar00"""Test CSS features.""" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4844034 weasyprint-62.3/tests/css/test_common.py0000644000000000000000000001512514635317053015445 0ustar00"""Test the CSS parsing, cascade, inherited and computed values.""" import pytest from weasyprint import CSS, default_url_fetcher from weasyprint.css import find_stylesheets, get_all_computed_styles from weasyprint.urls import path2url from ..testing_utils import ( # isort:skip BASE_URL, FakeHTML, assert_no_logs, capture_logs, resource_path) @assert_no_logs def test_find_stylesheets(): html = FakeHTML(resource_path('doc1.html')) sheets = list(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. sheet_names = [ sheet.base_url.rsplit('/', 1)[-1].rsplit(',', 1)[-1] for sheet in sheets] assert sheet_names == ['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_annotate_document(): document = FakeHTML(resource_path('doc1.html')) document._ua_stylesheets = ( lambda *_, **__: [CSS(resource_path('mini_ua.css'))]) style_for = get_all_computed_styles( document, user_stylesheets=[CSS(resource_path('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_path('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_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 @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 @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) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1718704578.330362 weasyprint-62.3/tests/css/test_counters.py0000644000000000000000000004605214634254702016022 0ustar00"""Test CSS counters.""" import pytest from ..testing_utils import ( # isort:skip FakeHTML, assert_no_logs, assert_tree, parse_all, render_pages) RENDER = FakeHTML(string='')._ua_counter_style()[0].render_value @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')])])])])]) @pytest.mark.xfail @assert_no_logs def test_counters_9(): page, = render_pages('''
''') html, = page._page_box.children body, = html.children ol1, = body.children oli1, oli2, oli3 = ol1.children marker, ol2 = oli2.children oli21, oli22 = ol2.children assert oli1.children[0].children[0].children[0].text == '1. ' assert oli2.children[0].children[0].children[0].text == '2. ' assert oli21.children[0].children[0].children[0].text == '1. ' assert oli22.children[0].children[0].children[0].text == '2. ' assert oli3.children[0].children[0].children[0].text == '3. ' @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(): 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(): 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(): 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(): 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(): 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(): 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(): 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(): 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(): 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. ' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1718704578.330362 weasyprint-62.3/tests/css/test_descriptors.py0000644000000000000000000001724714634254702016525 0ustar00"""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(https://example.com/fonts/Gentium.woff);' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src = list(preprocess_descriptors( 'font-face', 'https://weasyprint.org/foo/', tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ( 'src', (('external', 'https://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', 'https://weasyprint.org/foo/', tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Fonty Smiley') assert src == ( 'src', (('external', 'https://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', 'https://weasyprint.org/foo/', tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', None),)) @assert_no_logs def test_font_face_4(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/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', 'https://weasyprint.org/foo/', tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', 'Gentium Hard'),)) @assert_no_logs def test_font_face_5(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1653 stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: Gentium Hard;' ' src: local(Gentium Hard);' ' src: local(Gentium Soft),' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' with capture_logs() as logs: font_family, src = list(preprocess_descriptors( 'font-face', 'https://weasyprint.org/foo/', tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', 'Gentium Hard'),)) assert len(logs) == 1 assert 'invalid value' in logs[0] 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', 'https://weasyprint.org/foo/', tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Bad Font') assert src == ( 'src', (('external', 'https://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: preprocess_stylesheet( 'print', 'https://wp.org/foo/', stylesheet, None, None, None, None, None) 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: preprocess_stylesheet( 'print', 'https://wp.org/foo/', stylesheet, None, None, None, None, None) 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: preprocess_stylesheet( 'print', 'https://wp.org/foo/', stylesheet, None, None, None, None, None) 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: preprocess_stylesheet( 'print', 'https://wp.org/foo/', stylesheet, None, None, None, None, None) 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: preprocess_stylesheet( 'print', 'https://wp.org/foo/', stylesheet, None, None, None, None, None) 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: preprocess_stylesheet( 'print', 'https://wp.org/foo/', stylesheet, None, None, None, None, None) 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: preprocess_stylesheet( 'print', 'https://wp.org/foo/', stylesheet, None, None, None, None, {}) assert len(logs) >= 1 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1718704578.330362 weasyprint-62.3/tests/css/test_errors.py0000644000000000000000000000277214634254702015475 0ustar00"""Test CSS errors and warnings.""" import pytest from weasyprint import CSS from ..testing_utils import assert_no_logs, capture_logs, render_pages @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): 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(): with capture_logs() as logs: render_pages('') assert len(logs) == 1 assert 'ERROR: Failed to load stylesheet at' in logs[0] @assert_no_logs @pytest.mark.parametrize('style', ( '

''') html, = page.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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3313622 weasyprint-62.3/tests/css/test_nesting.py0000644000000000000000000000142114634254702015616 0ustar00"""Test CSS nesting.""" import pytest from ..testing_utils import assert_no_logs, render_pages @assert_no_logs @pytest.mark.parametrize('style', ( 'div { p { width: 10px } }', 'p { div & { width: 10px } }', 'p { width: 20px; div & { width: 10px } }', 'p { div & { width: 10px } width: 20px }', 'div { & { & { p { & { width: 10px } } } } }', '@media print { div { p { width: 10px } } }', 'div { em, p { width: 10px } }', 'p { a, div & { width: 10px } }', )) def test_nesting_block(style): page, = render_pages('''

''' % style) html, = page.children body, = html.children div, p = body.children div_p, = div.children assert div_p.width == 10 assert p.width != 10 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3313622 weasyprint-62.3/tests/css/test_pages.py0000644000000000000000000001444014634254702015253 0ustar00"""Test CSS pages.""" import pytest import tinycss2 from weasyprint import CSS from weasyprint.css import PageType, get_all_computed_styles, parse_page_selectors from weasyprint.layout.page import set_page_type_computed_styles from ..testing_utils import FakeHTML, assert_no_logs, render_pages, resource_path @assert_no_logs def test_page(): document = FakeHTML(resource_path('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 def test_named_pages(): page, = render_pages('''

a

''') html, = page.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' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3313622 weasyprint-62.3/tests/css/test_target.py0000644000000000000000000001463214634254702015445 0ustar00"""Test the CSS cross references using target-*() functions.""" from ..testing_utils import assert_no_logs, render_pages @assert_no_logs def test_target_counter(): page, = render_pages('''
''') html, = page.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(): page, = render_pages('''
''') html, = page.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(): page, = render_pages('''
''') html, = page.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(): page, = render_pages('''
1 Chapter 1
2 Chapter 2
3 Chapter 3
4 Chapter 4
''') html, = page.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(): page, = render_pages('''

abc

''') html, = page.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' @assert_no_logs def test_target_absolute(): page, = render_pages('''

abc

''') html, = page.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].text == '1' @assert_no_logs def test_target_absolute_non_root(): page, = render_pages('''

abc

''') html, = page.children body, = html.children section, h1 = body.children div, = section.children line, = div.children inline, = line.children text_box, after = inline.children assert text_box.text == 'link' assert after.children[0].text == '1' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4844034 weasyprint-62.3/tests/css/test_validation.py0000644000000000000000000010243214635317053016305 0ustar00"""Test validation of properties.""" from math import pi import pytest import tinycss2 from weasyprint.css import preprocess_declarations from weasyprint.css.validation.properties import PROPERTIES from weasyprint.images import LinearGradient, RadialGradient from ..testing_utils import assert_no_logs, capture_logs def get_value(css, expected_error=None): declarations = tinycss2.parse_blocks_contents(css) with capture_logs() as logs: base_url = 'https://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 if declarations: assert len(declarations) == 1 return declarations[0][1] def get_default_value(values, index, default): if index > len(values) - 1: return default return values[index] or default def assert_invalid(css, message='invalid'): assert get_value(css, message) is None @assert_no_logs def test_not_print(): assert_invalid('volume: 42', 'does not apply for the print media') @assert_no_logs def test_unstable_prefix(): assert get_value( '-weasy-max-lines: 3', 'prefixes on unstable attributes are deprecated') == 3 @assert_no_logs def test_normal_prefix(): assert_invalid( '-weasy-display: block', 'prefix on this attribute is not supported') @assert_no_logs def test_unknown_prefix(): assert_invalid('-unknown-display: block', 'prefixed selectors are ignored') @assert_no_logs @pytest.mark.parametrize('prop', PROPERTIES) def test_empty_property_value(prop): assert_invalid(f'{prop}:', message='Ignored') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('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_clip(rule, value): assert get_value(f'clip: rect({rule})') == value @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_clip_invalid(rule): assert_invalid(rule) @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('counter-reset: foo bar 2 baz', (('foo', 0), ('bar', 2), ('baz', 0))), ('counter-increment: foo bar 2 baz', (('foo', 1), ('bar', 2), ('baz', 1))), ('counter-reset: foo', (('foo', 0),)), ('counter-set: FoO', (('FoO', 0),)), ('counter-increment: foo bAr 2 Bar', (('foo', 1), ('bAr', 2), ('Bar', 1))), ('counter-reset: none', ()), )) def test_counters(rule, value): assert get_value(rule) == value @pytest.mark.parametrize('rule, warning', ( ('counter-reset: foo initial', 'Invalid counter name: initial.'), ('counter-reset: foo none', 'Invalid counter name: none.'), )) def test_counters_warning(rule, warning): assert_invalid(rule, warning) @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, value', ( ('letter-spacing: normal', 'normal'), ('letter-spacing: 3px', (3, 'px')), ('word-spacing: normal', 'normal'), ('word-spacing: 3px', (3, 'px')), )) def test_spacing(rule, value): assert get_value(rule) == value @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, value', ( ('none', 'none'), ('overline', {'overline'}), ('overline blink line-through', {'blink', 'line-through', 'overline'}), )) def test_text_decoration_line(rule, value): assert get_value(f'text-decoration-line: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('solid', 'solid'), ('double', 'double'), ('dotted', 'dotted'), ('dashed', 'dashed'), )) def test_text_decoration_style(rule, value): assert get_value(f'text-decoration-style: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('200px', ((200, 'px'), (200, 'px'))), ('200px 300pt', ((200, 'px'), (300, 'pt'))), ('auto', ((210, 'mm'), (297, 'mm'))), ('portrait', ((210, 'mm'), (297, 'mm'))), ('landscape', ((297, 'mm'), (210, 'mm'))), ('A3 portrait', ((297, 'mm'), (420, 'mm'))), ('A3 landscape', ((420, 'mm'), (297, 'mm'))), ('portrait A3', ((297, 'mm'), (420, 'mm'))), ('landscape A3', ((420, 'mm'), (297, 'mm'))), )) def test_size(rule, value): assert get_value(f'size: {rule}') == value @pytest.mark.parametrize('rule', ( 'A3 landscape A3', 'A12', 'foo', 'foo bar', '20%', )) def test_size_invalid(rule): assert_invalid(f'size: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('none', ()), ('translate(6px) rotate(90deg)', ( ('translate', ((6, 'px'), (0, 'px'))), ('rotate', pi / 2))), ('translate(-4px, 0)', (('translate', ((-4, 'px'), (0, None))),)), ('translate(6px, 20%)', (('translate', ((6, 'px'), (20, '%'))),)), ('scale(2)', (('scale', (2, 2)),)), ('translate(6px 20%)', (('translate', ((6, 'px'), (20, '%'))),)), )) def test_transform(rule, value): assert get_value(f'transform: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'lipsumize(6px)', 'foo', 'scale(2) foo', '6px', )) def test_transform_invalid(rule): assert_invalid(f'transform: {rule}') @pytest.mark.parametrize('rule', ( 'inexistent-gradient(blue, green)', )) def test_background_image_invalid(rule): assert_invalid(f'background-image: {rule}') @pytest.mark.parametrize('rule, value', ( # One token, vertical ('top', (('left', (50, '%'), 'top', (0, '%')),)), ('bottom', (('left', (50, '%'), 'top', (100, '%')),)), # Three tokens ('center top 10%', (('left', (50, '%'), 'top', (10, '%')),)), ('top 10% center', (('left', (50, '%'), 'top', (10, '%')),)), ('center bottom 10%', (('left', (50, '%'), 'bottom', (10, '%')),)), ('bottom 10% center', (('left', (50, '%'), 'bottom', (10, '%')),)), ('right top 10%', (('right', (0, '%'), 'top', (10, '%')),)), ('top 10% right', (('right', (0, '%'), 'top', (10, '%')),)), ('right bottom 10%', (('right', (0, '%'), 'bottom', (10, '%')),)), ('bottom 10% right', (('right', (0, '%'), 'bottom', (10, '%')),)), ('center left 10%', (('left', (10, '%'), 'top', (50, '%')),)), ('left 10% center', (('left', (10, '%'), 'top', (50, '%')),)), ('center right 10%', (('right', (10, '%'), 'top', (50, '%')),)), ('right 10% center', (('right', (10, '%'), 'top', (50, '%')),)), ('bottom left 10%', (('left', (10, '%'), 'bottom', (0, '%')),)), ('left 10% bottom', (('left', (10, '%'), 'bottom', (0, '%')),)), ('bottom right 10%', (('right', (10, '%'), 'bottom', (0, '%')),)), ('right 10% bottom', (('right', (10, '%'), 'bottom', (0, '%')),)), # Four tokens ('left 10% bottom 3px', (('left', (10, '%'), 'bottom', (3, 'px')),)), ('bottom 3px left 10%', (('left', (10, '%'), 'bottom', (3, 'px')),)), ('right 10% top 3px', (('right', (10, '%'), 'top', (3, 'px')),)), ('top 3px right 10%', (('right', (10, '%'), 'top', (3, 'px')),)), *tuple( (css_x, (('left', val_x, 'top', (50, '%')),)) for css_x, val_x in ( ('left', (0, '%')), ('center', (50, '%')), ('right', (100, '%')), ('4.5%', (4.5, '%')), ('12px', (12, 'px'))) ), *tuple( (f'{css_x} {css_y}', (('left', val_x, 'top', val_y),)) 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'))) ), )) def test_background_position(rule, value): assert get_value(f'background-position: {rule}') == value @pytest.mark.parametrize('rule', ( '10px lipsum', 'left center 3px', '3px left', 'bottom 4%', 'bottom top' )) def test_background_position_invalid(rule): assert_invalid(f'background-position: {rule}') @pytest.mark.parametrize('rule', ( ('"My" Font, serif'), ('"My" "Font", serif'), ('"My", 12pt, serif'), )) def test_font_family_invalid(rule): assert_invalid(f'font-family: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('1px', (1, 'px')), ('1.1%', (1.1, '%')), ('1em', (1, 'em')), ('1', (1, None)), ('1.3', (1.3, None)), ('-0', (0, None)), ('0px', (0, 'px')), )) def test_line_height(rule, value): assert get_value(f'line-height: {rule}') == value @pytest.mark.parametrize('rule', ( '1deg', '-1px', '-1', '-0.5%', '1px 1px', )) def test_line_height_invalid(rule): assert_invalid(f'line-height: {rule}') @assert_no_logs @pytest.mark.parametrize('rule', ( 'symbols()', 'symbols(cyclic)', 'symbols(symbolic)', 'symbols(fixed)', 'symbols(alphabetic "a")', 'symbols(numeric "1")', 'symbols(test "a" "b")', 'symbols(fixed symbolic "a" "b")', )) def test_list_style_type_invalid(rule): assert_invalid(f'list-style-type: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('none', 'none'), ('from-image', 'from-image'), ('90deg', (pi / 2, False)), ('30deg', (pi / 6, False)), ('180deg flip', (pi, True)), ('0deg flip', (0, True)), ('flip 90deg', (pi / 2, True)), ('flip', (0, True)), )) def test_image_orientation(rule, value): assert get_value(f'image-orientation: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'none none', 'unknown', 'none flip', 'from-image flip', '10', '10 flip', 'flip 10', 'flip flip', '90deg flop', '90deg 180deg', )) def test_image_orientation_invalid(rule): assert_invalid(f'image-orientation: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('1', ((1, None),)), ('1 2 3 4', ((1, None), (2, None), (3, None), (4, None))), ('50% 1000.1 0', ((50, '%'), (1000.1, None), (0, None))), ('1% 2% 3% 4%', ((1, '%'), (2, '%'), (3, '%'), (4, '%'))), ('fill 10% 20', ('fill', (10, '%'), (20, None))), ('0 1 0.5 fill', ((0, None), (1, None), (0.5, None), 'fill')), )) def test_border_image_slice(rule, value): assert get_value(f'border-image-slice: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'none', '1, 2', '-10', '-10%', '1 2 3 -10%', '-0.3', '1 fill 2', 'fill 1 2 3 fill', )) def test_border_image_slice_invalid(rule): assert_invalid(f'border-image-slice: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('1', ((1, None),)), ('1 2 3 4', ((1, None), (2, None), (3, None), (4, None))), ('50% 1000.1 0', ((50, '%'), (1000.1, None), (0, None))), ('1% 2px 3em 4', ((1, '%'), (2, 'px'), (3, 'em'), (4, None))), ('auto', ('auto',)), ('1 auto', ((1, None), 'auto')), ('auto auto', ('auto', 'auto')), ('auto auto auto 2', ('auto', 'auto', 'auto', (2, None))), )) def test_border_image_width(rule, value): assert get_value(f'border-image-width: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'none', '1, 2', '1 -2', '-10', '-10%', '1px 2px 3px -10%', '-3px', 'auto auto auto auto auto', '1 2 3 4 5', )) def test_border_image_width_invalid(rule): assert_invalid(f'border-image-width: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('1', ((1, None),)), ('1 2 3 4', ((1, None), (2, None), (3, None), (4, None))), ('50px 1000.1 0', ((50, 'px'), (1000.1, None), (0, None))), ('1in 2px 3em 4', ((1, 'in'), (2, 'px'), (3, 'em'), (4, None))), )) def test_border_image_outset(rule, value): assert get_value(f'border-image-outset: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'none', 'auto', '1, 2', '-10', '1 -2', '10%', '1px 2px 3px -10px', '-3px', '1 2 3 4 5', )) def test_border_image_outset_invalid(rule): assert_invalid(f'border-image-outset: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('stretch', ('stretch',)), ('repeat repeat', ('repeat', 'repeat')), ('round space', ('round', 'space')), )) def test_border_image_repeat(rule, value): assert get_value(f'border-image-repeat: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'none', 'test', 'round round round', 'stretch space round', 'repeat test', )) def test_border_image_repeat_invalid(rule): assert_invalid(f'border-image-repeat: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('test content(text)', (('test', (('content()', 'text'),)),)), ('test content(before)', (('test', (('content()', 'before'),)),)), ('test "string"', (('test', (('string', 'string'),)),)), ('test1 "string", test2 "string"', ( ('test1', (('string', 'string'),)), ('test2', (('string', 'string'),)))), ('test attr(class)', (('test', (('attr()', ('class', 'string', '')),)),)), ('test counter(count)', ( ('test', (('counter()', ('count', 'decimal')),)),)), ('test counter(count, upper-roman)', ( ('test', (('counter()', ('count', 'upper-roman')),)),)), ('test counters(count, ".")', ( ('test', (('counters()', ('count', '.', 'decimal')),)),)), ('test counters(count, ".", upper-roman)', ( ('test', (('counters()', ('count', '.', 'upper-roman')),)),)), ('test content(text) "string" attr(title) attr(title) counter(count)', ( ('test', ( ('content()', 'text'), ('string', 'string'), ('attr()', ('title', 'string', '')), ('attr()', ('title', 'string', '')), ('counter()', ('count', 'decimal')))),)), )) def test_string_set(rule, value): assert get_value(f'string-set: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'test', 'test test1', 'test content(test)', 'test unknown()', 'test attr(id, class)', )) def test_string_set_invalid(rule): assert_invalid(f'string-set: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('normal', 'normal'), ('break-word', 'break-word'), ('inherit', 'inherit'), )) def test_overflow_wrap(rule, value): assert get_value(f'overflow-wrap: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'none', 'normal, break-word', )) def test_overflow_wrap_invalid(rule): assert_invalid(f'overflow-wrap: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('blue', ()), ('red', (None, ((1, 0, 0, 1),))), ('blue 1%, lime,red 2em ', ( None, ((0, 0, 1, 1), (0, 1, 0, 1), (1, 0, 0, 1)), ((1, '%'), None, (2, 'em')))), ('18deg, blue', (('angle', pi / 10),)), ('4rad, blue', (('angle', 4),)), ('.25turn, blue', (('angle', pi / 2),)), ('100grad, blue', (('angle', pi / 2),)), ('12rad, blue 1%, lime,red 2em ', ( ('angle', 12), ((0, 0, 1, 1), (0, 1, 0, 1), (1, 0, 0, 1)), ((1, '%'), None, (2, 'em')))), ('to top, blue', (('angle', 0),)), ('to right, blue', (('angle', pi / 2),)), ('to bottom, blue', (('angle', pi),)), ('to left, blue', (('angle', pi * 3 / 2),)), ('to right, blue 1%, lime,red 2em ', ( ('angle', pi / 2), ((0, 0, 1, 1), (0, 1, 0, 1), (1, 0, 0, 1)), ((1, '%'), None, (2, 'em')))), ('to top left, blue', (('corner', 'top_left'),)), ('to left top, blue', (('corner', 'top_left'),)), ('to top right, blue', (('corner', 'top_right'),)), ('to right top, blue', (('corner', 'top_right'),)), ('to bottom left, blue', (('corner', 'bottom_left'),)), ('to left bottom, blue', (('corner', 'bottom_left'),)), ('to bottom right, blue', (('corner', 'bottom_right'),)), ('to right bottom, blue', (('corner', 'bottom_right'),)), )) def test_linear_gradient(rule, value): direction = get_default_value(value, 0, ('angle', pi)) colors = get_default_value(value, 1, ((0, 0, 1, 1),)) stop_positions = get_default_value(value, 2, (None,)) for repeating, prefix in ((False, ''), (True, 'repeating-')): (type_, image), = get_value( f'background-image: {prefix}linear-gradient({rule})') 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) @assert_no_logs @pytest.mark.parametrize('rule', ( ' ', '1% blue', 'blue 10deg', 'blue 4', 'soylent-green 4px', 'red 4px 2px', '18deg', '10arc-minutes, blue', '10px, blue', 'to 90deg, blue', 'to the top, blue', 'to up, blue', 'into top, blue', 'top, blue', 'to bottom up, blue', 'bottom left, blue', )) def test_linear_gradient_invalid(rule): assert_invalid(f'background-image: linear-gradient({rule})') assert_invalid(f'background-image: repeating-linear-gradient({rule})') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('blue', ()), ('red', (None, None, None, ((1, 0, 0, 1),))), ('blue 1%, lime,red 2em ', ( None, None, None, ((0, 0, 1, 1), (0, 1, 0, 1), (1, 0, 0, 1)), ((1, '%'), None, (2, 'em')))), ('circle, blue', ('circle',)), ('ellipse, blue', ()), ('ellipse closest-corner, blue', ( 'ellipse', ('keyword', 'closest-corner'))), ('circle closest-side, blue', ( 'circle', ('keyword', 'closest-side'))), ('farthest-corner circle, blue', ( 'circle', ('keyword', 'farthest-corner'))), ('farthest-side, blue', (None, (('keyword', 'farthest-side')))), ('5ch, blue', ('circle', ('explicit', ((5, 'ch'), (5, 'ch'))))), ('5ch circle, blue', ('circle', ('explicit', ((5, 'ch'), (5, 'ch'))))), ('circle 5ch, blue', ('circle', ('explicit', ((5, 'ch'), (5, 'ch'))))), ('10px 50px, blue', (None, ('explicit', ((10, 'px'), (50, 'px'))))), ('10px 50px ellipse, blue', ( None, ('explicit', ((10, 'px'), (50, 'px'))))), ('ellipse 10px 50px, blue', ( None, ('explicit', ((10, 'px'), (50, 'px'))))), ('10px 50px, blue', ( None, ('explicit', ((10, 'px'), (50, 'px'))))), ('at top 10% right, blue', ( None, None, ('right', (0, '%'), 'top', (10, '%')))), ('circle at bottom, blue', ( 'circle', None, ('left', (50, '%'), 'top', (100, '%')))), ('circle at 10px, blue', ( 'circle', None, ('left', (10, 'px'), 'top', (50, '%')))), ('closest-side circle at right 5em, blue', ( 'circle', ('keyword', 'closest-side'), ('left', (100, '%'), 'top', (5, 'em')))), )) def test_radial_gradient(rule, value): shape = get_default_value(value, 0, 'ellipse') size = get_default_value(value, 1, ('keyword', 'farthest-corner')) center = get_default_value(value, 2, ('left', (50, '%'), 'top', (50, '%'))) colors = get_default_value(value, 3, ((0, 0, 1, 1),)) stop_positions = get_default_value(value, 4, (None,)) for repeating, prefix in ((False, ''), (True, 'repeating-')): (type_, image), = get_value( f'background-image: {prefix}radial-gradient({rule})') 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) @assert_no_logs @pytest.mark.parametrize('rule', ( ' ', '1% blue', 'blue 10deg', 'blue 4', 'soylent-green 4px', 'red 4px 2px', 'circle', 'square, blue', 'closest-triangle, blue', 'center, blue', 'ellipse 5ch', '5ch ellipse', 'circle 10px 50px, blue', '10px 50px circle, blue', '10%, blue', '10% circle, blue', 'circle 10%, blue', 'at appex, blue', )) def test_radial_gradient_invalid(rule): assert_invalid(f'background-image: radial-gradient({rule})') assert_invalid(f'background-image: repeating-radial-gradient({rule})') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('40px', ((40, 'px'),)), ('2fr', ((2, 'fr'),)), ('18%', ((18, '%'),)), ('auto', ('auto',)), ('min-content', ('min-content',)), ('max-content', ('max-content',)), ('fit-content(20%)', (('fit-content()', (20, '%')),)), ('minmax(20px, 25px)', (('minmax()', (20, 'px'), (25, 'px')),)), ('minmax(min-content, max-content)', (('minmax()', 'min-content', 'max-content'),)), ('min-content max-content', ('min-content', 'max-content')), )) def test_grid_auto_columns_rows(rule, value): assert get_value(f'grid-auto-columns: {rule}') == value assert get_value(f'grid-auto-rows: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( '40', 'coucou', 'fit-content', 'fit-content(min-content)', 'minmax(40px)', 'minmax(2fr, 1fr)', '1fr 1fr coucou', 'fit-content()', 'fit-content(2%, 18%)', )) def test_grid_auto_columns_rows_invalid(rule): assert_invalid(f'grid-auto-columns: {rule}') assert_invalid(f'grid-auto-rows: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('row', ('row',)), ('column', ('column',)), ('row dense', ('row', 'dense')), ('column dense', ('column', 'dense')), ('dense row', ('dense', 'row')), ('dense column', ('dense', 'column')), ('dense', ('dense', 'row')), )) def test_grid_auto_flow(rule, value): assert get_value(f'grid-auto-flow: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'row row', 'column column', 'dense dense', 'coucou', 'row column', 'column row', 'row coucou', 'column coucou', 'coucou row', 'coucou column', 'row column dense', )) def test_grid_auto_flow_invalid(rule): assert_invalid(f'grid-auto-flow: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('none', 'none'), ('subgrid', ('subgrid', ())), ('subgrid [a] repeat(auto-fill, [b]) [c]', ('subgrid', (('a',), ('repeat()', 'auto-fill', (('b',),)), ('c',)))), ('subgrid [a] [a] [a] [a] repeat(auto-fill, [b]) [c] [c]', ('subgrid', (('a',), ('a',), ('a',), ('a',), ('repeat()', 'auto-fill', (('b',),)), ('c',), ('c',)))), ('subgrid [] [a]', ('subgrid', ((), ('a',)))), ('subgrid [a] [b] [c] [d] [e] [f]', ('subgrid', (('a',), ('b',), ('c',), ('d',), ('e',), ('f',)))), ('[outer-edge] 20px [main-start] 1fr [center] 1fr max-content [main-end]', (('outer-edge',), (20, 'px'), ('main-start',), (1, 'fr'), ('center',), (1, 'fr'), (), 'max-content', ('main-end',))), ('repeat(auto-fill, minmax(25ch, 1fr))', ((), ('repeat()', 'auto-fill', ( (), ('minmax()', (25, 'ch'), (1, 'fr')), ())), ())), ('[a] auto [b] minmax(min-content, 1fr) [b c d] ' 'repeat(2, [e] 40px) repeat(5, auto)', (('a',), 'auto', ('b',), ('minmax()', 'min-content', (1, 'fr')), ('b', 'c', 'd'), ('repeat()', 2, (('e',), (40, 'px'), ())), (), ('repeat()', 5, ((), 'auto', ())), ())), )) def test_grid_template_columns_rows(rule, value): assert get_value(f'grid-template-columns: {rule}') == value assert get_value(f'grid-template-rows: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'coucou', 'subgrid subgrid', 'subgrid coucou', 'subgrid [coucou] repeat(0, [wow])', 'subgrid [coucou] repeat(auto-fit [wow])', 'fit-content(18%) repeat(auto-fill, 15em)', '[coucou] [wow]', )) def test_grid_template_columns_rows_invalid(rule): assert_invalid(f'grid-template-columns: {rule}') assert_invalid(f'grid-template-rows: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('none', 'none'), ('"head head" "nav main" "foot ...."', (('head', 'head'), ('nav', 'main'), ('foot', None))), ('"title board" "stats board"', (('title', 'board'), ('stats', 'board'))), ('". a" "b a" ".a"', ((None, 'a'), ('b', 'a'), (None, 'a'))), )) def test_grid_template_areas(rule, value): assert get_value(f'grid-template-areas: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( '"head head coucou" "nav main" "foot ...."', '". a" "b c" ". a"', '". a" "b a" "a a"', '"a a a a" "a b b a" "a a a a"', '" "', )) def test_grid_template_areas_invalid(rule): assert_invalid(f'grid-template-areas: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('auto', 'auto'), ('4', (None, 4, None)), ('C', (None, None, 'c')), ('4 c', (None, 4, 'c')), ('col -4', (None, -4, 'col')), ('span c 4', ('span', 4, 'c')), ('span 4 c', ('span', 4, 'c')), ('4 span c', ('span', 4, 'c')), ('super 4 span', ('span', 4, 'super')), )) def test_grid_line(rule, value): assert get_value(f'grid-row-start: {rule}') == value assert get_value(f'grid-row-end: {rule}') == value assert get_value(f'grid-column-start: {rule}') == value assert get_value(f'grid-column-end: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'span', '0', '1.1', 'span 0', 'span -1', 'span 2.1', 'span auto', 'auto auto', '-4 cOL span', 'span 1.1 col', )) def test_grid_line_invalid(rule): assert_invalid(f'grid-row-start: {rule}') assert_invalid(f'grid-row-end: {rule}') assert_invalid(f'grid-column-start: {rule}') assert_invalid(f'grid-column-end: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('normal', ('normal',)), ('baseline', ('first', 'baseline')), ('first baseline', ('first', 'baseline')), ('last baseline', ('last', 'baseline')), ('baseline last', ('baseline', 'last')), ('space-between', ('space-between',)), ('space-around', ('space-around',)), ('space-evenly', ('space-evenly',)), ('stretch', ('stretch',)), ('center', ('center',)), ('start', ('start',)), ('end', ('end',)), ('flex-start', ('flex-start',)), ('flex-end', ('flex-end',)), ('safe center', ('safe', 'center')), ('unsafe start', ('unsafe', 'start')), ('safe end', ('safe', 'end')), ('safe flex-start', ('safe', 'flex-start')), ('unsafe flex-start', ('unsafe', 'flex-start')), )) def test_align_content(rule, value): assert get_value(f'align-content: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'auto', 'none', 'auto auto', 'first last', 'baseline baseline', 'start safe', 'start end', 'safe unsafe', 'left', 'right', )) def test_align_content_invalid(rule): assert_invalid(f'align-content: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('normal', ('normal',)), ('stretch', ('stretch',)), ('baseline', ('first', 'baseline')), ('first baseline', ('first', 'baseline')), ('last baseline', ('last', 'baseline')), ('baseline last', ('baseline', 'last')), ('center', ('center',)), ('self-start', ('self-start',)), ('self-end', ('self-end',)), ('start', ('start',)), ('end', ('end',)), ('flex-start', ('flex-start',)), ('flex-end', ('flex-end',)), ('safe center', ('safe', 'center')), ('unsafe start', ('unsafe', 'start')), ('safe end', ('safe', 'end')), ('unsafe self-start', ('unsafe', 'self-start')), ('safe self-end', ('safe', 'self-end')), ('safe flex-start', ('safe', 'flex-start')), ('unsafe flex-start', ('unsafe', 'flex-start')), )) def test_align_items(rule, value): assert get_value(f'align-items: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'auto', 'none', 'auto auto', 'first last', 'baseline baseline', 'start safe', 'start end', 'safe unsafe', 'left', 'right', 'space-between', )) def test_align_items_invalid(rule): assert_invalid(f'align-items: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('auto', ('auto',)), ('normal', ('normal',)), ('stretch', ('stretch',)), ('baseline', ('first', 'baseline')), ('first baseline', ('first', 'baseline')), ('last baseline', ('last', 'baseline')), ('baseline last', ('baseline', 'last')), ('center', ('center',)), ('self-start', ('self-start',)), ('self-end', ('self-end',)), ('start', ('start',)), ('end', ('end',)), ('flex-start', ('flex-start',)), ('flex-end', ('flex-end',)), ('safe center', ('safe', 'center')), ('unsafe start', ('unsafe', 'start')), ('safe end', ('safe', 'end')), ('unsafe self-start', ('unsafe', 'self-start')), ('safe self-end', ('safe', 'self-end')), ('safe flex-start', ('safe', 'flex-start')), ('unsafe flex-start', ('unsafe', 'flex-start')), )) def test_align_self(rule, value): assert get_value(f'align-self: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'none', 'auto auto', 'first last', 'baseline baseline', 'start safe', 'start end', 'safe unsafe', 'left', 'right', 'space-between', )) def test_align_self_invalid(rule): assert_invalid(f'align-self: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('normal', ('normal',)), ('space-between', ('space-between',)), ('space-around', ('space-around',)), ('space-evenly', ('space-evenly',)), ('stretch', ('stretch',)), ('center', ('center',)), ('left', ('left',)), ('right', ('right',)), ('start', ('start',)), ('end', ('end',)), ('flex-start', ('flex-start',)), ('flex-end', ('flex-end',)), ('safe center', ('safe', 'center')), ('unsafe start', ('unsafe', 'start')), ('safe end', ('safe', 'end')), ('unsafe left', ('unsafe', 'left')), ('safe right', ('safe', 'right')), ('safe flex-start', ('safe', 'flex-start')), ('unsafe flex-start', ('unsafe', 'flex-start')), )) def test_justify_content(rule, value): assert get_value(f'justify-content: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'auto', 'none', 'baseline', 'auto auto', 'first last', 'baseline baseline', 'start safe', 'start end', 'safe unsafe', )) def test_justify_content_invalid(rule): assert_invalid(f'justify-content: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('normal', ('normal',)), ('stretch', ('stretch',)), ('baseline', ('first', 'baseline')), ('first baseline', ('first', 'baseline')), ('last baseline', ('last', 'baseline')), ('baseline last', ('baseline', 'last')), ('center', ('center',)), ('self-start', ('self-start',)), ('self-end', ('self-end',)), ('start', ('start',)), ('end', ('end',)), ('left', ('left',)), ('right', ('right',)), ('flex-start', ('flex-start',)), ('flex-end', ('flex-end',)), ('safe center', ('safe', 'center')), ('unsafe start', ('unsafe', 'start')), ('safe end', ('safe', 'end')), ('unsafe self-start', ('unsafe', 'self-start')), ('safe self-end', ('safe', 'self-end')), ('safe flex-start', ('safe', 'flex-start')), ('unsafe flex-start', ('unsafe', 'flex-start')), ('legacy', ('legacy',)), ('legacy left', ('legacy', 'left')), ('left legacy', ('left', 'legacy')), ('legacy center', ('legacy', 'center')), )) def test_justify_items(rule, value): assert get_value(f'justify-items: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'auto', 'none', 'auto auto', 'first last', 'baseline baseline', 'start safe', 'start end', 'safe unsafe', 'space-between', )) def test_justify_items_invalid(rule): assert_invalid(f'justify-items: {rule}') @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('auto', ('auto',)), ('normal', ('normal',)), ('stretch', ('stretch',)), ('baseline', ('first', 'baseline')), ('first baseline', ('first', 'baseline')), ('last baseline', ('last', 'baseline')), ('baseline last', ('baseline', 'last')), ('center', ('center',)), ('self-start', ('self-start',)), ('self-end', ('self-end',)), ('start', ('start',)), ('end', ('end',)), ('left', ('left',)), ('right', ('right',)), ('flex-start', ('flex-start',)), ('flex-end', ('flex-end',)), ('safe center', ('safe', 'center')), ('unsafe start', ('unsafe', 'start')), ('safe end', ('safe', 'end')), ('unsafe left', ('unsafe', 'left')), ('safe right', ('safe', 'right')), ('unsafe self-start', ('unsafe', 'self-start')), ('safe self-end', ('safe', 'self-end')), ('safe flex-start', ('safe', 'flex-start')), ('unsafe flex-start', ('unsafe', 'flex-start')), )) def test_justify_self(rule, value): assert get_value(f'justify-self: {rule}') == value @assert_no_logs @pytest.mark.parametrize('rule', ( 'none', 'auto auto', 'first last', 'baseline baseline', 'start safe', 'start end', 'safe unsafe', 'space-between', )) def test_justify_self_invalid(rule): assert_invalid(f'justify-self: {rule}') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4844034 weasyprint-62.3/tests/css/test_variables.py0000644000000000000000000003576114635317053016135 0ustar00"""Test CSS custom properties, also known as CSS variables.""" import pytest from weasyprint.css.properties import KNOWN_PROPERTIES from ..testing_utils import assert_no_logs, capture_logs, render_pages SIDES = ('top', 'right', 'bottom', 'left') @assert_no_logs def test_variable_simple(): page, = render_pages('''

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

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

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

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

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

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

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

''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == 10 @assert_no_logs def test_variable_chain_root(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1656 page, = render_pages(''' ''') html, = page.children assert html.width == 10 def test_variable_self(): page, = render_pages(''' ''') def test_variable_loop(): page, = render_pages(''' ''') def test_variable_chain_root_missing(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1656 page, = render_pages(''' ''') def test_variable_chain_root_missing_inherited(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2164 page, = render_pages(''' a ''') @assert_no_logs def test_variable_shorthand_margin(): page, = render_pages('''
''') 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 @assert_no_logs def test_variable_shorthand_margin_multiple(): page, = render_pages('''
''') html, = page.children body, = html.children div, = body.children assert div.margin_top == 20 assert div.margin_right == 0 assert div.margin_bottom == 0 assert div.margin_left == 10 @assert_no_logs def test_variable_shorthand_margin_invalid(): with capture_logs() as logs: page, = render_pages('''
''') log, = logs assert 'invalid value' in log 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 == 0 @assert_no_logs def test_variable_shorthand_border(): page, = render_pages('''
''') html, = page.children body, = html.children div, = body.children assert div.border_top_width == 1 assert div.border_right_width == 1 assert div.border_bottom_width == 1 assert div.border_left_width == 1 @assert_no_logs def test_variable_shorthand_border_side(): page, = render_pages('''
''') html, = page.children body, = html.children div, = body.children assert div.border_top_width == 1 assert div.border_right_width == 0 assert div.border_bottom_width == 0 assert div.border_left_width == 0 @assert_no_logs def test_variable_shorthand_border_mixed(): page, = render_pages('''
''') html, = page.children body, = html.children div, = body.children assert div.border_top_width == 1 assert div.border_right_width == 1 assert div.border_bottom_width == 1 assert div.border_left_width == 1 def test_variable_shorthand_border_mixed_invalid(): with capture_logs() as logs: page, = render_pages('''
''') # TODO: we should only get one warning here assert 'multiple color values' in logs[0] html, = page.children body, = html.children div, = body.children assert div.border_top_width == 0 assert div.border_right_width == 0 assert div.border_bottom_width == 0 assert div.border_left_width == 0 @assert_no_logs @pytest.mark.parametrize('var, background', ( ('blue', 'var(--v)'), ('padding-box url(pattern.png)', 'var(--v)'), ('padding-box url(pattern.png)', 'white var(--v) center'), ('100%', 'url(pattern.png) var(--v) var(--v) / var(--v) var(--v)'), ('left / 100%', 'url(pattern.png) top var(--v) 100%'), )) def test_variable_shorthand_background(var, background): page, = render_pages('''
''' % (var, background)) @pytest.mark.parametrize('var, background', ( ('invalid', 'var(--v)'), ('blue', 'var(--v) var(--v)'), ('100%', 'url(pattern.png) var(--v) var(--v) var(--v)'), )) def test_variable_shorthand_background_invalid(var, background): with capture_logs() as logs: page, = render_pages('''
''' % (var, background)) log, = logs assert 'invalid value' in log @assert_no_logs def test_variable_initial(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2075 page, = render_pages('''

''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == body.width @assert_no_logs def test_variable_initial_default(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2075 page, = render_pages('''

''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == body.width @assert_no_logs def test_variable_initial_default_var(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2075 page, = render_pages('''

''') html, = page.children body, = html.children paragraph, = body.children assert paragraph.width == body.width @pytest.mark.parametrize('prop', sorted(KNOWN_PROPERTIES)) def test_variable_fallback(prop): render_pages('''
''' % prop) @assert_no_logs def test_variable_list_content(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1287 page, = render_pages('''
''') html, = page.children body, = html.children div, = body.children line, = div.children before, = line.children text, = before.children assert text.text == 'Page 1/1' @assert_no_logs @pytest.mark.parametrize('var, display', ( ('inline', 'var(--var)'), ('inline-block', 'var(--var)'), ('inline flow', 'var(--var)'), ('inline', 'var(--var) flow'), ('flow', 'inline var(--var)'), )) def test_variable_list_display(var, display): page, = render_pages('''
''' % (var, display)) html, = page.children body, = html.children section, = body.children child, = section.children assert type(child).__name__ == 'LineBox' @assert_no_logs @pytest.mark.parametrize('var, font', ( ('weasyprint', 'var(--var)'), ('"weasyprint"', 'var(--var)'), ('weasyprint', 'var(--var), monospace'), ('weasyprint, monospace', 'var(--var)'), ('monospace', 'weasyprint, var(--var)'), )) def test_variable_list_font(var, font): page, = render_pages('''
aa
''' % (var, font)) html, = page.children body, = html.children div, = body.children line, = div.children text, = line.children assert text.width == 4 @assert_no_logs def test_variable_in_function(): page, = render_pages('''

''') html, = page.children body, = html.children section, = body.children h11, div1, h12, div2 = section.children assert div1.children[0].children[0].children[0].text == '1' assert div2.children[0].children[0].children[0].text == '2' @assert_no_logs def test_variable_in_function_multiple_values(): page, = render_pages('''

''') html, = page.children body, = html.children section, = body.children h11, div1, h12, div2, h13, div3 = section.children assert div1.children[0].children[0].children[0].text == 'I' assert div2.children[0].children[0].children[0].text == 'II' assert div3.children[0].children[0].children[0].text == 'iii' @assert_no_logs def test_variable_in_variable_in_function(): page, = render_pages('''

''') html, = page.children body, = html.children section, = body.children h11, div1, h12, div2, h13, div3 = section.children assert div1.children[0].children[0].children[0].text == 'I' assert div2.children[0].children[0].children[0].text == 'II' assert div3.children[0].children[0].children[0].text == 'iii' def test_variable_in_function_missing(): with capture_logs() as logs: page, = render_pages('''

''') assert len(logs) == 2 assert 'no value' in logs[0] assert 'invalid value' in logs[1] html, = page.children body, = html.children section, = body.children h11, div1, h12, div2 = section.children assert not div1.children assert not div2.children @assert_no_logs def test_variable_in_function_in_variable(): page, = render_pages('''

''') html, = page.children body, = html.children section, = body.children h11, div1, h12, div2, h13, div3 = section.children assert div1.children[0].children[0].children[0].text == 'I' assert div2.children[0].children[0].children[0].text == 'II' assert div3.children[0].children[0].children[0].text == 'iii' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3323622 weasyprint-62.3/tests/draw/__init__.py0000644000000000000000000001170014634254702015015 0ustar00"""Test the final, drawn results and compare PNG images pixel per pixel.""" import io from itertools import zip_longest from pathlib import Path from PIL import Image from ..testing_utils import FakeHTML, resource_path # 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". PIXELS_BY_CHAR = { '_': (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 _ 'C': (0, 255, 255), # cyan 'M': (255, 0, 255), # magenta 'Y': (255, 255, 0), # yellow 'K': (0, 0, 0), # black '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 's': (255, 127, 127), # R above _ 't': (127, 255, 127), # G above _ 'u': (128, 0, 127), # r above B above _ 'h': (64, 0, 64), # half average of B and R 'a': (0, 0, 254), # R in lossy JPG 'p': (192, 0, 63), # R above R above B above _ 'z': None, } def parse_pixels(pixels): lines = (line.split('#')[0].strip() for line in pixels.splitlines()) lines = tuple(line for line in lines if line) widths = {len(line) for line in lines} assert len(widths) == 1, 'All lines of pixels must have the same width' width = widths.pop() height = len(lines) pixels = tuple(PIXELS_BY_CHAR[char] for line in lines for char in line) return width, height, pixels def assert_pixels(name, expected_pixels, html): """Helper testing the size of the image and the pixels values.""" expected_width, expected_height, expected_pixels = parse_pixels( expected_pixels) width, height, pixels = html_to_pixels(html) assert (expected_width, expected_height) == (width, height), ( 'Images do not have the same sizes:\n' f'- expected: {expected_width} × {expected_height}\n' f'- result: {width} × {height}') assert_pixels_equal(name, width, height, pixels, expected_pixels) def assert_same_renderings(name, *documents, tolerance=0): """Render HTML documents to PNG and check that they're the same.""" pixels_list = [] for html in documents: width, height, pixels = html_to_pixels(html) pixels_list.append(pixels) reference = pixels_list[0] for i, pixels in enumerate(pixels_list[1:], start=1): assert_pixels_equal( f'{name}_{i}', width, height, pixels, reference, tolerance) def assert_different_renderings(name, *documents): """Render HTML documents to PNG and check that they’re different.""" pixels_list = [] for html in documents: width, height, pixels = html_to_pixels(html) pixels_list.append(pixels) for i, pixels_1 in enumerate(pixels_list, start=1): for j, pixels_2 in enumerate(pixels_list[i:], start=i+1): if pixels_1 == pixels_2: # pragma: no cover name_1, name_2 = f'{name}_{i}', f'{name}_{j}' write_png(name_1, pixels_1, width, height) assert False, f'{name_1} and {name_2} are the same' 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(f'{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}') def write_png(name, pixels, width, height): # pragma: no cover """Take a pixel matrix and write a PNG file.""" directory = Path(__file__).parent / 'results' directory.mkdir(exist_ok=True) image = Image.new('RGB', (width, height)) image.putdata(pixels) image.save(directory / f'{name}.png') def html_to_pixels(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, base_url=resource_path('')) return document_to_pixels(document) def document_to_pixels(document): """Render an HTML document to PNG, check its size and return pixel data.""" image = Image.open(io.BytesIO(document.write_png())) return image.width, image.height, image.getdata() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1746473 weasyprint-62.3/tests/draw/svg/__init__.py0000644000000000000000000000000014564467510015610 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4844034 weasyprint-62.3/tests/draw/svg/test_bounding_box.py0000644000000000000000000002030414635317053017571 0ustar00"""Test how bounding boxes are defined for SVG tags.""" import pytest from ...testing_utils import assert_no_logs @assert_no_logs def test_bounding_box_rect(assert_pixels): assert_pixels(''' BBBBB BBBBR BBBRR BBRRR BRRRR ''', ''' ''') @assert_no_logs def test_bounding_box_circle(assert_pixels): assert_pixels(''' __________ __BBBBBB__ _BBBBBBBR_ _BBBBBBRR_ _BBBBBRRR_ _BBBBRRRR_ _BBBRRRRR_ _BBRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_bounding_box_ellipse(assert_pixels): assert_pixels(''' __________ __BBBBBB__ _BBBBBBBR_ _BBBBBBRR_ _BBBBBRRR_ _BBBBRRRR_ _BBBRRRRR_ _BBRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_bounding_box_line(assert_pixels): assert_pixels(''' BB___ BBB__ _BRR_ __RRR ___RR ''', ''' ''') @assert_no_logs def test_bounding_box_polygon(assert_pixels): assert_pixels(''' BBBBB BBBBR BBBRR BBRRR BRRRR ''', ''' ''') @assert_no_logs def test_bounding_box_polyline(assert_pixels): assert_pixels(''' BBBBB BBBBR BBBRR BBRRR BRRRR ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_bounding_box_text(assert_pixels): assert_pixels(''' BB BR ''', ''' A ''') @assert_no_logs def test_bounding_box_path_hv(assert_pixels): assert_pixels(''' BBBBB BBBBR BBBRR BBRRR BRRRR ''', ''' ''') @assert_no_logs def test_bounding_box_path_l(assert_pixels): assert_pixels(''' BBBBB BBBBR BBBRR BBRRR BRRRR ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_bounding_box_path_c(assert_pixels): assert_pixels(''' BBB__ BBR__ _____ BBB__ BBR__ ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_bounding_box_path_s(assert_pixels): assert_pixels(''' BBB__ BBR__ _____ BBB__ BBR__ ''', ''' ''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3333623 weasyprint-62.3/tests/draw/svg/test_clip.py0000644000000000000000000000452014634254702016045 0ustar00"""Test clip-path attribute.""" import pytest from ...testing_utils import assert_no_logs @assert_no_logs def test_clip_path(assert_pixels): assert_pixels(''' _________ _________ __RRRRR__ __RBBBR__ __RBBBR__ __RBBBR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_clip_path_on_group(assert_pixels): assert_pixels(''' _________ _________ __BBBB___ __BRRRR__ __BRRRR__ __BRRRR__ ___RRRR__ _________ _________ ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_clip_path_group_on_group(assert_pixels): assert_pixels(9, 9, ''' _________ _________ __BB_____ __BR_____ _________ _____RR__ _____RR__ _________ _________ ''', ''' ''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3333623 weasyprint-62.3/tests/draw/svg/test_defs.py0000644000000000000000000000176614634254702016050 0ustar00"""Test how SVG definitions are drawn.""" from base64 import b64encode from ...testing_utils import assert_no_logs SVG = ''' ''' RESULT = ''' RRRRR_____ RRRRR_____ __________ ___RRRRR__ ___RRRRR__ __________ _____RRRRR _____RRRRR __________ __________ ''' @assert_no_logs def test_use(assert_pixels): assert_pixels(RESULT, ''' ''' + SVG) @assert_no_logs def test_use_base64(assert_pixels): base64_svg = b64encode(SVG.encode()).decode() assert_pixels(RESULT, ''' ') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3333623 weasyprint-62.3/tests/draw/svg/test_gradients.py0000644000000000000000000006754314634254702017114 0ustar00"""Test how SVG simple gradients are drawn.""" import pytest from ...testing_utils import assert_no_logs @assert_no_logs def test_linear_gradient(assert_pixels): assert_pixels(''' __________ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _RRRRRRRR_ _RRRRRRRR_ _RRRRRRRR_ _RRRRRRRR_ __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_userspace(assert_pixels): assert_pixels(''' __________ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _RRRRRRRR_ _RRRRRRRR_ _RRRRRRRR_ _RRRRRRRR_ __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_multicolor(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_multicolor_userspace(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_transform(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_transform_repeat(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_transform_userspace(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_transform_repeat_userspace(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_repeat(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_repeat_long(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv BBBBBBBBBB RRRRRRRRRR GGGGGGGGGG vvvvvvvvvv __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_repeat_userspace(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_reflect(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv vvvvvvvvvv vvvvvvvvvv GGGGGGGGGG GGGGGGGGGG RRRRRRRRRR RRRRRRRRRR BBBBBBBBBB BBBBBBBBBB __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_reflect_userspace(assert_pixels): assert_pixels(''' __________ BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR GGGGGGGGGG GGGGGGGGGG vvvvvvvvvv vvvvvvvvvv vvvvvvvvvv vvvvvvvvvv GGGGGGGGGG GGGGGGGGGG RRRRRRRRRR RRRRRRRRRR BBBBBBBBBB BBBBBBBBBB __________ ''', ''' ''') @assert_no_logs def test_linear_gradient_inherit_attributes(assert_pixels): assert_pixels(''' BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR ''', ''' ''') @assert_no_logs def test_linear_gradient_inherit_children(assert_pixels): assert_pixels(''' BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR ''', ''' ''') @assert_no_logs def test_linear_gradient_inherit_no_override(assert_pixels): assert_pixels(''' BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR RRRRRRRRRR ''', ''' ''') @assert_no_logs def test_radial_gradient(assert_pixels): assert_pixels(''' ____________ _rrrrrrrrrr_ _rrrrrrrrrr_ _rrrrBBrrrr_ _rrrBBBBrrr_ _rrBBBBBBrr_ _rrBBBBBBrr_ _rrrBBBBrrr_ _rrrrBBrrrr_ _rrrrrrrrrr_ _rrrrrrrrrr_ ____________ ''', ''' ''') @assert_no_logs def test_radial_gradient_userspace(assert_pixels): assert_pixels(''' ____________ _rrrrrrrrrr_ _rrrrrrrrrr_ _rrrrBBrrrr_ _rrrBBBBrrr_ _rrBBBBBBrr_ _rrBBBBBBrr_ _rrrBBBBrrr_ _rrrrBBrrrr_ _rrrrrrrrrr_ _rrrrrrrrrr_ ____________ ''', ''' ''') @assert_no_logs def test_radial_gradient_multicolor(assert_pixels): assert_pixels(''' ____________ _rrrrrrrrrr_ _rrrGGGGrrr_ _rrGGBBGGrr_ _rGGBBBBGGr_ _rGBBBBBBGr_ _rGBBBBBBGr_ _rGGBBBBGGr_ _rrGGBBGGrr_ _rrrGGGGrrr_ _rrrrrrrrrr_ ____________ ''', ''' ''') @assert_no_logs def test_radial_gradient_multicolor_userspace(assert_pixels): assert_pixels(''' ____________ _rrrrrrrrrr_ _rrrGGGGrrr_ _rrGGBBGGrr_ _rGGBBBBGGr_ _rGBBBBBBGr_ _rGBBBBBBGr_ _rGGBBBBGGr_ _rrGGBBGGrr_ _rrrGGGGrrr_ _rrrrrrrrrr_ ____________ ''', ''' ''') @assert_no_logs def test_radial_gradient_repeat(assert_pixels): assert_pixels(''' ____________ _GBBrrrrBBG_ _BrrGGGGrrB_ _BrGBBBBGrB_ _rGBBrrBBGr_ _rGBrGGrBGr_ _rGBrGGrBGr_ _rGBBrrBBGr_ _BrGBBBBGrB_ _BrrGGGGrrB_ _GBBrrrrBBG_ ____________ ''', ''' ''') @assert_no_logs def test_radial_gradient_reflect(assert_pixels): assert_pixels(''' ____________ _GrrrrrrrrG_ _rrrGGGGrrr_ _rrGBBBBGrr_ _rGBBBBBBGr_ _rGBBGGBBGr_ _rGBBGGBBGr_ _rGBBBBBBGr_ _rrGBBBBGrr_ _rrrGGGGrrr_ _GrrrrrrrrG_ ____________ ''', ''' ''') @assert_no_logs def test_radial_gradient_inherit_attributes(assert_pixels): assert_pixels(''' rrrrrrrrrr rrrrrrrrrr rrrrBBrrrr rrrBBBBrrr rrBBBBBBrr rrBBBBBBrr rrrBBBBrrr rrrrBBrrrr rrrrrrrrrr rrrrrrrrrr ''', ''' ''') @assert_no_logs def test_radial_gradient_inherit_children(assert_pixels): assert_pixels(''' rrrrrrrrrr rrrrrrrrrr rrrrBBrrrr rrrBBBBrrr rrBBBBBBrr rrBBBBBBrr rrrBBBBrrr rrrrBBrrrr rrrrrrrrrr rrrrrrrrrr ''', ''' ''') @assert_no_logs def test_radial_gradient_inherit_no_override(assert_pixels): assert_pixels(''' rrrrrrrrrr rrrrrrrrrr rrrrBBrrrr rrrBBBBrrr rrBBBBBBrr rrBBBBBBrr rrrBBBBrrr rrrrBBrrrr rrrrrrrrrr rrrrrrrrrr ''', ''' ''') @assert_no_logs def test_gradient_opacity(assert_pixels): assert_pixels(''' BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB ssssssssss ssssssssss ssssssssss ssssssssss ssssssssss ''', ''' ''') @assert_no_logs @pytest.mark.parametrize('url', ('#grad\'', '\'#gra', '!', '#')) def test_gradient_bad_url(assert_pixels, url): assert_pixels(''' __________ __________ __________ __________ __________ __________ __________ __________ __________ __________ ''', ''' ''' % url) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3333623 weasyprint-62.3/tests/draw/svg/test_images.py0000644000000000000000000001502414634254702016364 0ustar00"""Test how images are drawn in SVG.""" import pytest from weasyprint.urls import path2url from ...testing_utils import assert_no_logs, resource_path @assert_no_logs def test_image_svg(assert_pixels): assert_pixels(''' ____ ____ __B_ ____ ''', ''' ''') @assert_no_logs def test_image_svg_viewbox(assert_pixels): assert_pixels(''' ____ ____ __B_ ____ ''', ''' ''') @assert_no_logs def test_image_svg_align_default(assert_pixels): assert_pixels(''' __BRRR__ __BRRR__ __RRRG__ __RRRG__ ________ ________ ________ ________ ''', ''' ''') @assert_no_logs def test_image_svg_align_none(assert_pixels): assert_pixels(''' BBRRRRRR BBRRRRRR RRRRRRGG RRRRRRGG ________ ________ ________ ________ ''', ''' ''') @assert_no_logs def test_image_svg_align_meet_x(assert_pixels): assert_pixels(''' ____BRRR ____BRRR ____RRRG ____RRRG ________ ________ ________ ________ ''', ''' ''') @assert_no_logs def test_image_svg_align_meet_y(assert_pixels): assert_pixels(''' ________ ________ ________ ________ BRRR____ BRRR____ RRRG____ RRRG____ ''', ''' ''') @assert_no_logs def test_image_svg_align_slice_x(assert_pixels): assert_pixels(''' BBRRRRRR BBRRRRRR BBRRRRRR BBRRRRRR ________ ________ ________ ________ ''', ''' ''') @assert_no_logs def test_image_svg_align_slice_y(assert_pixels): assert_pixels(''' BBRR____ BBRR____ BBRR____ BBRR____ RRRR____ RRRR____ RRRR____ RRRR____ ''', ''' ''') @pytest.mark.xfail @assert_no_logs def test_image_svg_percentage(assert_pixels): assert_pixels(''' ____ ____ __B_ ____ ''', ''' ''') def test_image_svg_wrong(assert_pixels): assert_pixels(''' ____ ____ ____ ____ ''', ''' ''') @assert_no_logs def test_image_image(assert_pixels): assert_pixels(''' rBBB BBBB BBBB BBBB ''', ''' ''' % path2url(resource_path('pattern.png'))) def test_image_image_wrong(assert_pixels): assert_pixels(''' ____ ____ ____ ____ ''', ''' ''') ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1718704578.3333623 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-62.3/tests/draw/svg/test_markers.py������������������������������������������������������0000644�0000000�0000000�00000016556�14634254702�016576� 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test how SVG markers are drawn.""" from ...testing_utils import assert_no_logs @assert_no_logs def test_markers(assert_pixels): assert_pixels(''' ___________ ___________ _____RRR___ _____RRR___ _____RRR___ ___________ _____RRR___ _____RRR___ _____RRR___ ___________ _____RRR___ _____RRR___ _____RRR___ ''', ''' ''') @assert_no_logs def test_markers_viewbox(assert_pixels): assert_pixels(''' ___________ ____RRR____ ____RRR____ ____RRR____ ___________ ____RRR____ ____RRR____ ____RRR____ ___________ ____RRR____ ____RRR____ ____RRR____ ___________ ''', ''' ''') @assert_no_logs def test_markers_size(assert_pixels): assert_pixels(''' ___________ ____BBR____ ____BBR____ ____RRR____ ___________ ____BBR____ ____BBR____ ____RRR____ ___________ ____BBR____ ____BBR____ ____RRR____ ___________ ''', ''' ''') @assert_no_logs def test_markers_viewbox_size(assert_pixels): assert_pixels(''' ___________ ____RRR____ ____RRR____ ____RRR____ ___________ ____RRR____ ____RRR____ ____RRR____ ___________ ____RRR____ ____RRR____ ____RRR____ ___________ ''', ''' ''') def test_markers_overflow(assert_pixels): assert_pixels(''' ___________ ____BBRR___ ____BBRR___ ____RRRR___ ____RRRR___ ____BBRR___ ____BBRR___ ____RRRR___ ____RRRR___ ____BBRR___ ____BBRR___ ____RRRR___ ____RRRR___ ''', ''' ''') @assert_no_logs def test_markers_userspace(assert_pixels): assert_pixels(''' ___________ ___________ _____R_____ ___________ ___________ ___________ _____R_____ ___________ ___________ ___________ _____R_____ ___________ ___________ ''', ''' ''') @assert_no_logs def test_markers_stroke_width(assert_pixels): assert_pixels(''' ___________ ___________ _____RRR___ _____RRR___ _____RRR___ ___________ _____RRR___ _____RRR___ _____RRR___ ___________ _____RRR___ _____RRR___ _____RRR___ ''', ''' ''') @assert_no_logs def test_markers_viewbox_stroke_width(assert_pixels): assert_pixels(''' ___________ ____BRR____ ____RRR____ ____RRR____ ___________ ____BRR____ ____RRR____ ____RRR____ ___________ ____BRR____ ____RRR____ ____RRR____ ___________ ''', ''' ''') ��������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1718895716.1495767 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-62.3/tests/draw/svg/test_opacity.py������������������������������������������������������0000644�0000000�0000000�00000012711�14635042144�016563� 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test how opacity is handled for SVG.""" import pytest from ...testing_utils import assert_no_logs opacity_source = ''' %s''' @assert_no_logs def test_opacity(assert_same_renderings): assert_same_renderings( opacity_source % ''' ''', opacity_source % ''' ''', ) @assert_no_logs def test_fill_opacity(assert_same_renderings): assert_same_renderings( opacity_source % ''' ''', opacity_source % ''' ''', ) @pytest.mark.xfail @assert_no_logs def test_stroke_opacity(assert_same_renderings): # TODO: This test (and the other ones) fail because of a difference between # the PDF and the SVG specifications: transparent borders have to be drawn # on top of the shape filling in SVG but not in PDF. See: # - PDF-1.7 11.7.4.4 Note 2 # - https://www.w3.org/TR/SVG2/render.html#PaintingShapesAndText assert_same_renderings( ''' ''', opacity_source % ''' ''', ) @pytest.mark.xfail @assert_no_logs def test_stroke_fill_opacity(assert_same_renderings): assert_same_renderings( opacity_source % ''' ''', opacity_source % ''' ''', ) @pytest.mark.xfail @assert_no_logs def test_pattern_gradient_stroke_fill_opacity(assert_same_renderings): # TODO: broken with old versons of Ghostscript assert_same_renderings( opacity_source % ''' ''', opacity_source % ''' ''', tolerance=1, ) @assert_no_logs def test_translate_opacity(assert_same_renderings): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1976 assert_same_renderings( opacity_source % ''' ''', opacity_source % ''' ''', ) �������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1718704578.3343623 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-62.3/tests/draw/svg/test_paths.py��������������������������������������������������������0000644�0000000�0000000�00000036154�14634254702�016245� 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test how SVG simple paths are drawn.""" from ...testing_utils import assert_no_logs @assert_no_logs def test_path_h(assert_pixels): assert_pixels(''' BBBBBBBB__ BBBBBBBB__ __________ RRRRRRRR__ RRRRRRRR__ __________ GGGGGGGG__ GGGGGGGG__ BBBBBBBB__ BBBBBBBB__ ''', ''' ''') @assert_no_logs def test_path_v(assert_pixels): assert_pixels(''' BB____GG__ BB____GG__ BB____GG__ BB____GG__ ___RR_____ ___RR_____ ___RR___BB ___RR___BB ___RR___BB ___RR___BB ''', ''' ''') @assert_no_logs def test_path_l(assert_pixels): assert_pixels(''' ______RR__ ______RR__ ______RR__ ___BB_RR__ ___BB_RR__ ___BB_RR__ ___BB_____ ___BB_____ ___BB_____ ___BB_____ ''', ''' ''') @assert_no_logs def test_path_z(assert_pixels): assert_pixels(''' BBBBBBB___ BBBBBBB___ BB___BB___ BB___BB___ BBBBBBB___ BBBBBBB___ ____RRRRRR ____RRRRRR ____RR__RR ____RRRRRR ''', ''' ''') @assert_no_logs def test_path_z_fill(assert_pixels): assert_pixels(''' BBBBBBB___ BBBBBBB___ BBGGGBB___ BBGGGBB___ BBBBBBB___ BBBBBBB___ ____RRRRRR ____RRRRRR ____RRGGRR ____RRRRRR ''', ''' ''') @assert_no_logs def test_path_c(assert_pixels): assert_pixels(''' __________ __________ __________ __________ __BBB_____ __BBB_____ __________ __RRR_____ __RRR_____ __________ ''', ''' ''') @assert_no_logs def test_path_s(assert_pixels): assert_pixels(''' __________ __________ __________ __________ __BBB_____ __BBB_____ __________ __RRR_____ __RRR_____ __________ ''', ''' ''') @assert_no_logs def test_path_cs(assert_pixels): assert_pixels(''' __BBBBBB__ __BBBBBBB_ _____BBBB_ __RRRRRR__ __RRRRRRR_ _____RRRR_ __GGGGGG__ __GGGGGGG_ _____GGGG_ __BBBBBB__ __BBBBBBB_ _____BBBB_ ''', ''' ''') @assert_no_logs def test_path_q(assert_pixels): assert_pixels(''' __________ __________ __________ __________ __BBBB____ __BBBB____ __________ __RRRR____ __RRRR____ __________ ''', ''' ''') @assert_no_logs def test_path_t(assert_pixels): assert_pixels(''' __________ __________ __________ __________ __BBBB____ __BBBB____ __________ __RRRR____ __RRRR____ __________ ''', ''' ''') @assert_no_logs def test_path_qt(assert_pixels): assert_pixels(''' _BBBB_______ BBBBBBB_____ BBBBBBBB__BB BB__BBBBBBBB _____BBBBBBB _______BBBB_ _RRRR_______ RRRRRRR_____ RRRRRRRR__RR RR__RRRRRRRR _____RRRRRRR _______RRRR_ ''', ''' ''') @assert_no_logs def test_path_qt2(assert_pixels): assert_pixels(''' _BBBB_______ BBBBBBB_____ BBBBBBBB__BB BB__BBBBBBBB _____BBBBBBB _______BBBB_ _RRRR_______ RRRRRRR_____ RRRRRRRR__RR RR__RRRRRRRR _____RRRRRRR _______RRRR_ ''', ''' ''') @assert_no_logs def test_path_a(assert_pixels): assert_pixels(''' __BBBB______ _BBBBB______ BBBBBB______ BBBB________ BBB_________ BBB____RRRR_ ______RRRRR_ _____RRRRRR_ _____RRRR___ _____RRR____ _____RRR____ ____________ ''', ''' ''') @assert_no_logs def test_path_a2(assert_pixels): assert_pixels(''' ______GGGG__ ______GGGGG_ ______GGGGGG ________GGGG _________GGG _________GGG GGG______GGG GGG______GGG GGGG____GGGG GGGGGGGGGGGG _GGGGGGGGGG_ __GGGGGGGG__ ''', ''' ''') @assert_no_logs def test_path_a3(assert_pixels): assert_pixels(''' ______GGGG__ ______GGGGG_ ______GGGGGG ________GGGG _________GGG _________GGG GGG______GGG GGG______GGG GGGG____GGGG GGGGGGGGGGGG _GGGGGGGGGG_ __GGGGGGGG__ ''', ''' ''') @assert_no_logs def test_path_a4(assert_pixels): assert_pixels(''' ____________ ____BBB_____ ____BBB_____ ___BBBB_____ _BBBBBB_____ _BBBBB______ _BBBB____RRR _________RRR ________RRRR ______RRRRRR ______RRRRR_ ______RRRR__ ''', ''' ''') @assert_no_logs def test_path_a5(assert_pixels): assert_pixels(''' __BBBBBBBB__ _BBBBBBBBBB_ BBBBBBBBBBBB BBBB____BBBB BBB______BBB BBB______BBB BBB_________ BBB_________ BBBB________ BBBBBB______ _BBBBB______ __BBBB______ ''', ''' ''') @assert_no_logs def test_path_a6(assert_pixels): assert_pixels(''' __BBBBBBBB__ _BBBBBBBBBB_ BBBBBBBBBBBB BBBB____BBBB BBB______BBB BBB______BBB BBB_________ BBB_________ BBBB________ BBBBBB______ _BBBBB______ __BBBB______ ''', ''' ''') @assert_no_logs def test_path_a7(assert_pixels): assert_pixels(''' ____________ ____________ ____________ ____________ ____________ ____________ GGG______GGG GGG______GGG GGGG____GGGG GGGGGGGGGGGG _GGGGGGGGGG_ __GGGGGGGG__ ''', ''' ''') @assert_no_logs def test_path_wrong_point(assert_pixels): assert_pixels(''' ____________ GG__________ GG__________ GG__________ GG__________ ____________ ____________ ____________ ____________ ____________ ____________ ____________ ''', ''' ''') @assert_no_logs def test_path_markers_l(assert_pixels): assert_pixels(''' _________zz_ _RR_____zzRz _RRGGGGzzRzz _RRGGGzzRzz_ _RR___zRzz__ ________zG__ _______RRRR_ _______RRRR_ ________GG__ _______RRRR_ _______RRRR_ ____________ ''', ''' ''') @assert_no_logs def test_path_markers_hv(assert_pixels): assert_pixels(''' _________zz_ _RR_____zzRz _RRGGGGzzRzz _RRGGGzzRzz_ _RR___zRzz__ ________zG__ _______RRRR_ _______RRRR_ ________GG__ _______RRRR_ _______RRRR_ ____________ ''', ''' ''') ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1718704578.3343623 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-62.3/tests/draw/svg/test_patterns.py�����������������������������������������������������0000644�0000000�0000000�00000016245�14634254702�016765� 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test how SVG simple patterns are drawn.""" from ...testing_utils import assert_no_logs @assert_no_logs def test_pattern(assert_pixels): assert_pixels(''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') @assert_no_logs def test_pattern_2(assert_pixels): assert_pixels(''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') @assert_no_logs def test_pattern_3(assert_pixels): assert_pixels(''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') @assert_no_logs def test_pattern_4(assert_pixels): assert_pixels(''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') @assert_no_logs def test_pattern_inherit_attributes(assert_pixels): assert_pixels(''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') @assert_no_logs def test_pattern_inherit_children(assert_pixels): assert_pixels(''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') @assert_no_logs def test_pattern_inherit_no_override(assert_pixels): assert_pixels(''' BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB BBrrBBrr BBrrBBrr rrBBrrBB rrBBrrBB ''', ''' ''') �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1718704578.3343623 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-62.3/tests/draw/svg/test_shapes.py�������������������������������������������������������0000644�0000000�0000000�00000023701�14634254702�016403� 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test how SVG simple shapes are drawn.""" from ...testing_utils import assert_no_logs @assert_no_logs def test_rect_stroke(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_rect_fill(assert_pixels): assert_pixels(''' _________ _________ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_rect_stroke_fill(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RRBBBRR_ _RRBBBRR_ _RRBBBRR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_rect_round(assert_pixels): assert_pixels(''' _zzzzzzz_ zzzzzzzzz zzRRRRRzz zzRRRRRzz zzRRRRRzz zzRRRRRzz zzRRRRRzz zzzzzzzzz _zzzzzzz_ ''', ''' ''') @assert_no_logs def test_rect_round_zero(assert_pixels): assert_pixels(''' RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR RRRRRRRRR ''', ''' ''') @assert_no_logs def test_line(assert_pixels): assert_pixels(''' _________ _________ _________ _________ RRRRRR___ RRRRRR___ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_polyline(assert_pixels): assert_pixels(''' _________ RRRRRR___ RRRRRR___ RR__RR___ RR__RR___ RR__RR___ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_polyline_fill(assert_pixels): assert_pixels(''' _________ RRRRRR___ RRRRRR___ RRBBRR___ RRBBRR___ RRBBRR___ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_polygon(assert_pixels): assert_pixels(''' _________ RRRRRR___ RRRRRR___ RR__RR___ RR__RR___ RRRRRR___ RRRRRR___ _________ _________ ''', ''' ''') @assert_no_logs def test_polygon_fill(assert_pixels): assert_pixels(''' _________ RRRRRR___ RRRRRR___ RRBBRR___ RRBBRR___ RRRRRR___ RRRRRR___ _________ _________ ''', ''' ''') @assert_no_logs def test_circle_stroke(assert_pixels): assert_pixels(''' __________ __RRRRRR__ _RRRRRRRR_ _RRRRRRRR_ _RRR__RRR_ _RRR__RRR_ _RRRRRRRR_ _RRRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_circle_fill(assert_pixels): assert_pixels(''' __________ __RRRRRR__ _RRRRRRRR_ _RRRRRRRR_ _RRRBBRRR_ _RRRBBRRR_ _RRRRRRRR_ _RRRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_ellipse_stroke(assert_pixels): assert_pixels(''' __________ __RRRRRR__ _RRRRRRRR_ _RRRRRRRR_ _RRR__RRR_ _RRR__RRR_ _RRRRRRRR_ _RRRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_ellipse_fill(assert_pixels): assert_pixels(''' __________ __RRRRRR__ _RRRRRRRR_ _RRRRRRRR_ _RRRBBRRR_ _RRRBBRRR_ _RRRRRRRR_ _RRRRRRRR_ __RRRRRR__ __________ ''', ''' ''') @assert_no_logs def test_rect_in_g(assert_pixels): assert_pixels(''' RRRRR____ RRRRR____ RRRRR____ RRRRR____ RRRRR____ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_rect_x_y_in_g(assert_pixels): assert_pixels(''' _________ _________ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_rect_stroke_zero(assert_pixels): assert_pixels(''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_rect_width_height_zero(assert_pixels): assert_pixels(''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_rect_fill_inherit(assert_pixels): assert_pixels(''' _________ _________ __KKKKK__ __KKKKK__ __KKKKK__ __KKKKK__ __KKKKK__ _________ _________ ''', ''' ''') ���������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1718984235.4854033 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-62.3/tests/draw/svg/test_text.py���������������������������������������������������������0000644�0000000�0000000�00000032326�14635317053�016107� 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test how SVG text is drawn.""" from ...testing_utils import assert_no_logs @assert_no_logs def test_text_fill(assert_pixels): assert_pixels(''' BBBBBB__BBBBBB______ BBBBBB__BBBBBB______ ''', ''' ABC DEF ''') @assert_no_logs def test_text_stroke(assert_pixels): assert_pixels(''' _BBBBBBBBBBBB_______ _BBBBBBBBBBBB_______ _BBBBBBBBBBBB_______ _BBBBBBBBBBBB_______ ''', ''' A B C ''') @assert_no_logs def test_text_x(assert_pixels): assert_pixels(''' BB__BB_BBBB_________ BB__BB_BBBB_________ ''', ''' ABCD ''') @assert_no_logs def test_text_y(assert_pixels): assert_pixels(''' __________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): assert_pixels(''' __________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): assert_pixels(''' BB__BB_BBBB_________ BB__BB_BBBB_________ ''', ''' ABCD ''') @assert_no_logs def test_text_dy(assert_pixels): assert_pixels(''' __________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): assert_pixels(''' __________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): assert_pixels(''' __BBBBBB____________ __BBBBBB____________ ____BBBBBB__________ ____BBBBBB__________ ''', ''' ABC ABC ''') @assert_no_logs def test_text_anchor_middle(assert_pixels): assert_pixels(''' _______BBBBBB_______ _______BBBBBB_______ ''', ''' ABC ''') @assert_no_logs def test_text_anchor_end(assert_pixels): assert_pixels(''' ____________BBBBBB__ ____________BBBBBB__ ''', ''' ABC ''') @assert_no_logs def test_text_tspan(assert_pixels): assert_pixels(''' BBBBBB__BBBBBB______ BBBBBB__BBBBBB______ ''', ''' ABC DEF ''') @assert_no_logs def test_text_tspan_anchor_middle(assert_pixels): assert_pixels(''' _______BBBBBB_______ _______BBBBBB_______ ''', ''' ABC ''') @assert_no_logs def test_text_tspan_anchor_end(assert_pixels): assert_pixels(''' ____________BBBBBB__ ____________BBBBBB__ ''', ''' ABC ''') @assert_no_logs def test_text_anchor_middle_tspan(assert_pixels): assert_pixels(''' _______BBBBBB_______ _______BBBBBB_______ ''', ''' ABC ''') @assert_no_logs def test_text_anchor_end_tspan(assert_pixels): assert_pixels(''' ____________BBBBBB__ ____________BBBBBB__ ''', ''' ABC ''') @assert_no_logs def test_text_rotate(assert_pixels): assert_pixels(''' __RR__RR__RR________ __RR__RR__RR________ BB__BB__BB__________ BB__BB__BB__________ ''', ''' abc abc ''') @assert_no_logs def test_text_text_length(assert_pixels): assert_pixels(''' __RRRRRR____________ __RRRRRR____________ __BB__BB__BB________ __BB__BB__BB________ ''', ''' abc abc ''') @assert_no_logs def test_text_length_adjust_glyphs_only(assert_pixels): assert_pixels(''' __RRRRRR____________ __RRRRRR____________ __BBBBBBBBBBBB______ __BBBBBBBBBBBB______ ''', ''' abc abc ''') @assert_no_logs def test_text_length_adjust_spacing_and_glyphs(assert_pixels): assert_pixels(''' __RR_RR_RR__________ __RR_RR_RR__________ __BBBB__BBBB__BBBB__ __BBBB__BBBB__BBBB__ ''', ''' abc abc ''') ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1718704578.3343623 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-62.3/tests/draw/svg/test_transform.py����������������������������������������������������0000644�0000000�0000000�00000023205�14634254702�017132� 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test how SVG shapes are transformed.""" from ...testing_utils import assert_no_logs @assert_no_logs def test_transform_translate(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_transform_translate_one(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_transform_translatex(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_transform_translatey(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_transform_rotate(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ _________ ''', ''' ''') @assert_no_logs def test_transform_rotate_cx_cy(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ _________ ''', ''' ''') @assert_no_logs def test_transform_skew(assert_pixels): assert_pixels(''' _________ _RRR_____ _RRRRRR__ __RRRRR__ __RRRRR__ __RRRRRR_ ____RRRR_ _________ _________ ''', ''' ''') @assert_no_logs def test_transform_skew_one(assert_pixels): assert_pixels(''' _________ _RRRRR___ _RRRRRR__ __RRRRR__ __RRRRR__ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_transform_skewx(assert_pixels): assert_pixels(''' _________ _RRRRR___ _RRRRRR__ __RRRRR__ __RRRRR__ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_transform_skewy(assert_pixels): assert_pixels(''' _________ _RR______ _RRRR____ _RRRR____ _RRRR____ _RRRR____ __RRR____ _________ _________ ''', ''' ''') @assert_no_logs def test_transform_scale(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_transform_scale_2(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_transform_scalex(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_transform_scaley(assert_pixels): assert_pixels(''' _________ _RRRR____ _RRRR____ _RRRR____ _RRRR____ _RRRR____ _RRRR____ _RRRR____ _________ ''', ''' ''') @assert_no_logs def test_transform_matrix(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_transform_multiple(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ _________ ''', ''' ''') @assert_no_logs def test_transform_unknown(assert_pixels): assert_pixels(''' RRRRR____ R__RR____ R__RR____ R__RR____ RRRRR____ RRRRR____ _________ _________ _________ ''', ''' ''') �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1718704578.3343623 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-62.3/tests/draw/svg/test_units.py��������������������������������������������������������0000644�0000000�0000000�00000004415�14634254702�016263� 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test SVG units.""" from ...testing_utils import assert_no_logs @assert_no_logs def test_units_px(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_units_em(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_units_ex(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _RR___RR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ ''', ''' ''') @assert_no_logs def test_units_unknown(assert_pixels): assert_pixels(''' _RRRRRRR_ _RR___RR_ _RR___RR_ _RR___RR_ _RRRRRRR_ _RRRRRRR_ _________ _________ _________ ''', ''' ''') ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1718704578.3343623 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-62.3/tests/draw/svg/test_visibility.py���������������������������������������������������0000644�0000000�0000000�00000010655�14634254702�017313� 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test how the visibility is controlled with "visibility" and "display".""" from ...testing_utils import assert_no_logs @assert_no_logs def test_visibility_visible(assert_pixels): assert_pixels(''' _________ _________ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_visibility_hidden(assert_pixels): assert_pixels(''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_visibility_inherit_hidden(assert_pixels): assert_pixels(''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_visibility_inherit_visible(assert_pixels): assert_pixels(''' _________ _________ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_display_inline(assert_pixels): assert_pixels(''' _________ _________ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ __RRRRR__ _________ _________ ''', ''' ''') @assert_no_logs def test_display_none(assert_pixels): assert_pixels(''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_display_inherit_none(assert_pixels): assert_pixels(''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') @assert_no_logs def test_display_inherit_inline(assert_pixels): assert_pixels(''' _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', ''' ''') �����������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1718984235.4854033 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������weasyprint-62.3/tests/draw/test_absolute.py���������������������������������������������������������0000644�0000000�0000000�00000050160�14635317053�016136� 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test how absolutes are drawn.""" import pytest from ..testing_utils import assert_no_logs @assert_no_logs def test_absolute_split_1(assert_pixels): assert_pixels(''' BBBBRRRRRRRR____ BBBBRRRRRRRR____ BBBBRR__________ BBBBRR__________ ''', '''
aa aa
bbbbbb bbb
''') @assert_no_logs def test_absolute_split_2(assert_pixels): assert_pixels(''' RRRRRRRRRRRRBBBB RRRRRRRRRRRRBBBB RRRR________BBBB RRRR________BBBB ''', '''
aa aa
bbbbbb bb
''') @assert_no_logs def test_absolute_split_3(assert_pixels): assert_pixels(''' BBBBRRRRRRRR____ BBBBRRRRRRRR____ RRRRRRRRRR______ RRRRRRRRRR______ ''', '''
aa
bbbbbb bbbbb
''') @assert_no_logs def test_absolute_split_4(assert_pixels): assert_pixels(''' RRRRRRRRRRRRBBBB RRRRRRRRRRRRBBBB RRRRRRRRRR______ RRRRRRRRRR______ ''', '''
aa
bbbbbb bbbbb
''') @assert_no_logs def test_absolute_split_5(assert_pixels): assert_pixels(''' BBBBRRRR____gggg BBBBRRRR____gggg BBBBRRRRRR__gggg BBBBRRRRRR__gggg ''', '''
aa aa
cc cc
bbbb bbbbb
''') @assert_no_logs def test_absolute_split_6(assert_pixels): assert_pixels(''' BBBBRRRR____gggg BBBBRRRR____gggg BBBBRRRRRR______ BBBBRRRRRR______ ''', '''
aa aa
cc
bbbb bbbbb
''') @assert_no_logs def test_absolute_split_7(assert_pixels): assert_pixels(''' BBBBRRRRRRRRgggg BBBBRRRRRRRRgggg ____RRRR____gggg ____RRRR____gggg ''', '''
aa
cc cc
bbbb bb
''') @assert_no_logs def test_absolute_split_8(assert_pixels): assert_pixels(''' ______ ______ ______ ______ __RR__ __RR__ ______ ______ ''', '''
a a a a
''') @assert_no_logs def test_absolute_split_9(assert_pixels): assert_pixels(''' ______ ______ BBRRBB BBRRBB BBRR__ BBRR__ ______ ______ ''', ''' aaa a
a a a a
''') @assert_no_logs def test_absolute_split_10(assert_pixels): assert_pixels(''' BB____ BB____ __RR__ __RR__ __RR__ __RR__ BBRR__ BBRR__ __RR__ __RR__ ______ ______ ''', '''
a
a a a a
a
''') @assert_no_logs def test_absolute_split_11(assert_pixels): assert_pixels(''' BBBBBB BBBBBB BBRRBB BBRRBB __RR__ __RR__ ''', ''' aaa aaa
a a
''') @assert_no_logs def test_absolute_split_12(assert_pixels): assert_pixels(''' BBBBBB__ BBBBBB__ ________ ________ ________ ________ ________ ________ BB______ BB______ BBRR____ BBRR____ BBRRRR__ BBRRRR__ BBRRRRRR BBRRRRRR ''', ''' aaa
a
x
xx
xxx

a
a
a
''') @pytest.mark.xfail @assert_no_logs def test_absolute_next_page(assert_pixels): # 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. assert_pixels(''' RRRRRRRRRR______ RRRRRRRRRR______ RRRRRRRRRR______ RRRRRRRRRR______ BBBBBBRRRR______ BBBBBBRRRR______ BBBBBB__________ ________________ ''', ''' aaaaa aaaaa
bb
aaaaa ''') @assert_no_logs def test_absolute_rtl_1(assert_pixels): assert_pixels(''' __________RRRRRR __________RRRRRR ________________ ''', '''
bbb
''') @assert_no_logs def test_absolute_rtl_2(assert_pixels): assert_pixels(''' ________________ _________RRRRRR_ _________RRRRRR_ ''', '''
bbb
''') @assert_no_logs def test_absolute_rtl_3(assert_pixels): assert_pixels(''' ________________ RRRRRR__________ RRRRRR__________ ''', '''
bbb
''') @assert_no_logs def test_absolute_rtl_4(assert_pixels): assert_pixels(''' ________________ _________RRRRRR_ _________RRRRRR_ ''', '''
bbb
''') @assert_no_logs def test_absolute_rtl_5(assert_pixels): assert_pixels(''' RRRRRR__________ RRRRRR__________ ________________ ''', '''
bbb
''') @assert_no_logs def test_absolute_pages_counter(assert_pixels): assert_pixels(''' ______ _RR___ _RR___ _RR___ _RR___ _____B ______ _RR___ _RR___ _BB___ _BB___ _____B ''', ''' a a a
a a
''') @assert_no_logs def test_absolute_pages_counter_orphans(assert_pixels): assert_pixels(''' ______ _RR___ _RR___ _RR___ _RR___ ______ ______ ______ _____B ______ _RR___ _RR___ _BB___ _BB___ _GG___ _GG___ ______ _____B ''', ''' a a a
a a a
a
a a a
''') @assert_no_logs def test_absolute_in_inline(assert_pixels): assert_pixels(''' ______ _GG___ _GG___ _GG___ _GG___ ______ ______ ______ ______ ______ _RR___ _RR___ _RR___ _RR___ _BB___ _BB___ ______ ______ ''', '''

a a

a a
a
''') @assert_no_logs def test_fixed_in_inline(assert_pixels): assert_pixels(''' ______ _GG___ _GG___ _GG___ _GG___ _BB___ _BB___ ______ ______ ______ _RR___ _RR___ _RR___ _RR___ _BB___ _BB___ ______ ______ ''', '''

a a

a a
a
''') @assert_no_logs def test_absolute_image_background(assert_pixels): assert_pixels(''' ____ _RBB _BBB _BBB ''', ''' ''') @assert_no_logs def test_absolute_in_absolute_break(assert_pixels): # Test regression: https://github.com/Kozea/WeasyPrint/issues/2134 assert_pixels(''' BBBB BBBB BBBB BBBB BBBB BBBB BBBB BBBB BBBB BBBB BBBB BBBB RRRR RRRR RRRR RRRR RRRR ____ ____ ____ ''', '''













''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3353622 weasyprint-62.3/tests/draw/test_background.py0000644000000000000000000006574614634254702016457 0ustar00"""Test how backgrounds are drawn.""" import pytest from ..testing_utils import assert_no_logs @assert_no_logs @pytest.mark.parametrize( 'expected_pixels, html', ( ((10 * (10 * 'B' + '\n')), ''' '''), (''' rrrrrrrrrr rrrrrrrrrr rrBBBBBBrr rrBBBBBBrr rrBBBBBBrr rrBBBBBBrr rrBBBBBBrr rrrrrrrrrr rrrrrrrrrr rrrrrrrrrr ''', ''' '''), )) def test_canvas_background(assert_pixels, expected_pixels, html): assert_pixels(expected_pixels, html) def test_canvas_background_size(assert_pixels): assert_pixels(''' __________ __________ __RRRRRR__ __RGGGGR__ __RRRRRR__ __BBBBBB__ __BBBBBB__ __BBBBBB__ __________ __________ ''', ''' ''') @assert_no_logs @pytest.mark.parametrize('css, pixels', ( ('url(pattern.png)', ''' ______________ ______________ __rBBBrBBBrB__ __BBBBBBBBBB__ __BBBBBBBBBB__ __BBBBBBBBBB__ __rBBBrBBBrB__ __BBBBBBBBBB__ __BBBBBBBBBB__ __BBBBBBBBBB__ __rBBBrBBBrB__ __BBBBBBBBBB__ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) repeat-x', ''' ______________ ______________ __rBBBrBBBrB__ __BBBBBBBBBB__ __BBBBBBBBBB__ __BBBBBBBBBB__ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) repeat-y', ''' ______________ ______________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ __rBBB________ __BBBB________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat 0 0%', ''' ______________ ______________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat 50% 0px', ''' ______________ ______________ _____rBBB_____ _____BBBB_____ _____BBBB_____ _____BBBB_____ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat 6px top', ''' ______________ ______________ ________rBBB__ ________BBBB__ ________BBBB__ ________BBBB__ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat bottom 6px right 0', ''' ______________ ______________ ________rBBB__ ________BBBB__ ________BBBB__ ________BBBB__ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat left center', ''' ______________ ______________ ______________ ______________ ______________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat center left', ''' ______________ ______________ ______________ ______________ ______________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat 3px 3px', ''' ______________ ______________ ______________ ______________ ______________ _____rBBB_____ _____BBBB_____ _____BBBB_____ _____BBBB_____ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat 100% 50%', ''' ______________ ______________ ______________ ______________ ______________ ________rBBB__ ________BBBB__ ________BBBB__ ________BBBB__ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat 0% bottom', ''' ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ __rBBB________ __BBBB________ __BBBB________ __BBBB________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat center 6px', ''' ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ _____rBBB_____ _____BBBB_____ _____BBBB_____ _____BBBB_____ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat bottom center', ''' ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ _____rBBB_____ _____BBBB_____ _____BBBB_____ _____BBBB_____ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat 6px 100%', ''' ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ________rBBB__ ________BBBB__ ________BBBB__ ________BBBB__ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) repeat-x 1px 2px', ''' ______________ ______________ ______________ ______________ __BrBBBrBBBr__ __BBBBBBBBBB__ __BBBBBBBBBB__ __BBBBBBBBBB__ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) repeat-y local 2px 1px', ''' ______________ ______________ ____BBBB______ ____rBBB______ ____BBBB______ ____BBBB______ ____BBBB______ ____rBBB______ ____BBBB______ ____BBBB______ ____BBBB______ ____rBBB______ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat fixed', ''' # The image is actually here: ####### ______________ ______________ __BB__________ __BB__________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat fixed right 3px', ''' # x x x x ______________ ______________ ______________ __________rB__ # __________BB__ # __________BB__ # __________BB__ # ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png)no-repeat fixed 50%center', ''' ______________ ______________ ______________ ______________ ______________ ______________ _____rBBB_____ _____BBBB_____ _____BBBB_____ _____BBBB_____ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat, url(pattern.png) no-repeat 2px 1px', ''' ______________ ______________ __rBBB________ __BBBBBB______ __BBBBBB______ __BBBBBB______ ____BBBB______ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), ('url(pattern.png) no-repeat 2px 1px, url(pattern.png) no-repeat', ''' ______________ ______________ __rBBB________ __BBrBBB______ __BBBBBB______ __BBBBBB______ ____BBBB______ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ ______________ '''), )) def test_background_image(assert_pixels, css, pixels): assert_pixels(pixels, '''

  ''' % css) @assert_no_logs def test_background_image_zero_size_background(assert_pixels): # Regression test for https://github.com/Kozea/WeasyPrint/issues/217 assert_pixels(''' __________ __________ __________ __________ __________ __________ __________ __________ __________ __________ ''', ''' ''') @assert_no_logs @pytest.mark.parametrize('css, pixels', ( ('border-box', ''' ____________ ____________ ____________ ____________ ____________ ____________ ____________ _______rBBB_ _______BBBB_ _______BBBB_ _______BBBB_ ____________ '''), ('padding-box', ''' ____________ ____________ ____________ ____________ ____________ ____________ ______rBBB__ ______BBBB__ ______BBBB__ ______BBBB__ ____________ ____________ '''), ('content-box', ''' ____________ ____________ ____________ ____________ ____________ _____rBBB___ _____BBBB___ _____BBBB___ _____BBBB___ ____________ ____________ ____________ '''), ('border-box; background-clip: content-box', ''' ____________ ____________ ____________ ____________ ____________ ____________ ____________ _______rB___ _______BB___ ____________ ____________ ____________ ''') )) def test_background_origin(assert_pixels, css, pixels): """Test the background-origin property.""" assert_pixels(pixels, ''' ''' % css) @assert_no_logs def test_background_transform(assert_pixels): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1809 assert_pixels(''' _______ _RRRRR_ _RRRRR_ _RRRRR_ _RRRRR_ _RRRRR_ _______ ''', ''' ''') @assert_no_logs def test_background_repeat_space_1(assert_pixels): assert_pixels(''' ____________ _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): assert_pixels(''' ____________ _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): assert_pixels(''' ____________ _rBBBrBBBrB_ _BBBBBBBBBB_ _BBBBBBBBBB_ _BBBBBBBBBB_ ____________ ____________ ____________ _rBBBrBBBrB_ _BBBBBBBBBB_ _BBBBBBBBBB_ _BBBBBBBBBB_ ____________ ''', ''' ''') @assert_no_logs def test_background_repeat_space_4(assert_pixels): assert_pixels(''' ________ _rBBBGG_ _BBBBGG_ _BBBBGG_ _BBBBGG_ _GGGGGG_ _GGGGGG_ ________ ''', ''' ''') @assert_no_logs def test_background_repeat_round_1(assert_pixels): assert_pixels(''' __________ _rrBBBBBB_ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _rrBBBBBB_ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ ''', ''' ''') @assert_no_logs def test_background_repeat_round_2(assert_pixels): assert_pixels(''' __________ _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): assert_pixels(''' __________ _rrBBBBBB_ _rrBBBBBB_ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ ''', ''' ''') @assert_no_logs def test_background_repeat_round_4(assert_pixels): assert_pixels(''' __________ _rBBBrBBB_ _rBBBrBBB_ _rBBBrBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ ''', ''' ''') @assert_no_logs @pytest.mark.parametrize('css, 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(assert_pixels, css, pixels): assert_pixels(pixels, ''' ''' % css) @assert_no_logs @pytest.mark.parametrize('expected_pixels, html', ( (''' ____________ ____________ ____________ ___rrBBBBBB_ ___rrBBBBBB_ ___BBBBBBBB_ ___BBBBBBBB_ ___BBBBBBBB_ ___BBBBBBBB_ ___BBBBBBBB_ ___BBBBBBBB_ ____________ ''', ''' '''), (''' ____________ ____________ ____________ ____________ ____________ ____________ ____________ _______rBBB_ _______BBBB_ _______BBBB_ _______BBBB_ ____________ ''', ''' '''), (''' ______________ _rrBBBBBB_____ _rrBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ ______________ ''', ''' '''), (''' ______________ _rrBBBBBB_____ _rrBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ ______________ ''', ''' '''), (''' ______________ _rrBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ _BBBBBBBB_____ ______________ ______________ ______________ ______________ ______________ ''', ''' '''), (''' ______________ _rrrBBBBBBBBB_ _rrrBBBBBBBBB_ _rrrBBBBBBBBB_ _BBBBBBBBBBBB_ _BBBBBBBBBBBB_ _BBBBBBBBBBBB_ _BBBBBBBBBBBB_ _BBBBBBBBBBBB_ ______________ ''', ''' '''), ) ) def test_background_size(assert_pixels, expected_pixels, html): assert_pixels(expected_pixels, html) @assert_no_logs def test_bleed_background_size(assert_pixels): assert_pixels(''' RRRR RRRR RRRR RRRR ''', ''' ''') @assert_no_logs def test_background_size_clip(assert_pixels): assert_pixels(''' BBBB BRBB BBBB BBBB ''', ''' ''') @assert_no_logs def test_page_background_fixed(assert_pixels): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1993 assert_pixels(''' RBBB BBBB BBBB BBBB ''', ''' ''') @assert_no_logs def test_page_background_fixed_bleed(assert_pixels): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1993 assert_pixels(''' RRRRRR RRBBBR RBBBBR RBBBBR RBBBBR RRRRRR ''', ''' ''') @assert_no_logs def test_bleed_background_size_clip(assert_pixels): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1943 assert_pixels(''' BBBBBB BBBBBB BBRBBB BBBBBB BBBBBB BBBBBB ''', ''' ''') @assert_no_logs def test_marks_crop(assert_pixels): assert_pixels(''' KK__KK K____K ______ ______ K____K KK__KK ''', ''' ''') @assert_no_logs def test_marks_cross(assert_pixels): assert_pixels(''' __KK__ ______ K____K K____K ______ __KK__ ''', ''' ''') @assert_no_logs def test_marks_crop_cross(assert_pixels): assert_pixels(''' KKKKKK K____K K____K K____K K____K KKKKKK ''', ''' ''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703402.7982314 weasyprint-62.3/tests/draw/test_before_after.py0000644000000000000000000000403614634252453016745 0ustar00"""Test how before and after pseudo elements are drawn.""" from ..testing_utils import assert_no_logs @assert_no_logs def test_before_after_1(assert_same_renderings): assert_same_renderings( '''

some content

''', '''

[some url] some content

''', tolerance=10) @assert_no_logs def test_before_after_2(assert_same_renderings): assert_same_renderings( '''

Lorem ipsum dolor sit amet

''', '''

« Lorem ipsum “ dolor ” sit amet »

''', tolerance=10) @assert_no_logs def test_before_after_3(assert_same_renderings): assert_same_renderings( '''

c

''', '''

aMissing imagebc

''', tolerance=10) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4854033 weasyprint-62.3/tests/draw/test_box.py0000644000000000000000000003154414635317053015115 0ustar00"""Test how boxes, borders, outlines are drawn.""" import itertools import pytest from weasyprint import HTML from ..testing_utils import assert_no_logs @assert_no_logs def test_borders(assert_pixels, assert_different_renderings, 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. documents = ( source % (margin, prop, border_style) for border_style in ( 'none', 'solid', 'dashed', 'dotted', 'double', 'inset', 'outset', 'groove', 'ridge')) assert_different_renderings(*documents) css_margin = margin width = 140 height = 110 margin = 10 border = 10 solid_pixels = [['_'] * width for _ in range(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][x] = '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][x] = 'B' pixels = '\n'.join(''.join(chars) for chars in solid_pixels) html = source % (css_margin, prop, 'solid') assert_pixels(pixels, html) @assert_no_logs def test_borders_table_collapse(assert_pixels, assert_different_renderings): """Test the rendering of collapsing borders.""" source = ''' ''' # Do not test the exact rendering of earch border style but at least # check that they do not do the same. documents = ( source % border_style for border_style in ( 'none', 'solid', 'dashed', 'dotted', 'double', 'inset', 'outset', 'groove', 'ridge')) assert_different_renderings(*documents) @assert_no_logs def test_outlines(assert_pixels, assert_different_renderings): return test_borders( assert_pixels, assert_different_renderings, 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_borders_box_sizing(assert_pixels): assert_pixels(''' ________ _RRRRRR_ _R____R_ _RRRRRR_ ________ ''', '''
''') @assert_no_logs def test_margin_boxes(assert_pixels): assert_pixels(''' _______________ _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() @assert_no_logs def test_draw_border_radius(assert_pixels): assert_pixels(''' ___zzzzz __zzzzzz _zzzzzzz zzzzzzzz zzzzzzzz zzzzzzzR zzzzzzRR zzzzzRRR ''', '''
''') @assert_no_logs def test_draw_split_border_radius(assert_pixels): assert_pixels(''' ___zzzzz __zzzzzz _zzzzzzz zzzzzzzz zzzzzzzz zzzzzzzz zzzzzzRR zzzzzRRR RRRRRRRR RRRRRRRR RRRRRRRR RRRRRRRR RRRRRRRR RRRRRRRR RRRRRRRR RRRRRRRR zzzzzzRR zzzzzzzR zzzzzzzz zzzzzzzz zzzzzzzz zzzzzzzz _zzzzzzz __zzzzzz ''', '''
a b c
''') @assert_no_logs def test_border_image_stretch(assert_pixels): assert_pixels(''' __________ _RYYYMMMG_ _M______C_ _M______C_ _Y______Y_ _Y______Y_ _BYYYCCCK_ __________ ''', '''
''') @assert_no_logs def test_border_image_fill(assert_pixels): assert_pixels(''' __________ _RYYYMMMG_ _MbbbgggC_ _MbbbgggC_ _YgggbbbY_ _YgggbbbY_ _BYYYCCCK_ __________ ''', '''
''') @assert_no_logs def test_border_image_default_slice(assert_pixels): assert_pixels(''' _____________ _RYMG___RYMG_ _MbgC___MbgC_ _YgbY___YgbY_ _BYCK___BYCK_ _____________ _____________ _RYMG___RYMG_ _MbgC___MbgC_ _YgbY___YgbY_ _BYCK___BYCK_ _____________ ''', '''
''') @assert_no_logs def test_border_image_uneven_width(assert_pixels): assert_pixels(''' ____________ _RRRYYYMMMG_ _MMM______C_ _MMM______C_ _YYY______Y_ _YYY______Y_ _BBBYYYCCCK_ ____________ ''', '''
''') @assert_no_logs def test_border_image_not_percent(assert_pixels): assert_pixels(''' __________ _RYYYMMMG_ _M______C_ _M______C_ _Y______Y_ _Y______Y_ _BYYYCCCK_ __________ ''', '''
''') @assert_no_logs def test_border_image_repeat(assert_pixels): assert_pixels(''' ___________ _RYMYMYMYG_ _M_______C_ _Y_______Y_ _M_______C_ _Y_______Y_ _BYCYCYCYK_ ___________ ''', '''
''') @assert_no_logs def test_border_image_space(assert_pixels): assert_pixels(''' _________ _R_YMC_G_ _________ _M_____C_ _Y_____Y_ _C_____M_ _________ _B_YCM_K_ _________ ''', '''
''') @assert_no_logs def test_border_image_outset(assert_pixels): assert_pixels(''' ____________ _RYYYYMMMMG_ _M________C_ _M_bbbbbb_C_ _M_bbbbbb_C_ _Y_bbbbbb_Y_ _Y_bbbbbb_Y_ _Y________Y_ _BYYYYCCCCK_ ____________ ''', '''
''') @assert_no_logs def test_border_image_width(assert_pixels): assert_pixels(''' __________ _RRYYMMGG_ _RRYYMMGG_ _MM____CC_ _YY____YY_ _BBYYCCKK_ _BBYYCCKK_ __________ ''', '''
''') @assert_no_logs def test_border_image_gradient(assert_pixels): assert_pixels(''' __________ _RRRRRRRR_ _RRRRRRRR_ _RR____RR_ _BB____BB_ _BBBBBBBB_ _BBBBBBBB_ __________ ''', '''
''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703423.6414611 weasyprint-62.3/tests/draw/test_column.py0000644000000000000000000000404514634252500015610 0ustar00"""Test how columns are drawn.""" from ..testing_utils import assert_no_logs @assert_no_logs def test_column_rule_1(assert_pixels): assert_pixels(''' a_r_a a_r_a _____ ''', '''
''') @assert_no_logs def test_column_rule_2(assert_pixels): assert_pixels(''' a_r_a a___a a_r_a ''', '''
''') @assert_no_logs def test_column_rule_span(assert_pixels): assert_pixels(''' ___________ ___________ ___________ ___a_______ ___a_r_a___ ___a_r_a___ ___________ ___________ ___________ ''', '''
''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4854033 weasyprint-62.3/tests/draw/test_current_color.py0000644000000000000000000000565514635317053017211 0ustar00"""Test the currentColor value.""" import pytest from ..testing_utils import assert_no_logs @assert_no_logs def test_current_color_1(assert_pixels): assert_pixels('GG\nGG', ''' ''') @assert_no_logs def test_current_color_2(assert_pixels): assert_pixels('GG\nGG', ''' ''') @assert_no_logs def test_current_color_3(assert_pixels): assert_pixels('GG\nGG', ''' ''') @assert_no_logs def test_current_color_4(assert_pixels): assert_pixels('GG\nGG', '''
abc
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('''
''') @assert_no_logs def test_current_color_svg_1(assert_pixels): assert_pixels('KK\nKK', ''' ''') @pytest.mark.xfail @assert_no_logs def test_current_color_svg_2(assert_pixels): assert_pixels('GG\nGG', ''' ''') @assert_no_logs def test_current_color_variable(assert_pixels): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2010 assert_pixels('GG\nGG', '''
aa''') @assert_no_logs def test_current_color_variable_border(assert_pixels): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2010 assert_pixels('GG\nGG', '''
''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4854033 weasyprint-62.3/tests/draw/test_float.py0000644000000000000000000005273114635317053015433 0ustar00"""Test how floats are drawn.""" import pytest from ..testing_utils import assert_no_logs @assert_no_logs def test_float(assert_pixels): assert_pixels(''' rBBB__aaaa BBBB__aaaa BBBB__aaaa BBBB__aaaa __________ ''', '''
''') @assert_no_logs def test_float_rtl(assert_pixels): assert_pixels(''' rBBB__aaaa BBBB__aaaa BBBB__aaaa BBBB__aaaa __________ ''', '''
''') @assert_no_logs def test_float_inline(assert_pixels): assert_pixels(''' rBBBGG_____aaaa BBBBGG_____aaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_inline_rtl(assert_pixels): assert_pixels(''' rBBB_____GGaaaa BBBB_____GGaaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_inline_block(assert_pixels): assert_pixels(''' rBBBGG_____aaaa BBBBGG_____aaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_inline_block_rtl(assert_pixels): assert_pixels(''' rBBB_____GGaaaa BBBB_____GGaaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_table(assert_pixels): assert_pixels(''' rBBBGG_____aaaa BBBBGG_____aaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_table_rtl(assert_pixels): assert_pixels(''' rBBB_____GGaaaa BBBB_____GGaaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_inline_table(assert_pixels): assert_pixels(''' rBBBGG_____aaaa BBBBGG_____aaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_inline_table_rtl(assert_pixels): assert_pixels(''' rBBB_____GGaaaa BBBB_____GGaaaa BBBB_______aaaa BBBB_______aaaa _______________ ''', '''
a
''') @assert_no_logs def test_float_replaced_block(assert_pixels): assert_pixels(''' rBBBaaaa___rBBB BBBBaaaa___BBBB BBBBaaaa___BBBB BBBBaaaa___BBBB _______________ ''', '''
''') @assert_no_logs def test_float_replaced_block_rtl(assert_pixels): assert_pixels(''' rBBB___aaaarBBB BBBB___aaaaBBBB BBBB___aaaaBBBB BBBB___aaaaBBBB _______________ ''', '''
''') @pytest.mark.xfail @assert_no_logs def test_float_replaced_inline(assert_pixels): assert_pixels(''' rBBBaaaa___rBBB BBBBaaaa___BBBB BBBBaaaa___BBBB BBBBaaaa___BBBB _______________ ''', '''
''') @pytest.mark.xfail @assert_no_logs def test_float_replaced_inline_rtl(assert_pixels): assert_pixels(''' rBBB___aaaarBBB BBBB___aaaaBBBB BBBB___aaaaBBBB BBBB___aaaaBBBB _______________ ''', '''
''') @assert_no_logs def test_float_margin(assert_pixels): assert_pixels(''' BBBBRRRRRRRRRR__ BBBBRRRRRRRRRR__ __RRRRRRRRRR____ __RRRRRRRRRR____ ''', '''
aa
bbbbb bbbbb
''') @assert_no_logs def test_float_split_1(assert_pixels): assert_pixels(''' BBBBRRRRRRRRRRRR BBBBRRRRRRRRRRRR BBBBRRRR________ BBBBRRRR________ ''', '''
aa aa
bbbbbb bb
''') @assert_no_logs def test_float_split_2(assert_pixels): assert_pixels(''' RRRRRRRRRRRRBBBB RRRRRRRRRRRRBBBB RRRR________BBBB RRRR________BBBB ''', '''
aa aa
bbbbbb bb
''') @assert_no_logs def test_float_split_3(assert_pixels): assert_pixels(''' BBBBRRRRRRRRRRRR BBBBRRRRRRRRRRRR RRRRRRRRRR______ RRRRRRRRRR______ ''', '''
aa
bbbbbb bbbbb
''') @assert_no_logs def test_float_split_4(assert_pixels): assert_pixels(''' RRRRRRRRRRRRBBBB RRRRRRRRRRRRBBBB RRRRRRRRRR______ RRRRRRRRRR______ ''', '''
aa
bbbbbb bbbbb
''') @assert_no_logs def test_float_split_5(assert_pixels): assert_pixels(''' BBBBRRRRRRRRgggg BBBBRRRRRRRRgggg BBBBRRRR____gggg BBBBRRRR____gggg ''', '''
aa aa
cc cc
bbbb bb
''') @assert_no_logs def test_float_split_6(assert_pixels): assert_pixels(''' BBBBRRRRRRRRgggg BBBBRRRRRRRRgggg BBBBRRRR________ BBBBRRRR________ ''', '''
aa aa
cc
bbbb bb
''') @assert_no_logs def test_float_split_7(assert_pixels): assert_pixels(''' BBBBRRRRRRRRgggg BBBBRRRRRRRRgggg RRRR________gggg RRRR________gggg ''', '''
aa
cc cc
bbbb bb
''') @assert_no_logs def test_float_split_8(assert_pixels): assert_pixels(''' BBBB__RRRRRRRRRR BBBB__RRRRRRRRRR BBBB__RRRR______ BBBB__RRRR______ ''', '''
aa aa
bbbbb bb
''') @assert_no_logs def test_float_split_9(assert_pixels): assert_pixels(''' RRRRRRRRRRBBBB__ RRRRRRRRRRBBBB__ RRRR______BBBB__ RRRR______BBBB__ ''', '''
aa aa
bbbbb bb
''') @assert_no_logs def test_float_split_10(assert_pixels): assert_pixels(''' RRRRRRRRRR______ RRRRRRRRRR______ RRRRRRRRRR______ RRRRRRRRRR______ ________________ RRRRRR____BBBB__ RRRRRR____BBBB__ RRRRRR____BBBB__ RRRRRR____BBBB__ ________________ ''', '''
bbbbb bbbbb
aa aa
bbb bbb
''') @assert_no_logs def test_float_split_11(assert_pixels): assert_pixels(''' ________________ _BBBBBBBBBB_____ _BBBBBBBBBB_____ _BBBBBBBBBB_____ _BBBBBBBBBB_____ ________________ ________________ ________________ _BBBB___________ _BBBB___________ _rrrrrrrrrrrrrr_ _rrrrrrrrrrrrrr_ ________________ ________________ ''', '''
aaaaa aaaaa aa
bbbbbbb''') @assert_no_logs def test_float_split_12(assert_pixels): assert_pixels(''' BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBGG______BBBB BBBBGG______BBBB BBBB________BBBB BBBB________BBBB ________________ ''', '''
a

aa

aa

bb

bb

''') @pytest.mark.xfail @assert_no_logs def test_float_split_13(assert_pixels): assert_pixels(''' BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBGG______BBBB BBBBGG______BBBB BBBB________BBBB BBBB________BBBB ________________ ''', '''

a

aa

a

bb

bb

''') @assert_no_logs def test_float_split_14(assert_pixels): assert_pixels(''' BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBGG______BBBB BBBBGG______BBBB BBBB________BBBB BBBB________BBBB ________________ ''', '''
a

aa

aa

bb

bb

''') @pytest.mark.xfail @assert_no_logs def test_float_split_15(assert_pixels): assert_pixels(''' BB__RRRRRRRRRR__ BB__RRRRRRRRRR__ BB__RRRRRRRRRR__ BB__RRRRRRRRRR__ GGBBRRRRRRRRRR__ GGBBRRRRRRRRRR__ GGBBRRRRRRRRRR__ GGBBRRRRRRRRRR__ RRRRRRRRRR______ RRRRRRRRRR______ ''', '''
a a a
a a
a a
bbbbb bbbbb bbbbb bbbbb bbbbb
''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4854033 weasyprint-62.3/tests/draw/test_footnote.py0000644000000000000000000002162214635317053016156 0ustar00"""Test how footnotes are drawn.""" from ..testing_utils import assert_no_logs @assert_no_logs def test_inline_footnote(assert_pixels): assert_pixels(''' RRRRRRRR_ RRRRRRRR_ _________ _________ _________ RRRRRRRR_ RRRRRRRR_ ''', '''
abcde
''') @assert_no_logs def test_block_footnote(assert_pixels): assert_pixels(''' RRRRRRRR_ RRRRRRRR_ _________ _________ _________ RRRRRRRR_ RRRRRRRR_ ''', '''
abc
de
''') @assert_no_logs def test_long_footnote(assert_pixels): assert_pixels(''' RRRRRRRR_ RRRRRRRR_ _________ RRRRRRRR_ RRRRRRRR_ RR_______ RR_______ ''', '''
abcde f
''') @assert_no_logs def test_footnote_margin(assert_pixels): assert_pixels(''' RRRRRRRR_ RRRRRRRR_ _________ _________ _RRRRRR__ _RRRRRR__ _________ ''', '''
abcd
''') @assert_no_logs def test_footnote_multiple_margin(assert_pixels): assert_pixels(''' RRRR___ RRRR___ RRRR___ RRRR___ RRRR___ RRRR___ RRRRRR_ RRRRRR_ _______ _______ RRRR___ RRRR___ _______ _______ _______ _______ RRRRRR_ RRRRRR_ RRRRRR_ RRRRRR_ ''', '''
ab
ab
ab
ade
ab
''') @assert_no_logs def test_footnote_with_absolute(assert_pixels): assert_pixels(''' _RRRR____ _RRRR____ _________ _RRRR____ _RRRR____ BB_______ BB_______ ''', '''
ad
''') @assert_no_logs def test_footnote_max_height_1(assert_pixels): assert_pixels(''' RRRRRRRR_ RRRRRRRR_ RRRR_____ RRRR_____ _BBBBBB__ _BBBBBB__ _________ _________ _________ _________ _BBBBBB__ _BBBBBB__ ''', '''
ab
c
d
ef
''') @assert_no_logs def test_footnote_max_height_2(assert_pixels): assert_pixels(''' RRRRRRRR_ RRRRRRRR_ _________ _________ _BBBBBB__ _BBBBBB__ _________ _________ _________ _________ _BBBBBB__ _BBBBBB__ ''', '''
ab
c
d
''') @assert_no_logs def test_footnote_max_height_3(assert_pixels): # This case is crazy and the rendering is not really defined, but this test # is useful to check that there’s no endless loop. assert_pixels(''' RRRRRRRR_ RRRRRRRR_ _________ _________ _________ _________ _________ _________ _________ _________ _________ _BBBBBB__ _________ _________ _________ _________ _________ _BBBBBB__ ''', '''
ab
c
d
''') @assert_no_logs def test_footnote_max_height_4(assert_pixels): assert_pixels(''' RRRRRRRR_ RRRRRRRR_ RRRR_____ RRRR_____ _BBBBBB__ _BBBBBB__ RRRR_____ RRRR_____ _________ _________ _BBBBBB__ _BBBBBB__ ''', '''
ab
c
d
ef
gh
''') @assert_no_logs def test_footnote_max_height_5(assert_pixels): assert_pixels(''' RRRRRRRR__RR RRRRRRRR__RR _BBBBBB_____ _BBBBBB_____ _BBBBBB_____ _BBBBBB_____ RRRR________ RRRR________ ____________ ____________ _BBBBBB_____ _BBBBBB_____ ''', '''
ab
c
d
e
fg
''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4854033 weasyprint-62.3/tests/draw/test_footnote_column.py0000644000000000000000000002202114635317053017525 0ustar00"""Test how footnotes in columns are drawn.""" import pytest from ..testing_utils import assert_no_logs @assert_no_logs def test_footnote_column_margin_top(assert_pixels): assert_pixels(''' RRRR_RRRR RRRR_RRRR _________ _________ _________ RRRRRRRR_ RRRRRRRR_ RRRR_RRRR RRRR_RRRR RRRR_RRRR RRRR_RRRR RRRR_____ RRRR_____ _________ ''', '''
ade ab ab ab ab ab ab
''') @assert_no_logs def test_footnote_column_fill_auto(assert_pixels): assert_pixels(''' RRRR_____ RRRR_____ RRRR_____ RRRR_____ RRRR_____ RRRR_____ _________ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ ''', '''
ade ade ade
''') @assert_no_logs def test_footnote_column_fill_auto_break_inside_avoid(assert_pixels): assert_pixels(''' RRRR_RRRR RRRR_RRRR RRRR_RRRR RRRR_RRRR RRRR_RRRR RRRR_RRRR _________ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ ''', '''
ade ade
ab
ade ab
ab
''') @assert_no_logs def test_footnote_column_p_after(assert_pixels): assert_pixels(''' RRRR_RRRR RRRR_RRRR RRRR_RRRR RRRR_RRRR KK__KK___ KK__KK___ _________ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ KK__KK___ KK__KK___ _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', '''
ade ade ab ab

a a a a

''') @assert_no_logs def test_footnote_column_p_before(assert_pixels): assert_pixels(''' KKKK_____ KKKK_____ RRRR_RRRR RRRR_RRRR RRRR_RR__ RRRR_RR__ _________ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRR_RR__ RRRR_RR__ _________ _________ _________ _________ _________ _________ _________ _________ _________ _________ _________ ''', '''

ab

ade ade ade a ab a
''') @assert_no_logs def test_footnote_column_3(assert_pixels): assert_pixels(''' RRRR_RRRR_RRRR RRRR_RRRR_RRRR ______________ RRRRRRRR______ RRRRRRRR______ RRRR_RRRR_____ RRRR_RRRR_____ ______________ ______________ ______________ ''', '''
ab ab ade ab ab
''') @assert_no_logs def test_footnote_column_3_p_before(assert_pixels): assert_pixels(''' KKKK__________ KKKK__________ RRRR_RRRR_RRRR RRRR_RRRR_RRRR RRRR_RRRR_RRRR RRRR_RRRR_RRRR ______________ RRRRRRRR______ RRRRRRRR______ RRRR_RRRR_____ RRRR_RRRR_____ ______________ ______________ ______________ ______________ ______________ RRRRRRRR______ RRRRRRRR______ ''', '''

ab

ab ab ade ab ab ab ade ab
''') @assert_no_logs def test_footnote_column_clone_decoration(assert_pixels): assert_pixels(''' _________ RRRR_RRRR RRRR_RRRR _________ _________ RRRRRRRR_ RRRRRRRR_ _________ RRRR_RRRR RRRR_RRRR _________ _________ _________ _________ ''', '''
ade ab ab ab
''') @assert_no_logs def test_footnote_column_max_height(assert_pixels): assert_pixels(''' RRRR_RRRR RRRR_RRRR RRRR_RRRR RRRR_RRRR _________ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRR_RRRR RRRR_RRRR _________ _________ _________ _________ _________ RRRRRRRR_ RRRRRRRR_ ''', '''
ade ade ade ab ab ab
''') @pytest.mark.xfail @assert_no_logs def test_footnote_column_reported_split(assert_pixels): # When calling block_container_layout() in remove_placeholders(), we should # use the whole skip stack and not just [skip:] assert_pixels(''' RRRR_RRRR RRRR_RRRR RRRR_RRRR RRRR_RRRR _________ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRRRRRR_ RRRR_____ RRRR_____ _________ _________ _________ _________ _________ RRRRRRRR_ RRRRRRRR_ ''', '''
ade ade
ade ab ab
''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703423.6414611 weasyprint-62.3/tests/draw/test_gradient.py0000644000000000000000000001416314634252500016112 0ustar00"""Test how gradients are drawn.""" from ..testing_utils import assert_no_logs @assert_no_logs def test_linear_gradients_1(assert_pixels): assert_pixels(''' _____ _____ _____ 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(assert_pixels, filename): assert_pixels(resized_image, '''
''' % filename) def test_image_overflow(assert_pixels): assert_pixels(centered_image_overflow, '''
''') @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(assert_pixels, viewbox, width, height): assert_pixels(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(assert_pixels, viewbox, width, height, image): assert_pixels(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): assert_pixels(centered_image, '''
''') @assert_no_logs def test_images_not_found(assert_pixels): with capture_logs() as logs: assert_pixels(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): assert_pixels(no_image, '''
''') @assert_no_logs def test_images_alt(assert_same_renderings): with capture_logs() as logs: documents = ( '''
%s
''' % html for html in ( 'Hello', 'Hello', 'Hello', 'Hello', )) assert_same_renderings(*documents) 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(assert_pixels): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1440 assert_pixels('_\n_\n_', '''
''') @assert_no_logs def test_images_no_width(assert_pixels): assert_pixels(no_image, '''
not shown
''') @assert_no_logs def test_images_no_height(assert_pixels): assert_pixels(no_image, '''
not shown
''') @assert_no_logs def test_images_no_width_height(assert_pixels): assert_pixels(no_image, '''
not shown
''') @assert_no_logs def test_images_page_break(assert_pixels): assert_pixels(page_break, '''
''') @assert_no_logs def test_image_repeat_inline(assert_pixels): # Test regression: https://github.com/Kozea/WeasyPrint/issues/808 assert_pixels(table, '''
''') @assert_no_logs def test_image_repeat_block(assert_pixels): # Test regression: https://github.com/Kozea/WeasyPrint/issues/808 assert_pixels(table, '''
''') @assert_no_logs def test_images_padding(assert_pixels): # Regression test: padding used to be ignored on images assert_pixels(centered_image, '''
''') @assert_no_logs def test_images_in_inline_block(assert_pixels): # Regression test: this used to cause an exception assert_pixels(centered_image, '''

''') @assert_no_logs def test_images_transparent_text(assert_pixels): # Test regression: https://github.com/Kozea/WeasyPrint/issues/2131 assert_pixels(centered_image, '''
123
''') @assert_no_logs def test_images_shared_pattern(assert_pixels): # 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(''' ____________ ____________ __aaaaaaaa__ __aaaaaaaa__ ____________ __aaaa______ __aaaa______ __aaaa______ __aaaa______ ____________ ____________ ____________ ''', '''
''') @assert_no_logs def test_image_resolution(assert_same_renderings): assert_same_renderings( '''
''', '''
''', '''
''', '''
''', ) @assert_no_logs def test_image_cover(assert_pixels): assert_pixels(cover_image, ''' ''') @assert_no_logs def test_image_contain(assert_pixels): assert_pixels(centered_image, ''' ''') @assert_no_logs def test_image_none(assert_pixels): assert_pixels(centered_image, ''' ''') @assert_no_logs def test_image_scale_down(assert_pixels): assert_pixels(centered_image, ''' ''') @assert_no_logs def test_image_position(assert_pixels): assert_pixels(centered_image, ''' ''') @assert_no_logs def test_images_border(assert_pixels): assert_pixels(border_image, '''
''') @assert_no_logs def test_images_border_absolute(assert_pixels): assert_pixels(border_image, '''
''') @assert_no_logs def test_image_exif(assert_same_renderings): assert_same_renderings( ''' ''', ''' ''', tolerance=25, ) @assert_no_logs def test_image_exif_image_orientation(assert_same_renderings): assert_same_renderings( ''' ''', ''' ''', tolerance=25, ) @assert_no_logs def test_image_exif_image_orientation_keep_format(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1755 pdf = FakeHTML( string=''' ''', base_url=resource_path('')).write_pdf() assert b'DCTDecode' in pdf ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4854033 weasyprint-62.3/tests/draw/test_leader.py0000644000000000000000000003253514635317053015562 0ustar00"""Test how leaders are drawn.""" import pytest from ..testing_utils import assert_no_logs @assert_no_logs def test_leader_simple(assert_pixels): assert_pixels(''' RR__BBBBBBBB__BB RR__BBBBBBBB__BB RRRR__BBBB__BBBB RRRR__BBBB__BBBB RR__BBBB__BBBBBB RR__BBBB__BBBBBB ''', '''
a
bb
c
''') @assert_no_logs def test_leader_too_long(assert_pixels): assert_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 ''', '''
aaaaa
a a a a a a a
a a a a a
''') @assert_no_logs def test_leader_alone(assert_pixels): assert_pixels(''' RRBBBBBBBBBBBBBB RRBBBBBBBBBBBBBB ''', '''
a
''') @assert_no_logs def test_leader_content(assert_pixels): assert_pixels(''' RR____BB______BB RR____BB______BB ''', '''
a
''') @pytest.mark.xfail @assert_no_logs def test_leader_float(assert_pixels): assert_pixels(''' bbGRR___BB____BB bbGRR___BB____BB GGGRR___BB____BB ___RR___BB____BB ''', '''
a
a
a
''') @pytest.mark.xfail @assert_no_logs def test_leader_float_small(assert_pixels): assert_pixels(''' bbRRBB__BB____BB bbRRBB__BB____BB RR__BB__BB____BB RR__BB__BB____BB ''', '''
a
a
a
''') @assert_no_logs def test_leader_in_inline(assert_pixels): assert_pixels(''' RR__GGBBBBBB__RR RR__GGBBBBBB__RR ''', '''
a a a
''') @pytest.mark.xfail @assert_no_logs def test_leader_bad_alignment(assert_pixels): assert_pixels(''' RRRRRR__________ RRRRRR__________ ______BB______RR ______BB______RR ''', '''
aaa
''') @assert_no_logs def test_leader_simple_rtl(assert_pixels): assert_pixels(''' BB__BBBBBBBB__RR BB__BBBBBBBB__RR BBBB__BBBB__RRRR BBBB__BBBB__RRRR BBBBBB__BBBB__RR BBBBBB__BBBB__RR ''', '''
a
bb
c
''') @assert_no_logs def test_leader_too_long_rtl(assert_pixels): assert_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 ''', '''
aaaaa
a a a a a a a
a a a a a
''') @assert_no_logs def test_leader_float_leader(assert_pixels): # 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… assert_pixels(''' RR____________BB RR____________BB RRRR__________BB RRRR__________BB RR____________BB RR____________BB ''', '''
a
bb
c
''') @assert_no_logs def test_leader_empty_string(assert_pixels): assert_pixels(''' RRRR____ ________ ''', '''
aaaa
''') @assert_no_logs def test_leader_zero_width_string(assert_pixels): assert_pixels(''' RRRR____ ________ ''', '''
aaaa
''') @assert_no_logs def test_leader_absolute(assert_pixels): assert_pixels(''' BBBBRRRR ______GG ''', '''
aa
bb
aa
''') @assert_no_logs def test_leader_padding(assert_pixels): assert_pixels(''' RR__BBBBBBBB__BB RR__BBBBBBBB__BB __RR__BBBB__BBBB __RR__BBBB__BBBB ''', '''
a
b
''') @assert_no_logs def test_leader_inline_padding(assert_pixels): assert_pixels(''' RR__BBBBBBBB__BB RR__BBBBBBBB__BB __RR__BBBB__BBBB __RR__BBBB__BBBB ''', '''
a
b
''') @assert_no_logs def test_leader_margin(assert_pixels): assert_pixels(''' RR__BBBBBBBB__BB RR__BBBBBBBB__BB __RR__BBBB__BBBB __RR__BBBB__BBBB ''', '''
a
b
''') @assert_no_logs def test_leader_inline_margin(assert_pixels): assert_pixels(''' RR__BBBBBBBB__BB RR__BBBBBBBB__BB __RR__BBBB__BBBB __RR__BBBB__BBBB ''', '''
a
b
''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703402.7992315 weasyprint-62.3/tests/draw/test_list.py0000644000000000000000000000366214634252453015301 0ustar00"""Test how lists are drawn.""" import pytest from ..testing_utils import SANS_FONTS, assert_no_logs @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(assert_pixels, position, pixels): assert_pixels(pixels, '''
    ''' % (SANS_FONTS, position)) @assert_no_logs def test_list_style_image_none(assert_pixels): assert_pixels(''' __________ __________ __________ __________ __________ __________ __________ __________ __________ __________ ''', '''
    • ''' % (SANS_FONTS,)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3373623 weasyprint-62.3/tests/draw/test_opacity.py0000644000000000000000000000351514634254702015772 0ustar00"""Test opacity.""" from ..testing_utils import assert_no_logs opacity_source = ''' %s''' @assert_no_logs def test_opacity_zero(assert_same_renderings): assert_same_renderings( opacity_source % '
      ', opacity_source % '
      ', opacity_source % '
      ', ) @assert_no_logs def test_opacity_normal_range(assert_same_renderings): assert_same_renderings( opacity_source % '
      ', opacity_source % '
      ', opacity_source % '
      ', opacity_source % '
      ', ) @assert_no_logs def test_opacity_nested(assert_same_renderings): assert_same_renderings( opacity_source % '
      ', opacity_source % '
      ', opacity_source % '''
      ''', # 0.9 * 0.666666 == 0.6 ) @assert_no_logs def test_opacity_percent_clamp_down(assert_same_renderings): assert_same_renderings( opacity_source % '
      ', opacity_source % '
      ', opacity_source % '
      ', ) @assert_no_logs def test_opacity_percent_clamp_up(assert_same_renderings): assert_same_renderings( opacity_source % '
      ', opacity_source % '
      ', opacity_source % '
      ', ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4854033 weasyprint-62.3/tests/draw/test_overflow.py0000644000000000000000000001137114635317053016164 0ustar00"""Test overflow and clipping.""" import pytest from ..testing_utils import assert_no_logs @assert_no_logs def test_overflow_1(assert_pixels): # See test_images assert_pixels(''' ________ ________ __rBBB__ __BBBB__ ________ ________ ________ ________ ''', '''
      ''') @assert_no_logs def test_overflow_2(assert_pixels): # is only 1px high, but its overflow is propageted to the viewport # ie. the padding edge of the page box. assert_pixels(''' ________ ________ __rBBB__ __BBBB__ __BBBB__ ________ ________ ________ ''', '''
      ''') @assert_no_logs def test_overflow_3(assert_pixels): # Assert that the border is not clipped by overflow: hidden assert_pixels(''' ________ ________ __BBBB__ __B__B__ __B__B__ __BBBB__ ________ ________ ''', '''
      ''') @assert_no_logs def test_overflow_4(assert_pixels): # Assert that the page margins aren't clipped by body's overflow assert_pixels(''' rr______ rr______ __BBBB__ __BBBB__ __BBBB__ __BBBB__ ________ ________ ''', ''' ''') @assert_no_logs def test_overflow_5(assert_pixels): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2026 assert_pixels(''' BBBBBB__ BBBBBB__ BBBB____ BBBB____ BBBB____ ________ ________ ________ ''', '''

      abc

      ab
      ab
      ab
      ab

      ''') @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(assert_pixels, number, css, pixels): assert_pixels(pixels, '''
      ''' % css) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703402.7992315 weasyprint-62.3/tests/draw/test_page.py0000644000000000000000000000134314634252453015234 0ustar00"""Test how pages are drawn.""" import pytest from ..testing_utils import assert_no_logs @assert_no_logs @pytest.mark.parametrize('rule, pixels', ( ('2n', '_R_R_R_R_R'), ('even', '_R_R_R_R_R'), ('2n+1', 'R_R_R_R_R_'), ('odd', 'R_R_R_R_R_'), ('2n+3', '__R_R_R_R_'), ('n', 'RRRRRRRRRR'), ('n-1', 'RRRRRRRRRR'), ('-n+3', 'RRR_______'), ('-2n+3', 'R_R_______'), ('-n-3', '__________'), ('3', '__R_______'), ('0n+0', '__________'), )) def test_nth_page(assert_pixels, rule, pixels): assert_pixels('\n'.join(pixels), ''' ''' % rule + 10 * '

      ') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4854033 weasyprint-62.3/tests/draw/test_table.py0000644000000000000000000013656514635317053015425 0ustar00"""Test how tables are drawn.""" import pytest from ..testing_utils import assert_no_logs tables_source = ''' ''' @assert_no_logs def test_tables_1(assert_pixels): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__ssssss_ssssss_ssssss__B_ _B__s____s_s____s_s____s__B_ _B__s____s_s____s_s____s__B_ _B__s____s_s____s_s____s__B_ _B__s____s_s____s_s____s__B_ _B__ssssss_s____s_ssssss__B_ _B_________s____s_________B_ _B__sssssssSssssS_ssssss__B_ _B__s______s____S_s____s__B_ _B__s______s____S_s____s__B_ _B__s______s____S_s____s__B_ _B__s______s____S_s____s__B_ _B__sssssssSSSSSS_ssssss__B_ _B________________________B_ _B__ssssss_ssssss_________B_ _B__s____s_s____s_________B_ _B__s____s_s____s_________B_ _B__s____s_s____s_________B_ _B__s____s_s____s_________B_ _B__ssssss_ssssss_________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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__ssssss_ssssss_ssssss__B_ _B__s____s_s____s_s____s__B_ _B__s____s_s____s_s____s__B_ _B__s____s_s____s_s____s__B_ _B__s____s_s____s_s____s__B_ _B__ssssss_s____s_ssssss__B_ _B_________s____s_________B_ _B__ssssss_SssssSsssssss__B_ _B__s____s_S____s______s__B_ _B__s____s_S____s______s__B_ _B__s____s_S____s______s__B_ _B__s____s_S____s______s__B_ _B__ssssss_SSSSSSsssssss__B_ _B________________________B_ _B_________ssssss_ssssss__B_ _B_________s____s_s____s__B_ _B_________s____s_s____s__B_ _B_________s____s_s____s__B_ _B_________s____s_s____s__B_ _B_________ssssss_ssssss__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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBB_________ _BBBBBBBBBBBBBBBBBB_________ _BB____s____s____BB_________ _BB____s____s____BB_________ _BB____s____s____BB_________ _BB____s____s____BB_________ _BBsssss____sssssBB_________ _BB_________s____BB_________ _BB_________s____BB_________ _BB_________s____BB_________ _BB_________s____BB_________ _BBssssssssssssssBB_________ _BB____s____s____BB_________ _BB____s____s____BB_________ _BB____s____s____BB_________ _BB____s____s____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): assert_pixels(''' ____________________________ _________BBBBBBBBBBBBBBBBBB_ _________BBBBBBBBBBBBBBBBBB_ _________BB____s____s____BB_ _________BB____s____s____BB_ _________BB____s____s____BB_ _________BB____s____s____BB_ _________BBsssss____sssssBB_ _________BB____s_________BB_ _________BB____s_________BB_ _________BB____s_________BB_ _________BB____s_________BB_ _________BBssssssssssssssBB_ _________BB____s____s____BB_ _________BB____s____s____BB_ _________BB____s____s____BB_ _________BB____s____s____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): assert_pixels(''' ____________________________ _tttttttttttttttttttttttttt_ _t________________________t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BB____s____s____BB_____t_ _t_BB____s____s____BB_____t_ _t_BB____s____s____BB_____t_ _t_BB____s____s____BB_____t_ _t_BBsssss____sssssBB_____t_ _t_BB_________s____BB_____t_ _t_BB_________s____BB_____t_ _t_BB_________s____BB_____t_ _t_BB_________s____BB_____t_ _t_BBssssssssssssssBB_____t_ _t________________________t_ _t________________________t_ _t________________________t_ _tttttttttttttttttttttttttt_ ____________________________ ____________________________ _tttttttttttttttttttttttttt_ _t_BBssssssssssssssBB_____t_ _t_BB____s____s____BB_____t_ _t_BB____s____s____BB_____t_ _t_BB____s____s____BB_____t_ _t_BB____s____s____BB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t_BBBBBBBBBBBBBBBBBB_____t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _tttttttttttttttttttttttttt_ ____________________________ ''', 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): assert_pixels(''' ____________________________ _tttttttttttttttttttttttttt_ _t________________________t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BBsssss____sssssBB_t_ _t_____BB____s_________BB_t_ _t_____BB____s_________BB_t_ _t_____BB____s_________BB_t_ _t_____BB____s_________BB_t_ _t_____BBssssssssssssssBB_t_ _t________________________t_ _t________________________t_ _t________________________t_ _tttttttttttttttttttttttttt_ ____________________________ ____________________________ _tttttttttttttttttttttttttt_ _t_____BBssssssssssssssBB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _tttttttttttttttttttttttttt_ ____________________________ ''', 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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__ssssss_ssssss_ssssss__B_ _B__ssssss_ssssss_ssssss__B_ _B__ssssss_ssssss_ssssss__B_ _B__ssssss_ssssss_ssssss__B_ _B__ssssss_ssssss_ssssss__B_ _B__ssssss_ssssss_ssssss__B_ _B_________ssssss_________B_ _B__sssssssSSSSSS_ssssss__B_ _B__sssssssSSSSSS_ssssss__B_ _B__sssssssSSSSSS_ssssss__B_ _B__sssssssSSSSSS_ssssss__B_ _B__sssssssSSSSSS_ssssss__B_ _B__sssssssSSSSSS_ssssss__B_ _B________________________B_ _B__ssssss_ssssss_________B_ _B__ssssss_ssssss_________B_ _B__ssssss_ssssss_________B_ _B__ssssss_ssssss_________B_ _B__ssssss_ssssss_________B_ _B__ssssss_ssssss_________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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__ssssss_ssssss_ssssss__B_ _B__ssssss_ssssss_ssssss__B_ _B__ssssss_ssssss_ssssss__B_ _B__ssssss_ssssss_ssssss__B_ _B__ssssss_ssssss_ssssss__B_ _B__ssssss_ssssss_ssssss__B_ _B_________ssssss_________B_ _B__ssssss_SSSSSSsssssss__B_ _B__ssssss_SSSSSSsssssss__B_ _B__ssssss_SSSSSSsssssss__B_ _B__ssssss_SSSSSSsssssss__B_ _B__ssssss_SSSSSSsssssss__B_ _B__ssssss_SSSSSSsssssss__B_ _B________________________B_ _B_________ssssss_ssssss__B_ _B_________ssssss_ssssss__B_ _B_________ssssss_ssssss__B_ _B_________ssssss_ssssss__B_ _B_________ssssss_ssssss__B_ _B_________ssssss_ssssss__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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B_________uuuuuu_________B_ _B__uuuuuuupppppp_uuuuuu__B_ _B__uuuuuuupppppp_uuuuuu__B_ _B__uuuuuuupppppp_uuuuuu__B_ _B__uuuuuuupppppp_uuuuuu__B_ _B__uuuuuuupppppp_uuuuuu__B_ _B__uuuuuuupppppp_uuuuuu__B_ _B________________________B_ _B__ssssss_ssssss_________B_ _B__ssssss_ssssss_________B_ _B__ssssss_ssssss_________B_ _B__ssssss_ssssss_________B_ _B__ssssss_ssssss_________B_ _B__ssssss_ssssss_________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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B_________uuuuuu_________B_ _B__uuuuuu_ppppppuuuuuuu__B_ _B__uuuuuu_ppppppuuuuuuu__B_ _B__uuuuuu_ppppppuuuuuuu__B_ _B__uuuuuu_ppppppuuuuuuu__B_ _B__uuuuuu_ppppppuuuuuuu__B_ _B__uuuuuu_ppppppuuuuuuu__B_ _B________________________B_ _B_________ssssss_ssssss__B_ _B_________ssssss_ssssss__B_ _B_________ssssss_ssssss__B_ _B_________ssssss_ssssss__B_ _B_________ssssss_ssssss__B_ _B_________ssssss_ssssss__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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__uuuuuu_uuuuuu_ssssss__B_ _B__uuuuuu_uuuuuu_ssssss__B_ _B__uuuuuu_uuuuuu_ssssss__B_ _B__uuuuuu_uuuuuu_ssssss__B_ _B__uuuuuu_uuuuuu_ssssss__B_ _B__uuuuuu_uuuuuu_ssssss__B_ _B_________uuuuuu_________B_ _B__uuuuuuupppppp_ssssss__B_ _B__uuuuuuupppppp_ssssss__B_ _B__uuuuuuupppppp_ssssss__B_ _B__uuuuuuupppppp_ssssss__B_ _B__uuuuuuupppppp_ssssss__B_ _B__uuuuuuupppppp_ssssss__B_ _B________________________B_ _B__uuuuuu_uuuuuu_________B_ _B__uuuuuu_uuuuuu_________B_ _B__uuuuuu_uuuuuu_________B_ _B__uuuuuu_uuuuuu_________B_ _B__uuuuuu_uuuuuu_________B_ _B__uuuuuu_uuuuuu_________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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__ssssss_uuuuuu_uuuuuu__B_ _B__ssssss_uuuuuu_uuuuuu__B_ _B__ssssss_uuuuuu_uuuuuu__B_ _B__ssssss_uuuuuu_uuuuuu__B_ _B__ssssss_uuuuuu_uuuuuu__B_ _B__ssssss_uuuuuu_uuuuuu__B_ _B_________uuuuuu_________B_ _B__ssssss_ppppppuuuuuuu__B_ _B__ssssss_ppppppuuuuuuu__B_ _B__ssssss_ppppppuuuuuuu__B_ _B__ssssss_ppppppuuuuuuu__B_ _B__ssssss_ppppppuuuuuuu__B_ _B__ssssss_ppppppuuuuuuu__B_ _B________________________B_ _B_________uuuuuu_uuuuuu__B_ _B_________uuuuuu_uuuuuu__B_ _B_________uuuuuu_uuuuuu__B_ _B_________uuuuuu_uuuuuu__B_ _B_________uuuuuu_uuuuuu__B_ _B_________uuuuuu_uuuuuu__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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uBBBBu_uBBBBu_uBBBBu__B_ _B__uBBBBu_uBBBBu_uBBBBu__B_ _B__uBBBBu_uBBBBu_uBBBBu__B_ _B__uBBBBu_uBBBBu_uBBBBu__B_ _B__uuuuuu_uBBBBu_uuuuuu__B_ _B_________uBBBBu_________B_ _B__ssssssspuuuup_ssssss__B_ _B__s______uBBBBp_s____s__B_ _B__s______uBBBBp_s____s__B_ _B__s______uBBBBp_s____s__B_ _B__s______uBBBBp_s____s__B_ _B__ssssssspppppp_ssssss__B_ _B________________________B_ _B__ssssss_ssssss_________B_ _B__s____s_s____s_________B_ _B__s____s_s____s_________B_ _B__s____s_s____s_________B_ _B__s____s_s____s_________B_ _B__ssssss_ssssss_________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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__uuuuuu_uuuuuu_uuuuuu__B_ _B__uBBBBu_uBBBBu_uBBBBu__B_ _B__uBBBBu_uBBBBu_uBBBBu__B_ _B__uBBBBu_uBBBBu_uBBBBu__B_ _B__uBBBBu_uBBBBu_uBBBBu__B_ _B__uuuuuu_uBBBBu_uuuuuu__B_ _B_________uBBBBu_________B_ _B__ssssss_puuuupsssssss__B_ _B__s____s_pBBBBu______s__B_ _B__s____s_pBBBBu______s__B_ _B__s____s_pBBBBu______s__B_ _B__s____s_pBBBBu______s__B_ _B__ssssss_ppppppsssssss__B_ _B________________________B_ _B_________ssssss_ssssss__B_ _B_________s____s_s____s__B_ _B_________s____s_s____s__B_ _B_________s____s_s____s__B_ _B_________s____s_s____s__B_ _B_________ssssss_ssssss__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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__uuuuuu_ssssss_ssssss__B_ _B__uBBBBu_s____s_s____s__B_ _B__uBBBBu_s____s_s____s__B_ _B__uBBBBu_s____s_s____s__B_ _B__uBBBBu_s____s_s____s__B_ _B__uuuuuu_s____s_ssssss__B_ _B_________s____s_________B_ _B__uuuuuuupuuuup_ssssss__B_ _B__uBBBBBBuBBBBp_s____s__B_ _B__uBBBBBBuBBBBp_s____s__B_ _B__uBBBBBBuBBBBp_s____s__B_ _B__uBBBBBBuBBBBp_s____s__B_ _B__uuuuuuupppppp_ssssss__B_ _B________________________B_ _B__uuuuuu_ssssss_________B_ _B__uBBBBu_s____s_________B_ _B__uBBBBu_s____s_________B_ _B__uBBBBu_s____s_________B_ _B__uBBBBu_s____s_________B_ _B__uuuuuu_ssssss_________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): assert_pixels(''' ____________________________ _BBBBBBBBBBBBBBBBBBBBBBBBBB_ _B________________________B_ _B________________________B_ _B__ssssss_ssssss_uuuuuu__B_ _B__s____s_s____s_uBBBBu__B_ _B__s____s_s____s_uBBBBu__B_ _B__s____s_s____s_uBBBBu__B_ _B__s____s_s____s_uBBBBu__B_ _B__ssssss_s____s_uuuuuu__B_ _B_________s____s_________B_ _B__ssssss_puuuupuuuuuuu__B_ _B__s____s_pBBBBuBBBBBBu__B_ _B__s____s_pBBBBuBBBBBBu__B_ _B__s____s_pBBBBuBBBBBBu__B_ _B__s____s_pBBBBuBBBBBBu__B_ _B__ssssss_ppppppuuuuuuu__B_ _B________________________B_ _B_________ssssss_uuuuuu__B_ _B_________s____s_uBBBBu__B_ _B_________s____s_uBBBBu__B_ _B_________s____s_uBBBBu__B_ _B_________s____s_uBBBBu__B_ _B_________ssssss_uuuuuu__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): assert_pixels(''' ______________________ _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): assert_pixels(''' ______________________ __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(assert_pixels): # Regression test for inline table with collapsed border and alignment # rendering borders incorrectly # https://github.com/Kozea/WeasyPrint/issues/82 assert_pixels(''' ____________________ ________RRRRRRRRRRR_ ________R____R____R_ ________R____R____R_ ________R____R____R_ ________RRRRRRRRRRR_ ____________________ ____________________ ____________________ ____________________ ''', '''
      ''') @assert_no_logs def test_tables_12(assert_pixels): assert_pixels(''' ____________________________ _________BBBBBBBBBBBBBBBBBB_ _________BBBBBBBBBBBBBBBBBB_ _________BB____s____s____BB_ _________BB____s____s____BB_ _________BB____s____s____BB_ _________BB____s____s____BB_ _________BBsssss____sssssBB_ _________BB____s_________BB_ _________BB____s_________BB_ _________BB____s_________BB_ _________BB____s_________BB_ _________BBssssssssssssssBB_ _________BB____s____s____BB_ _________BB____s____s____BB_ _________BB____s____s____BB_ _________BB____s____s____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): assert_pixels(''' ____________________________ _tttttttttttttttttttttttttt_ _t________________________t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BBsssss____sssssBB_t_ _t_____BB____s_________BB_t_ _t_____BB____s_________BB_t_ _t_____BB____s_________BB_t_ _t_____BB____s_________BB_t_ _t_____BBssssssssssssssBB_t_ _t________________________t_ _t________________________t_ _t________________________t_ _tttttttttttttttttttttttttt_ ____________________________ ____________________________ _tttttttttttttttttttttttttt_ _t_____BBssssssssssssssBB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BB____s____s____BB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t_____BBBBBBBBBBBBBBBBBB_t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _t________________________t_ _tttttttttttttttttttttttttt_ ____________________________ ''', 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): assert_pixels(''' ____________________________ _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(assert_pixels): # Regression test for colspan in last body line with footer # https://github.com/Kozea/WeasyPrint/issues/1250 assert_pixels(''' ______________________ __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): assert_pixels(''' ____________________ _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): assert_pixels(''' ________________ _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): assert_pixels(''' ____________ _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(assert_pixels): # Regression test: https://github.com/Kozea/WeasyPrint/issues/1523 assert_pixels(''' RR RR RR RR RR RR RR RR ''', '''
      a a a a
      a a a a
      ''') @assert_no_logs def test_tables_20(assert_pixels): assert_pixels(''' ____________________ _RRRRRRRRRRRR_______ _RBBBBBBBBBBR_______ _RRRRRRRRRRRR_______ ____________________ ''', ''' ''') @assert_no_logs def test_tables_21(assert_pixels): assert_pixels(''' _________________________ _rrrrrrrrrrrrrrrrrrrrrrr_ _rBBBBBBBBBBrBBBBBBBBBBr_ _rBKKKKKKBBBrBKKKKKKBBBr_ _rBKKKKKKBBBrBKKKKKKBBBr_ _rBBBBBBBBBBrBBBBBBBBBBr_ _rrrrrrrrrrrrrrrrrrrrrrr_ _________________________ _________________________ _________________________ _________________________ _________________________ _rrrrrrrrrrrrrrrrrrrrrrr_ _rBBBBBBBBBBrBBBBBBBBBBr_ _rBKKKKKKBBBrBBBBBBBBBBr_ _rBKKKKKKBBBrBBBBBBBBBBr_ _rBBBBBBBBBBrBBBBBBBBBBr_ _rrrrrrrrrrrrrrrrrrrrrrr_ _________________________ _________________________ _________________________ _________________________ ''', '''
      ''') @assert_no_logs def test_tables_22(assert_pixels): assert_pixels(''' _________________________ _rrrrrrrrrrrrrrrrrrrrrrr_ _rKKKKKKKKKKrKKKKKKKKKKr_ _rKKKKKKKKKKrKKKKKKKKKKr_ _rrrrrrrrrrrrrrrrrrrrrrr_ _rKKKKKKBBBBrBBBBBBBBBBr_ _rKKKKKKBBBBrBBBBBBBBBBr_ _rBBBBBBBBBBrBBBBBBBBBBr_ _________________________ _________________________ _rrrrrrrrrrrrrrrrrrrrrrr_ _rKKKKKKKKKKrKKKKKKKKKKr_ _rKKKKKKKKKKrKKKKKKKKKKr_ _rrrrrrrrrrrrrrrrrrrrrrr_ _rKKKKKKBBBBrBBBBBBBBBBr_ _rKKKKKKBBBBrBBBBBBBBBBr_ _rrrrrrrrrrrrrrrrrrrrrrr_ _________________________ ''', '''
      abcabc
      abc
      ''') @pytest.mark.xfail @assert_no_logs def test_tables_23(assert_pixels): assert_pixels(''' _________________________ _rrrrrrrrrrrrrrrrrrrrrrr_ _rKKKKKKKKKKrKKKKKKKKKKr_ _rKKKKKKKKKKrKKKKKKKKKKr_ _rrrrrrrrrrrrrrrrrrrrrrr_ _rKKKKKKBBBBrBBBBBBBBBBr_ _rKKKKKKBBBBrBBBBBBBBBBr_ _rBBBBBBBBBBrBBBBBBBBBBr_ _________________________ _________________________ _rrrrrrrrrrrrrrrrrrrrrrr_ _rKKKKKKKKKKrKKKKKKKKKKr_ _rKKKKKKKKKKrKKKKKKKKKKr_ _rKKKKKKBBBBrBBBBBBBBBBr_ _rKKKKKKBBBBrBBBBBBBBBBr_ _rrrrrrrrrrrrrrrrrrrrrrr_ _________________________ _________________________ ''', '''
      abcdeabcde
      abc abc
      ''') @assert_no_logs def test_running_elements_table_border_collapse(assert_pixels): assert_pixels(2 * ''' KK_____________ KK_____________ _______________ _______________ _______________ KKKKKKK________ KRRKRRK________ KRRKRRK________ KKKKKKK________ KRRKRRK________ KRRKRRK________ KKKKKKK________ _______________ _______________ _______________ ''', '''
      abcdeabcde
      abc abc
      A B
      C D
      1
      2
      ''') @assert_no_logs def test_running_elements_table_border_collapse_empty(assert_pixels): assert_pixels(2 * ''' KK________ KK________ __________ __________ __________ __________ __________ __________ __________ __________ ''', '''
      1
      2
      ''') @pytest.mark.xfail @assert_no_logs def test_running_elements_table_border_collapse_border_style(assert_pixels): assert_pixels(2 * ''' KK_____________ KK_____________ _______________ _______________ _______________ KKKZ___________ KRR_RR_________ KRR_RR_________ KKKK__Z________ KRRKRRK________ KRRKRRK________ KKKKKKK________ _______________ _______________ _______________ ''', '''
      A B
      C D
      1
      2
      ''') @assert_no_logs def test_running_elements_table_border_collapse_span(assert_pixels): assert_pixels(2 * ''' KK_____________ KK_____________ _______________ _______________ _______________ KKKKKKKKKK_____ KRRKRRKRRK_____ KRRKRRKRRK_____ K__KKKKKKK_____ K__KRR___K_____ K__KRR___K_____ KKKKKKKKKK_____ _______________ _______________ _______________ ''', '''
      A B C
      D
      1
      2
      ''') @assert_no_logs def test_running_elements_table_border_collapse_margin(assert_pixels): assert_pixels(2 * ''' KK_____________ KK_____________ _______________ _______________ _______________ _______________ ____KKKKKKK____ ____KRRKRRK____ ____KRRKRRK____ ____KKKKKKK____ ____KRRKRRK____ ____KRRKRRK____ ____KKKKKKK____ _______________ _______________ ''', '''
      A B
      C D
      1
      2
      ''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4864032 weasyprint-62.3/tests/draw/test_text.py0000644000000000000000000004701514635317053015311 0ustar00"""Test how text is drawn.""" import pytest from ..testing_utils import SANS_FONTS def test_text_overflow_clip(assert_pixels): assert_pixels(''' _________ _RRRRRRR_ _RRRRRRR_ _________ _RR__RRR_ _RR__RRR_ _________ ''', '''
      abcde
      a bcde
      ''') def test_text_overflow_ellipsis(assert_pixels): assert_pixels(''' _________ _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(assert_pixels): # Test text alignment for rtl text with trailing space. # Test regression: https://github.com/Kozea/WeasyPrint/issues/1111 assert_pixels(''' _________ _rrrrBBB_ _________ _rrrrBBB_ _________ _BBBrrrr_ _________ _BBBrrrr_ _________ ''', '''

      abc

      ‏abc

      abc

      ‏abc

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

      abcd efgh ijkl

      ''') @pytest.mark.xfail def test_max_lines_nested(assert_pixels): assert_pixels(''' BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB BBBBBBBBBB rrrrrrrrrr rrrrrrrrrr rrrrrrrrrr rrrrrrrrrr BBBBBBBBBB BBBBBBBBBB __________ __________ ''', '''
      aaaaa aaaaa
      bbbbb bbbbb bbbbb bbbbb
      aaaaa aaaaa
      ''') def test_line_clamp(assert_pixels): assert_pixels(''' 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): assert_pixels(''' 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): assert_pixels(''' BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBB__BBBB BBBB__BBBB __________ __________ __________ __________ ''', '''

      aa a bb b cc c dddd eeee

      ''') def test_line_clamp_nested(assert_pixels): assert_pixels(''' 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_nested_after(assert_pixels): assert_pixels(''' BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBB__BB__ BBBBBBBBBB BBBBBBBBBB __________ __________ __________ __________ ''', '''
      aa a

      bb b

      cc c dddd eeee ffff gggg hhhh
      ''') @pytest.mark.xfail def test_ellipsis_nested(assert_pixels): assert_pixels(''' BBBBBB____ BBBBBB____ BBBBBB____ BBBBBB____ BBBBBB____ BBBBBB____ BBBBBB____ BBBBBB____ BBBBBBBB__ BBBBBBBB__ ''', '''

      aaa

      aaa

      aaa

      aaa

      aaa

      aaa

      ''') def test_text_align_right(assert_pixels): assert_pixels(''' _________ __RR__RR_ __RR__RR_ ______RR_ ______RR_ _________ ''', '''
      a c e
      ''') def test_text_align_justify(assert_pixels): assert_pixels(''' _________ _RR___RR_ _RR___RR_ _RR______ _RR______ _________ ''', '''
      a c e
      ''') def test_text_word_spacing(assert_pixels): assert_pixels(''' ___________________ _RR____RR____RR____ _RR____RR____RR____ ___________________ ''', '''
      a c e
      ''') def test_text_letter_spacing(assert_pixels): assert_pixels(''' ___________________ _RR____RR____RR____ _RR____RR____RR____ ___________________ ''', '''
      ace
      ''') def test_text_underline(assert_pixels): assert_pixels(''' _____________ _zzzzzzzzzzz_ _zsssssssssz_ _zsssssssssz_ _zuuuuuuuuuz_ _zzzzzzzzzzz_ _____________ ''', '''
      abc
      ''') def test_text_overline(assert_pixels): # Ascent value seems to be a bit random, don’t try to get the exact # position of the line assert_pixels(''' _____________ _zzzzzzzzzzz_ _zzzzzzzzzzz_ _zsssssssssz_ _zsssssssssz_ _zzzzzzzzzzz_ _____________ ''', '''
      abc
      ''') def test_text_line_through(assert_pixels): assert_pixels(''' _____________ _zzzzzzzzzzz_ _zBBBBBBBBBz_ _zuuuuuuuuuz_ _zBBBBBBBBBz_ _zzzzzzzzzzz_ _____________ ''', '''
      abc
      ''') def test_text_multiple_text_decoration(assert_pixels): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1621 assert_pixels(''' _____________ _zzzzzzzzzzz_ _zsssssssssz_ _zBBBBBBBBBz_ _zuuuuuuuuuz_ _zzzzzzzzzzz_ _____________ ''', '''
      abc
      ''') def test_text_nested_text_decoration(assert_pixels): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1621 assert_pixels(''' _____________ _zzzzzzzzzzz_ _zsssssssssz_ _zsssBBBsssz_ _zuuuuuuuuuz_ _zzzzzzzzzzz_ _____________ ''', '''
      abc
      ''') @pytest.mark.xfail def test_text_nested_text_decoration_color(assert_pixels): # See weasyprint.css.text_decoration’s TODO assert_pixels(''' _____________ _zzzzzzzzzzz_ _zRRRRRRRRRz_ _zRRRGGGRRRz_ _zBBBBBBBBBz_ _zzzzzzzzzzz_ _____________ ''', '''
      abc
      ''') @pytest.mark.xfail def test_text_nested_block_text_decoration(assert_pixels): # See weasyprint.css.text_decoration’s TODO assert_pixels(''' _______ _zzzzz_ _zRRRz_ _zRRRz_ _zBBBz_ _zRRRz_ _zGGGz_ _zBBBz_ _zRRRz_ _zRRRz_ _zBBBz_ _zzzzz_ _______ ''', '''
      a
      b
      c
      ''') @pytest.mark.xfail def test_text_float_text_decoration(assert_pixels): # See weasyprint.css.text_decoration’s TODO assert_pixels(''' _____________ _zzzzz_______ _zRRRz__RRR__ _zRRRz__RRR__ _zBBBz__RRR__ _zzzzz_______ _____________ ''', '''
      ab
      ''') def test_text_decoration_var(assert_pixels): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1697 assert_pixels(''' _____________ _zzzzzzzzzzz_ _zRRRRRRRRRz_ _zBBBBBBBBBz_ _zRRRRRRRRRz_ _zzzzzzzzzzz_ _____________ ''', '''
      abc
      ''') def test_zero_width_character(assert_pixels): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1508 assert_pixels(''' ______ _RRRR_ _RRRR_ ______ ''', '''
      a‌b
      ''') def test_font_size_very_small(assert_pixels): assert_pixels(''' __________ __________ __________ __________ ''', ''' test font size zero ''') def test_missing_glyph_fallback(assert_pixels): # The apostrophe is not included in weasyprint.otf assert_pixels(''' ___zzzzzzzzzzzzzzzzz _RRzzzzzzzzzzzzzzzzz _RRzzzzzzzzzzzzzzzzz ___zzzzzzzzzzzzzzzzz ''', ''' a\'''' % SANS_FONTS) def test_tabulation_character(assert_pixels): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1515 assert_pixels(''' __________ _RR____RR_ _RR____RR_ __________ ''', '''
      a	b
      ''') def test_otb_font(assert_pixels): assert_pixels(''' ____________________ __RR______RR________ __RR__RR__RR________ __RR__RR__RR________ ____________________ ____________________ ''', ''' AaA''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703402.7992315 weasyprint-62.3/tests/draw/test_transform.py0000644000000000000000000001554114634252453016340 0ustar00"""Test transformations.""" from ..testing_utils import assert_no_logs @assert_no_logs def test_2d_transform_1(assert_pixels): assert_pixels(''' ________ ________ __BBBr__ __BBBB__ __BBBB__ __BBBB__ ________ ________ ''', '''
      ''') @assert_no_logs def test_2d_transform_2(assert_pixels): assert_pixels(''' ____________ ____________ _____BBBr___ _____BBBB___ _____BBBB___ _____BBBB___ ____________ ____________ ____________ ____________ ____________ ____________ ''', '''
      ''') @assert_no_logs def test_2d_transform_3(assert_pixels): # A translateX after the rotation is actually a translateY assert_pixels(''' ____________ ____________ ____________ ____________ ____________ __BBBr______ __BBBB______ __BBBB______ __BBBB______ ____________ ____________ ____________ ''', '''
      ''') @assert_no_logs def test_2d_transform_4(assert_pixels): assert_pixels(''' ____________ ____________ ____________ ____________ ____________ __BBBr______ __BBBB______ __BBBB______ __BBBB______ ____________ ____________ ____________ ''', '''
      ''') @assert_no_logs def test_2d_transform_5(assert_pixels): assert_pixels(''' ________ ________ __BBBr__ __BBBB__ __BBBB__ __BBBB__ ________ ________ ''', '''
      ''') @assert_no_logs def test_2d_transform_6(assert_pixels): assert_pixels(''' ________ ________ ________ ________ ___rBBB_ ___BBBB_ ___BBBB_ ___BBBB_ ''', '''
      ''') @assert_no_logs def test_2d_transform_7(assert_pixels): assert_pixels(''' ________ ________ ___rBBB_ ___BBBB_ ___BBBB_ ___BBBB_ ________ ________ ''', '''
      ''') @assert_no_logs def test_2d_transform_8(assert_pixels): assert_pixels(''' ________ ________ _____rBB _____BBB _____BBB _____BBB ________ ________ ''', '''
      ''') @assert_no_logs def test_2d_transform_9(assert_pixels): assert_pixels(''' ________ __rBBB__ __BBBB__ __BBBB__ __BBBB__ ________ ________ ________ ''', '''
      ''') @assert_no_logs def test_2d_transform_10(assert_pixels): assert_pixels(''' __________ _rrBBBBBB_ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ ''', '''
      ''') @assert_no_logs def test_2d_transform_11(assert_pixels): assert_pixels(''' __________ __rBBB____ __rBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __________ ''', '''
      ''') @assert_no_logs def test_2d_transform_12(assert_pixels): assert_pixels(''' __________ __rBBB____ __rBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __BBBB____ __________ ''', '''
      ''') @assert_no_logs def test_2d_transform_13(assert_pixels): assert_pixels(''' __________ __________ _rrBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ _BBBBBBBB_ __________ __________ __________ __________ ''', '''
      ''') @assert_no_logs def test_2d_transform_opacity(assert_pixels): assert_pixels(''' __________ __________ __________ _ss_______ _ss_______ __________ __________ __________ __________ __________ ''', '''
      ''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703402.7992315 weasyprint-62.3/tests/draw/test_visibility.py0000644000000000000000000000272414634252453016513 0ustar00"""Test visibility.""" from ..testing_utils import assert_no_logs visibility_source = '''
      ''' @assert_no_logs def test_visibility_1(assert_pixels): assert_pixels(''' ____________ _rBBB_rBBB__ _BBBB_BBBB__ _BBBB_BBBB__ _BBBB_BBBB__ ____________ ____________ ''', visibility_source % '') @assert_no_logs def test_visibility_2(assert_pixels): assert_pixels(''' ____________ ____________ ____________ ____________ ____________ ____________ ____________ ''', visibility_source % 'div { visibility: hidden }') @assert_no_logs def test_visibility_3(assert_pixels): assert_pixels(''' ____________ ______rBBB__ ______BBBB__ ______BBBB__ ______BBBB__ ____________ ____________ ''', visibility_source % 'div { visibility: hidden } ' 'span { visibility: visible }') @assert_no_logs def test_visibility_4(assert_pixels): assert_pixels(''' ____________ _rBBB_rBBB__ _BBBB_BBBB__ _BBBB_BBBB__ _BBBB_BBBB__ ____________ ____________ ''', visibility_source % '@page { visibility: hidden; background: red }') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4864032 weasyprint-62.3/tests/draw/test_whitespace.py0000644000000000000000000001364714635317053016465 0ustar00"""Test how white spaces collapse.""" from ..testing_utils import assert_no_logs @assert_no_logs def test_whitespace_inline(assert_pixels): assert_pixels(''' RRRR__RRRR____ RRRR__RRRR____ ______________ ______________ ''', ''' aa aa ''') @assert_no_logs def test_whitespace_nested_inline(assert_pixels): assert_pixels(''' RRRR__RRRR____ RRRR__RRRR____ ______________ ______________ ''', ''' aa aa ''') @assert_no_logs def test_whitespace_inline_space_between(assert_pixels): assert_pixels(''' RRRR__RRRR____ RRRR__RRRR____ ______________ ______________ ''', ''' aa aa ''') @assert_no_logs def test_whitespace_float_between(assert_pixels): assert_pixels(''' RRRR__RRRR__BB RRRR__RRRR__BB ______________ ______________ ''', ''' aa
      a
      aa ''') @assert_no_logs def test_whitespace_in_float(assert_pixels): assert_pixels(''' RRRRRRRR____BB RRRRRRRR____BB ______________ ______________ ''', ''' aa
      a
      aa ''') @assert_no_logs def test_whitespace_absolute_between(assert_pixels): assert_pixels(''' RRRR__RRRR__BB RRRR__RRRR__BB ______________ ______________ ''', ''' aa
      a
      aa ''') @assert_no_logs def test_whitespace_in_absolute(assert_pixels): assert_pixels(''' RRRRRRRR____BB RRRRRRRR____BB ______________ ______________ ''', ''' aa
      a
      aa ''') @assert_no_logs def test_whitespace_running_between(assert_pixels): assert_pixels(''' RRRR__RRRR____ RRRR__RRRR____ ______BB______ ______BB______ ''', ''' aa
      a
      aa ''') @assert_no_logs def test_whitespace_in_running(assert_pixels): assert_pixels(''' RRRRRRRR______ RRRRRRRR______ ______BB______ ______BB______ ''', ''' aa
      a
      aa ''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703377.2069473 weasyprint-62.3/tests/layout/__init__.py0000644000000000000000000000014514634252421015372 0ustar00"""Tests for layout. Includes positioning and dimensioning of boxes, line breaks, page breaks. """ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4864032 weasyprint-62.3/tests/layout/test_block.py0000644000000000000000000007347314635317053016006 0ustar00"""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): # https://www.w3.org/TR/css-ui-3/#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): # https://www.w3.org/TR/css-ui-3/#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(): # https://www.w3.org/TR/css-backgrounds-3/#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(): # https://www.w3.org/TR/css-backgrounds-3/#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 def test_overflow_hidden_in_flow_layout(): page, = render_pages('''
      abc
      def
      ''') html, = page.children body, = html.children parent_div, = body.children assert parent_div.height == 3 def test_overflow_hidden_out_of_flow_layout(): page, = render_pages('''
      abc
      def
      ''') html, = page.children body, = html.children parent_div, = body.children assert parent_div.height == 3 @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' @assert_no_logs def test_page_breaks_1(): # last div 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) 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_page_breaks_2(): # last div 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) 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_page_breaks_3(): # center div must be the last element, # but div 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) positions_y = [[div.position_y for div in divs] for divs in page_divs] assert positions_y == [[10], [10], [10]] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4864032 weasyprint-62.3/tests/layout/test_column.py0000644000000000000000000011106614635317053016200 0ustar00"""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), ('5%', 15), ('-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_column_span_3(): page1, page2 = render_pages('''
      abc def ghi jkl
      line1 line2
      mno pqr
      ''') html, = page1.children body, = html.children div, = body.children column1, column2, section = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (4, 0) assert (section.position_x, section.position_y) == (0, 2) assert column1.children[0].children[0].children[0].text == 'abc' assert column1.children[0].children[1].children[0].text == 'def' assert column2.children[0].children[0].children[0].text == 'ghi' assert column2.children[0].children[1].children[0].text == 'jkl' assert section.children[0].children[0].text == 'line1' html, = page2.children body, = html.children div, = body.children section, column1, column2 = div.children assert (section.position_x, section.position_y) == (0, 0) assert (column1.position_x, column1.position_y) == (0, 1) assert (column2.position_x, column2.position_y) == (4, 1) assert section.children[0].children[0].text == 'line2' assert column1.children[0].children[0].children[0].text == 'mno' assert column2.children[0].children[0].children[0].text == 'pqr' @assert_no_logs def test_column_span_4(): page1, page2 = render_pages('''
      abc def
      line1
      ghi jkl mno pqr
      ''') html, = page1.children body, = html.children div, = body.children column1, column2, section, column3, column4 = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (4, 0) assert (section.position_x, section.position_y) == (0, 1) assert (column3.position_x, column3.position_y) == (0, 2) assert (column4.position_x, column4.position_y) == (4, 2) assert column1.children[0].children[0].children[0].text == 'abc' assert column2.children[0].children[0].children[0].text == 'def' assert section.children[0].children[0].text == 'line1' assert column3.children[0].children[0].children[0].text == 'ghi' assert column4.children[0].children[0].children[0].text == 'jkl' html, = page2.children body, = html.children div, = body.children column1, column2 = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (4, 0) assert column1.children[0].children[0].children[0].text == 'mno' assert column2.children[0].children[0].children[0].text == 'pqr' @assert_no_logs def test_column_span_5(): page1, page2 = render_pages('''
      abc def ghi jkl
      line1
      mno pqr
      ''') html, = page1.children body, = html.children div, = body.children column1, column2, section = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (4, 0) assert (section.position_x, section.position_y) == (0, 2) assert column1.children[0].children[0].children[0].text == 'abc' assert column1.children[0].children[1].children[0].text == 'def' assert column2.children[0].children[0].children[0].text == 'ghi' assert column2.children[0].children[1].children[0].text == 'jkl' assert section.children[0].children[0].text == 'line1' html, = page2.children body, = html.children div, = body.children column1, column2 = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (4, 0) assert column1.children[0].children[0].children[0].text == 'mno' assert column2.children[0].children[0].children[0].text == 'pqr' @assert_no_logs def test_column_span_6(): page1, page2 = render_pages('''
      abc def ghi jkl mno pqr
      line1
      ''') html, = page1.children body, = html.children div, = body.children column1, column2 = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (4, 0) assert column1.children[0].children[0].children[0].text == 'abc' assert column1.children[0].children[1].children[0].text == 'def' assert column1.children[0].children[2].children[0].text == 'ghi' assert column2.children[0].children[0].children[0].text == 'jkl' assert column2.children[0].children[1].children[0].text == 'mno' assert column2.children[0].children[2].children[0].text == 'pqr' html, = page2.children body, = html.children div, = body.children section, = div.children assert section.children[0].children[0].text == 'line1' assert (section.position_x, section.position_y) == (0, 0) @assert_no_logs def test_column_span_7(): page1, page2 = render_pages('''
      abc def ghi jkl
      l1
      mno pqr
      ''') html, = page1.children body, = html.children div, = body.children column1, column2 = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (4, 0) assert column1.children[0].children[0].children[0].text == 'abc' assert column1.children[0].children[1].children[0].text == 'def' assert column2.children[0].children[0].children[0].text == 'ghi' assert column2.children[0].children[1].children[0].text == 'jkl' html, = page2.children body, = html.children div, = body.children section, column1, column2 = div.children assert (section.position_x, section.position_y) == (0, 0) assert (column1.position_x, column1.position_y) == (0, 2) assert (column2.position_x, column2.position_y) == (4, 2) assert section.children[0].children[0].text == 'l1' assert column1.children[0].children[0].children[0].text == 'mno' assert column2.children[0].children[0].children[0].text == 'pqr' @assert_no_logs def test_column_span_8(): page1, page2 = render_pages('''
      abc def ghi jkl mno pqr
      line1
      ''') html, = page1.children body, = html.children div, = body.children column1, column2 = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (4, 0) assert column1.children[0].children[0].children[0].text == 'abc' assert column1.children[0].children[1].children[0].text == 'def' assert column2.children[0].children[0].children[0].text == 'ghi' assert column2.children[0].children[1].children[0].text == 'jkl' html, = page2.children body, = html.children div, = body.children column1, column2, section = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (4, 0) assert (section.position_x, section.position_y) == (0, 1) assert column1.children[0].children[0].children[0].text == 'mno' assert column2.children[0].children[0].children[0].text == 'pqr' assert section.children[0].children[0].text == 'line1' @assert_no_logs def test_column_span_9(): page1, = render_pages('''
      abc
      line1
      def ghi
      ''') html, = page1.children body, = html.children div, = body.children column1, section, column2, column3 = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (section.position_x, section.position_y) == (0, 1) assert (column2.position_x, column2.position_y) == (0, 2) assert (column3.position_x, column3.position_y) == (4, 2) assert column1.children[0].children[0].children[0].text == 'abc' assert section.children[0].children[0].text == 'line1' assert column2.children[0].children[0].children[0].text == 'def' assert column3.children[0].children[0].children[0].text == 'ghi' @assert_no_logs def test_column_span_balance(): page, = render_pages('''
      abc def
      line1
      ghi jkl
      ''') html, = page.children body, = html.children div, = body.children column1, column2, section, column3 = div.children assert (column1.position_x, column1.position_y) == (0, 0) assert (column2.position_x, column2.position_y) == (4, 0) assert (section.position_x, section.position_y) == (0, 1) assert (column3.position_x, column3.position_y) == (0, 2) assert column1.children[0].children[0].children[0].text == 'abc' assert column2.children[0].children[0].children[0].text == 'def' assert section.children[0].children[0].text == 'line1' assert column3.children[0].children[0].children[0].text == 'ghi' assert column3.children[0].children[1].children[0].text == 'jkl' @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 assert columns[0].children[0].children[0].text == 'a' assert columns[0].children[1].children[0].text == 'b' assert columns[1].children[0].children[0].text == 'c' assert 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 assert columns[0].children[0].children[0].text == 'e' assert columns[0].children[1].children[0].text == 'f' assert columns[1].children[0].children[0].text == 'g' @assert_no_logs def test_columns_breaks(): page1, page2 = render_pages('''
      a
      b
      c
      ''') html, = page1.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert len(columns[0].children) == 1 assert len(columns[1].children) == 1 assert columns[0].children[0].children[0].children[0].text == 'a' assert columns[1].children[0].children[0].children[0].text == 'b' html, = page2.children body, = html.children div, = body.children columns = div.children assert len(columns) == 1 assert len(columns[0].children) == 1 assert columns[0].children[0].children[0].children[0].text == 'c' @assert_no_logs def test_columns_break_after_column_1(): page1, = render_pages('''
      a b
      c
      d
      ''') html, = page1.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert columns[0].children[0].children[0].children[0].text == 'a' assert columns[0].children[0].children[1].children[0].text == 'b' assert columns[0].children[1].children[0].children[0].text == 'c' assert columns[1].children[0].children[0].children[0].text == 'd' @assert_no_logs def test_columns_break_after_column_2(): page1, = render_pages('''
      a
      b c d
      ''') html, = page1.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert columns[0].children[0].children[0].children[0].text == 'a' assert columns[1].children[0].children[0].children[0].text == 'b' assert columns[1].children[0].children[1].children[0].text == 'c' assert columns[1].children[0].children[2].children[0].text == 'd' @assert_no_logs def test_columns_break_after_avoid_column(): page1, = render_pages('''
      a
      b
      c d
      ''') html, = page1.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert columns[0].children[0].children[0].children[0].text == 'a' assert columns[0].children[1].children[0].children[0].text == 'b' assert columns[0].children[2].children[0].children[0].text == 'c' assert columns[1].children[0].children[0].children[0].text == 'd' @assert_no_logs def test_columns_break_before_column_1(): page1, = render_pages('''
      a b c
      d
      ''') html, = page1.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert columns[0].children[0].children[0].children[0].text == 'a' assert columns[0].children[0].children[1].children[0].text == 'b' assert columns[0].children[0].children[2].children[0].text == 'c' assert columns[1].children[0].children[0].children[0].text == 'd' @assert_no_logs def test_columns_break_before_column_2(): page1, = render_pages('''
      a
      b
      c d
      ''') html, = page1.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert columns[0].children[0].children[0].children[0].text == 'a' assert columns[1].children[0].children[0].children[0].text == 'b' assert columns[1].children[1].children[0].children[0].text == 'c' assert columns[1].children[1].children[1].children[0].text == 'd' @assert_no_logs def test_columns_break_before_avoid_column(): page1, = render_pages('''
      a b
      c
      d
      ''') html, = page1.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert columns[0].children[0].children[0].children[0].text == 'a' assert columns[0].children[0].children[1].children[0].text == 'b' assert columns[0].children[1].children[0].children[0].text == 'c' assert columns[1].children[0].children[0].children[0].text == 'd' @assert_no_logs def test_columns_break_inside_column_1(): page1, = render_pages('''
      a b c
      d
      ''') html, = page1.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert columns[0].children[0].children[0].children[0].text == 'a' assert columns[0].children[0].children[1].children[0].text == 'b' assert columns[0].children[0].children[2].children[0].text == 'c' assert columns[1].children[0].children[0].children[0].text == 'd' @assert_no_logs def test_columns_break_inside_column_2(): page1, = render_pages('''
      a
      b c d
      ''') html, = page1.children body, = html.children div, = body.children columns = div.children assert len(columns) == 2 assert columns[0].children[0].children[0].children[0].text == 'a' assert columns[1].children[0].children[0].children[0].text == 'b' assert columns[1].children[0].children[1].children[0].text == 'c' assert columns[1].children[0].children[2].children[0].text == 'd' @assert_no_logs def test_columns_break_inside_column_not_empty_page(): page1, = render_pages('''

      p

      a b c
      d
      ''') html, = page1.children body, = html.children p, div, = body.children assert p.children[0].children[0].text == 'p' columns = div.children assert len(columns) == 2 assert columns[0].children[0].children[0].children[0].text == 'a' assert columns[0].children[0].children[1].children[0].text == 'b' assert columns[0].children[0].children[2].children[0].text == 'c' assert columns[1].children[0].children[0].children[0].text == 'd' @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_higher_than_page(): page1, page2 = render_pages('''
      a b c d e f g h
      ''') html, = page1.children body, = html.children div, = body.children assert div.width == 5 columns = div.children assert len(columns) == 5 assert columns[0].children[0].children[0].text == 'a' assert columns[1].children[0].children[0].text == 'b' assert columns[2].children[0].children[0].text == 'c' assert columns[3].children[0].children[0].text == 'd' assert columns[4].children[0].children[0].text == 'e' html, = page2.children body, = html.children div, = body.children assert div.width == 5 columns = div.children assert len(columns) == 3 assert columns[0].children[0].children[0].text == 'f' assert columns[1].children[0].children[0].text == 'g' assert columns[2].children[0].children[0].text == 'h' @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

      ''') @assert_no_logs def test_columns_regression_6(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2103 render_pages('''
      abc def
      ''') @assert_no_logs def test_columns_regression_7(): page1, page2 = render_pages('''
      a
      b
      c
      d
      ''') html, = page1.children body, = html.children div, = body.children assert div.position_y == 0 assert not div.children html, = page2.children body, = html.children div, = body.children column1, column2 = div.children assert column1.position_y == 0 assert column2.position_y == 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4864032 weasyprint-62.3/tests/layout/test_flex.py0000644000000000000000000004274014635317053015643 0ustar00"""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
      ''') @assert_no_logs def test_flex_percent_height(): page, = render_pages('''
      ''') html, = page.children body, = html.children a, = body.children b, = a.children assert a.height == 10 assert b.height == 1 @assert_no_logs def test_flex_percent_height_auto(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2146 page, = render_pages('''
      ''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4864032 weasyprint-62.3/tests/layout/test_float.py0000644000000000000000000005474014635317053016015 0ustar00"""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) 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) 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) 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_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 @assert_no_logs def test_float_previous_break(): page1, page2 = render_pages('''
      oooooo oooooo oooooo oooooo

      xxxxxx

      yyyyyy

      aaaaaa
      cccccc
      dddddd
      ''') html, = page1.children body, = html.children article, = body.children html, = page2.children body, = html.children p1, p2, section, article = body.children @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 def test_float_table_aborted_row(): page1, page2 = render_pages('''
      abc
      abc
      def
      f
      g
      ghi
      ''') html, = page1.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children tbody, = table.children for tr in tbody.children: td, = tr.children line, = td.children textbox, = line.children assert textbox.text == 'abc' html, = page2.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children tbody, = table.children tr, = tbody.children td, = tr.children line1, line2 = td.children textbox, div = line1.children assert textbox.text == 'def ' textbox, = line2.children assert textbox.text == 'ghi' line1, line2 = div.children textbox, br = line1.children assert textbox.text == 'f' textbox, = line2.children assert textbox.text == 'g' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4864032 weasyprint-62.3/tests/layout/test_footnotes.py0000644000000000000000000006535414635317053016733 0ustar00"""Tests for footnotes layout.""" import pytest from ..testing_utils import assert_no_logs, render_pages, tree_position @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_reported_sequential_footnote(): pages = render_pages('''
      abcde
      ''') positions = [ tree_position(pages, lambda box: getattr(box, 'text', None) == letter) for letter in 'abcde'] assert sorted(positions) == positions @assert_no_logs def test_reported_sequential_footnote_second_line(): pages = render_pages('''
      aaa abcde
      ''') positions = [ tree_position(pages, lambda box: getattr(box, 'text', None) == letter) for letter in 'abc'] assert sorted(positions) == positions @assert_no_logs @pytest.mark.parametrize('css, tail', ( ('p { break-inside: avoid }', '
      e
      f'), ('p { widows: 4 }', '
      e
      f'), ('p + p { break-before: avoid }', '

      e
      f'), ('p + p { break-before: avoid }', 'yz

      e'), )) def test_footnote_area_after_call(css, tail): pages = render_pages('''

      a
      b

      c
      dx%s

      ''' % (css, tail)) footnote_call = tree_position( pages, lambda box: box.element_tag == 'p::footnote-call') footnote_area = tree_position( pages, lambda box: type(box).__name__ == 'FootnoteAreaBox') assert footnote_call < footnote_area @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 @assert_no_logs def test_reported_footnote_repagination(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1700 page1, page2 = render_pages('''
      a bbde fg
      ''') html, = page1.children body, = html.children div, = body.children line1, line2 = div.children a, = line1.children assert a.children[0].text == 'a' assert a.children[1].children[0].text == '2' b, footnote_call, _ = line2.children assert b.text == 'bb' assert footnote_call.children[0].text == '1' html, footnote_area = page2.children body, = html.children div, = body.children line1, = div.children i, = line1.children assert i.children[0].text == 'fg' 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 == 3 @assert_no_logs def test_footnote_max_height(): page1, page2 = render_pages('''
      ab
      c
      d
      e
      fg
      ''') html1, footnote_area1 = page1.children body1, = html1.children div, = body1.children div_textbox, footnote_call1, footnote_call2, space, footnote_call3 = ( div.children[0].children) assert div_textbox.text == 'ab' assert footnote_call1.children[0].text == '1' assert footnote_call2.children[0].text == '2' assert space.text == ' ' assert footnote_call3.children[0].text == '3' footnote1, footnote2 = footnote_area1.children footnote_line1, = footnote1.children footnote_marker1, footnote_content1 = footnote_line1.children assert footnote_marker1.children[0].text == '1.' assert footnote_content1.text == 'c' footnote_line2, = footnote2.children footnote_marker2, footnote_content2 = footnote_line2.children assert footnote_marker2.children[0].text == '2.' assert footnote_content2.text == 'd' html2, footnote_area2 = page2.children body2, = html2.children div2, = body2.children div_textbox2, = div2.children[0].children assert div_textbox2.text == 'fg' footnote_line3, = footnote_area2.children[0].children footnote_marker3, footnote_content3 = footnote_line3.children assert footnote_marker3.children[0].text == '3.' assert footnote_content3.text == 'e' def test_footnote_table_aborted_row(): page1, page2 = render_pages('''
      abc
      abc
      abc
      def
      f
      ''') html, = page1.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children tbody, = table.children for tr in tbody.children: td, = tr.children line, = td.children textbox, = line.children assert textbox.text == 'abc' html, footnote_area = page2.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children tbody, = table.children tr, = tbody.children td, = tr.children line, = td.children textbox, call = line.children assert textbox.text == 'def' footnote, = footnote_area.children line, = footnote.children marker, textbox = line.children assert textbox.text == 'f' def test_footnote_table_aborted_group(): page1, page2 = render_pages('''
      abc
      abc
      def
      f
      ghi
      ''') html, = page1.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children tbody, = table.children for tr in tbody.children: td, = tr.children line, = td.children textbox, = line.children assert textbox.text == 'abc' html, footnote_area = page2.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children tbody, = table.children tr1, tr2 = tbody.children td, = tr1.children line, = td.children textbox, call = line.children assert textbox.text == 'def' td, = tr2.children line, = td.children textbox, = line.children assert textbox.text == 'ghi' footnote, = footnote_area.children line, = footnote.children marker, textbox = line.children assert textbox.text == 'f' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4864032 weasyprint-62.3/tests/layout/test_grid.py0000644000000000000000000007626514635317053015643 0ustar00"""Tests for grid layout.""" from ..testing_utils import assert_no_logs, render_pages @assert_no_logs def test_grid_empty(): page, = render_pages('''
      ''') html, = page.children body, = html.children article, = body.children assert article.position_x == 0 assert article.position_y == 0 assert article.width == html.width assert article.height == 0 @assert_no_logs def test_grid_single_item(): page, = render_pages('''
      a
      ''') html, = page.children body, = html.children article, = body.children div, = article.children assert article.position_x == div.position_x == 0 assert article.position_y == div.position_y == 0 assert article.width == div.width == html.width @assert_no_logs def test_grid_rows(): page, = render_pages('''
      a
      b
      c
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c = article.children assert div_a.position_x == div_b.position_x == div_c.position_x == 0 assert div_a.position_y < div_b.position_y < div_c.position_y assert div_a.height == div_b.height == div_c.height assert div_a.width == div_b.width == div_c.width == html.width == article.width @assert_no_logs def test_grid_template_fr(): page, = render_pages('''
      a
      b
      c
      d
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d = article.children assert div_a.position_x == div_c.position_x == 0 assert div_b.position_x == div_d.position_x == 2 assert div_a.height == div_b.height == div_c.height == div_d.height == 2 assert div_a.width == div_c.width == 2 assert div_b.width == div_d.width == 8 assert article.width == 10 @assert_no_logs def test_grid_template_areas(): page, = render_pages('''
      a
      b
      c
      d
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d = article.children assert div_a.position_x == div_c.position_x == 0 assert div_b.position_x == div_d.position_x == 5 assert div_a.position_y == div_b.position_y == 0 assert div_c.position_y == div_d.position_y == 2 assert div_a.height == div_b.height == div_c.height == div_d.height == 2 assert div_a.width == div_b.width == div_c.width == div_d.width == 5 assert article.width == 10 @assert_no_logs def test_grid_template_areas_grid_area(): page, = render_pages('''
      a
      b
      c
      d
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d = article.children assert div_b.position_x == div_d.position_x == 0 assert div_a.position_x == div_c.position_x == 5 assert div_a.position_y == div_b.position_y == 0 assert div_c.position_y == div_d.position_y == 2 assert div_a.height == div_b.height == div_c.height == div_d.height == 2 assert div_a.width == div_b.width == div_c.width == div_d.width == 5 assert article.width == 10 @assert_no_logs def test_grid_template_areas_empty_row(): page, = render_pages('''
      a
      b
      c
      d
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d = article.children assert div_b.position_x == div_d.position_x == 0 assert div_a.position_x == div_c.position_x == 5 assert div_a.position_y == div_b.position_y == 0 assert div_c.position_y == div_d.position_y == 2 assert div_a.height == div_b.height == div_c.height == div_d.height == 2 assert div_a.width == div_b.width == div_c.width == div_d.width == 5 assert article.width == 10 @assert_no_logs def test_grid_template_areas_multiple_rows(): page, = render_pages('''
      a
      b
      c
      d
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d = article.children assert div_b.position_x == div_d.position_x == 0 assert div_a.position_x == div_c.position_x == 5 assert div_a.position_y == div_b.position_y == 0 assert div_c.position_y == 4 assert div_d.position_y == 2 assert div_a.height == 4 assert div_b.height == div_c.height == div_d.height == 2 assert div_a.width == div_b.width == div_c.width == div_d.width == 5 assert article.width == 10 @assert_no_logs def test_grid_template_areas_multiple_columns(): page, = render_pages('''
      a
      b
      c
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c = article.children assert div_b.position_x == div_c.position_x == 0 assert div_a.position_x == 5 assert div_a.position_y == div_c.position_y == 2 assert div_b.position_y == 0 assert div_a.height == div_b.height == div_c.height == 2 assert div_a.width == div_c.width == 5 assert div_b.width == 10 assert article.width == 10 @assert_no_logs def test_grid_template_areas_overlap(): page, = render_pages('''
      a
      a
      a
      ''') html, = page.children body, = html.children article, = body.children div_a1, div_a2, div_a3 = article.children assert div_a1.position_x == div_a2.position_x == div_a3.position_x == 0 assert div_a1.position_y == div_a2.position_y == div_a3.position_y == 0 assert div_a1.width == div_a2.width == div_a3.width == 6 # 2 + (10-2) / 2 assert div_a1.height == div_a2.height == div_a3.height == 2 assert article.width == 10 @assert_no_logs def test_grid_template_areas_span_overflow(): page, = render_pages('''
      a
      a
      a
      ''') html, = page.children body, = html.children article, = body.children div_a1, div_a2, div_a3 = article.children assert div_a1.position_x == div_a2.position_x == div_a3.position_x == 0 assert div_a1.position_y == 0 assert div_a2.position_y == 2 assert div_a3.position_y == 4 assert div_a1.width == div_a3.width == 5 assert div_a2.width == 10 assert div_a1.height == div_a2.height == div_a3.height == 2 assert article.width == 10 @assert_no_logs def test_grid_template_areas_extra_span(): page, = render_pages('''
      a
      b
      c
      d
      e
      f
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d, div_e, div_f = article.children assert div_a.position_x == div_c.position_x == div_e.position_x == 0 assert div_d.position_x == 4 # 2 + (10 - 2×3) / 2 assert div_b.position_x == div_f.position_x == 6 assert div_a.position_y == div_b.position_y == 0 assert div_c.position_y == div_d.position_y == 2 assert div_e.position_y == div_f.position_y == 4 assert div_a.width == div_b.width == div_c.width == div_f.width == 4 assert div_d.width == div_e.width == 6 assert {div.height for div in article.children} == {2} assert article.width == 10 @assert_no_logs def test_grid_template_areas_extra_span_dense(): page, = render_pages('''
      a
      b
      c
      d
      e
      f
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d, div_e, div_f = article.children assert div_a.position_x == div_c.position_x == div_e.position_x == 0 assert div_d.position_x == div_f.position_x == 3 assert div_b.position_x == 6 assert div_a.position_y == div_b.position_y == div_f.position_y == 0 assert div_c.position_y == div_d.position_y == 2 assert div_e.position_y == 4 assert div_a.width == div_b.width == div_c.width == div_f.width == 3 assert div_d.width == div_e.width == 6 assert {div.height for div in article.children} == {2} assert article.height == 6 assert article.width == 9 @assert_no_logs def test_grid_area_multiple_values(): page, = render_pages('''
      a
      b
      c
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c = article.children assert div_a.position_x == div_b.position_x == 0 assert div_c.position_x == 5 assert div_b.position_y == 0 assert div_a.position_y == div_c.position_y == 2 assert div_a.height == div_b.height == div_c.height == 2 assert div_a.width == div_c.width == 5 assert div_b.width == 10 assert article.width == 10 @assert_no_logs def test_grid_template_repeat_fr(): page, = render_pages('''
      a
      b
      c
      d
      e
      f
      g
      h
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d, div_e, div_f, div_g, div_h = article.children assert div_a.position_x == div_e.position_x == 0 assert div_b.position_x == div_f.position_x == 2 assert div_c.position_x == div_g.position_x == 6 assert div_d.position_x == div_h.position_x == 8 assert div_a.position_y == div_b.position_y == 0 assert div_c.position_y == div_d.position_y == 0 assert div_e.position_y == div_f.position_y == 2 assert div_g.position_y == div_h.position_y == 2 assert div_a.width == div_c.width == div_e.width == div_g.width == 2 assert div_b.width == div_d.width == div_f.width == div_h.width == 4 assert {div.height for div in article.children} == {2} assert article.width == 12 @assert_no_logs def test_grid_template_shorthand_fr(): page, = render_pages('''
      a
      b
      c
      d
      e
      f
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d, div_e, div_f = article.children assert div_a.position_x == div_d.position_x == 0 assert div_b.position_x == div_e.position_x == 2 assert div_c.position_x == div_f.position_x == 8 assert div_a.position_y == div_b.position_y == div_c.position_y == 0 assert div_d.position_y == div_e.position_y == div_f.position_y == 2 assert div_a.width == div_c.width == div_d.width == div_f.width == 2 assert div_b.width == div_e.width == 6 assert {div.height for div in article.children} == {2} assert article.width == 10 @assert_no_logs def test_grid_shorthand_auto_flow_rows_fr_size(): page, = render_pages('''
      a
      b
      c
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c = article.children assert div_a.position_x == div_b.position_x == div_c.position_x == 0 assert div_a.position_y == 0 assert div_b.position_y == 2 assert div_c.position_y == 4 assert div_a.width == div_b.width == div_c.width == 6 assert {div.height for div in article.children} == {2} assert article.width == 10 @assert_no_logs def test_grid_template_fr_too_large(): page, = render_pages('''
      a
      bbb
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b = article.children assert div_a.position_x == 0 assert div_b.position_x == 4 assert div_a.position_y == div_b.position_y == 0 assert div_a.height == div_b.height == 2 assert div_a.width == 4 assert div_b.width == 6 assert article.width == 10 def test_grid_shorthand_auto_flow_columns_none_dense(): page, = render_pages('''
      a
      b
      c
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c = article.children assert div_a.position_x == 0 assert div_b.position_x == 4 assert div_c.position_x == 8 assert div_a.position_y == div_b.position_y == div_c.position_y == 0 assert div_a.height == div_b.height == div_c.height == 2 assert {div.width for div in article.children} == {4} assert article.width == 12 @assert_no_logs def test_grid_template_fr_undefined_free_space(): page, = render_pages('''
      a
      b
      b
      b
      b
      c
      d
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d = article.children assert div_a.position_x == div_c.position_x == 0 assert div_b.position_x == div_d.position_x == 5 assert div_a.height == div_b.height == div_c.height == div_d.height == 8 assert div_a.width == div_c.width == 5 assert div_b.width == div_d.width == 5 assert article.width == 10 assert article.height == 16 @assert_no_logs def test_grid_column_start(): page, = render_pages('''
      A
      A1
      A2
      B
      B1
      B2
      ''') html, = page.children body, = html.children dl, = body.children dt_a, dd_a1, dd_a2, dt_b, dd_b1, dd_b2 = dl.children assert dt_a.position_y == dd_a1.position_y == 0 assert dd_a2.position_y == 2 assert dt_b.position_y == dd_b1.position_y == 4 assert dd_b2.position_y == 6 assert dt_a.position_x == dt_b.position_x == 0 assert dd_a1.position_x == dd_a2.position_x == 2 assert dd_b1.position_x == dd_b2.position_x == 2 @assert_no_logs def test_grid_column_start_blockified(): page, = render_pages('''
      A
      A1
      A2
      B
      B1
      B2
      ''') html, = page.children body, = html.children dl, = body.children dt_a, dd_a1, dd_a2, dt_b, dd_b1, dd_b2 = dl.children assert dt_a.position_y == dd_a1.position_y == 0 assert dd_a2.position_y == 2 assert dt_b.position_y == dd_b1.position_y == 4 assert dd_b2.position_y == 6 assert dt_a.position_x == dt_b.position_x == 0 assert dd_a1.position_x == dd_a2.position_x == 2 assert dd_b1.position_x == dd_b2.position_x == 2 @assert_no_logs def test_grid_undefined_free_space(): page, = render_pages('''
      aa
      b
      c
      c
      d
      ''') html, = page.children body, = html.children div_c, = body.children div_c1, div_c2 = div_c.children div_r11, div_r12 = div_c1.children div_r21, div_r22 = div_c2.children assert div_r11.position_x == div_r12.position_x == 0 assert div_r21.position_x == div_r22.position_x == 4 assert div_r11.position_y == div_r21.position_y == 0 assert div_r12.position_y == div_r22.position_y == 4 assert div_r11.height == div_r12.height == div_r21.height == div_r22.height == 4 assert div_r11.width == div_r12.width == div_r21.width == div_r22.width == 4 assert div_c.width == 8 @assert_no_logs def test_grid_padding(): page, = render_pages('''
      a
      b
      c
      d
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d = article.children assert div_a.position_x == div_c.position_x == div_c.content_box_x() == 0 assert div_a.content_box_x() == 1 assert div_b.position_x == div_b.content_box_x() == div_d.position_x == 4 assert div_d.content_box_x() == 6 assert div_a.width == 2 assert div_b.width == 10 assert div_c.width == 4 assert div_d.width == 6 assert article.width == 14 assert div_a.position_y == div_b.position_y == div_b.content_box_y() == 0 assert div_a.content_box_y() == 1 assert div_c.position_y == div_c.content_box_y() == div_d.position_y == 4 assert div_d.content_box_y() == 6 assert div_a.height == div_d.height == 2 assert div_b.height == 4 assert div_c.height == 6 assert article.height == 10 @assert_no_logs def test_grid_border(): page, = render_pages('''
      a
      b
      c
      d
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d = article.children assert div_a.position_x == div_c.position_x == div_c.padding_box_x() == 0 assert div_a.padding_box_x() == 1 assert div_b.position_x == div_b.padding_box_x() == div_d.position_x == 4 assert div_d.padding_box_x() == 6 assert div_a.width == 2 assert div_b.width == 10 assert div_c.width == 4 assert div_d.width == 6 assert article.width == 14 assert div_a.position_y == div_b.position_y == div_b.padding_box_y() == 0 assert div_a.padding_box_y() == 1 assert div_c.position_y == div_c.padding_box_y() == div_d.position_y == 4 assert div_d.padding_box_y() == 6 assert div_a.height == div_d.height == 2 assert div_b.height == 4 assert div_c.height == 6 assert article.height == 10 @assert_no_logs def test_grid_margin(): page, = render_pages('''
      a
      b
      c
      d
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d = article.children assert div_a.position_x == div_c.position_x == div_c.border_box_x() == 0 assert div_a.border_box_x() == 1 assert div_b.position_x == div_b.border_box_x() == div_d.position_x == 4 assert div_d.border_box_x() == 6 assert div_a.width == 2 assert div_b.width == 10 assert div_c.width == 4 assert div_d.width == 6 assert article.width == 14 assert div_a.position_y == div_b.position_y == div_b.border_box_y() == 0 assert div_a.border_box_y() == 1 assert div_c.position_y == div_c.border_box_y() == div_d.position_y == 4 assert div_d.border_box_y() == 6 assert div_a.height == div_d.height == 2 assert div_b.height == 4 assert div_c.height == 6 assert article.height == 10 @assert_no_logs def test_grid_item_margin(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2154 page, = render_pages('''
      a
      b
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b = article.children # TODO: Test auto margin values. @assert_no_logs def test_grid_auto_flow_column(): page, = render_pages('''
      a
      a
      a
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c = article.children assert div_a.position_x < div_b.position_x < div_c.position_x assert div_a.position_y == div_b.position_y == div_c.position_y == 0 assert div_a.width == div_b.width == div_c.width assert div_a.height == div_b.height == div_c.height == html.height == article.height @assert_no_logs def test_grid_template_areas_extra_span_column_dense(): page, = render_pages('''
      a
      b
      c
      d
      e
      f
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b, div_c, div_d, div_e, div_f = article.children assert div_a.position_x == div_c.position_x == 0 assert div_d.position_x == div_f.position_x == 3 assert div_b.position_x == 6 assert div_e.position_x == 9 assert ( div_a.position_y == div_b.position_y == div_e.position_y == div_f.position_y == 0) assert div_c.position_y == div_d.position_y == 2 assert ( div_a.width == div_b.width == div_c.width == div_e.width == div_f.width == 3) assert div_d.width == 6 assert ( div_a.height == div_b.height == div_c.height == div_d.height == div_f.height == 2) assert div_e.height == 4 assert article.height == 4 assert article.width == 12 @assert_no_logs def test_grid_gap_explicit_grid_column(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/2187 page, = render_pages('''
      a
      b
      ''') html, = page.children body, = html.children article, = body.children div_a, div_b = article.children assert div_a.position_x == div_b.position_x == 0 assert div_a.position_y == 0 assert div_b.position_y == 4 assert article.height == 6 assert article.width == 12 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718960782.8123398 weasyprint-62.3/tests/layout/test_image.py0000644000000000000000000004611114635241217015761 0ustar00"""Tests for images 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 if body.children: line, = body.children img, = line.children else: img = None 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(f"invalid image") 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(''' ''') @pytest.mark.parametrize('html, children', ( ('', []), ('', []), ('', []), ('', []), ('abc', ['TextBox']), ('abc', ['TextBox']), )) def test_images_19(html, children): body, img = get_img(html) img_children = [ type(child).__name__ for child in getattr(img, 'children', [])] assert img_children == children @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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4874034 weasyprint-62.3/tests/layout/test_inline.py0000644000000000000000000011152514635317053016161 0ustar00"""Tests for inlines 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 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 https://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_breaking_linebox_regression_14(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1638 page, = render_pages( '' ' a bc') html, = page.children body, = html.children line1, line2 = body.children assert line1.children[0].children[0].children[0].text == 'a' assert line2.children[0].children[0].text == 'b' assert line2.children[1].children[0].text == 'c' @assert_no_logs def test_breaking_linebox_regression_15(): # Regression test for https://github.com/ietf-tools/datatracker/issues/5507 page, = render_pages( '' '
      ab©\n'
              'déf\n'
              'ghïj\n'
              'klm
      ') html, = page.children body, = html.children pre, = body.children line1, line2, line3, line4 = pre.children assert line1.children[0].text == 'ab©' assert line2.children[0].text == 'déf' assert line3.children[0].text == 'ghïj' assert line4.children[0].text == 'klm' assert line1.children[0].width == 4 * 3 assert line2.children[0].width == 4 * 3 assert line3.children[0].width == 4 * 4 assert line4.children[0].width == 4 * 3 assert pre.width == 4 * 4 @assert_no_logs def test_breaking_linebox_regression_16(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1973 page, = render_pages( '' '

      tést

      ' '
      ab©\n'
              'déf\n'
              'ghïj\n'
              'klm
      ') html, = page.children body, = html.children p, pre = body.children line1, = p.children assert line1.children[0].text == 'tést' assert p.width == 4 * 4 line1, line2, line3, line4 = pre.children assert line1.children[0].text == 'ab©' assert line2.children[0].text == 'déf' assert line3.children[0].text == 'ghïj' assert line4.children[0].text == 'klm' assert line1.children[0].width == 4 * 3 assert line2.children[0].width == 4 * 3 assert line3.children[0].width == 4 * 4 assert line4.children[0].width == 4 * 3 @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(): # https://www.w3.org/TR/css-backgrounds-3/#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(): # https://www.w3.org/TR/css-backgrounds-3/#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 @assert_no_logs def test_bidi_position_x_invariant(): page, = render_pages('''
      abc
       
      abc
      ''') html, = page.children body, = html.children block_ltr, _, block_rtl = body.children line_ltr, = block_ltr.children text_ltr, = line_ltr.children line_rtl, = block_rtl.children text_rtl, = line_rtl.children assert block_ltr.position_x == block_rtl.position_x assert line_ltr.position_x == line_rtl.position_x assert text_ltr.position_x == text_rtl.position_x ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4874034 weasyprint-62.3/tests/layout/test_inline_block.py0000644000000000000000000000724314635317053017334 0ustar00"""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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4874034 weasyprint-62.3/tests/layout/test_list.py0000644000000000000000000000723314635317053015656 0ustar00"""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 def test_lists_page_break_margin(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1058 page1, page2 = render_pages('''
      • a

      • a

      • a

      • a

      ''') for page in (page1, page2): html, = page.children body, = html.children ul, = body.children assert len(ul.children) == 2 for li in ul.children: assert len(li.children) == 2 assert ( li.children[0].position_y == li.children[1].children[0].position_y) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4874034 weasyprint-62.3/tests/layout/test_page.py0000644000000000000000000013161214635317053015616 0ustar00"""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_box_split(): # If floats round the wrong way, a block that gets filled to the end of a # page due to breaking over the page may be forced onto the next page # because it is slightly taller than can fit on the previous page, even if # it wouldn't have been without being filled. These numbers aren't ideal, # but they do seem to trigger the issue. page_1, page_2 = render_pages('''
      text
      text
      line1
      line2
      line3
      line4
      ''') html, = page_1.children body, = html.children assert len(body.children) == 3 div1, div2, section = body.children assert len(section.children) == 2 html, = page_2.children body, = html.children section, = body.children assert len(section.children) == 2 @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 def test_page_breaks_complex_9(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1979 page_1, page_2, page_3, page_4, page_5 = render_pages('''
      ''') html, = page_1.children body, = html.children div_1, = body.children assert div_1.content_box_x() == 10 assert div_1.content_box_y() == 10 html, = page_2.children body, = html.children div_2, = body.children assert div_2.content_box_x() == 10 assert div_2.content_box_y() == 0 # Unforced page break html, = page_3.children assert not html.children # Empty page html, = page_4.children body, = html.children div_3, = body.children assert div_3.content_box_x() == 10 assert div_3.content_box_y() == 10 # Forced page break html, = page_5.children body, = html.children div_4, = body.children assert div_4.content_box_x() == 10 assert div_4.content_box_y() == 10 # Forced page break @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 @pytest.mark.parametrize('direction, page_break, first_page', ( ('ltr', 'recto', 'right'), ('ltr', 'verso', 'left'), ('rtl', 'recto', 'left'), ('rtl', 'verso', 'right'), ('ltr', 'right', 'right'), ('ltr', 'left', 'left'), ('rtl', 'right', 'right'), ('rtl', 'left', 'left'), )) def test_recto_verso_break_root(direction, page_break, first_page): page, = render_pages(''' abc ''' % (direction, page_break)) assert page.width == len(first_page) @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 def test_page_names_10(): pages = render_pages('''
      running
      fixed

      text

      text
      ''') page1, page2 = pages assert (page1.width, page1.height) == (100, 100) html, runing = page1.children body, = html.children fixed, section, = body.children h1, pagebreak = section.children assert h1.element_tag == 'h1' assert (page2.width, page2.height) == (100, 100) html, running = page2.children fixed, body = html.children section, = body.children article, = section.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('''

      test1

      test2

      test3

      test4

      test5

      test6

      ''') footer1_text = ''.join( getattr(node, 'text', '') for node in pages[0].children[1].descendants()) assert footer1_text == '0 of 3 (1)' footer2_text = ''.join( getattr(node, 'text', '') for node in pages[1].children[1].descendants()) assert footer2_text == '0 of 3 (2)' footer3_text = ''.join( getattr(node, 'text', '') for node in pages[2].children[1].descendants()) assert footer3_text == '0 of 3 (3)' @assert_no_logs def test_margin_boxes_running_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!

      ''') @assert_no_logs def test_running_flex(): # Test regression render_pages('''
      Hello!
      ''') @assert_no_logs def test_running_float(): # Test regression render_pages('''
      Hello!
      ''') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3413625 weasyprint-62.3/tests/layout/test_position.py0000644000000000000000000003632414634254702016552 0ustar00"""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_relative_positioning_3(): page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span1, span2, span3 = line.children assert span2.position_x == 20 + 10 @assert_no_logs def test_relative_positioning_4(): page, = render_pages(''' ''') html, = page.children body, = html.children line, = body.children span1, span2, span3 = line.children assert span2.position_x == 100 - 20 - 5 - 20 @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, img3, img4 = 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) assert (img3.position_x, img3.position_y) == (10, 15) assert (img3.width, img3.height) == (4, 4) assert (img4.position_x, img4.position_y) == (10, 21) # (50 - 4) - 25 assert (img4.width, img4.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/pull/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)
      ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4874034
      weasyprint-62.3/tests/layout/test_shrink_to_fit.py0000644000000000000000000000324114635317053017540 0ustar00"""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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4874034 weasyprint-62.3/tests/layout/test_table.py0000644000000000000000000027313514635317053016000 0ustar00"""Tests for layout of tables.""" import pytest from weasyprint.formatting_structure import boxes from weasyprint.layout.table import collapse_table_borders from ..testing_utils import assert_no_logs, capture_logs, parse_all, render_pages def _get_grid(html, grid_width, grid_height): html = parse_all(html) body, = html.children table_wrapper, = body.children table, = table_wrapper.children border_lists = collapse_table_borders(table, grid_width, grid_height) return tuple( [[(style, width, color) if width else None for _score, (style, width, color) in border] for border in border_list] for border_list in border_lists) @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('''
    aaa aaa
    ''') 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_2.width == 16 * 3 # Percentage column is set to max-width assert td_1.width == (16 * 3) * 3 # Pixel column constraint is ignored assert table.width == (16 * 3) * 4 @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: # https://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 def test_layout_table_auto_51(): # Test regression: # https://github.com/Kozea/WeasyPrint/issues/2174 page, = render_pages('''
    a a
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children row_group, = table.children row_1, = row_group.children td_1, td_2 = row_1.children assert abs(td_1.width - 30) < 0.1 assert abs(td_2.width - 70) < 0.1 @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/937 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_row_height_4(): # A row cannot be shorter than the border-height of its tallest cell page, = render_pages('''
    ''') html, = page.children body, = html.children wrapper, = body.children table, = wrapper.children row_group, = table.children assert row_group.height == 12 @assert_no_logs def test_table_vertical_align(assert_pixels): assert_pixels(''' 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
    ''') @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_group_break_inside_avoid_absolute(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/2134 html = '''
    a
    a
    b
    a
    b
    ''' page1, page2 = render_pages(html) html, = page1.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children group, = table.children # Only first group html, = page2.children body, = html.children # No absolute div here table_wrapper, = body.children table, = table_wrapper.children group, = table.children # Only second group def test_table_row_break_inside_avoid_absolute(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/2134 html = '''
    a
    a
    b
    a
    b
    ''' page1, page2 = render_pages(html) html, = page1.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children group, = table.children row, = group.children # Only first row html, = page2.children body, = html.children # No absolute div here table_wrapper, = body.children table, = table_wrapper.children group, = table.children row, = group.children # Only second row def test_table_break_inside_avoid_absolute(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/2134 html = '''

    text

    a
    b
    a
    b
    ''' page1, page2 = render_pages(html) html, = page1.children body, = html.children p, = body.children line, = p.children text, = line.children assert text.text == 'text' html, = page2.children body, = html.children # No absolute div here 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 @assert_no_logs def test_table_bad_int_td_th_span(): 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 assert td_1.width == td_2.width th_1, th_2 = row_2.children assert th_1.width == th_2.width @assert_no_logs def test_table_bad_int_col_span(): 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 == 25 @assert_no_logs def test_table_bad_int_colgroup_span(): 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 == 25 @assert_no_logs def test_table_different_display(): # Test display attribute set on different table elements 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
    ''') @assert_no_logs def test_min_width_with_overflow(): # issue 1383 page, = render_pages('''
    Normal Key 1 Normal Value 1
    Normal Key 2 Normal Value 2
    Short value Works as expected
    Long Value Annoyingly breaks my table layout: Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
    ''') html, = page.children body, = html.children table_wrapper_1, table_wrapper_2 = body.children table1, = table_wrapper_1.children tbody1, = table1.children tr1, tr2 = tbody1.children table1_td1, table1_td2 = tr1.children table2, = table_wrapper_2.children tbody2, = table2.children tr1, tr2 = tbody2.children table2_td1, table2_td2 = tr1.children assert table1_td1.min_width == table2_td1.min_width assert table1_td1.width == table2_td1.width @assert_no_logs def test_table_cell_max_width(): page, = render_pages('''
    abcdefghijkl
    ''') html, = page.children body, = html.children table_wrapper, = body.children table, = table_wrapper.children tbody, = table.children tr, = tbody.children td, = tr.children assert td.max_width == 45 assert td.width == 45 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('
    ', 0, 0) assert grid == ([], []) @assert_no_logs def test_border_collapse_2(): vertical_borders, horizontal_borders = _get_grid('''
    A B
    C D
    ''', 2, 2) 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
    ''', 2, 2) 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('''
    ''', 3, 4) 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('''
    ''', 3, 2) 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], ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1710588566.1485844 weasyprint-62.3/tests/resources/acid2-reference.html0000644000000000000000000000477414575301226017600 0ustar00 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.

    ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1710596759.3231966 weasyprint-62.3/tests/resources/acid2-test.html0000644000000000000000000003367314575321227016624 0ustar00 The Second Acid Test

    Standards compliant?

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

    Hello World!

                                  
    ERROR
     
    ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1846476 weasyprint-62.3/tests/resources/blue.jpg0000644000000000000000000000044114564467510015417 0ustar00JFIFHHCC"   ?p+././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3423624 weasyprint-62.3/tests/resources/border.svg0000644000000000000000000000200714634254702015757 0ustar00 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3423624 weasyprint-62.3/tests/resources/border2.svg0000644000000000000000000000547314634254702016053 0ustar00 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703377.2089474 weasyprint-62.3/tests/resources/doc1.html0000644000000000000000000000407514634252421015500 0ustar00

    WeasyPrint test document (with Ünicōde)

    Hello

    WeasyPrint
    ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1846476 weasyprint-62.3/tests/resources/doc1_UTF-16BE.html0000644000000000000000000000144414564467510016657 0ustar00<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> ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1846476 weasyprint-62.3/tests/resources/icon.png0000644000000000000000000000145614564467510015433 0ustar00PNG  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`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1846476 weasyprint-62.3/tests/resources/latin1-test.css0000644000000000000000000000012514564467510016644 0ustar00h1::before { content: "Ilv Unicode"; background-image: url(pattern.png) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1846476 weasyprint-62.3/tests/resources/logo_small.png0000644000000000000000000001457314564467510016637 0ustar00PNG  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`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1846476 weasyprint-62.3/tests/resources/mini_ua.css0000644000000000000000000000023114564467510016116 0ustar00/* Minimal user-agent stylesheet */ p { margin: 1em 0px } /* 0px should be translated to 0*/ a { text-decoration: underline } h1 { font-weight: bolder } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703423.6424613 weasyprint-62.3/tests/resources/not-optimized-exif.jpg0000644000000000000000000000144514634252500020215 0ustar00JFIFbExifMM*JR(C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222 " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ? kw^8q MQX&qn ׮ZCmi TvZ'4($*A*GB lbgր?././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703377.2089474 weasyprint-62.3/tests/resources/not-optimized.jpg0000644000000000000000000000130014634252421017254 0ustar00JFIFC    $.' ",#(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_@././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3433626 weasyprint-62.3/tests/resources/pattern-transparent.svg0000644000000000000000000000016414634254702020520 0ustar00 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/pattern.gif0000644000000000000000000000005514564467510016133 0ustar00GIF89a!, ;././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/pattern.palette.png0000644000000000000000000000021414564467510017604 0ustar00PNG  IHDR? =sRGBPLTE pHYs  tIME 6 J IDATch`usIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/pattern.png0000644000000000000000000000011414564467510016146 0ustar00PNG  IHDR& )IDATxcaHIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/pattern.svg0000644000000000000000000000031314564467510016162 0ustar00 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/really-a-png.svg0000644000000000000000000000011414564467510016774 0ustar00PNG  IHDR& )IDATxcaHIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/really-a-svg.png0000644000000000000000000000031314564467510016775 0ustar00 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/sheet2.css0000644000000000000000000000017014564467510015671 0ustar00li { margin-bottom: 3em; /* Should be masked*/ margin: 2em 0; margin-left: 4em; /* Should not be masked*/ } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/sub_directory/sheet1.css0000644000000000000000000000037114564467510020550 0ustar00@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; } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/user.css0000644000000000000000000000012414564467510015454 0ustar00html { /* Reversed contrast */ color: white; background-color: black; } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/utf8-test.css0000644000000000000000000000013014564467510016336 0ustar00h1::before { content: "I løvë Unicode"; background-image: url(pattern.png) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718703402.8002315 weasyprint-62.3/tests/resources/weasyprint.otb0000644000000000000000000000304014634252453016673 0ustar00 @EBDT `#EBLC OS/2VBaVH`cmapJglyf head6hheaJ$hmtxlocamaxpG( nameh 4post3@ Շ-_< ްްZ@1 PfEdZ D(AaAa  @Y-@t  *  K Z` "   "PPublic domainPublic domainWeasyPrintWeasyPrintBitmapBitmapFontForge 2.0 : WeasyPrint Bitmap : 23-5-2022FontForge 2.0 : WeasyPrint Bitmap : 23-5-2022WeasyPrint BitmapWeasyPrint BitmapVersion 001.000Version 001.000WeasyPrint-BitmapWeasyPrint-Bitmap28d 0@P  ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1708289864.1856475 weasyprint-62.3/tests/resources/weasyprint.otf0000644000000000000000000003071014564467510016707 0ustar00pFFTMkN1GDEF!-\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././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3433626 weasyprint-62.3/tests/test_acid2.py0000644000000000000000000000223114634254702014341 0ustar00"""Check the famous Acid2 test.""" import io from PIL import Image from weasyprint import CSS, HTML from .testing_utils import assert_no_logs, capture_logs, resource_path @assert_no_logs def test_acid2(assert_pixels_equal): # Reduce image size and avoid Ghostscript rounding problems stylesheets = (CSS(string='@page { size: 500px 800px }'),) def render(filename): return HTML(resource_path(filename)).render(stylesheets=stylesheets) with capture_logs(): # This is a copy of https://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 https://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(width, height, test_pixels, ref_pixels, tolerance=2) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4884033 weasyprint-62.3/tests/test_api.py0000644000000000000000000012656714635317053014153 0ustar00"""Test the public API.""" import contextlib import gzip import io import os import sys import threading import unicodedata import wsgiref.simple_server import zlib from functools import partial from pathlib import Path from urllib.parse import urljoin, uses_relative import pytest from PIL import Image from weasyprint import CSS, HTML, __main__, default_url_fetcher from weasyprint.pdf.anchors import resolve_links from weasyprint.urls import path2url from .draw import parse_pixels from .testing_utils import FakeHTML, assert_no_logs, capture_logs, resource_path try: # Available in Python 3.11+ from contextlib import chdir except ImportError: # Backported from Python 3.11 from contextlib import AbstractContextManager class chdir(AbstractContextManager): # noqa: N801 def __init__(self, path): self.path = path self._old_cwd = [] def __enter__(self): self._old_cwd.append(os.getcwd()) os.chdir(self.path) def __exit__(self, *excinfo): os.chdir(self._old_cwd.pop()) def _test_resource(class_, name, check, **kwargs): """Common code for testing the HTML and CSS classes.""" absolute_path = resource_path(name) absolute_filename = str(absolute_path) url = path2url(absolute_path) check(class_(absolute_path, **kwargs)) check(class_(absolute_filename, **kwargs)) check(class_(guess=absolute_path, **kwargs)) check(class_(guess=absolute_filename, **kwargs)) check(class_(filename=absolute_path, **kwargs)) check(class_(filename=absolute_filename, **kwargs)) check(class_(url, **kwargs)) check(class_(guess=url, **kwargs)) url = path2url(absolute_filename.encode()) check(class_(url=url, **kwargs)) with absolute_path.open('rb') as fd: check(class_(fd, **kwargs)) with absolute_path.open('rb') as fd: check(class_(guess=fd, **kwargs)) with absolute_path.open('rb') as fd: check(class_(file_obj=fd, **kwargs)) content = absolute_path.read_bytes() with chdir(Path(__file__).parent): relative_path = Path('resources') / name relative_filename = str(relative_path) check(class_(relative_path, **kwargs)) check(class_(relative_filename, **kwargs)) kwargs.pop('base_url', None) check(class_(string=content, base_url=relative_filename, **kwargs)) encoding = kwargs.pop('encoding', 'utf-8') with absolute_path.open('r', encoding=encoding) as fd: check(class_(file_obj=fd, **kwargs)) check(class_( string=content.decode(encoding), 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() HTML = partial(FakeHTML, force_uncompressed_pdf=False) # noqa: N806 __main__.main(args.split(), stdin=stdin, stdout=stdout, HTML=HTML) return stdout.getvalue() class FakeFile: 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, box = 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)), box) 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') with chdir(Path(__file__).parent): path = Path('resources') / 'doc1.html' string = path.read_text('utf-8') _test_resource(FakeHTML, 'doc1.html', _check_doc1, base_url=path) _check_doc1(FakeHTML(string=string, base_url=path)) _check_doc1(FakeHTML(string=string), has_base_url=False) string_with_base = 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(tmp_path): css = b''' @page { margin: 2px; size: 8px } @media screen { img { transform: rotate(-90deg) } } body { margin: 0; font-size: 0 } ''' html = b'' combined = b'' + html linked = b'' + html not_optimized = b'a' for name in ('pattern.png', 'not-optimized.jpg'): pattern_bytes = resource_path(name).read_bytes() (tmp_path / name).write_bytes(pattern_bytes) with chdir(tmp_path): # Reference html_obj = FakeHTML( string=combined, base_url='dummy.html', force_uncompressed_pdf=False) pdf_bytes = html_obj.write_pdf() rotated_pdf_bytes = FakeHTML( string=combined, base_url='dummy.html', media_type='screen', force_uncompressed_pdf=False).write_pdf() (tmp_path / 'no_css.html').write_bytes(html) (tmp_path / 'combined.html').write_bytes(combined) (tmp_path / 'combined-UTF-16BE.html').write_bytes( combined.decode().encode('UTF-16BE')) (tmp_path / 'linked.html').write_bytes(linked) (tmp_path / 'not_optimized.html').write_bytes(not_optimized) (tmp_path / 'style.css').write_bytes(css) _run('combined.html out2.pdf') assert (tmp_path / 'out2.pdf').read_bytes() == pdf_bytes _run('combined-UTF-16BE.html out3.pdf --encoding UTF-16BE') assert (tmp_path / 'out3.pdf').read_bytes() == pdf_bytes _run(f'{(tmp_path / "combined.html")} out4.pdf') assert (tmp_path / 'out4.pdf').read_bytes() == pdf_bytes _run(f'{path2url((tmp_path / "combined.html"))} out5.pdf') assert (tmp_path / 'out5.pdf').read_bytes() == pdf_bytes _run('linked.html --debug out6.pdf') # test relative URLs assert (tmp_path / 'out6.pdf').read_bytes() == pdf_bytes _run('combined.html --verbose out7') _run('combined.html --quiet out8') assert (tmp_path / 'out7').read_bytes() == pdf_bytes assert (tmp_path / 'out8').read_bytes() == pdf_bytes _run('no_css.html out9.pdf') _run('no_css.html out10.pdf -s style.css') assert (tmp_path / 'out9.pdf').read_bytes() != pdf_bytes assert (tmp_path / 'out10.pdf').read_bytes() == pdf_bytes stdout = _run('combined.html -') assert stdout == pdf_bytes _run('- out11.pdf', stdin=combined) assert (tmp_path / 'out11.pdf').read_bytes() == 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 (tmp_path / 'out12.pdf').read_bytes() == rotated_pdf_bytes assert (tmp_path / 'out13.pdf').read_bytes() == rotated_pdf_bytes assert (tmp_path / 'out14.pdf').read_bytes() == rotated_pdf_bytes os.environ['SOURCE_DATE_EPOCH'] = '0' _run('not_optimized.html out15.pdf') _run('not_optimized.html out16.pdf --optimize-images') _run('not_optimized.html out17.pdf --optimize-images -j 10') _run('not_optimized.html out18.pdf --optimize-images -j 10 -D 1') _run('not_optimized.html out19.pdf --hinting') _run('not_optimized.html out20.pdf --full-fonts') _run('not_optimized.html out21.pdf --full-fonts --uncompressed-pdf') _run(f'not_optimized.html out22.pdf -c {tmp_path}') assert ( len((tmp_path / 'out18.pdf').read_bytes()) < len((tmp_path / 'out17.pdf').read_bytes()) < len((tmp_path / 'out16.pdf').read_bytes()) < len((tmp_path / 'out15.pdf').read_bytes()) < len((tmp_path / 'out19.pdf').read_bytes()) < len((tmp_path / 'out20.pdf').read_bytes()) < len((tmp_path / 'out21.pdf').read_bytes())) assert len({ (tmp_path / f'out{i}.pdf').read_bytes() for i in (15, 22)}) == 1 os.environ.pop('SOURCE_DATE_EPOCH') stdout = _run('combined.html --uncompressed-pdf -') assert stdout.count(b'attachment') == 0 stdout = _run('combined.html --uncompressed-pdf -') assert stdout.count(b'attachment') == 0 stdout = _run('-a pattern.png --uncompressed-pdf combined.html -') assert stdout.count(b'attachment') == 1 stdout = _run( '-a style.css -a pattern.png --uncompressed-pdf combined.html -') assert stdout.count(b'attachment') == 2 _run('combined.html out23.pdf --timeout 30') assert (tmp_path / 'out23.pdf').read_bytes() == pdf_bytes subdirectory = tmp_path / 'subdirectory' subdirectory.mkdir() with chdir(subdirectory): 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') @pytest.mark.parametrize('version, pdf_version', ( (1, '1.4'), (2, '1.7'), (3, '1.7'), (4, '2.0'), )) def test_pdfa(version, pdf_version): stdout = _run( f'--pdf-variant=pdf/a-{version}b --uncompressed-pdf - -', b'test') assert f'PDF-{pdf_version}'.encode() in stdout assert f'part="{version}"'.encode() in stdout @pytest.mark.parametrize('version, pdf_version', ( (1, '1.4'), (2, '1.7'), (3, '1.7'), (4, '2.0'), )) def test_pdfa_compressed(version, pdf_version): _run(f'--pdf-variant=pdf/a-{version}b - -', b'test') def test_pdfa1b_cidset(): stdout = _run('--pdf-variant=pdf/a-1b --uncompressed-pdf - -', b'test') assert b'PDF-1.4' in stdout assert b'CIDSet' in stdout def test_pdfua(): stdout = _run('--pdf-variant=pdf/ua-1 --uncompressed-pdf - -', b'test') assert b'part="1"' in stdout def test_pdfua_compressed(): _run('--pdf-variant=pdf/ua-1 - -', b'test') def test_pdf_identifier(): stdout = _run('--pdf-identifier=abc --uncompressed-pdf - -', b'test') assert b'abc' in stdout def test_pdf_version(): stdout = _run('--pdf-version=1.4 --uncompressed-pdf - -', b'test') assert b'PDF-1.4' in stdout def test_pdf_custom_metadata(): stdout = _run( '--custom-metadata --uncompressed-pdf - -', b'') assert b'/key' in stdout assert b'value' in stdout def test_bad_pdf_custom_metadata(): stdout = _run( '--custom-metadata --uncompressed-pdf - -', ''.encode('latin1')) assert b'value' not in stdout def test_partial_pdf_custom_metadata(): stdout = _run( '--custom-metadata --uncompressed-pdf - -', ''.encode('latin1')) assert b'/abcd0' in stdout assert b'value' in stdout @pytest.mark.parametrize('html, fields', ( ('', ['/Tx', '/V ()']), ('', ['/Tx', '/V ()']), ('', ['/Btn']), ('', ['/Tx', '/V ()']), ('', ['/Ch', '/Opt']), ('', ['/Ch', '/Opt', '/V (b)']), ('', ['/Ch', '/Opt', '[(b) (c)]']), )) def test_pdf_inputs(html, fields): stdout = _run('--pdf-forms --uncompressed-pdf - -', html.encode()) assert b'AcroForm' in stdout assert all(field.encode() in stdout for field in fields) stdout = _run('--uncompressed-pdf - -', html.encode()) assert b'AcroForm' not in stdout @pytest.mark.parametrize('css, with_forms, without_forms', ( ('appearance: auto', True, True), ('appearance: none', False, False), ('', True, False), )) def test_appearance(css, with_forms, without_forms): html = f''.encode() assert with_forms is ( b'AcroForm' in _run('--pdf-forms --uncompressed-pdf - -', html)) assert without_forms is ( b'AcroForm' in _run(' --uncompressed-pdf - -', html)) def test_appearance_non_input(): html = '
    '.encode() assert b'AcroForm' not in _run('--pdf-forms --uncompressed-pdf - -', html) def test_reproducible(): os.environ['SOURCE_DATE_EPOCH'] = '0' stdout1 = _run('- -', b'a') stdout2 = _run('- -', b'a') os.environ.pop('SOURCE_DATE_EPOCH') assert stdout1 == stdout2 @assert_no_logs def test_unicode_filenames(assert_pixels_equal, tmp_path): """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(assert_pixels_equal, png_bytes) unicode_filename = 'Unicödé' if sys.platform.startswith('darwin'): # pragma: no cover unicode_filename = unicodedata.normalize('NFD', unicode_filename) with chdir(tmp_path): (tmp_path / unicode_filename).write_bytes(html) bytes_file, = tuple(tmp_path.iterdir()) assert bytes_file.name == unicode_filename assert FakeHTML(unicode_filename).write_png() == png_bytes assert FakeHTML(bytes_file).write_png() == png_bytes os.remove(unicode_filename) assert not tuple(tmp_path.iterdir()) FakeHTML(string=html).write_png(unicode_filename) assert bytes_file.read_bytes() == png_bytes @assert_no_logs def test_low_level_api(assert_pixels_equal): html = FakeHTML(string='') css = CSS(string=''' @page { margin: 2px; size: 8px } 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(stylesheets=[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(stylesheets=[css]) page, = document.pages assert (page.width, page.height) == (8, 8) png_bytes = document.write_png(resolution=192) check_png_pattern(assert_pixels_equal, png_bytes, x2=True) document = html.render(stylesheets=[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_path('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), ('''

    ! ''', [[(1, '!', (-5, -5), 'open')]], [('!', (0, -5, -5), [], '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 def simplify_links(links): return [ (link_type, link_target, rectangle) for link_type, link_target, rectangle, box in links] def assert_links(html, links, anchors, resolved_links, base_url=resource_path(''), warnings=(), round=False): with capture_logs() as logs: document = FakeHTML(string=html, base_url=base_url).render() if round: _round_meta(document.pages) document_resolved_links = [ (simplify_links(page_links), page_anchors) for page_links, page_anchors in resolve_links(document.pages)] assert len(logs) == len(warnings) for message, expected in zip(logs, warnings): assert expected in message document_links = [simplify_links(page.links) for page in document.pages] document_anchors = [page.anchors for page in document.pages] assert document_links == links assert document_anchors == anchors assert document_resolved_links == resolved_links @assert_no_logs def test_links_1(): assert_links('''

    Hello, World

    ''', [ [ ('external', 'https://weasyprint.org', (0, 0, 30, 20)), ('external', 'https://weasyprint.org', (0, 0, 30, 30)), ('internal', 'lipsum', (10, 100, 42, 120)), ('internal', 'lipsum', (10, 100, 42, 132)) ], [('internal', 'hello', (0, 0, 200, 30))], ], [ {'hello': (0, 200)}, {'lipsum': (0, 0)} ], [ ( [ ('external', 'https://weasyprint.org', (0, 0, 30, 20)), ('external', 'https://weasyprint.org', (0, 0, 30, 30)), ('internal', 'lipsum', (10, 100, 42, 120)), ('internal', 'lipsum', (10, 100, 42, 132)) ], [('hello', 0, 200)], ), ( [('internal', 'hello', (0, 0, 200, 30))], [('lipsum', 0, 0)]), ]) @assert_no_logs def test_links_2(): assert_links( ''' ''', [[('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9', (5, 10, 195, 10))]], [{}], [([('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9', (5, 10, 195, 10))], [])], base_url='https://weasyprint.org/foo/bar/') @assert_no_logs def test_links_3(): assert_links( '''
    ''', [[('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9', (5, 10, 195, 10))]], [{}], [([('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9', (5, 10, 195, 10))], [])], base_url='https://weasyprint.org/foo/bar/') @assert_no_logs def test_links_4(): # Relative URI reference without a base URI: allowed for links assert_links( ''' ''', [[('external', '../lipsum', (5, 10, 195, 10))]], [{}], [([('external', '../lipsum', (5, 10, 195, 10))], [])], base_url=None) @assert_no_logs def test_links_5(): # 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']) @assert_no_logs def test_links_6(): # Internal or absolute URI reference without a base URI: OK assert_links( ''' ''', [[ ('internal', 'lipsum', (5, 10, 195, 10)), ('external', 'https://weasyprint.org/', (0, 10, 200, 10))]], [{'lipsum': (5, 10)}], [([('internal', 'lipsum', (5, 10, 195, 10)), ('external', 'https://weasyprint.org/', (0, 10, 200, 10))], [('lipsum', 5, 10)])], base_url=None) @assert_no_logs def test_links_7(): assert_links( '''
    ''', [[('internal', 'lipsum', (5, 10, 195, 10))]], [{'lipsum': (5, 10)}], [([('internal', 'lipsum', (5, 10, 195, 10))], [('lipsum', 5, 10)])], base_url=None) @assert_no_logs def test_links_8(): assert_links( ''' ''', [[('internal', 'lipsum', (0, 0, 200, 15)), ('internal', 'missing', (0, 15, 200, 30))]], [{'lipsum': (0, 15)}], [([('internal', 'lipsum', (0, 0, 200, 15))], [('lipsum', 0, 15)])], base_url=None, warnings=[ 'ERROR: No anchor #missing for internal URI reference']) @assert_no_logs def test_links_9(): assert_links( ''' ''', [[('internal', 'lipsum', (30, 10, 70, 210))]], [{'lipsum': (70, 10)}], [([('internal', 'lipsum', (30, 10, 70, 210))], [('lipsum', 70, 10)])], round=True) @assert_no_logs def test_links_10(): # Download for attachment assert_links( ''' ''', [[('attachment', 'pattern.png', (5, 10, 195, 10))]], [{}], [([('attachment', 'pattern.png', (5, 10, 195, 10))], [])], base_url=None) @assert_no_logs def test_links_11(): # Attachment with missing href assert_links( ''' ''', [[]], [{}], [([], [])], base_url=None) @assert_no_logs def test_links_12(): # Absolute URI with no fragment and the same base URI: keep external URI # Regression test for https://github.com/Kozea/WeasyPrint/issues/1767 assert_links( ''' ''', [[('external', 'https://weasyprint.org', (5, 10, 195, 10))]], [{}], [([('external', 'https://weasyprint.org', (5, 10, 195, 10))], [])], base_url='https://weasyprint.org') # Make relative URL references work with our custom URL scheme. uses_relative.append('weasyprint-custom') @assert_no_logs def test_url_fetcher(assert_pixels_equal): path = resource_path('pattern.png') pattern_png = path.read_bytes() 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 = str(resource_path('dummy.html')) css = CSS(string=''' @page { size: 8px; margin: 2px } 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( assert_pixels_equal, 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_no_logs def test_html_meta_2(): 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', lang='en', custom={'dummy': 'ignored'}) @assert_no_logs def test_html_meta_3(): assert_meta( ''' One Two Three ''', title='One', authors=['', 'Me']) @assert_no_logs def test_html_meta_4(): with capture_logs() as logs: assert_meta( ''' Title ''', title='Title', authors=['Me']) assert len(logs) == 1 assert 'Invalid date' in logs[0] @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() @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 # https://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() 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(f'{root_url}/gzip').etree_element.get('test') == 'ok' assert HTML(f'{root_url}/deflate').etree_element.get('test') == 'ok' assert HTML( f'{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] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4884033 weasyprint-62.3/tests/test_boxes.py0000644000000000000000000011767414635317053014521 0ustar00"""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 ( # isort:skip FakeHTML, assert_no_logs, assert_tree, capture_logs, parse, parse_all, render_pages) @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(f'

    {quote}abc{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(): 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 https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes # Rule 1.3 # Also table model: https://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(): # https://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_quotes_auto(): 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_quotes_none(): 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_quotes_lang(): 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_quotes_lang_alternate(): 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_quotes_lang_parent(): 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_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

    ''' % (content_val, 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_margin_box_string_set_10(): page_1, page_2, page_3, page_4 = render_pages('''

        1

        2

        3

        ''') html, top_left = page_1.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[]' html, top_left = page_2.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[1]' html, top_left = page_3.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[3]' html, top_left = page_4.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[3]' @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 == f'Page {page_number} of 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4884033 weasyprint-62.3/tests/test_fonts.py0000644000000000000000000001154614635317053014521 0ustar00"""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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3443625 weasyprint-62.3/tests/test_pdf.py0000644000000000000000000005263314634254702014143 0ustar00"""Test PDF-related code, including metadata, bookmarks and hyperlinks.""" import hashlib import io 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_path # 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() 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() 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() 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_bookmarks_15(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1815 pdf = FakeHTML(string='''

        a

        ''').write_pdf() assert re.findall(b'/Count ([0-9-]*)', pdf)[-1] == b'1' assert re.findall(b'/Title \\((.*)\\)', pdf) == [b'a'] assert b'/XYZ 0 10 0' in pdf @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_path('')).write_pdf() uris = re.findall(b'/URI \\((.*)\\)', pdf) types = re.findall(b'/S (/\\w*)', pdf) subtypes = re.findall(b'/Subtype (/\\w*)', 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'https://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'https://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_path('')).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='https://weasyprint.org/foo/bar/').write_pdf() assert b'/S /URI\n/URI (https://weasyprint.org/foo/lipsum)' assert f'/Rect [0 {TOP} {RIGHT} {TOP}]'.encode() 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() 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='https://weasyprint.org/foo/bar/').write_pdf() assert b'https://weasyprint.org/test/lipsum' in pdf @assert_no_logs def test_relative_links_same_base(): pdf = FakeHTML( string='a', base_url='https://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_anchor_multiple_pages(): pdf = FakeHTML(string='''
        ''', base_url=None).write_pdf() first_page, = re.findall(b'/Kids \\[(\\d+) 0 R', pdf) assert b'/Names [(lipsum) [' + first_page in pdf @assert_no_logs def test_embed_gif(): assert b'/Filter /DCTDecode' not in FakeHTML( base_url=resource_path('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_path('dummy.html'), string='').write_pdf() @assert_no_logs def test_embed_image_once(): # Image repeated multiple times, embedded once assert FakeHTML( base_url=resource_path('dummy.html'), string='''
        ''').write_pdf().count(b'/Filter /DCTDecode') == 1 @assert_no_logs def test_embed_images_from_pages(): page1, = FakeHTML( base_url=resource_path('dummy.html'), string='').render().pages page2, = FakeHTML( base_url=resource_path('dummy.html'), string='').render().pages document = Document( (page1, page2), metadata=DocumentMetadata(), font_config=FontConfiguration(), url_fetcher=None).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 (D:20110421230000Z)' in pdf assert b"/ModDate (D:20130721234600+01'00)" in pdf @assert_no_logs def test_embedded_files_attachments(tmp_path): absolute_tmp_path = tmp_path / 'some_file.txt' absolute_data = b'12345678' absolute_tmp_path.write_bytes(absolute_data) absolute_url = path2url(absolute_tmp_path) assert absolute_url.startswith('file://') relative_tmp_path = tmp_path / 'äöü.txt' relative_data = b'abcdefgh' relative_tmp_path.write_bytes(relative_data) pdf = FakeHTML( string=f''' Test document

        Heading 1

        Heading 2

        ''', base_url=tmp_path, ).write_pdf( attachments=[ Attachment('data:,oob attachment', description='Hello'), 'data:,raw URL', io.BytesIO(b'file like obj') ] ) assert f'<{hashlib.md5(b"hi there").hexdigest()}>'.encode() 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() + b'>' in pdf assert hashlib.md5(absolute_data).hexdigest().encode() in pdf assert absolute_tmp_path.name.encode() in pdf assert hashlib.md5(relative_data).hexdigest().encode() in pdf name = BOM_UTF16_BE + 'some file attachment äöü'.encode('utf-16-be') assert b'/Desc <' + name.hex().encode() + b'>' in pdf assert hashlib.md5(b'oob attachment').hexdigest().encode() in pdf assert b'/Desc (Hello)' in pdf assert hashlib.md5(b'raw URL').hexdigest().encode() in pdf assert hashlib.md5(b'file like obj').hexdigest().encode() 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 = f'<{hashlib.md5(b"some data").hexdigest()}>'.encode() assert md5 in pdf assert b'EmbeddedFiles' in pdf @assert_no_logs def test_attachments_data_with_anchor(): pdf = FakeHTML(string=''' Test document 2

        Title

        example ''').write_pdf() md5 = f'<{hashlib.md5(b"some data").hexdigest()}>'.encode() assert md5 in pdf assert b'EmbeddedFiles' in pdf @assert_no_logs def test_attachments_no_href(): with capture_logs() as logs: pdf = FakeHTML(string=''' Test document 2 ''').write_pdf() assert b'Names' not in pdf assert b'Outlines' not in pdf assert len(logs) == 1 assert 'Missing href' in logs[0] @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() 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 f'/MediaBox {str(media).replace(",", "")}'.encode() in pdf assert f'/BleedBox {str(bleed).replace(",", "")}'.encode() in pdf assert f'/TrimBox {str(trim).replace(",", "")}'.encode() in pdf ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4884033 weasyprint-62.3/tests/test_presentational_hints.py0000644000000000000000000002241014635317053017615 0ustar00"""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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3453627 weasyprint-62.3/tests/test_stacking.py0000644000000000000000000000426414634254702015172 0ustar00"""Test CSS stacking contexts.""" import pytest from weasyprint.stacking import StackingContext 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(assert_pixels, z_indexes, color): assert_pixels('\n'.join([color * 10] * 10), z_index_source % z_indexes) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4884033 weasyprint-62.3/tests/test_text.py0000644000000000000000000012545214635317053014356 0ustar00"""Test the text layout.""" import pytest from weasyprint.css.properties import INITIAL_VALUES from weasyprint.formatting_structure.build import capitalize 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 = INITIAL_VALUES.copy() 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()[resume_index:].decode() == 'is a text for test' _, _, resume_index, _, _, _ = make_text( string, 100, font_family=SANS_FONTS.split(','), font_size=19) assert string.encode()[resume_index:].decode() == '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()[resume_index:].decode() == 'ايبسوم دولا' @assert_no_logs def test_line_breaking_nbsp(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1561 page, = render_pages(''' a b c d ef ''') html, = page.children body, = html.children line_1, line_2 = body.children assert line_1.children[0].text == 'a ' assert line_1.children[1].children[0].text == 'b' assert line_1.children[2].text == ' c' assert line_2.children[0].text == 'd\xa0' assert line_2.children[1].children[0].text == 'ef' @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 @pytest.mark.parametrize('text', ( 'Lorem ipsum dolorsit amet', 'Lorem ipsum dolorsit amet', 'Lorem ipsum dolorsit amet', 'Lorem ipsum dolorsit amet', 'Lorem ipsum dolorsit amet', 'Lorem ipsum dolorsit amet', )) @assert_no_logs def test_word_spacing(text): # keep the empty Lorem ipsum dolorsit amet''') html, = page.children body, = html.children line, = body.children strong_1, = line.children page, = render_pages(''' %s''' % text) 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 @pytest.mark.parametrize('i', (range(1, len('hyphénation')))) def test_hyphenate_manual_1(i): for hyphenate_character in ('!', 'ù ù'): word = f'{"hyphénation"[:i]}\xad{"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 @pytest.mark.parametrize('i', (range(1, len('hy phénation')))) def test_hyphenate_manual_2(i): for hyphenate_character in ('!', 'ù ù'): word = f'{"hy phénation"[:i]}\xad{"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.rstrip('\xad').endswith('y') if len(lines) == 3: assert lines[1].children[0].text.rstrip('\xad').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_manual_4(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1878 page, = render_pages( '' 'test­') html, = page.children body, = html.children line_1, = body.children # TODO: should not end with an hyphen # assert line_1.children[0].text == 'test\xad' @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 text_box, = box.children lines.append(text_box.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('span_css, expected_lines', ( # overflow-wrap: anywhere and break-word are only allowed to break a word # "if there are no otherwise-acceptable break points in the line", which # means they should not split a word if it fits cleanly into the next line. # This can be done accidentally if it is in its own inline element. ('overflow-wrap: anywhere', ['aaa', 'bbb']), ('overflow-wrap: break-word', ['aaa', 'bbb']), # On the other hand, word-break: break-all mandates a break anywhere at the # end of a line, even if the word could fit cleanly onto the next line. ('word-break: break-all', ['aaa b', 'bb']), )) def test_wrap_overflow_word_break(span_css, expected_lines): page, = render_pages(''' aaa bbb ''' % span_css) html, = page.children body, = html.children lines = body.children lines = [] for line in body.children: line_text = '' for span_box in line.children: line_text += span_box.children[0].text lines.append(line_text) assert lines == expected_lines @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', ( ('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 test_line_break_before_trailing_space(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1852 page, = render_pages('''

                        test\u2028

                        a

                        test\u2028

                        a ''') html, = page.children body, = html.children line, = body.children p1, space1, p2, space2 = line.children assert p1.width == p2.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 @pytest.mark.parametrize( 'original, transformed', ( ('abc def ghi', 'Abc Def Ghi'), ('AbC def ghi', 'AbC Def Ghi'), ('I’m SO cool', 'I’m SO Cool'), ('Wow.wow!wow', 'Wow.wow!wow'), ('!now not tomorrow', '!Now Not Tomorrow'), ('SUPER cool', 'SUPER Cool'), ('i 😻 non‑breaking characters', 'I 😻 Non‑breaking Characters'), ('3lite 3lite', '3lite 3lite'), ('one/two/three', 'One/two/three'), ('supernatural,super', 'Supernatural,super'), ('éternel αιώνια', 'Éternel Αιώνια'), ) ) def test_text_transform_capitalize(original, transformed): # Results are different for different browsers, we almost get the same # results as Firefox, that’s good enough! assert capitalize(original) == transformed @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' def test_first_letter_text_transform(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/1906 page, = render_pages('''

                        abc ''') html, = page.children body, = html.children paragraph, = body.children line, = paragraph.children first_letter, _ = line.children first_letter_text, = first_letter.children assert first_letter_text.text == 'A' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3453627 weasyprint-62.3/tests/test_unicode.py0000644000000000000000000000224514634254702015012 0ustar00"""Test various unicode texts and filenames.""" from weasyprint.urls import ensure_url from .draw import document_to_pixels, html_to_pixels from .testing_utils import FakeHTML, assert_no_logs, resource_path @assert_no_logs def test_unicode(assert_pixels_equal, tmp_path): text = 'I løvë Unicode' style = ''' @page { size: 200px 50px } p { color: blue } ''' expected_width, expected_height, expected_lines = html_to_pixels(f'''

                        {text}

                        ''') stylesheet = tmp_path / 'style.css' image = tmp_path / 'pattern.png' html = tmp_path / 'doc.html' stylesheet.write_text(style, 'utf-8') image.write_bytes(resource_path('pattern.png').read_bytes()) html_content = f'''

                        {text}

                        ''' html.write_text(html_content, 'utf-8') document = FakeHTML(html, encoding='utf-8') width, height, lines = document_to_pixels(document) assert (expected_width, expected_height) == (width, height) assert_pixels_equal(width, height, lines, expected_lines) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3453627 weasyprint-62.3/tests/test_url.py0000644000000000000000000000163014634254702014163 0ustar00"""Test URLs.""" import re import pytest from .testing_utils import FakeHTML, capture_logs, resource_path @pytest.mark.parametrize('url, base_url', ( ('https://weasyprint.org]', resource_path('')), ('https://weasyprint.org]', 'https://weasyprint.org]'), ('https://weasyprint.org/', 'https://weasyprint.org]'), )) def test_malformed_url_link(url, base_url): """Test malformed URLs.""" with capture_logs() as logs: pdf = FakeHTML( string=f'

                        My Link

                        ', base_url=base_url).write_pdf() assert len(logs) == 1 assert "Malformed" in logs[0] assert "]" in logs[0] uris = re.findall(b'/URI \\((.*)\\)', pdf) types = re.findall(b'/S (/\\w*)', pdf) subtypes = re.findall(b'/Subtype (/\\w*)', pdf) assert uris.pop(0) == url.encode() assert subtypes.pop(0) == b'/Link' assert types.pop(0) == b'/URI' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4884033 weasyprint-62.3/tests/testing_utils.py0000644000000000000000000002012414635317053015216 0ustar00"""Helpers for tests.""" import contextlib import functools import logging import sys from pathlib import Path from weasyprint import CSS, DEFAULT_OPTIONS, 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.html import HTML5_UA_STYLESHEET 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( Path(__file__).parent.parent / 'weasyprint' / 'css' / 'tests_ua.css') PROPER_CHILDREN = { # 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,),), } class FakeHTML(HTML): """Like weasyprint.HTML, but with a lighter UA stylesheet.""" def __init__(self, *args, force_uncompressed_pdf=True, **kwargs): super().__init__(*args, **kwargs) self._force_uncompressed_pdf = force_uncompressed_pdf def _ua_stylesheets(self, forms=False): return [ TEST_UA_STYLESHEET if stylesheet == HTML5_UA_STYLESHEET else stylesheet for stylesheet in super()._ua_stylesheets(forms)] def write_pdf(self, target=None, zoom=1, finisher=None, **options): # Override function to force the generation of uncompressed PDFs if self._force_uncompressed_pdf: options['uncompressed_pdf'] = True return super().write_pdf(target, zoom, finisher, **options) def resource_path(name): """Return the absolute path of the resource called ``name``.""" return Path(__file__).parent / 'resources' / name # Dummy filename, but in the right directory. BASE_URL = path2url(resource_path('')) 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 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 tree_position(box_list, matcher): """Return a list identifying the first matching box's tree position. Given a list of Boxes, this function returns a list containing the first (depth-first) Box that the matcher function identifies. This list can then be compared to another similarly-obtained list to assert that one Box is in the document tree before or after another. box_list: a list of Box objects, possibly PageBoxes matcher: a function that takes a Box and returns truthy when it matches """ for i, box in enumerate(box_list): if matcher(box): return [i] elif hasattr(box, 'children'): position = tree_position(box.children, matcher) if position: return [i, *position] 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, options=DEFAULT_OPTIONS) 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) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1718984247.972615 weasyprint-62.3/weasyprint/__init__.py0000644000000000000000000004077514635317070015140 0ustar00"""The Awesome Document Factory. The public API is what is accessible from this "root" packages without importing sub-modules. """ import contextlib from datetime import datetime from os.path import getctime, getmtime from pathlib import Path from urllib.parse import urljoin import cssselect2 import html5lib import tinycss2 VERSION = __version__ = '62.3' #: Default values for command-line and Python API options. See #: :func:`__main__.main` to learn more about specific options for #: command-line. #: #: :param list stylesheets: #: An optional list of user stylesheets. The list can include #: are :class:`CSS` objects, filenames, URLs, or file-like #: objects. (See :ref:`Stylesheet Origins`.) #: :param str media_type: #: Media type to use for @media. #: :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 bytes pdf_identifier: #: A bytestring used as PDF file identifier. #: :param str pdf_variant: #: A PDF variant name. #: :param str pdf_version: #: A PDF version number. #: :param bool pdf_forms: #: Whether PDF forms have to be included. #: :param bool uncompressed_pdf: #: Whether PDF content should be compressed. #: :param bool custom_metadata: #: Whether custom HTML metadata should be stored in the generated PDF. #: :param bool presentational_hints: #: Whether HTML presentational hints are followed. #: :param bool optimize_images: #: Whether size of embedded images should be optimized, with no quality #: loss. #: :param int jpeg_quality: #: JPEG quality between 0 (worst) to 95 (best). #: :param int dpi: #: Maximum resolution of images embedded in the PDF. #: :param bool full_fonts: #: Whether unmodified font files should be embedded when possible. #: :param bool hinting: #: Whether hinting information should be kept in embedded fonts. #: :type cache: :obj:`dict`, :class:`pathlib.Path` or :obj:`str` #: :param cache: #: A dictionary used to cache images in memory, or a folder path where #: images are temporarily stored. DEFAULT_OPTIONS = { 'stylesheets': None, 'media_type': 'print', 'attachments': None, 'pdf_identifier': None, 'pdf_variant': None, 'pdf_version': None, 'pdf_forms': None, 'uncompressed_pdf': False, 'custom_metadata': False, 'presentational_hints': False, 'optimize_images': False, 'jpeg_quality': None, 'dpi': None, 'full_fonts': False, 'hinting': False, 'cache': None, } __all__ = [ 'HTML', 'CSS', 'DEFAULT_OPTIONS', '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: I001, E402 fetch, default_url_fetcher, path2url, ensure_url, url_is_absolute) from .logger import LOGGER, PROGRESS_LOGGER # noqa: E402 # Some imports are at the end of the file (after the CSS class) # to work around circular imports. def _find_base_url(html_document, fallback_base_url): """Return the base URL for the document. See https://www.w3.org/TR/html5/urls.html#document-base-url """ first_base_element = next(iter(html_document.iter('base')), None) if first_base_element is not None: href = first_base_element.get('href', '').strip() if href: return urljoin(fallback_base_url, href) return fallback_base_url 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. :type base_url: str or pathlib.Path :param 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:`callable` :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')) if isinstance(base_url, Path): base_url = str(base_url) 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: kwargs = {'namespaceHTMLElements': False} if protocol_encoding is not None: kwargs['transport_encoding'] = protocol_encoding if encoding is not None: kwargs['override_encoding'] = encoding result = html5lib.parse(source, **kwargs) 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, forms=False): if forms: return [HTML5_UA_STYLESHEET, HTML5_UA_FORM_STYLESHEET] 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, font_config=None, counter_style=None, **options): """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. :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 options: The ``options`` parameter includes by default the :data:`DEFAULT_OPTIONS` values. :returns: A :class:`document.Document` object. """ new_options = DEFAULT_OPTIONS.copy() new_options.update(options) options = new_options return Document._render(self, font_config, counter_style, options) def write_pdf(self, target=None, zoom=1, finisher=None, font_config=None, counter_style=None, **options): """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 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". :type finisher: :term:`callable` :param finisher: A finisher function or callable 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. :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 options: The ``options`` parameter includes by default the :data:`DEFAULT_OPTIONS` values. :returns: The PDF as :obj:`bytes` if ``target`` is not provided or :obj:`None`, otherwise :obj:`None` (the PDF is written to ``target``). """ new_options = DEFAULT_OPTIONS.copy() new_options.update(options) options = new_options return ( self.render(font_config, counter_style, **options) .write_pdf(target, zoom, finisher, **options)) 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 == 'file_obj': source = source.read() if isinstance(source, str): # unicode, no encoding stylesheet = tinycss2.parse_stylesheet(source) else: 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 preprocess_stylesheet( media_type, base_url, stylesheet, url_fetcher, self.matcher, self.page_rules, font_config, counter_style) class Attachment: """File attachment for a PDF document. An instance is created in the same way as :class:`HTML`, except that the HTML specific arguments (``encoding`` and ``media_type``) are not supported. :param str description: A description of the attachment to be included in the PDF document. May be :obj:`None`. :type created: :obj:`datetime.datetime` :param created: Creation date and time. Default is current date and time. :type modified: :obj:`datetime.datetime` :param modified: Modification date and time. Default is current date and time. :param str relationship: A string that represents the relationship between the attachment and the PDF it is embedded in. Default is 'Unspecified', other common values are defined in ISO-32000-2:2020, 7.11.3. """ def __init__(self, guess=None, filename=None, url=None, file_obj=None, string=None, base_url=None, url_fetcher=default_url_fetcher, description=None, created=None, modified=None, relationship='Unspecified'): self.source = _select_source( guess, filename, url, file_obj, string, base_url=base_url, url_fetcher=url_fetcher) self.description = description self.relationship = relationship self.md5 = None if created is None: if filename: created = datetime.fromtimestamp(getctime(filename)) else: created = datetime.now() if modified is None: if filename: modified = datetime.fromtimestamp(getmtime(filename)) else: modified = datetime.now() self.created = created self.modified = modified @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: source = ', '.join(selected_params) or 'nothing' raise TypeError(f'Expected exactly one source, got {source}') 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 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: I001, E402 from .html import ( # noqa: E402 HTML5_UA_COUNTER_STYLE, HTML5_UA_STYLESHEET, HTML5_UA_FORM_STYLESHEET, HTML5_PH_STYLESHEET) from .document import Document, Page # noqa: E402 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3463626 weasyprint-62.3/weasyprint/__main__.py0000644000000000000000000001501214634254702015104 0ustar00"""Command-line interface to WeasyPrint.""" import argparse import logging import platform import sys from functools import partial import pydyf from . import DEFAULT_OPTIONS, HTML, LOGGER, __version__ from .pdf import VARIANTS from .text.ffi import pango from .urls import default_url_fetcher 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() class Parser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): self._arguments = {} super().__init__(*args, **kwargs) def add_argument(self, *args, **kwargs): super().add_argument(*args, **kwargs) key = args[-1].lstrip('-') kwargs['flags'] = args kwargs['positional'] = args[-1][0] != '-' self._arguments[key] = kwargs @property def docstring(self): self._arguments['help'] = self._arguments.pop('help') data = [] for key, args in self._arguments.items(): data.append('.. option:: ') action = args.get('action', 'store') for flag in args['flags']: data.append(flag) if not args['positional'] and action in ('store', 'append'): data.append(f' <{key}>') data.append(', ') data[-1] = '\n\n' data.append(f' {args["help"][0].upper()}{args["help"][1:]}.\n\n') if 'choices' in args: choices = ", ".join(args['choices']) data.append(f' Possible choices: {choices}.\n\n') if action == 'append': data.append(' This option can be passed multiple times.\n\n') return ''.join(data) PARSER = Parser(prog='weasyprint', description='Render web pages to PDF.') 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') PARSER.add_argument( '-e', '--encoding', help='force the input character encoding') PARSER.add_argument( '-s', '--stylesheet', action='append', dest='stylesheets', help='URL or filename for a user CSS stylesheet') PARSER.add_argument( '-m', '--media-type', 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', dest='attachments', help='URL or filename of a file to attach to the PDF document') PARSER.add_argument('--pdf-identifier', help='PDF file identifier') PARSER.add_argument( '--pdf-variant', choices=VARIANTS, help='PDF variant to generate') PARSER.add_argument('--pdf-version', help='PDF version number') PARSER.add_argument( '--pdf-forms', action='store_true', help='include PDF forms') PARSER.add_argument( '--uncompressed-pdf', action='store_true', help='do not compress PDF content, mainly for debugging purpose') PARSER.add_argument( '--custom-metadata', action='store_true', help='include custom HTML meta tags in PDF metadata') PARSER.add_argument( '-p', '--presentational-hints', action='store_true', help='follow HTML presentational hints') PARSER.add_argument( '--optimize-images', action='store_true', help='optimize size of embedded images with no quality loss') PARSER.add_argument( '-j', '--jpeg-quality', type=int, help='JPEG quality between 0 (worst) to 95 (best)') PARSER.add_argument( '--full-fonts', action='store_true', help='embed unmodified font files when possible') PARSER.add_argument( '--hinting', action='store_true', help='keep hinting information in embedded fonts') PARSER.add_argument( '-c', '--cache-folder', dest='cache', help='store cache on disk instead of memory, folder is ' 'created if needed and cleaned after the PDF is generated') PARSER.add_argument( '-D', '--dpi', type=int, help='set maximum resolution of images embedded in the PDF') 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( '--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( '-t', '--timeout', type=int, help='Set timeout in seconds for HTTP requests') PARSER.set_defaults(**DEFAULT_OPTIONS) def main(argv=None, stdout=None, stdin=None, HTML=HTML): # noqa: N803 """The ``weasyprint`` program takes at least two arguments: .. code-block:: sh weasyprint [options] """ 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 url_fetcher = default_url_fetcher if args.timeout is not None: url_fetcher = partial(default_url_fetcher, timeout=args.timeout) options = vars(args) # 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, url_fetcher=url_fetcher) html.write_pdf(output, **options) main.__doc__ += '\n\n' + PARSER.docstring if __name__ == '__main__': # pragma: no cover main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718984235.4884033 weasyprint-62.3/weasyprint/anchors.py0000644000000000000000000001354414635317053015031 0ustar00"""Find anchors, links, bookmarks and inputs in documents.""" import math from .formatting_structure import boxes from .layout.percent import percentage from .matrix import Matrix def rectangle_aabb(matrix, pos_x, pos_y, width, height): """Apply a transformation matrix to an axis-aligned rectangle. Return its axis-aligned bounding box as ``(x1, y1, x2, y2)``. """ if not matrix: return pos_x, pos_y, pos_x + width, pos_y + height transform_point = matrix.transform_point x1, y1 = transform_point(pos_x, pos_y) x2, y2 = transform_point(pos_x + width, pos_y) x3, y3 = transform_point(pos_x, pos_y + height) x4, y4 = transform_point(pos_x + width, pos_y + height) box_x1 = min(x1, x2, x3, x4) box_y1 = min(y1, y2, y3, y4) box_x2 = max(x1, x2, x3, x4) box_y2 = max(y1, y2, y3, y4) return box_x1, box_y1, box_x2, box_y2 def gather_anchors(box, anchors, links, bookmarks, inputs, parent_matrix=None): """Gather anchors and other data related to specific positions in PDF. Currently finds anchors, links, bookmarks and inputs. """ # Get box transformation matrix. # "Transforms apply to block-level and atomic inline-level elements, # but do not apply to elements which may be split into # multiple inline-level boxes." # https://www.w3.org/TR/css-transforms-1/#introduction if box.style['transform'] and not isinstance(box, boxes.InlineBox): border_width = box.border_width() border_height = box.border_height() origin_x, origin_y = box.style['transform_origin'] offset_x = percentage(origin_x, border_width) offset_y = percentage(origin_y, border_height) origin_x = box.border_box_x() + offset_x origin_y = box.border_box_y() + offset_y matrix = Matrix(e=origin_x, f=origin_y) for name, args in box.style['transform']: a, b, c, d, e, f = 1, 0, 0, 1, 0, 0 if name == 'scale': a, d = args elif name == 'rotate': a = d = math.cos(args) b = math.sin(args) c = -b elif name == 'translate': e = percentage(args[0], border_width) f = percentage(args[1], border_height) elif name == 'skew': b, c = math.tan(args[1]), math.tan(args[0]) else: assert name == 'matrix' a, b, c, d, e, f = args matrix = Matrix(a, b, c, d, e, f) @ matrix box.transformation_matrix = ( Matrix(e=-origin_x, f=-origin_y) @ matrix) if parent_matrix: matrix = box.transformation_matrix @ parent_matrix else: matrix = box.transformation_matrix else: matrix = parent_matrix bookmark_label = box.bookmark_label if box.style['bookmark_level'] == 'none': bookmark_level = None else: bookmark_level = box.style['bookmark_level'] state = box.style['bookmark_state'] link = box.style['link'] anchor_name = box.style['anchor'] has_bookmark = bookmark_label and bookmark_level # 'link' is inherited but redundant on text boxes has_link = link and not isinstance(box, (boxes.TextBox, boxes.LineBox)) # In case of duplicate IDs, only the first is an anchor. has_anchor = anchor_name and anchor_name not in anchors is_input = box.is_input() if has_bookmark or has_link or has_anchor or is_input: if is_input: pos_x, pos_y = box.content_box_x(), box.content_box_y() width, height = box.width, box.height else: pos_x, pos_y, width, height = box.hit_area() if has_link or is_input: rectangle = rectangle_aabb(matrix, pos_x, pos_y, width, height) if has_link: token_type, link = link assert token_type == 'url' link_type, target = link assert isinstance(target, str) if link_type == 'external' and box.is_attachment(): link_type = 'attachment' links.append((link_type, target, rectangle, box)) if is_input: inputs.append((box.element, box.style, rectangle)) if matrix and (has_bookmark or has_anchor): pos_x, pos_y = matrix.transform_point(pos_x, pos_y) if has_bookmark: bookmark = (bookmark_level, bookmark_label, (pos_x, pos_y), state) bookmarks.append(bookmark) if has_anchor: anchors[anchor_name] = pos_x, pos_y for child in box.all_children(): gather_anchors(child, anchors, links, bookmarks, inputs, matrix) def make_page_bookmark_tree(page, skipped_levels, last_by_depth, previous_level, page_number, matrix): """Make a tree of all bookmarks in a given page.""" for level, label, (point_x, point_y), state in page.bookmarks: if level > previous_level: # Example: if the previous bookmark is a

                        , the next # depth "should" be for

                        . If now we get a

                        we’re # skipping two levels: append 6 - 3 - 1 = 2 skipped_levels.append(level - previous_level - 1) else: temp = level while temp < previous_level: temp += 1 + skipped_levels.pop() if temp > previous_level: # We remove too many "skips", add some back: skipped_levels.append(temp - previous_level - 1) previous_level = level depth = level - sum(skipped_levels) assert depth == len(skipped_levels) assert depth >= 1 children = [] point_x, point_y = matrix.transform_point(point_x, point_y) subtree = (label, (page_number, point_x, point_y), children, state) last_by_depth[depth - 1].append(subtree) del last_by_depth[depth:] last_by_depth.append(children) return previous_level ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718704578.3473628 weasyprint-62.3/weasyprint/css/__init__.py0000644000000000000000000014453614634254702015731 0ustar00"""Find and apply 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. https://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 itertools import groupby from logging import DEBUG, WARNING import cssselect2 import tinycss2 import tinycss2.ast import tinycss2.nth from .. import CSS from ..logger import LOGGER, PROGRESS_LOGGER from ..urls import URLFetchingError, get_url_attribute, url_join from . import counters, media_queries from .computed_values import COMPUTER_FUNCTIONS from .properties import INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES, ZERO_PIXELS from .validation import preprocess_declarations from .validation.descriptors import preprocess_descriptors from .utils import ( # isort:skip InvalidValues, Pending, check_var_function, get_url, parse_function, remove_whitespace) # 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 # https://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}'] = 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}'] = 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) 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 offset = page_type.index + 1 - b if a == 0: return offset == 0 else: return offset / a >= 0 and not offset % a 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 text_decoration(key, value, parent_value, cascaded): # The text-decoration-* properties are not inherited but propagated # using specific rules. # See https://drafts.csswg.org/css-text-decor-3/#line-decoration # TODO: these rules don’t follow the specification. if key in ('text_decoration_color', 'text_decoration_style'): if not cascaded: value = parent_value elif key == 'text_decoration_line': if parent_value != 'none': if value == 'none': value = parent_value else: value = value | parent_value return value 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