Codebase list weasyprint / f888ecc
New upstream version 57.0 Scott Kitterman 1 year, 6 months ago
95 changed file(s) with 3665 addition(s) and 1639 deletion(s). Raw diff Collapse all Expand all
00 Metadata-Version: 2.1
11 Name: weasyprint
2 Version: 56.1
2 Version: 57.0
33 Summary: The Awesome Document Factory
44 Keywords: html,css,pdf,converter
55 Author-email: Simon Sapin <simon.sapin@exyr.org>
2323 Classifier: Topic :: Text Processing :: Markup :: HTML
2424 Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
2525 Classifier: Topic :: Printing
26 Requires-Dist: pydyf >=0.2.0
26 Requires-Dist: pydyf >=0.5.0
2727 Requires-Dist: cffi >=0.6
2828 Requires-Dist: html5lib >=1.1
2929 Requires-Dist: tinycss2 >=1.0.0
3434 Requires-Dist: sphinx ; extra == "doc"
3535 Requires-Dist: sphinx_rtd_theme ; extra == "doc"
3636 Requires-Dist: pytest ; extra == "test"
37 Requires-Dist: pytest-xdist ; extra == "test"
38 Requires-Dist: pytest-flake8 ; extra == "test"
39 Requires-Dist: pytest-isort ; extra == "test"
40 Requires-Dist: pytest-cov ; extra == "test"
41 Requires-Dist: coverage[toml] ; extra == "test"
37 Requires-Dist: isort ; extra == "test"
38 Requires-Dist: flake8 ; extra == "test"
4239 Project-URL: Changelog, https://github.com/Kozea/WeasyPrint/releases
4340 Project-URL: Code, https://github.com/Kozea/WeasyPrint
4441 Project-URL: Documentation, https://doc.courtbouillon.org/weasyprint/
8484 are currently not supported, although a custom :ref:`URL fetcher
8585 <URL Fetchers>` can help.
8686
87 .. _data URIs: http://en.wikipedia.org/wiki/Data_URI_scheme
87 .. _data URIs: https://en.wikipedia.org/wiki/Data_URI_scheme
8888
8989
9090 HTML
120120 HTML, try to enable this option.
121121
122122 .. _User-Agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/master/weasyprint/css/html5_ua.css
123 .. _presentational hints: http://www.w3.org/TR/html5/rendering.html#presentational-hints
123 .. _presentational hints: https://www.w3.org/TR/html5/rendering.html#presentational-hints
124124 .. _Pillow: https://python-pillow.org/
125125
126126 Stylesheet Origins
139139 to raise their priority.
140140
141141 .. _user agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/master/weasyprint/css/html5_ua.css
142 .. _cascade: http://www.w3.org/TR/CSS21/cascade.html#cascading-order
143 .. _!important: http://www.w3.org/TR/CSS21/cascade.html#important-rules
142 .. _cascade: https://www.w3.org/TR/CSS21/cascade.html#cascading-order
143 .. _!important: https://www.w3.org/TR/CSS21/cascade.html#important-rules
144144
145145
146146 PDF
172172 specifications. The major rules to follow are to include a PDF identifier, to
173173 check the PDF version, and to avoid anti-aliasing for images using
174174 ``image-rendering: crisp-edges``.
175
176 The generation of PDF/UA documents (UA-1) is supported. However, the generated
177 documents are not guaranteed to be valid, and users have the responsibility to
178 check that they follow the rules listed by the related specifications. The main
179 constraint is to use a correct HTML structure to avoid inconsistencies in the
180 PDF structure.
175181
176182
177183 Fonts
221227 * `System colors`_ and `system fonts`_. The former are deprecated in `CSS Color
222228 Module Level 3`_.
223229
224 .. _CSS Level 2 Revision 1: http://www.w3.org/TR/CSS21/
225 .. _Acid2 Test: http://www.webstandards.org/files/acid2/test.html
226 .. _::first-line: http://www.w3.org/TR/CSS21/selector.html#first-line-pseudo
227 .. _empty-cells: http://www.w3.org/TR/CSS21/tables.html#empty-cells
228 .. _visibility\: collapse: http://www.w3.org/TR/CSS21/tables.html#dynamic-effects
229 .. _width: http://www.w3.org/TR/CSS21/visudet.html#min-max-widths
230 .. _height: http://www.w3.org/TR/CSS21/visudet.html#min-max-heights
231 .. _font matching algorithm: http://www.w3.org/TR/CSS21/fonts.html#algorithm
232 .. _Bi-directional text: http://www.w3.org/TR/CSS21/visuren.html#direction
233 .. _System colors: http://www.w3.org/TR/CSS21/ui.html#system-colors
234 .. _system fonts: http://www.w3.org/TR/CSS21/fonts.html#propdef-font
230 .. _CSS Level 2 Revision 1: https://www.w3.org/TR/CSS21/
231 .. _Acid2 Test: https://www.webstandards.org/files/acid2/test.html
232 .. _::first-line: https://www.w3.org/TR/CSS21/selector.html#first-line-pseudo
233 .. _empty-cells: https://www.w3.org/TR/CSS21/tables.html#empty-cells
234 .. _visibility\: collapse: https://www.w3.org/TR/CSS21/tables.html#dynamic-effects
235 .. _width: https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
236 .. _height: https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
237 .. _font matching algorithm: https://www.w3.org/TR/CSS21/fonts.html#algorithm
238 .. _Bi-directional text: https://www.w3.org/TR/CSS21/visuren.html#direction
239 .. _System colors: https://www.w3.org/TR/CSS21/ui.html#system-colors
240 .. _system fonts: https://www.w3.org/TR/CSS21/fonts.html#propdef-font
235241
236242 To the best of our knowledge, everything else that applies to the
237243 print media **is** supported. Please report a bug if you find this list
238244 incomplete.
239245
240 Selectors Level 3
241 +++++++++++++++++
246 Selectors Level 3 / 4
247 +++++++++++++++++++++
242248
243249 With the exceptions noted here, all `Selectors Level 3`_ are supported.
244250
246252 ``:target`` and ``:visited`` pseudo-classes are accepted as valid but
247253 never match anything.
248254
249 .. _Selectors Level 3: http://www.w3.org/TR/css3-selectors/
255 Everything in `Selectors Level 4`_ is supported, except:
256
257 - ``:dir``,
258 - input pseudo-classes (``:valid``, ``:invalid``…),
259 - column selector (``||``, ``:nth-col()``, ``:nth-last-col()``).
260
261 .. _Selectors Level 3: https://www.w3.org/TR/selectors-3/
262 .. _Selectors Level 4: https://www.w3.org/TR/selectors-4/
250263
251264 CSS Text Module Level 3 / 4
252265 +++++++++++++++++++++++++++
264277 - the ``overflow-wrap`` property replacing ``word-wrap``;
265278 - the ``break-all`` value of the ``word-break`` property (see `#1153`_);
266279 - the ``full-width`` value of the ``text-transform`` property; and
280 - the ``start``, ``end`` and ``justify-all`` values of the ``text-align`` property;
281 - the ``text-align-last`` and ``text-justify`` properties; and
267282 - the ``tab-size`` property.
268283
269 Experimental_ properties controling hyphenation_ are supported by WeasyPrint:
284 Properties controling hyphenation_ are supported by WeasyPrint:
270285
271286 - ``hyphens``,
272287 - ``hyphenate-character``,
297312 supported:
298313
299314 - the ``line-break`` property;
300 - the ``start``, ``end``, ``match-parent`` and ``start end`` values of the
301 ``text-align`` property;
302 - the ``text-align-last`` and ``text-justify`` properties; and
315 - the ``match-parent`` value of the ``text-align`` property;
303316 - the ``text-indent`` and ``hanging-punctuation`` properties.
304317
305318 The other features provided by `CSS Text Module Level 4`_ are **not**
314327
315328 .. _#1153: https://github.com/Kozea/WeasyPrint/issues/1153
316329 .. _supported by Pyphen: https://github.com/Kozea/Pyphen/tree/master/pyphen/dictionaries
317 .. _hyphenation: http://www.w3.org/TR/css3-text/#hyphenation
330 .. _hyphenation: https://www.w3.org/TR/css-text-3/#hyphenation
318331 .. _CSS Text Module Level 3: https://www.w3.org/TR/css-text-3/
319332 .. _CSS Text Module Level 4: https://www.w3.org/TR/css-text-4/
320333
321 CSS Fonts Module Level 3
322 ++++++++++++++++++++++++
334 CSS Fonts Module Level 3 / 4
335 ++++++++++++++++++++++++++++
323336
324337 The `CSS Fonts Module Level 3`_ is a candidate recommendation describing "how
325338 font properties are specified and how font resources are loaded dynamically".
343356
344357 The shorthand ``font`` and ``font-variant`` properties are supported.
345358
346 WeasyPrint supports the ``@font-face`` rule, provided that Pango >= 1.38 is installed.
359 WeasyPrint supports the ``@font-face`` rule.
347360
348361 WeasyPrint does **not** support the ``@font-feature-values`` rule and the
349362 values of ``font-variant-alternates`` other than ``normal`` and
352365 The ``font-variant-caps`` property is supported but needs the small-caps variant of
353366 the font to be installed. WeasyPrint does **not** simulate missing small-caps
354367 fonts.
368
369 From `CSS Fonts Module Level 4`_ we only support the
370 ``font-variation-settings`` property enabling specific font variations.
371
372 .. _CSS Fonts Module Level 3: https://www.w3.org/TR/css-fonts-3/
373 .. _CSS Fonts Module Level 4: https://www.w3.org/TR/css-fonts-4/
374
355375
356376 CSS Paged Media Module Level 3
357377 ++++++++++++++++++++++++++++++
375395 - the page ``size``, ``bleed`` and ``marks`` properties;
376396 - the named pages.
377397
378 .. _CSS Paged Media Module Level 3: http://dev.w3.org/csswg/css3-page/
398 .. _CSS Paged Media Module Level 3: https://drafts.csswg.org/css-page-3/
379399 .. _#93: https://github.com/Kozea/WeasyPrint/issues/93
380400
381401 CSS Generated Content for Paged Media Module
405425
406426 Page groups (``:nth(X of pagename)`` pseudo-class) are not supported.
407427
408 .. _CSS Generated Content for Paged Media Module: http://www.w3.org/TR/css-gcpm-3/
428 .. _CSS Generated Content for Paged Media Module: https://www.w3.org/TR/css-gcpm-3/
409429 .. _Page selectors: https://www.w3.org/TR/css-gcpm-3/#document-page-selectors
410430 .. _running elements: https://www.w3.org/TR/css-gcpm-3/#running-elements
411431 .. _Footnotes: https://www.w3.org/TR/css-gcpm-3/#footnotes
444464 In particular, ``target-counter()`` and ``target-text()`` are useful when it
445465 comes to tables of contents (see `an example`_).
446466
447 You can also control `PDF bookmarks`_ with WeasyPrint. Using the experimental_
467 You can also control `PDF bookmarks`_ with WeasyPrint. Using the
448468 ``bookmark-level``, ``bookmark-label`` and ``bookmark-state`` properties, you
449469 can add bookmarks that will be available in your PDF reader.
450470
462482 - quotes (``content: *-quote``);
463483 - leaders (``content: leader()``).
464484
465 .. _CSS Generated Content Module Level 3: http://www.w3.org/TR/css-content-3/
485 .. _CSS Generated Content Module Level 3: https://www.w3.org/TR/css-content-3/
466486 .. _Quotes: https://www.w3.org/TR/css-content-3/#quotes
467487 .. _Named strings: https://www.w3.org/TR/css-content-3/#named-strings
468488 .. _Cross-references: https://www.w3.org/TR/css-content-3/#cross-references
469489 .. _an example: https://github.com/Kozea/WeasyPrint/pull/652#issuecomment-403276559
470490 .. _PDF bookmarks: https://www.w3.org/TR/css-content-3/#bookmark-generation
471 .. _experimental: http://www.w3.org/TR/css-2010/#experimental
472491 .. _user agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/master/weasyprint/css/html5_ua.css
473492
474493 CSS Color Module Level 3
484503 This recommendation is fully implemented in WeasyPrint, except the deprecated
485504 System Colors.
486505
487 .. _CSS Color Module Level 3: http://www.w3.org/TR/css3-color/
506 .. _CSS Color Module Level 3: https://www.w3.org/TR/css-color-3/
488507
489508 CSS Transforms Module Level 1
490509 +++++++++++++++++++++++++++++
495514 rotated and scaled in two or three dimensional space."
496515
497516 WeasyPrint supports the ``transform`` and ``transform-origin`` properties, and
498 all the 2D transformations (``matrix``, ``rotate``, ``translate(X|Y)?``,
499 ``scale(X|Y)?``, ``skew(X|Y)?``).
517 all the 2D transformations (``matrix``, ``rotate``, ``translate``,
518 ``translateX``, ``translateY``, ``scale``, ``scaleX``, ``scaleY``, ``skew``,
519 ``skewX``, ``skewY``).
500520
501521 WeasyPrint does **not** support the ``transform-style``, ``perspective``,
502522 ``perspective-origin`` and ``backface-visibility`` properties, and all the 3D
503 transformations (``matrix3d``, ``rotate(3d|X|Y|Z)``, ``translate(3d|Z)``,
504 ``scale(3d|Z)``).
505
506 .. _CSS Transforms Module Level 1: http://dev.w3.org/csswg/css3-transforms/
523 transformations (``matrix3d``, ``rotate3d``, ``rotateX``, ``rotateY``,
524 ``rotateZ``, ``translate3d``, ``translateZ``, ``scale3d``, ``scaleZ``).
525
526 .. _CSS Transforms Module Level 1: https://drafts.csswg.org/css-transforms-1/
507527
508528 CSS Backgrounds and Borders Module Level 3
509529 ++++++++++++++++++++++++++++++++++++++++++
534554 `git branch`_ that is not released, as it relies on raster implementation of
535555 shadows.
536556
537 .. _CSS Backgrounds and Borders Level 3: http://www.w3.org/TR/css3-background/
538 .. _border part: http://www.w3.org/TR/css3-background/#borders
539 .. _background part: http://www.w3.org/TR/css3-background/#backgrounds
540 .. _rounded corners part: http://www.w3.org/TR/css3-background/#corners
541 .. _border images part: http://www.w3.org/TR/css3-background/#border-images
542 .. _box shadow part: http://www.w3.org/TR/css3-background/#misc
557 .. _CSS Backgrounds and Borders Level 3: https://www.w3.org/TR/css-backgrounds-3/
558 .. _border part: https://www.w3.org/TR/css-backgrounds-3/#borders
559 .. _background part: https://www.w3.org/TR/css-backgrounds-3/#backgrounds
560 .. _rounded corners part: https://www.w3.org/TR/css-backgrounds-3/#corners
561 .. _border images part: https://www.w3.org/TR/css-backgrounds-3/#border-images
562 .. _box shadow part: https://www.w3.org/TR/css-backgrounds-3/#misc
543563 .. _git branch: https://github.com/Kozea/WeasyPrint/pull/149
544564
545565 CSS Image Values and Replaced Content Module Level 3 / 4
566586 The ``from-image`` and ``snap`` values of the ``image-resolution`` property are
567587 **not** supported, but the ``resolution`` value is supported.
568588
569 The ``image-rendering`` property is supported.
570
571 The ``image-orientation`` property is **not** supported.
572
573 .. _Image Values and Replaced Content Module Level 3: http://www.w3.org/TR/css3-images/
574 .. _Image Values and Replaced Content Module Level 4: http://www.w3.org/TR/css4-images/
589 The ``image-rendering`` and ``image-orientation`` properties are supported.
590
591 .. _Image Values and Replaced Content Module Level 3: https://www.w3.org/TR/css-images-3/
592 .. _Image Values and Replaced Content Module Level 4: https://www.w3.org/TR/css-images-4/
575593
576594 CSS Box Sizing Module Level 3
577595 +++++++++++++++++++++++++++++
654672 The ``column-fill`` property is supported, with a column balancing algorithm
655673 that should be efficient with simple cases.
656674
657 .. _CSS Multi-column Layout Module: https://www.w3.org/TR/css3-multicol/
675 .. _CSS Multi-column Layout Module: https://www.w3.org/TR/css-multicol-1/
658676
659677 CSS Fragmentation Module Level 3 / 4
660678 ++++++++++++++++++++++++++++++++++++
00 Changelog
11 =========
2
3
4 Version 57.0
5 ------------
6
7 Released on 2022-10-18.
8
9 This version also includes the changes from unstable b1 version listed
10 below.
11
12 New features:
13
14 * `a4fc7a1 <https://github.com/Kozea/WeasyPrint/commit/a4fc7a1>`_:
15 Support image-orientation
16
17 Bug fixes:
18
19 * `#1739 <https://github.com/Kozea/WeasyPrint/issues/1739>`_:
20 Set baseline on all flex containers
21 * `#1740 <https://github.com/Kozea/WeasyPrint/issues/1740>`_:
22 Don’t crash when currentColor is set on root svg tag
23 * `#1718 <https://github.com/Kozea/WeasyPrint/issues/1718>`_:
24 Don’t crash with empty bitmap glyphs
25 * `#1736 <https://github.com/Kozea/WeasyPrint/issues/1736>`_:
26 Always use the font’s vector variant when possible
27 * `eef8b4d <https://github.com/Kozea/WeasyPrint/commit/eef8b4d>`_:
28 Always set color and state before drawing
29 * `#1662 <https://github.com/Kozea/WeasyPrint/issues/1662>`_:
30 Use a stable key to store stream fonts
31 * `#1733 <https://github.com/Kozea/WeasyPrint/issues/1733>`_:
32 Don’t remove attachments when adding internal anchors
33 * `3c4fa50 <https://github.com/Kozea/WeasyPrint/commit/3c4fa50>`_,
34 `c215697 <https://github.com/Kozea/WeasyPrint/commit/c215697>`_,
35 `d275dac <https://github.com/Kozea/WeasyPrint/commit/d275dac>`_,
36 `b04bfff <https://github.com/Kozea/WeasyPrint/commit/b04bfff>`_:
37 Fix many bugs related to PDF/UA structure
38
39 Performance:
40
41 * `dfccf1b <https://github.com/Kozea/WeasyPrint/commit/dfccf1b>`_:
42 Use faces as fonts dictionary keys
43 * `0dc12b6 <https://github.com/Kozea/WeasyPrint/commit/0dc12b6>`_:
44 Cache add_font to avoid calling get_face too often
45 * `75e17bf <https://github.com/Kozea/WeasyPrint/commit/75e17bf>`_:
46 Don’t call process_whitespace twice on many children
47 * `498d3e1 <https://github.com/Kozea/WeasyPrint/commit/498d3e1>`_:
48 Optimize __missing__ functions
49
50 Documentation:
51
52 * `863b3d6 <https://github.com/Kozea/WeasyPrint/commit/863b3d6>`_:
53 Update documentation of installation on macOS with Homebrew
54
55 Contributors:
56
57 * Guillaume Ayoub
58
59 Backers and sponsors:
60
61 * Grip Angebotssoftware
62 * Manuel Barkhau
63 * Crisp BV
64 * SimonSoft
65 * Menutech
66 * Spacinov
67 * KontextWork
68 * René Fritz
69 * NCC Group
70 * Kobalt
71 * Tom Pohl
72 * John R Ellis
73 * Castedo Ellerman
74 * Moritz Mahringer
75 * Gábor
76 * Piotr Horzycki
77
78
79 Version 57.0b1
80 --------------
81
82 Released on 2022-09-22.
83
84 **This version is experimental, don't use it in production. If you find bugs,
85 please report them!**
86
87 New features:
88
89 * `#1704 <https://github.com/Kozea/WeasyPrint/pull/1704>`_:
90 Support PDF/UA, with financial support from Novareto
91 * `#1454 <https://github.com/Kozea/WeasyPrint/issues/1454>`_:
92 Support variable fonts
93
94 Bug fixes:
95
96 * `#1058 <https://github.com/Kozea/WeasyPrint/issues/1058>`_:
97 Fix bullet position after page break, with financial support from OpenZeppelin
98 * `#1707 <https://github.com/Kozea/WeasyPrint/issues/1707>`_:
99 Fix footnote positioning in multicolumn layout, with financial support from Code & Co.
100 * `#1722 <https://github.com/Kozea/WeasyPrint/issues/1722>`_:
101 Handle skew transformation with only one parameter
102 * `#1715 <https://github.com/Kozea/WeasyPrint/issues/1715>`_:
103 Don’t crash when images are truncated
104 * `#1697 <https://github.com/Kozea/WeasyPrint/issues/1697>`_:
105 Don’t crash when attr() is used in text-decoration-color
106 * `#1695 <https://github.com/Kozea/WeasyPrint/pull/1695>`_:
107 Include language information in PDF metadata
108 * `#1612 <https://github.com/Kozea/WeasyPrint/issues/1612>`_:
109 Don’t lowercase letters when capitalizing text
110 * `#1700 <https://github.com/Kozea/WeasyPrint/issues/1700>`_:
111 Fix crash when rendering footnote with repagination
112 * `#1667 <https://github.com/Kozea/WeasyPrint/issues/1667>`_:
113 Follow EXIF metadata for image rotation
114 * `#1669 <https://github.com/Kozea/WeasyPrint/issues/1669>`_:
115 Take care of floats when remvoving placeholders
116 * `#1638 <https://github.com/Kozea/WeasyPrint/issues/1638>`_:
117 Use the original box when breaking waiting children
118
119 Contributors:
120
121 * Guillaume Ayoub
122 * Konstantin Weddige
123 * VeteraNovis
124 * Lucie Anglade
125
126 Backers and sponsors:
127
128 * Grip Angebotssoftware
129 * Manuel Barkhau
130 * Crisp BV
131 * SimonSoft
132 * Menutech
133 * Spacinov
134 * KontextWork
135 * René Fritz
136 * NCC Group
137 * Kobalt
138 * Tom Pohl
139 * John R Ellis
140 * Moritz Mahringer
141 * Gábor
142 * Piotr Horzycki
143 * Andrew Ittner
2144
3145
4146 Version 56.1
23182460 Release process:
23192461
23202462 * Drop Python 3.1 support.
2321 * Set up [Travis CI](http://travis-ci.org/)
2463 * Set up [Travis CI](https://travis-ci.org/)
23222464 to automatically test all pushes and pull requests.
23232465 * Start testing on Python 3.4 locally. (Travis does not support 3.4 yet.)
23242466
23302472
23312473 New features:
23322474
2333 * Add the `overflow-wrap <http://dev.w3.org/csswg/css-text/#overflow-wrap>`_
2475 * Add the `overflow-wrap <https://drafts.csswg.org/css-text/#overflow-wrap>`_
23342476 property, allowing line breaks inside otherwise-unbreakable words.
23352477 Thanks Frédérick Deslandes!
23362478 * Add the `image-resolution
2337 <http://dev.w3.org/csswg/css-images-3/#the-image-resolution>`_ property,
2479 <https://drafts.csswg.org/css-images-3/#the-image-resolution>`_ property,
23382480 allowing images to be sized proportionally to their intrinsic size
23392481 at a resolution other than 96 image pixels per CSS ``in``
23402482 (ie. one image pixel per CSS ``px``)
26052747 WeasyPrint for fetching linked stylesheets or images, eg. to generate them
26062748 on the fly without going through the network.
26072749 This enables the creation of `Flask-WeasyPrint
2608 <http://packages.python.org/Flask-WeasyPrint/>`_.
2750 <https://packages.python.org/Flask-WeasyPrint/>`_.
26092751
26102752
26112753 Version 0.11
26572799
26582800 Bookmarks can be controlled by the ``-weasy-bookmark-level`` and
26592801 ``-weasy-bookmark-label`` properties, as described in `CSS Generated Content
2660 for Paged Media Module <http://dev.w3.org/csswg/css3-gcpm/#bookmarks>`_.
2802 for Paged Media Module <https://drafts.csswg.org/css-gcpm-3/#bookmarks>`_.
26612803
26622804 The default UA stylesheet sets a matching bookmark level on all ``<h1>``
26632805 to ``<h6>`` elements.
26772819 * Speed improvements on big stylesheets / small documents thanks to tinycss.
26782820 * Many bug fixes
26792821
2680 .. _tinycss: http://packages.python.org/tinycss/
2681 .. _cssselect: http://packages.python.org/cssselect/
2822 .. _tinycss: https://packages.python.org/tinycss/
2823 .. _cssselect: https://packages.python.org/cssselect/
26822824
26832825
26842826 Version 0.7.1
4141 -----
4242
4343 Tests are stored in the ``tests`` folder at the top of the repository. They use
44 the `pytest`_ library.
44 the pytest_ library.
4545
46 You can launch tests (with code coverage and lint) using the following command::
46 You can launch tests using the following command::
4747
4848 venv/bin/python -m pytest
4949
50 WeasyPrint also uses isort_ to check imports and flake8_ to check the coding
51 style::
52
53 venv/bin/python -m isort . --check --diff
54 venv/bin/python -m flake8
55
5056 .. _pytest: https://docs.pytest.org/
57 .. _isort: https://pycqa.github.io/isort/
58 .. _flake8: https://flake8.pycqa.org/
5159
5260
5361 Documentation
1010
1111 * Python_ ≥ 3.7.0
1212 * Pango_ ≥ 1.44.0
13 * pydyf_ ≥ 0.2.0
13 * pydyf_ ≥ 0.5.0
1414 * CFFI_ ≥ 0.6
1515 * html5lib_ ≥ 1.1
1616 * tinycss2_ ≥ 1.0.0
1919 * Pillow_ ≥ 4.0.0
2020 * fontTools_ ≥ 4.0.0
2121
22 .. _Python: http://www.python.org/
23 .. _Pango: http://pango.gnome.org/
22 .. _Python: https://www.python.org/
23 .. _Pango: https://pango.gnome.org/
2424 .. _CFFI: https://cffi.readthedocs.io/
2525 .. _html5lib: https://html5lib.readthedocs.io/
2626 .. _pydyf: https://doc.courtbouillon.org/pydyf/
2727 .. _tinycss2: https://doc.courtbouillon.org/tinycss2/
2828 .. _cssselect2: https://doc.courtbouillon.org/cssselect2/
29 .. _Pyphen: http://pyphen.org/
29 .. _Pyphen: https://pyphen.org/
3030 .. _Pillow: https://python-pillow.org/
3131 .. _fontTools: https://github.com/fonttools/fonttools
3232
4848
4949 If WeasyPrint is not available on your distribution, or if you want to use a
5050 more recent version of WeasyPrint, you have to be sure that Python_ (at least
51 version 3.7.0) and Pango_ (at least version 1.44.0) are installed on your
52 system. You can verify this by launching::
51 version 3.7.0) and Pango_ (at least version 1.44.0, 1.46.0 or newer is
52 preferred to get smaller documents) are installed on your system. You can
53 verify this by launching::
5354
5455 python3 --version
5556 pango-view --version
162163 macOS
163164 ~~~~~
164165
165 The easiest way to install WeasyPrint on macOS is to use Homebrew_.
166
167 When Homebrew is installed, install Python, Pango and libffi::
168
169 brew install python pango libffi
170
171 You can then install WeasyPrint in a `virtual environment`_ using `pip`_::
172
173 python3 -m venv venv
174 source venv/bin/activate
175 pip3 install weasyprint
176 weasyprint --info
166 The easiest way to install WeasyPrint on macOS is to use Homebrew_::
167
168 brew install weasyprint
177169
178170 .. _Homebrew: https://brew.sh/
179171
306298
307299 .. code-block:: sh
308300
309 weasyprint http://weasyprint.org /tmp/weasyprint-website.pdf
301 weasyprint https://weasyprint.org /tmp/weasyprint-website.pdf
310302
311303 You may see warnings on the standard error output about unsupported CSS
312304 properties. See :ref:`Command-Line API` for the details of all available
319311
320312 .. code-block:: sh
321313
322 weasyprint http://weasyprint.org /tmp/weasyprint-website.pdf \
314 weasyprint https://weasyprint.org /tmp/weasyprint-website.pdf \
323315 -s <(echo 'body { font-family: serif !important }')
324316
325317 If you have many documents to convert you may prefer using the Python API
342334 .. code-block:: python
343335
344336 from weasyprint import HTML
345 HTML('http://weasyprint.org/').write_pdf('/tmp/weasyprint-website.pdf')
337 HTML('https://weasyprint.org/').write_pdf('/tmp/weasyprint-website.pdf')
346338
347339 … or with the inline stylesheet:
348340
349341 .. code-block:: python
350342
351343 from weasyprint import HTML, CSS
352 HTML('http://weasyprint.org/').write_pdf('/tmp/weasyprint-website.pdf',
344 HTML('https://weasyprint.org/').write_pdf('/tmp/weasyprint-website.pdf',
353345 stylesheets=[CSS(string='body { font-family: serif !important }')])
354346
355347 Instantiating HTML and CSS Objects
366358 HTML('../foo.html') # Same as …
367359 HTML(filename='../foo.html')
368360
369 HTML('http://weasyprint.org') # Same as …
370 HTML(url='http://weasyprint.org')
361 HTML('https://weasyprint.org') # Same as …
362 HTML(url='https://weasyprint.org')
371363
372364 HTML(sys.stdin) # Same as …
373365 HTML(file_obj=sys.stdin)
399391 css = CSS(string='''
400392 @font-face {
401393 font-family: Gentium;
402 src: url(http://example.com/fonts/Gentium.otf);
394 src: url(https://example.com/fonts/Gentium.otf);
403395 }
404396 h1 { font-family: Gentium }''', font_config=font_config)
405397 html.write_pdf(
444436 .. code-block:: python
445437
446438 # Print the outline of the document.
447 # Output on http://www.w3.org/TR/CSS21/intro.html
439 # Output on https://www.w3.org/TR/CSS21/intro.html
448440 # 1. Introduction to CSS 2.1 (page 2)
449441 # 1. A brief CSS 2.1 tutorial for HTML (page 2)
450442 # 2. A brief CSS 2.1 tutorial for XML (page 5)
509501 the function internally used by WeasyPrint to retreive data.
510502
511503 .. _Flask-Weasyprint: https://github.com/Kozea/Flask-WeasyPrint
512 .. _Flask: http://flask.pocoo.org/
504 .. _Flask: https://flask.pocoo.org/
513505 .. _Django-WeasyPrint: https://github.com/fdemmer/django-weasyprint
514506 .. _Django: https://www.djangoproject.com/
515507
526518 .. code-block:: python
527519
528520 # No size optimization, faster, but generated PDF is larger
529 HTML('http://example.org/').write_pdf(
521 HTML('https://example.org/').write_pdf(
530522 'example.pdf', optimize_size=())
531523
532524 # Full size optimization, slower, but generated PDF is smaller
533 HTML('http://example.org/').write_pdf(
525 HTML('https://example.org/').write_pdf(
534526 'example.pdf', optimize_size=('fonts', 'images'))
535527
536528 ``image_cache`` gives the possibility to use a cache for images, avoiding to
544536
545537 cache = {}
546538 for i in range(10):
547 HTML(f'http://example.org/?id={i}').write_pdf(
539 HTML(f'https://example.org/?id={i}').write_pdf(
548540 f'example-{i}.pdf', image_cache=cache)
549541
550542
609601 Infinite Requests
610602 ~~~~~~~~~~~~~~~~~
611603
612 WeasyPrint can reach files on the network, for example using ``http://``
604 WeasyPrint can reach files on the network, for example using ``https://``
613605 URIs. For various reasons, HTTP requests may take a long time and lead to
614606 problems similar to :ref:`Long Renderings`.
615607
686678
687679 - locally installed fonts (using ``font-family`` and ``@font-face``),
688680 - network configuration (IPv4 and IPv6 support, IP addressing, firewall
689 configuration, using ``http://`` URIs and tracking time used to render
681 configuration, using ``https://`` URIs and tracking time used to render
690682 documents),
691683 - Python, Pango and other libraries versions (implementation details
692684 lead to different renderings).
9999 7. Metadata −such as document information, attachments, embedded files,
100100 hyperlinks, and PDF trim and bleed boxes− are added to the PDF.
101101
102 .. _like in web browsers: http://www.html5rocks.com/en/tutorials/internals/howbrowserswork/#The_main_flow
102 .. _like in web browsers: https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/#The_main_flow
103103
104104
105105 Parsing HTML
146146 *inheritance* (from the parent element) or the property’s *initial value*,
147147 so that every element has a *specified value* for every property.
148148
149 .. _cascade: http://www.w3.org/TR/CSS21/cascade.html
149 .. _cascade: https://www.w3.org/TR/CSS21/cascade.html
150150
151151 These *specified values* are turned into *computed values* in the
152152 ``css.computed_values`` module. Keywords and lengths in various units are
173173 generally close but not identical to the ElementTree tree: some elements
174174 generate more than one box or none.
175175
176 .. _visual formatting model: http://www.w3.org/TR/CSS21/visuren.html
176 .. _visual formatting model: https://www.w3.org/TR/CSS21/visuren.html
177177
178178 Boxes are of a lot of different kinds. For example you should not confuse
179179 *block-level boxes* and *block containers*, though *block boxes* are both. The
216216 According to the `box model`_, each box has rectangular margin, border,
217217 padding and content areas:
218218
219 .. _box model: http://www.w3.org/TR/CSS21/box.html
219 .. _box model: https://www.w3.org/TR/CSS21/box.html
220220
221221 .. image:: https://www.w3.org/TR/CSS21/images/boxdim.png
222222 :alt: CSS Box Model
238238 .. [#] These are the coordinates *if* no `CSS transform`_ applies.
239239 Transforms change the actual location of boxes, but they are applied
240240 later during drawing and do not affect layout.
241 .. _used values: http://www.w3.org/TR/CSS21/cascade.html#used-value
242 .. _CSS transform: http://www.w3.org/TR/css3-transforms/
241 .. _used values: https://www.w3.org/TR/CSS21/cascade.html#used-value
242 .. _CSS transform: https://www.w3.org/TR/css-transforms-1/
243243
244244
245245 Stacking & Drawing
256256
257257 The code lives in the ``draw`` module.
258258
259 .. _stacking rules: http://www.w3.org/TR/CSS21/zindex.html
259 .. _stacking rules: https://www.w3.org/TR/CSS21/zindex.html
260260
261261
262262 Metadata
1111 readme = {file = 'README.rst', content-type = 'text/x-rst'}
1212 license = {file = 'LICENSE'}
1313 dependencies = [
14 'pydyf >=0.2.0',
14 'pydyf >=0.5.0',
1515 'cffi >=0.6',
1616 'html5lib >=1.1',
1717 'tinycss2 >=1.0.0',
5151
5252 [project.optional-dependencies]
5353 doc = ['sphinx', 'sphinx_rtd_theme']
54 test = ['pytest', 'pytest-xdist', 'pytest-flake8', 'pytest-isort', 'pytest-cov', 'coverage[toml]']
54 test = ['pytest', 'isort', 'flake8']
5555
5656 [project.scripts]
5757 weasyprint = 'weasyprint.__main__:main'
5858
5959 [tool.flit.sdist]
60 exclude = ['.*', 'tests/results']
61
62 [tool.pytest.ini_options]
63 addopts = '--isort --flake8 --numprocesses=auto'
60 exclude = ['.*']
6461
6562 [tool.coverage.run]
6663 branch = true
3333 pngs = run(command, stdout=PIPE).stdout
3434 os.remove(pdf.name)
3535
36 assert pngs.startswith(MAGIC_NUMBER), (
37 'Ghostscript error: '
38 f'{pngs.split(MAGIC_NUMBER)[0].decode().strip()}')
36 error = pngs.split(MAGIC_NUMBER)[0].decode().strip()
37 assert pngs.startswith(MAGIC_NUMBER), f'Ghostscript error: {error}'
3938
4039 if split_images:
4140 assert target is None
4848 expected_pixels)
4949 width, height, pixels = html_to_pixels(html)
5050 assert (expected_width, expected_height) == (width, height), (
51 'Images do not have the same sizes')
51 'Images do not have the same sizes:\n'
52 f'- expected: {expected_width} × {expected_height}\n'
53 f'- result: {width} × {height}')
5254 assert_pixels_equal(name, width, height, pixels, expected_pixels)
5355
5456
1717 @page { size: 5px }
1818 svg { display: block }
1919 </style>
20 <svg width="5px" height="5px" xmlns="http://www.w3.org/2000/svg">
20 <svg width="5px" height="5px" xmlns="https://www.w3.org/2000/svg">
2121 <defs>
2222 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
2323 gradientUnits="objectBoundingBox">
4848 @page { size: 10px }
4949 svg { display: block }
5050 </style>
51 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
51 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
5252 <defs>
5353 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
5454 gradientUnits="objectBoundingBox">
7979 @page { size: 10px }
8080 svg { display: block }
8181 </style>
82 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
82 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
8383 <defs>
8484 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
8585 gradientUnits="objectBoundingBox">
105105 @page { size: 5px }
106106 svg { display: block }
107107 </style>
108 <svg width="5px" height="5px" xmlns="http://www.w3.org/2000/svg">
108 <svg width="5px" height="5px" xmlns="https://www.w3.org/2000/svg">
109109 <defs>
110110 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
111111 gradientUnits="objectBoundingBox">
132132 @page { size: 5px }
133133 svg { display: block }
134134 </style>
135 <svg width="5px" height="5px" xmlns="http://www.w3.org/2000/svg">
135 <svg width="5px" height="5px" xmlns="https://www.w3.org/2000/svg">
136136 <defs>
137137 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
138138 gradientUnits="objectBoundingBox">
158158 @page { size: 5px }
159159 svg { display: block }
160160 </style>
161 <svg width="5px" height="5px" xmlns="http://www.w3.org/2000/svg">
161 <svg width="5px" height="5px" xmlns="https://www.w3.org/2000/svg">
162162 <defs>
163163 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
164164 gradientUnits="objectBoundingBox">
183183 @page { size: 2px }
184184 svg { display: block }
185185 </style>
186 <svg width="2px" height="2px" xmlns="http://www.w3.org/2000/svg">
186 <svg width="2px" height="2px" xmlns="https://www.w3.org/2000/svg">
187187 <defs>
188188 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
189189 gradientUnits="objectBoundingBox">
212212 @page { size: 5px }
213213 svg { display: block }
214214 </style>
215 <svg width="5px" height="5px" xmlns="http://www.w3.org/2000/svg">
215 <svg width="5px" height="5px" xmlns="https://www.w3.org/2000/svg">
216216 <defs>
217217 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
218218 gradientUnits="objectBoundingBox">
238238 @page { size: 5px }
239239 svg { display: block }
240240 </style>
241 <svg width="5px" height="5px" xmlns="http://www.w3.org/2000/svg">
241 <svg width="5px" height="5px" xmlns="https://www.w3.org/2000/svg">
242242 <defs>
243243 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
244244 gradientUnits="objectBoundingBox">
265265 @page { size: 5px }
266266 svg { display: block }
267267 </style>
268 <svg width="5px" height="5px" xmlns="http://www.w3.org/2000/svg">
268 <svg width="5px" height="5px" xmlns="https://www.w3.org/2000/svg">
269269 <defs>
270270 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
271271 gradientUnits="objectBoundingBox">
295295 @page { size: 5px }
296296 svg { display: block }
297297 </style>
298 <svg width="5px" height="5px" xmlns="http://www.w3.org/2000/svg">
298 <svg width="5px" height="5px" xmlns="https://www.w3.org/2000/svg">
299299 <defs>
300300 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"
301301 gradientUnits="objectBoundingBox">
2121 @page { size: 9px }
2222 svg { display: block }
2323 </style>
24 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
24 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
2525 <defs>
2626 <clipPath id="clip">
2727 <rect x="2" y="2" width="5" height="5" />
5050 @page { size: 9px }
5151 svg { display: block }
5252 </style>
53 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
53 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
5454 <defs>
5555 <clipPath id="clip">
5656 <rect x="2" y="2" width="5" height="5" />
8282 @page { size: 9px }
8383 svg { display: block }
8484 </style>
85 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
85 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
8686 <defs>
8787 <clipPath id="clip">
8888 <rect x="2" y="2" width="2" height="2" />
44 from ...testing_utils import assert_no_logs
55
66 SVG = '''
7 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
7 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
88 <defs>
99 <rect id="rectangle" width="5" height="2" fill="red" />
1010 </defs>
2222 @page { size: 10px }
2323 svg { display: block }
2424 </style>
25 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
25 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
2626 <defs>
2727 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1"
2828 gradientUnits="objectBoundingBox">
5353 @page { size: 10px }
5454 svg { display: block }
5555 </style>
56 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
56 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
5757 <defs>
5858 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="10"
5959 gradientUnits="userSpaceOnUse">
8282 @page { size: 10px 8px }
8383 svg { display: block }
8484 </style>
85 <svg width="10px" height="8px" xmlns="http://www.w3.org/2000/svg">
85 <svg width="10px" height="8px" xmlns="https://www.w3.org/2000/svg">
8686 <defs>
8787 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1"
8888 gradientUnits="objectBoundingBox">
115115 @page { size: 10px 8px }
116116 svg { display: block }
117117 </style>
118 <svg width="10px" height="8px" xmlns="http://www.w3.org/2000/svg">
118 <svg width="10px" height="8px" xmlns="https://www.w3.org/2000/svg">
119119 <defs>
120120 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="8"
121121 gradientUnits="userSpaceOnUse">
149149 @page { size: 10px 8px}
150150 svg { display: block }
151151 </style>
152 <svg width="10px" height="8px" xmlns="http://www.w3.org/2000/svg">
152 <svg width="10px" height="8px" xmlns="https://www.w3.org/2000/svg">
153153 <defs>
154154 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1"
155155 gradientUnits="objectBoundingBox" gradientTransform="scale(0.5)">
190190 @page { size: 10px 16px }
191191 svg { display: block }
192192 </style>
193 <svg width="11px" height="16px" xmlns="http://www.w3.org/2000/svg">
193 <svg width="11px" height="16px" xmlns="https://www.w3.org/2000/svg">
194194 <defs>
195195 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="0.5"
196196 gradientUnits="objectBoundingBox" spreadMethod="repeat">
232232 @page { size: 10px 16px }
233233 svg { display: block }
234234 </style>
235 <svg width="11px" height="16px" xmlns="http://www.w3.org/2000/svg">
235 <svg width="11px" height="16px" xmlns="https://www.w3.org/2000/svg">
236236 <defs>
237237 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="0.25"
238238 gradientUnits="objectBoundingBox" spreadMethod="repeat">
273273 @page { size: 10px 16px }
274274 svg { display: block }
275275 </style>
276 <svg width="11px" height="16px" xmlns="http://www.w3.org/2000/svg">
276 <svg width="11px" height="16px" xmlns="https://www.w3.org/2000/svg">
277277 <defs>
278278 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="0.5"
279279 gradientUnits="objectBoundingBox" spreadMethod="reflect">
308308 @page { size: 10px }
309309 svg { display: block }
310310 </style>
311 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
311 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
312312 <defs>
313313 <radialGradient id="grad" cx="0.5" cy="0.5" r="0.5"
314314 fx="0.5" fy="0.5" fr="0.2"
340340 @page { size: 10px }
341341 svg { display: block }
342342 </style>
343 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
343 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
344344 <defs>
345345 <radialGradient id="grad" cx="5" cy="5" r="5" fx="5" fy="5" fr="2"
346346 gradientUnits="userSpaceOnUse">
371371 @page { size: 10px }
372372 svg { display: block }
373373 </style>
374 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
374 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
375375 <defs>
376376 <radialGradient id="grad" cx="0.5" cy="0.5" r="0.5"
377377 fx="0.5" fy="0.5" fr="0.2"
405405 @page { size: 10px }
406406 svg { display: block }
407407 </style>
408 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
408 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
409409 <defs>
410410 <radialGradient id="grad" cx="5" cy="5" r="5"
411411 fx="5" fy="5" fr="2"
440440 @page { size: 10px }
441441 svg { display: block }
442442 </style>
443 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
443 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
444444 <defs>
445445 <radialGradient id="grad" cx="0.5" cy="0.5" r="0.5"
446446 fx="0.5" fy="0.5" fr="0.2"
475475 @page { size: 10px }
476476 svg { display: block }
477477 </style>
478 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
478 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
479479 <defs>
480480 <radialGradient id="grad" cx="0.5" cy="0.5" r="0.5"
481481 fx="0.5" fy="0.5" fr="0.2"
520520 <rect x="0" y="0" width="10" height="10" fill="url(#grad)" />
521521 </svg>
522522 ''')
523
524
525 @assert_no_logs
526 @pytest.mark.parametrize('url', ('#grad\'', '\'#gra', '!', '#'))
527 def test_gradient_bad_url(assert_pixels, url):
528 assert_pixels('''
529 __________
530 __________
531 __________
532 __________
533 __________
534 __________
535 __________
536 __________
537 __________
538 __________
539 ''', '''
540 <style>
541 @page { size: 10px }
542 svg { display: block }
543 </style>
544 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
545 <defs>
546 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1"
547 gradientUnits="objectBoundingBox">
548 <stop stop-color="blue"></stop>
549 </linearGradient>
550 </defs>
551 <rect x="0" y="0" width="10" height="10" fill="url(%s)" />
552 </svg>
553 ''' % url)
1717 @page { size: 4px 4px }
1818 svg { display: block }
1919 </style>
20 <svg width="4px" height="4px" xmlns="http://www.w3.org/2000/svg">
20 <svg width="4px" height="4px" xmlns="https://www.w3.org/2000/svg">
2121 <svg x="1" y="1" width="2" height="2" viewBox="0 0 10 10">
2222 <rect x="5" y="5" width="5" height="5" fill="blue" />
2323 </svg>
3737 @page { size: 4px 4px }
3838 svg { display: block }
3939 </style>
40 <svg viewBox="0 0 4 4" xmlns="http://www.w3.org/2000/svg">
40 <svg viewBox="0 0 4 4" xmlns="https://www.w3.org/2000/svg">
4141 <svg x="1" y="1" width="2" height="2" viewBox="10 10 10 10">
4242 <rect x="15" y="15" width="5" height="5" fill="blue" />
4343 </svg>
6262 svg { display: block }
6363 </style>
6464 <svg width="8px" height="4px" viewBox="0 0 4 4"
65 xmlns="http://www.w3.org/2000/svg">
65 xmlns="https://www.w3.org/2000/svg">
6666 <rect width="4" height="4" fill="red" />
6767 <rect width="1" height="2" fill="blue" />
6868 <rect x="3" y="2" width="1" height="2" fill="lime" />
8888 </style>
8989 <svg width="8px" height="4px" viewBox="0 0 4 4"
9090 preserveAspectRatio="none"
91 xmlns="http://www.w3.org/2000/svg">
91 xmlns="https://www.w3.org/2000/svg">
9292 <rect width="4" height="4" fill="red" />
9393 <rect width="1" height="2" fill="blue" />
9494 <rect x="3" y="2" width="1" height="2" fill="lime" />
114114 </style>
115115 <svg width="8px" height="4px" viewBox="0 0 4 4"
116116 preserveAspectRatio="xMaxYMax meet"
117 xmlns="http://www.w3.org/2000/svg">
117 xmlns="https://www.w3.org/2000/svg">
118118 <rect width="4" height="4" fill="red" />
119119 <rect width="1" height="2" fill="blue" />
120120 <rect x="3" y="2" width="1" height="2" fill="lime" />
140140 </style>
141141 <svg width="4px" height="8px" viewBox="0 0 4 4"
142142 preserveAspectRatio="xMaxYMax meet"
143 xmlns="http://www.w3.org/2000/svg">
143 xmlns="https://www.w3.org/2000/svg">
144144 <rect width="4" height="4" fill="red" />
145145 <rect width="1" height="2" fill="blue" />
146146 <rect x="3" y="2" width="1" height="2" fill="lime" />
166166 </style>
167167 <svg width="8px" height="4px" viewBox="0 0 4 4"
168168 preserveAspectRatio="xMinYMin slice"
169 xmlns="http://www.w3.org/2000/svg">
169 xmlns="https://www.w3.org/2000/svg">
170170 <rect width="4" height="4" fill="red" />
171171 <rect width="1" height="2" fill="blue" />
172172 <rect x="3" y="2" width="1" height="2" fill="lime" />
192192 </style>
193193 <svg width="4px" height="8px" viewBox="0 0 4 4"
194194 preserveAspectRatio="xMinYMin slice"
195 xmlns="http://www.w3.org/2000/svg">
195 xmlns="https://www.w3.org/2000/svg">
196196 <rect width="4" height="4" fill="red" />
197197 <rect width="1" height="2" fill="blue" />
198198 <rect x="3" y="2" width="1" height="2" fill="lime" />
213213 @page { size: 4px 4px }
214214 svg { display: block }
215215 </style>
216 <svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
216 <svg width="100%" height="100%" xmlns="https://www.w3.org/2000/svg">
217217 <svg x="1" y="1" width="50%" height="50%" viewBox="0 0 10 10">
218218 <rect x="5" y="5" width="5" height="5" fill="blue" />
219219 </svg>
232232 @page { size: 4px 4px }
233233 svg { display: block }
234234 </style>
235 <svg width="4px" height="4px" xmlns="http://www.w3.org/2000/svg">
235 <svg width="4px" height="4px" xmlns="https://www.w3.org/2000/svg">
236236 <That’s bad!
237237 </svg>
238238 ''')
250250 @page { size: 4px 4px }
251251 svg { display: block }
252252 </style>
253 <svg width="4px" height="4px" xmlns="http://www.w3.org/2000/svg">
253 <svg width="4px" height="4px" xmlns="https://www.w3.org/2000/svg">
254254 <image xlink:href="%s" />
255255 </svg>
256256 ''' % path2url(resource_filename('pattern.png')))
267267 @page { size: 4px 4px }
268268 svg { display: block }
269269 </style>
270 <svg width="4px" height="4px" xmlns="http://www.w3.org/2000/svg">
270 <svg width="4px" height="4px" xmlns="https://www.w3.org/2000/svg">
271271 <image xlink:href="it doesn’t exist, mouhahahaha" />
272272 </svg>
273273 ''')
00 """Test how opacity is handled for SVG."""
1
2 import pytest
13
24 from ...testing_utils import assert_no_logs
35
68 @page { size: 9px }
79 svg { display: block }
810 </style>
9 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">%s</svg>'''
11 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">%s</svg>'''
1012
1113
1214 @assert_no_logs
1315 def test_opacity(assert_same_renderings):
1416 assert_same_renderings(
15 '''
17 opacity_source % '''
1618 <rect x="2" y="2" width="5" height="5" stroke-width="2"
1719 stroke="rgb(127, 255, 127)" fill="rgb(127, 127, 255)" />
1820 ''',
19 '''
21 opacity_source % '''
2022 <rect x="2" y="2" width="5" height="5" stroke-width="2"
2123 stroke="lime" fill="blue" opacity="0.5" />
2224 ''',
2628 @assert_no_logs
2729 def test_fill_opacity(assert_same_renderings):
2830 assert_same_renderings(
29 '''
31 opacity_source % '''
3032 <rect x="2" y="2" width="5" height="5"
3133 fill="blue" opacity="0.5" />
3234 <rect x="2" y="2" width="5" height="5" stroke-width="2"
3335 stroke="lime" fill="transparent" />
3436 ''',
35 '''
37 opacity_source % '''
3638 <rect x="2" y="2" width="5" height="5" stroke-width="2"
3739 stroke="lime" fill="blue" fill-opacity="0.5" />
3840 ''',
3941 )
4042
4143
44 @pytest.mark.xfail
4245 @assert_no_logs
4346 def test_stroke_opacity(assert_same_renderings):
47 # TODO: This test (and the other ones) fail because of a difference between
48 # the PDF and the SVG specifications: transparent borders have to be drawn
49 # on top of the shape filling in SVG but not in PDF. See:
50 # - PDF-1.7 11.7.4.4 Note 2
51 # - https://www.w3.org/TR/SVG2/render.html#PaintingShapesAndText
4452 assert_same_renderings(
4553 '''
4654 <rect x="2" y="2" width="5" height="5"
4856 <rect x="2" y="2" width="5" height="5" stroke-width="2"
4957 stroke="lime" fill="transparent" opacity="0.5" />
5058 ''',
51 '''
59 opacity_source % '''
5260 <rect x="2" y="2" width="5" height="5" stroke-width="2"
5361 stroke="lime" fill="blue" stroke-opacity="0.5" />
5462 ''',
5563 )
5664
5765
66 @pytest.mark.xfail
5867 @assert_no_logs
5968 def test_stroke_fill_opacity(assert_same_renderings):
6069 assert_same_renderings(
61 '''
70 opacity_source % '''
6271 <rect x="2" y="2" width="5" height="5"
6372 fill="blue" opacity="0.5" />
6473 <rect x="2" y="2" width="5" height="5" stroke-width="2"
6574 stroke="lime" fill="transparent" opacity="0.5" />
6675 ''',
67 '''
76 opacity_source % '''
6877 <rect x="2" y="2" width="5" height="5" stroke-width="2"
6978 stroke="lime" fill="blue"
7079 stroke-opacity="0.5" fill-opacity="0.5" />
7281 )
7382
7483
84 @pytest.mark.xfail
7585 @assert_no_logs
7686 def test_pattern_gradient_stroke_fill_opacity(assert_same_renderings):
7787 assert_same_renderings(
78 '''
88 opacity_source % '''
7989 <defs>
8090 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1"
8191 gradientUnits="objectBoundingBox">
96106 <rect x="2" y="2" width="5" height="5" stroke-width="2"
97107 stroke="url(#grad)" fill="transparent" opacity="0.5" />
98108 ''',
99 '''
109 opacity_source % '''
100110 <defs>
101111 <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1"
102112 gradientUnits="objectBoundingBox">
116126 stroke="url(#grad)" fill="url(#pat)"
117127 stroke-opacity="0.5" fill-opacity="0.5" />
118128 ''',
129 tolerance=1,
119130 )
2020 @page { size: 10px }
2121 svg { display: block }
2222 </style>
23 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
23 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
2424 <path d="M 0 1 H 8 H 1"
2525 stroke="blue" stroke-width="2" fill="none"/>
2626 <path d="M 0 4 H 8 4"
5151 @page { size: 10px }
5252 svg { display: block }
5353 </style>
54 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
54 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
5555 <path d="M 1 0 V 1 V 4"
5656 stroke="blue" stroke-width="2" fill="none"/>
5757 <path d="M 4 6 V 4 10"
8282 @page { size: 10px }
8383 svg { display: block }
8484 </style>
85 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
85 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
8686 <path d="M 4 3 L 4 10"
8787 stroke="blue" stroke-width="2" fill="none"/>
8888 <path d="M 7 0 l 0 6"
109109 @page { size: 10px }
110110 svg { display: block }
111111 </style>
112 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
112 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
113113 <path d="M 1 1 H 6 V 5 H 1 Z"
114114 stroke="blue" stroke-width="2" fill="none"/>
115115 <path d="M 9 10 V 7 H 5 V 10 z"
136136 @page { size: 10px }
137137 svg { display: block }
138138 </style>
139 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
139 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
140140 <path d="M 1 1 H 6 V 5 H 1 Z"
141141 stroke="blue" stroke-width="2" fill="lime"/>
142142 <path d="M 9 10 V 7 H 5 V 10 z"
163163 @page { size: 10px }
164164 svg { display: block }
165165 </style>
166 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
166 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
167167 <path d="M 2 5 C 2 5 3 5 5 5"
168168 stroke="blue" stroke-width="2" fill="none"/>
169169 <path d="M 2 8 c 0 0 1 0 3 0"
190190 @page { size: 10px }
191191 svg { display: block }
192192 </style>
193 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
193 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
194194 <path d="M 2 5 S 3 5 5 5"
195195 stroke="blue" stroke-width="2" fill="none"/>
196196 <path d="M 2 8 s 1 0 3 0"
219219 @page { size: 10px 12px }
220220 svg { display: block }
221221 </style>
222 <svg width="10px" height="12px" xmlns="http://www.w3.org/2000/svg">
222 <svg width="10px" height="12px" xmlns="https://www.w3.org/2000/svg">
223223 <path d="M 2 1 C 2 1 3 1 5 1 S 8 3 8 1"
224224 stroke="blue" stroke-width="2" fill="none"/>
225225 <path d="M 2 4 C 2 4 3 4 5 4 s 3 2 1 0"
250250 @page { size: 10px }
251251 svg { display: block }
252252 </style>
253 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
253 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
254254 <path d="M 2 5 Q 4 5 6 5"
255255 stroke="blue" stroke-width="2" fill="none"/>
256256 <path d="M 2 8 q 2 0 4 0"
277277 @page { size: 10px }
278278 svg { display: block }
279279 </style>
280 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
280 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
281281 <path d="M 2 5 T 6 5"
282282 stroke="blue" stroke-width="2" fill="none"/>
283283 <path d="M 2 8 t 4 0"
306306 @page { size: 12px }
307307 svg { display: block }
308308 </style>
309 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
309 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
310310 <path d="M 0 3 Q 3 0 6 3 T 12 3"
311311 stroke="blue" stroke-width="2" fill="none"/>
312312 <path d="M 0 9 Q 3 6 6 9 t 6 0"
335335 @page { size: 12px }
336336 svg { display: block }
337337 </style>
338 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
338 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
339339 <path d="M 0 3 q 3 -3 6 0 T 12 3"
340340 stroke="blue" stroke-width="2" fill="none"/>
341341 <path d="M 0 9 q 3 -3 6 0 t 6 0"
364364 @page { size: 12px }
365365 svg { display: block }
366366 </style>
367 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
367 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
368368 <path d="M 1 6 A 5 5 0 0 1 6 1"
369369 stroke="blue" stroke-width="2" fill="none"/>
370370 <path d="M 6 11 a 5 5 0 0 1 5 -5"
393393 @page { size: 12px }
394394 svg { display: block }
395395 </style>
396 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
396 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
397397 <path d="M 1 6 A 5 5 0 1 0 6 1"
398398 stroke="lime" stroke-width="2" fill="none"/>
399399 </svg>
420420 @page { size: 12px }
421421 svg { display: block }
422422 </style>
423 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
423 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
424424 <path d="M 1 6 a 5 5 0 1 0 5 -5"
425425 stroke="lime" stroke-width="2" fill="none"/>
426426 </svg>
447447 @page { size: 12px }
448448 svg { display: block }
449449 </style>
450 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
450 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
451451 <path d="M 1 6 A 5 5 0 0 0 6 1"
452452 stroke="blue" stroke-width="2" fill="none"/>
453453 <path d="M 6 11 a 5 5 0 0 0 5 -5"
476476 @page { size: 12px }
477477 svg { display: block }
478478 </style>
479 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
479 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
480480 <path d="M 6 11 A 5 5 0 1 1 11 6"
481481 stroke="blue" stroke-width="2" fill="none"/>
482482 </svg>
503503 @page { size: 12px }
504504 svg { display: block }
505505 </style>
506 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
506 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
507507 <path d="M 6 11 a 5 5 0 1 1 5 -5"
508508 stroke="blue" stroke-width="2" fill="none"/>
509509 </svg>
530530 @page { size: 12px }
531531 svg { display: block }
532532 </style>
533 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
533 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
534534 <path d="M 1 6 A 5 5 0 0 0 11 6"
535535 stroke="lime" stroke-width="2" fill="none"/>
536536 </svg>
557557 @page { size: 12px }
558558 svg { display: block }
559559 </style>
560 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
560 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
561561 <path d="M 1 1 L 1 5 L"
562562 stroke="lime" stroke-width="2" fill="none"/>
563563 </svg>
584584 @page { size: 12px }
585585 svg { display: block }
586586 </style>
587 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
587 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
588588 <marker id="line"
589589 viewBox="0 0 1 2" refX="0.5" refY="1"
590590 markerUnits="strokeWidth"
618618 @page { size: 12px }
619619 svg { display: block }
620620 </style>
621 <svg width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
621 <svg width="12px" height="12px" xmlns="https://www.w3.org/2000/svg">
622622 <marker id="line"
623623 viewBox="0 0 1 2" refX="0.5" refY="1"
624624 markerUnits="strokeWidth"
1818 @page { size: 8px }
1919 svg { display: block }
2020 </style>
21 <svg width="8px" height="8px" xmlns="http://www.w3.org/2000/svg">
21 <svg width="8px" height="8px" xmlns="https://www.w3.org/2000/svg">
2222 <defs>
2323 <pattern id="pat" x="0" y="0" width="4" height="4"
2424 patternUnits="userSpaceOnUse"
5050 @page { size: 8px }
5151 svg { display: block }
5252 </style>
53 <svg width="8px" height="8px" xmlns="http://www.w3.org/2000/svg">
53 <svg width="8px" height="8px" xmlns="https://www.w3.org/2000/svg">
5454 <defs>
5555 <pattern id="pat" x="0" y="0" width="50%" height="50%"
5656 patternUnits="objectBoundingBox"
8282 @page { size: 8px }
8383 svg { display: block }
8484 </style>
85 <svg width="8px" height="8px" xmlns="http://www.w3.org/2000/svg">
85 <svg width="8px" height="8px" xmlns="https://www.w3.org/2000/svg">
8686 <defs>
8787 <pattern id="pat" x="0" y="0" width="4" height="4"
8888 patternUnits="userSpaceOnUse"
114114 @page { size: 8px }
115115 svg { display: block }
116116 </style>
117 <svg width="8px" height="8px" xmlns="http://www.w3.org/2000/svg">
117 <svg width="8px" height="8px" xmlns="https://www.w3.org/2000/svg">
118118 <defs>
119119 <pattern id="pat" x="0" y="0" width="4" height="4"
120120 patternUnits="userSpaceOnUse"
1919 @page { size: 9px }
2020 svg { display: block }
2121 </style>
22 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
22 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
2323 <rect x="2" y="2" width="5" height="5"
2424 stroke-width="2" stroke="red" fill="none" />
2525 </svg>
4343 @page { size: 9px }
4444 svg { display: block }
4545 </style>
46 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
46 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
4747 <rect x="2" y="2" width="5" height="5" fill="red" />
4848 </svg>
4949 ''')
6666 @page { size: 9px }
6767 svg { display: block }
6868 </style>
69 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
69 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
7070 <rect x="2" y="2" width="5" height="5"
7171 stroke-width="2" stroke="red" fill="blue" />
7272 </svg>
9090 @page { size: 9px }
9191 svg { display: block }
9292 </style>
93 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
93 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
9494 <rect width="9" height="9" fill="red" rx="4" ry="4" />
9595 </svg>
9696 ''')
113113 @page { size: 9px }
114114 svg { display: block }
115115 </style>
116 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
116 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
117117 <rect width="9" height="9" fill="red" rx="0" ry="4" />
118118 </svg>
119119 ''')
136136 @page { size: 9px }
137137 svg { display: block }
138138 </style>
139 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
139 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
140140 <line x1="0" y1="5" x2="6" y2="5"
141141 stroke="red" stroke-width="2"/>
142142 </svg>
160160 @page { size: 9px }
161161 svg { display: block }
162162 </style>
163 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
163 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
164164 <polyline points="1,6, 1,2, 5,2, 5,6"
165165 stroke="red" stroke-width="2" fill="none"/>
166166 </svg>
184184 @page { size: 9px }
185185 svg { display: block }
186186 </style>
187 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
187 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
188188 <polyline points="1,6, 1,2, 5,2, 5,6"
189189 stroke="red" stroke-width="2" fill="blue"/>
190190 </svg>
208208 @page { size: 9px }
209209 svg { display: block }
210210 </style>
211 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
211 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
212212 <polygon points="1,6, 1,2, 5,2, 5,6"
213213 stroke="red" stroke-width="2" fill="none"/>
214214 </svg>
232232 @page { size: 9px }
233233 svg { display: block }
234234 </style>
235 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
235 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
236236 <polygon points="1,6, 1,2, 5,2, 5,6"
237237 stroke="red" stroke-width="2" fill="blue"/>
238238 </svg>
257257 @page { size: 10px }
258258 svg { display: block }
259259 </style>
260 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
260 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
261261 <circle cx="5" cy="5" r="3"
262262 stroke="red" stroke-width="2" fill="none"/>
263263 </svg>
282282 @page { size: 10px }
283283 svg { display: block }
284284 </style>
285 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
285 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
286286 <circle cx="5" cy="5" r="3"
287287 stroke="red" stroke-width="2" fill="blue"/>
288288 </svg>
307307 @page { size: 10px }
308308 svg { display: block }
309309 </style>
310 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
310 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
311311 <ellipse cx="5" cy="5" rx="3" ry="3"
312312 stroke="red" stroke-width="2" fill="none"/>
313313 </svg>
332332 @page { size: 10px }
333333 svg { display: block }
334334 </style>
335 <svg width="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
335 <svg width="10px" height="10px" xmlns="https://www.w3.org/2000/svg">
336336 <ellipse cx="5" cy="5" rx="3" ry="3"
337337 stroke="red" stroke-width="2" fill="blue"/>
338338 </svg>
356356 @page { size: 9px }
357357 svg { display: block }
358358 </style>
359 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
359 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
360360 <g x="5" y="5">
361361 <rect width="5" height="5" fill="red" />
362362 </g>
381381 @page { size: 9px }
382382 svg { display: block }
383383 </style>
384 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
384 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
385385 <g x="5" y="5">
386386 <rect x="2" y="2" width="5" height="5" fill="red" />
387387 </g>
406406 @page { size: 9px }
407407 svg { display: block }
408408 </style>
409 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
409 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
410410 <rect x="2" y="2" width="5" height="5"
411411 stroke-width="0" stroke="red" fill="none" />
412412 </svg>
430430 @page { size: 9px }
431431 svg { display: block }
432432 </style>
433 <svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
433 <svg width="0" height="0" xmlns="https://www.w3.org/2000/svg">
434434 <rect x="2" y="2" width="5" height="5" fill="red" />
435435 </svg>
436436 ''')
453453 @page { size: 9px }
454454 svg { display: block }
455455 </style>
456 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
456 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
457457 <rect x="2" y="2" width="5" height="5" fill="inherit" />
458458 </svg>
459459 ''')
1313 @page { size: 20px 2px }
1414 svg { display: block }
1515 </style>
16 <svg width="20px" height="2px" xmlns="http://www.w3.org/2000/svg">
16 <svg width="20px" height="2px" xmlns="https://www.w3.org/2000/svg">
1717 <text x="0" y="1.5" font-family="weasyprint" font-size="2" fill="blue">
1818 ABC DEF
1919 </text>
3434 @page { font-size: 1px; size: 20em 8ex }
3535 svg { display: block }
3636 </style>
37 <svg width="20px" height="4px" xmlns="http://www.w3.org/2000/svg">
37 <svg width="20px" height="4px" xmlns="https://www.w3.org/2000/svg">
3838 <text x="2" y="2.5" font-family="weasyprint" font-size="2"
3939 fill="transparent" stroke="blue" stroke-width="1ex">
4040 A B C
5454 @page { size: 20px 2px }
5555 svg { display: block }
5656 </style>
57 <svg width="20px" height="2px" xmlns="http://www.w3.org/2000/svg">
57 <svg width="20px" height="2px" xmlns="https://www.w3.org/2000/svg">
5858 <text x="0 4 7" y="1.5" font-family="weasyprint" font-size="2"
5959 fill="blue">
6060 ABCD
8282 @page { size: 30px 10px }
8383 svg { display: block }
8484 </style>
85 <svg width="30px" height="10px" xmlns="http://www.w3.org/2000/svg">
85 <svg width="30px" height="10px" xmlns="https://www.w3.org/2000/svg">
8686 <text x="0" y="9 9 4 9 4" font-family="weasyprint" font-size="5"
8787 fill="blue">
8888 ABCDEF
110110 @page { size: 30px 10px }
111111 svg { display: block }
112112 </style>
113 <svg width="30px" height="10px" xmlns="http://www.w3.org/2000/svg">
113 <svg width="30px" height="10px" xmlns="https://www.w3.org/2000/svg">
114114 <text x="0 10" y="9 4 9 4" font-family="weasyprint" font-size="5"
115115 fill="blue">
116116 ABCDE
130130 @page { size: 20px 2px }
131131 svg { display: block }
132132 </style>
133 <svg width="20px" height="2px" xmlns="http://www.w3.org/2000/svg">
133 <svg width="20px" height="2px" xmlns="https://www.w3.org/2000/svg">
134134 <text dx="0 2 1" y="1.5" font-family="weasyprint" font-size="2"
135135 fill="blue">
136136 ABCD
158158 @page { size: 30px 10px }
159159 svg { display: block }
160160 </style>
161 <svg width="30px" height="10px" xmlns="http://www.w3.org/2000/svg">
161 <svg width="30px" height="10px" xmlns="https://www.w3.org/2000/svg">
162162 <text x="0" dy="9 0 -5 5 -5" font-family="weasyprint" font-size="5"
163163 fill="blue">
164164 ABCDEF
186186 @page { size: 30px 10px }
187187 svg { display: block }
188188 </style>
189 <svg width="30px" height="10px" xmlns="http://www.w3.org/2000/svg">
189 <svg width="30px" height="10px" xmlns="https://www.w3.org/2000/svg">
190190 <text dx="0 5" dy="9 -5 5 -5" font-family="weasyprint" font-size="5"
191191 fill="blue">
192192 ABCDE
208208 @page { size: 20px 4px }
209209 svg { display: block }
210210 </style>
211 <svg width="20px" height="4px" xmlns="http://www.w3.org/2000/svg">
211 <svg width="20px" height="4px" xmlns="https://www.w3.org/2000/svg">
212212 <text x="2" y="1.5" font-family="weasyprint" font-size="2"
213213 fill="blue">
214214 ABC
232232 @page { size: 20px 2px }
233233 svg { display: block }
234234 </style>
235 <svg width="20px" height="2px" xmlns="http://www.w3.org/2000/svg">
235 <svg width="20px" height="2px" xmlns="https://www.w3.org/2000/svg">
236236 <text x="10" y="1.5" font-family="weasyprint" font-size="2"
237237 fill="blue" text-anchor="middle">
238238 ABC
252252 @page { size: 20px 2px }
253253 svg { display: block }
254254 </style>
255 <svg width="20px" height="2px" xmlns="http://www.w3.org/2000/svg">
255 <svg width="20px" height="2px" xmlns="https://www.w3.org/2000/svg">
256256 <text x="18" y="1.5" font-family="weasyprint" font-size="2"
257257 fill="blue" text-anchor="end">
258258 ABC
272272 @page { size: 20px 2px }
273273 svg { display: block }
274274 </style>
275 <svg width="20px" height="2px" xmlns="http://www.w3.org/2000/svg">
275 <svg width="20px" height="2px" xmlns="https://www.w3.org/2000/svg">
276276 <text x="10" y="10" font-family="weasyprint" font-size="2" fill="blue">
277277 <tspan x="0" y="1.5">ABC DEF</tspan>
278278 </text>
293293 @page { size: 20px 4px }
294294 svg { display: block }
295295 </style>
296 <svg width="20px" height="4px" xmlns="http://www.w3.org/2000/svg">
296 <svg width="20px" height="4px" xmlns="https://www.w3.org/2000/svg">
297297 <text x="2" y="1.5" font-family="weasyprint" font-size="2" fill="red"
298298 letter-spacing="2">abc</text>
299299 <text x="2" y="1.5" font-family="weasyprint" font-size="2" fill="blue"
1919 @page { size: 9px }
2020 svg { display: block }
2121 </style>
22 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
22 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
2323 <rect x="0" y="4" width="5" height="5" transform="translate(2, -2)"
2424 stroke-width="2" stroke="red" fill="none" />
2525 </svg>
2727
2828
2929 @assert_no_logs
30 def test_transform_translate_one(assert_pixels):
31 assert_pixels('''
32 _________
33 _RRRRRRR_
34 _RRRRRRR_
35 _RR___RR_
36 _RR___RR_
37 _RR___RR_
38 _RRRRRRR_
39 _RRRRRRR_
40 _________
41 ''', '''
42 <style>
43 @page { size: 9px }
44 svg { display: block }
45 </style>
46 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
47 <rect x="0" y="2" width="5" height="5" transform="translate(2)"
48 stroke-width="2" stroke="red" fill="none" />
49 </svg>
50 ''')
51
52
53 @assert_no_logs
3054 def test_transform_translatex(assert_pixels):
3155 assert_pixels('''
3256 _________
4367 @page { size: 9px }
4468 svg { display: block }
4569 </style>
46 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
70 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
4771 <rect x="0" y="2" width="5" height="5" transform="translateX(2)"
4872 stroke-width="2" stroke="red" fill="none" />
4973 </svg>
6791 @page { size: 9px }
6892 svg { display: block }
6993 </style>
70 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
94 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
7195 <rect x="2" y="0" width="5" height="5" transform="translateY(2)"
7296 stroke-width="2" stroke="red" fill="none" />
7397 </svg>
91115 @page { size: 9px }
92116 svg { display: block }
93117 </style>
94 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
118 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
95119 <rect x="2" y="-7" width="4" height="5" transform="rotate(90)"
96120 stroke-width="2" stroke="red" fill="none" />
97121 </svg>
115139 @page { size: 9px }
116140 svg { display: block }
117141 </style>
118 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
142 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
119143 <rect x="7" y="2" width="4" height="5" transform="rotate(90 7 2)"
120144 stroke-width="2" stroke="red" fill="none" />
121145 </svg>
139163 @page { size: 9px }
140164 svg { display: block }
141165 </style>
142 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
166 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
143167 <rect x="2" y="2" width="2" height="2" transform="skew(20 20)"
144168 stroke-width="2" stroke="red" fill="none" />
145169 </svg>
147171
148172
149173 @assert_no_logs
150 def test_transform_skewx(assert_pixels):
174 def test_transform_skew_one(assert_pixels):
151175 assert_pixels('''
152176 _________
153177 _RRRRR___
163187 @page { size: 9px }
164188 svg { display: block }
165189 </style>
166 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
190 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
191 <rect x="2" y="2" width="2" height="2" transform="skew(20)"
192 stroke-width="2" stroke="red" fill="none" />
193 </svg>
194 ''')
195
196
197 @assert_no_logs
198 def test_transform_skewx(assert_pixels):
199 assert_pixels('''
200 _________
201 _RRRRR___
202 _RRRRRR__
203 __RRRRR__
204 __RRRRR__
205 _________
206 _________
207 _________
208 _________
209 ''', '''
210 <style>
211 @page { size: 9px }
212 svg { display: block }
213 </style>
214 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
167215 <rect x="2" y="2" width="2" height="2" transform="skewX(20)"
168216 stroke-width="2" stroke="red" fill="none" />
169217 </svg>
187235 @page { size: 9px }
188236 svg { display: block }
189237 </style>
190 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
238 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
191239 <rect x="2" y="2" width="2" height="2" transform="skewY(20)"
192240 stroke-width="2" stroke="red" fill="none" />
193241 </svg>
211259 @page { size: 9px }
212260 svg { display: block }
213261 </style>
214 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
262 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
215263 <rect x="2" y="2" width="2" height="2" transform="scale(1.5)"
216264 stroke-width="2" stroke="red" fill="none" />
217265 </svg>
235283 @page { size: 9px }
236284 svg { display: block }
237285 </style>
238 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
286 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
239287 <rect x="2" y="2" width="2" height="2" transform="scale(1.5 1.5)"
240288 stroke-width="2" stroke="red" fill="none" />
241289 </svg>
259307 @page { size: 9px }
260308 svg { display: block }
261309 </style>
262 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
310 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
263311 <rect x="2" y="2" width="2" height="2" transform="scaleX(1.5)"
264312 stroke-width="2" stroke="red" fill="none" />
265313 </svg>
283331 @page { size: 9px }
284332 svg { display: block }
285333 </style>
286 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
334 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
287335 <rect x="2" y="2" width="2" height="2" transform="scaleY(1.5)"
288336 stroke-width="2" stroke="red" fill="none" />
289337 </svg>
307355 @page { size: 9px }
308356 svg { display: block }
309357 </style>
310 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
358 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
311359 <rect x="0" y="0" width="2" height="2"
312360 transform="matrix(1.5 0 0 1.5 3 3)"
313361 stroke-width="2" stroke="red" fill="none" />
332380 @page { size: 9px }
333381 svg { display: block }
334382 </style>
335 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
383 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
336384 <rect x="0" y="0" width="4" height="5"
337385 transform="rotate(90) translateY(-7) translateX(2)"
338386 stroke-width="2" stroke="red" fill="none" />
339387 </svg>
340388 ''')
389
390
391 @assert_no_logs
392 def test_transform_unknown(assert_pixels):
393 assert_pixels('''
394 RRRRR____
395 R__RR____
396 R__RR____
397 R__RR____
398 RRRRR____
399 RRRRR____
400 _________
401 _________
402 _________
403 ''', '''
404 <style>
405 @page { size: 9px }
406 svg { display: block }
407 </style>
408 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
409 <rect x="0" y="0" width="4" height="5" transform="unknown(2)"
410 stroke-width="2" stroke="red" fill="none" />
411 </svg>
412 ''')
0 """Test SVG units."""
1
2 from ...testing_utils import assert_no_logs
3
4
5 @assert_no_logs
6 def test_units_px(assert_pixels):
7 assert_pixels('''
8 _________
9 _RRRRRRR_
10 _RRRRRRR_
11 _RR___RR_
12 _RR___RR_
13 _RR___RR_
14 _RRRRRRR_
15 _RRRRRRR_
16 _________
17 ''', '''
18 <style>
19 @page { size: 9px }
20 svg { display: block }
21 </style>
22 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
23 <rect x="2px" y="2px" width="5px" height="5px"
24 stroke-width="2px" stroke="red" fill="none" />
25 </svg>
26 ''')
27
28
29 @assert_no_logs
30 def test_units_em(assert_pixels):
31 assert_pixels('''
32 _________
33 _RRRRRRR_
34 _RRRRRRR_
35 _RR___RR_
36 _RR___RR_
37 _RR___RR_
38 _RRRRRRR_
39 _RRRRRRR_
40 _________
41 ''', '''
42 <style>
43 @page { size: 9px }
44 svg { display: block }
45 </style>
46 <svg width="9px" height="9px" font-size="1px"
47 xmlns="https://www.w3.org/2000/svg">
48 <rect x="2em" y="2em" width="5em" height="5em"
49 stroke-width="2em" stroke="red" fill="none" />
50 </svg>
51 ''')
52
53
54 @assert_no_logs
55 def test_units_ex(assert_pixels):
56 assert_pixels('''
57 _________
58 _RRRRRRR_
59 _RRRRRRR_
60 _RR___RR_
61 _RR___RR_
62 _RR___RR_
63 _RRRRRRR_
64 _RRRRRRR_
65 _________
66 ''', '''
67 <style>
68 @page { size: 9px }
69 svg { display: block }
70 </style>
71 <svg width="9px" height="9px" font-size="1px"
72 xmlns="https://www.w3.org/2000/svg">
73 <rect x="4ex" y="4ex" width="10ex" height="10ex"
74 stroke-width="4ex" stroke="red" fill="none" />
75 </svg>
76 ''')
77
78
79 @assert_no_logs
80 def test_units_unknown(assert_pixels):
81 assert_pixels('''
82 _RRRRRRR_
83 _RR___RR_
84 _RR___RR_
85 _RR___RR_
86 _RRRRRRR_
87 _RRRRRRR_
88 _________
89 _________
90 _________
91 ''', '''
92 <style>
93 @page { size: 9px }
94 svg { display: block }
95 </style>
96 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
97 <rect x="2px" y="2unk" width="5px" height="5px"
98 stroke-width="2px" stroke="red" fill="none" />
99 </svg>
100 ''')
1919 @page { size: 9px }
2020 svg { display: block }
2121 </style>
22 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
22 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
2323 <rect visibility="visible"
2424 x="2" y="2" width="5" height="5" fill="red" />
2525 </svg>
4343 @page { size: 9px }
4444 svg { display: block }
4545 </style>
46 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
46 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
4747 <rect visibility="hidden"
4848 x="2" y="2" width="5" height="5" fill="red" />
4949 </svg>
6767 @page { size: 9px }
6868 svg { display: block }
6969 </style>
70 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
70 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
7171 <g visibility="hidden">
7272 <rect x="2" y="2" width="5" height="5" fill="red" />
7373 </g>
9292 @page { size: 9px }
9393 svg { display: block }
9494 </style>
95 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
95 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
9696 <g visibility="hidden">
9797 <rect visibility="visible"
9898 x="2" y="2" width="5" height="5" fill="red" />
118118 @page { size: 9px }
119119 svg { display: block }
120120 </style>
121 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
121 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
122122 <rect display="inline"
123123 x="2" y="2" width="5" height="5" fill="red" />
124124 </svg>
142142 @page { size: 9px }
143143 svg { display: block }
144144 </style>
145 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
145 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
146146 <rect display="none"
147147 x="2" y="2" width="5" height="5" fill="red" />
148148 </svg>
166166 @page { size: 9px }
167167 svg { display: block }
168168 </style>
169 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
169 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
170170 <g display="none">
171171 <rect x="2" y="2" width="5" height="5" fill="red" />
172172 </g>
191191 @page { size: 9px }
192192 svg { display: block }
193193 </style>
194 <svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
194 <svg width="9px" height="9px" xmlns="https://www.w3.org/2000/svg">
195195 <g display="none">
196196 <rect display="inline"
197197 x="2" y="2" width="5" height="5" fill="red" />
1414 source = '''
1515 <style>
1616 @page { size: 140px 110px }
17 body { width: 100px; height: 70px;
18 margin: %s; %s: 10px %s blue }
17 body { width: 100px; height: 70px; margin: %s; %s: 10px %s blue }
1918 </style>
2019 <body>'''
2120
9493
9594
9695 @assert_no_logs
96 def test_borders_box_sizing(assert_pixels):
97 assert_pixels('''
98 ________
99 _RRRRRR_
100 _R____R_
101 _RRRRRR_
102 ________
103 ''', '''
104 <style>
105 @page {
106 size: 8px 5px;
107 }
108 div {
109 border: 1px solid red;
110 box-sizing: border-box;
111 height: 3px;
112 margin: 1px;
113 min-height: auto;
114 min-width: auto;
115 width: 6px;
116 }
117 </style>
118 <div></div>
119 ''')
120
121
122 @assert_no_logs
97123 def test_margin_boxes(assert_pixels):
98124 assert_pixels('''
99125 _______________
4848 <img src=blue.jpg>
4949 <img src=blue.jpg>
5050 </div>''')
51
52
53 @assert_no_logs
54 def test_column_rule_span(assert_pixels):
55 assert_pixels('''
56 ___________
57 ___________
58 ___________
59 ___a_______
60 ___a_r_a___
61 ___a_r_a___
62 ___________
63 ___________
64 ___________
65 ''', '''
66 <style>
67 img { display: inline-block; width: 1px; height: 1px }
68 div { columns: 2; column-rule: 1px solid red; column-gap: 3px }
69 article { column-span: all }
70 body { margin: 0; font-size: 0 }
71 @page { margin: 3px; size: 11px 9px }
72 </style>
73 <div>
74 <article>
75 <img src=blue.jpg>
76 </article>
77 <img src=blue.jpg>
78 <img src=blue.jpg>
79 <img src=blue.jpg>
80 <img src=blue.jpg>
81 </div>''')
00 """Test the currentColor value."""
1
2 import pytest
13
24 from ..testing_utils import assert_no_logs
35
4951 color: lime; border: 1px solid; border-color: inherit }
5052 </style>
5153 <table><td>''')
54
55
56 @assert_no_logs
57 def test_current_color_svg_1(assert_pixels):
58 assert_pixels('KK\nKK', '''
59 <style>
60 @page { size: 2px }
61 svg { display: block }
62 </style>
63 <svg xmlns="http://www.w3.org/2000/svg"
64 width="2" height="2" fill="currentColor">
65 <rect width="2" height="2"></rect>
66 </svg>''')
67
68
69 @pytest.mark.xfail
70 @assert_no_logs
71 def test_current_color_svg_2(assert_pixels):
72 assert_pixels('GG\nGG', '''
73 <style>
74 @page { size: 2px }
75 svg { display: block }
76 body { color: lime }
77 </style>
78 <svg xmlns="http://www.w3.org/2000/svg"
79 width="2" height="2">
80 <rect width="2" height="2" fill="currentColor"></rect>
81 </svg>''')
697697 </style>
698698 <div class="split">aaaaa aaaaa aa</div>
699699 bbbbbbb''')
700
701
702 @assert_no_logs
703 def test_float_split_12(assert_pixels):
704 assert_pixels('''
705 BBBBBBBBBBBBBBBB
706 BBBBBBBBBBBBBBBB
707 BBBBBBBBBBBBBBBB
708 BBBBBBBBBBBBBBBB
709 BBBBBBBBBBBBBBBB
710 BBBBGG______BBBB
711 BBBBGG______BBBB
712 BBBB________BBBB
713 BBBB________BBBB
714 ________________
715 ''', '''
716 <style>
717 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
718 @page {
719 size: 16px 5px;
720 }
721 body {
722 color: lime;
723 font-family: weasyprint;
724 font-size: 2px;
725 line-height: 1;
726 }
727 article {
728 background: blue;
729 height: 5px;
730 }
731 div {
732 background: red;
733 color: blue;
734 }
735 </style>
736 <article></article>
737 <section>
738 a
739 <div style="float: left"><p>aa<p>aa</div>
740 <div style="float: right"><p>bb<p>bb</div>''')
741
742
743 @pytest.mark.xfail
744 @assert_no_logs
745 def test_float_split_13(assert_pixels):
746 assert_pixels('''
747 BBBBBBBBBBBBBBBB
748 BBBBBBBBBBBBBBBB
749 BBBBBBBBBBBBBBBB
750 BBBBBBBBBBBBBBBB
751 BBBBBBBBBBBBBBBB
752 BBBBGG______BBBB
753 BBBBGG______BBBB
754 BBBB________BBBB
755 BBBB________BBBB
756 ________________
757 ''', '''
758 <style>
759 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
760 @page {
761 size: 16px 5px;
762 }
763 body {
764 color: lime;
765 font-family: weasyprint;
766 font-size: 2px;
767 line-height: 1;
768 }
769 article {
770 background: blue;
771 height: 5px;
772 }
773 div {
774 background: red;
775 color: blue;
776 }
777 </style>
778 <article></article>
779 <section>
780 <div style="float: left"><p>a<p>aa</div>
781 a
782 <div style="float: right"><p>bb<p>bb</div>''')
783
784
785 @assert_no_logs
786 def test_float_split_14(assert_pixels):
787 assert_pixels('''
788 BBBBBBBBBBBBBBBB
789 BBBBBBBBBBBBBBBB
790 BBBBBBBBBBBBBBBB
791 BBBBBBBBBBBBBBBB
792 BBBBBBBBBBBBBBBB
793 BBBBGG______BBBB
794 BBBBGG______BBBB
795 BBBB________BBBB
796 BBBB________BBBB
797 ________________
798 ''', '''
799 <style>
800 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
801 @page {
802 size: 16px 5px;
803 }
804 body {
805 color: lime;
806 font-family: weasyprint;
807 font-size: 2px;
808 line-height: 1;
809 }
810 article {
811 background: blue;
812 height: 5px;
813 }
814 div {
815 background: red;
816 color: blue;
817 }
818 </style>
819 <article></article>
820 a
821 <div style="float: left"><p>aa<p>aa</div>
822 <div style="float: right"><p>bb<p>bb</div>''')
0 """Test how footnotes are drawn."""
1
2 from ..testing_utils import assert_no_logs
3
4
5 @assert_no_logs
6 def test_inline_footnote(assert_pixels):
7 assert_pixels('''
8 RRRRRRRR_
9 RRRRRRRR_
10 _________
11 _________
12 _________
13 RRRRRRRR_
14 RRRRRRRR_
15 ''', '''
16 <style>
17 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
18 @page {
19 size: 9px 7px;
20 }
21 div {
22 color: red;
23 font-family: weasyprint;
24 font-size: 2px;
25 line-height: 1;
26 }
27 span {
28 float: footnote;
29 }
30 </style>
31 <div>abc<span>de</span></div>''')
32
33
34 @assert_no_logs
35 def test_block_footnote(assert_pixels):
36 assert_pixels('''
37 RRRRRRRR_
38 RRRRRRRR_
39 _________
40 _________
41 _________
42 RRRRRRRR_
43 RRRRRRRR_
44 ''', '''
45 <style>
46 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
47 @page {
48 size: 9px 7px;
49 }
50 div {
51 color: red;
52 font-family: weasyprint;
53 font-size: 2px;
54 line-height: 1;
55 }
56 div.footnote {
57 float: footnote;
58 }
59 </style>
60 <div>abc<div class="footnote">de</div></div>''')
61
62
63 @assert_no_logs
64 def test_long_footnote(assert_pixels):
65 assert_pixels('''
66 RRRRRRRR_
67 RRRRRRRR_
68 _________
69 RRRRRRRR_
70 RRRRRRRR_
71 RR_______
72 RR_______
73 ''', '''
74 <style>
75 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
76 @page {
77 size: 9px 7px;
78 }
79 div {
80 color: red;
81 font-family: weasyprint;
82 font-size: 2px;
83 line-height: 1;
84 }
85 span {
86 float: footnote;
87 }
88 </style>
89 <div>abc<span>de f</span></div>''')
90
91
92 @assert_no_logs
93 def test_footnote_margin(assert_pixels):
94 assert_pixels('''
95 RRRRRRRR_
96 RRRRRRRR_
97 _________
98 _________
99 _RRRRRR__
100 _RRRRRR__
101 _________
102 ''', '''
103 <style>
104 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
105 @page {
106 size: 9px 7px;
107
108 @footnote {
109 margin: 1px;
110 }
111 }
112 div {
113 color: red;
114 font-family: weasyprint;
115 font-size: 2px;
116 line-height: 1;
117 }
118 span {
119 float: footnote;
120 }
121 </style>
122 <div>abc<span>d</span></div>''')
123
124
125 @assert_no_logs
126 def test_footnote_with_absolute(assert_pixels):
127 assert_pixels('''
128 _RRRR____
129 _RRRR____
130 _________
131 _RRRR____
132 _RRRR____
133 BB_______
134 BB_______
135 ''', '''
136 <style>
137 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
138 @page {
139 size: 9px 7px;
140 margin: 0 1px 2px;
141 }
142 div {
143 color: red;
144 font-family: weasyprint;
145 font-size: 2px;
146 line-height: 1;
147 }
148 span {
149 float: footnote;
150 }
151 mark {
152 display: block;
153 position: absolute;
154 left: -1px;
155 color: blue;
156 }
157 </style>
158 <div>a<span><mark>d</mark></span></div>''')
159
160
161 @assert_no_logs
162 def test_footnote_max_height_1(assert_pixels):
163 assert_pixels('''
164 RRRRRRRR_
165 RRRRRRRR_
166 RRRR_____
167 RRRR_____
168 _BBBBBB__
169 _BBBBBB__
170 _________
171 _________
172 _________
173 _________
174 _BBBBBB__
175 _BBBBBB__
176 ''', '''
177 <style>
178 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
179 @page {
180 size: 9px 6px;
181
182 @footnote {
183 margin-left: 1px;
184 max-height: 3px;
185 }
186 }
187 div {
188 color: red;
189 font-family: weasyprint;
190 font-size: 2px;
191 line-height: 1;
192 }
193 div.footnote {
194 float: footnote;
195 color: blue;
196 }
197 </style>
198 <div>ab<div class="footnote">c</div><div class="footnote">d</div></div>
199 <div>ef</div>''')
200
201
202 @assert_no_logs
203 def test_footnote_max_height_2(assert_pixels):
204 assert_pixels('''
205 RRRRRRRR_
206 RRRRRRRR_
207 _________
208 _________
209 _BBBBBB__
210 _BBBBBB__
211 _________
212 _________
213 _________
214 _________
215 _BBBBBB__
216 _BBBBBB__
217 ''', '''
218 <style>
219 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
220 @page {
221 size: 9px 6px;
222
223 @footnote {
224 margin-left: 1px;
225 max-height: 3px;
226 }
227 }
228 div {
229 color: red;
230 font-family: weasyprint;
231 font-size: 2px;
232 line-height: 1;
233 }
234 div.footnote {
235 float: footnote;
236 color: blue;
237 }
238 </style>
239 <div>ab<div class="footnote">c</div><div class="footnote">d</div></div>''')
240
241
242 @assert_no_logs
243 def test_footnote_max_height_3(assert_pixels):
244 # This case is crazy and the rendering is not really defined, but this test
245 # is useful to check that there’s no endless loop.
246 assert_pixels('''
247 RRRRRRRR_
248 RRRRRRRR_
249 _________
250 _________
251 _________
252 _________
253 _________
254 _________
255 _________
256 _________
257 _________
258 _BBBBBB__
259 _________
260 _________
261 _________
262 _________
263 _________
264 _BBBBBB__
265 ''', '''
266 <style>
267 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
268 @page {
269 size: 9px 6px;
270
271 @footnote {
272 margin-left: 1px;
273 max-height: 1px;
274 }
275 }
276 div {
277 color: red;
278 font-family: weasyprint;
279 font-size: 2px;
280 line-height: 1;
281 }
282 div.footnote {
283 float: footnote;
284 color: blue;
285 }
286 </style>
287 <div>ab<div class="footnote">c</div><div class="footnote">d</div></div>''')
288
289
290 @assert_no_logs
291 def test_footnote_max_height_4(assert_pixels):
292 assert_pixels('''
293 RRRRRRRR_
294 RRRRRRRR_
295 RRRR_____
296 RRRR_____
297 _BBBBBB__
298 _BBBBBB__
299 RRRR_____
300 RRRR_____
301 _________
302 _________
303 _BBBBBB__
304 _BBBBBB__
305 ''', '''
306 <style>
307 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
308 @page {
309 size: 9px 6px;
310
311 @footnote {
312 margin-left: 1px;
313 max-height: 3px;
314 }
315 }
316 div {
317 color: red;
318 font-family: weasyprint;
319 font-size: 2px;
320 line-height: 1;
321 }
322 div.footnote {
323 float: footnote;
324 color: blue;
325 }
326 </style>
327 <div>ab<div class="footnote">c</div><div class="footnote">d</div></div>
328 <div>ef</div>
329 <div>gh</div>''')
330
331
332 @assert_no_logs
333 def test_footnote_max_height_5(assert_pixels):
334 assert_pixels('''
335 RRRRRRRR__RR
336 RRRRRRRR__RR
337 _BBBBBB_____
338 _BBBBBB_____
339 _BBBBBB_____
340 _BBBBBB_____
341 RRRR________
342 RRRR________
343 ____________
344 ____________
345 _BBBBBB_____
346 _BBBBBB_____
347 ''', '''
348 <style>
349 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
350 @page {
351 size: 12px 6px;
352
353 @footnote {
354 margin-left: 1px;
355 max-height: 4px;
356 }
357 }
358 div {
359 color: red;
360 font-family: weasyprint;
361 font-size: 2px;
362 line-height: 1;
363 }
364 div.footnote {
365 float: footnote;
366 color: blue;
367 }
368 </style>
369 <div>ab<div class="footnote">c</div><div class="footnote">d</div>
370 <div class="footnote">e</div></div>
371 <div>fg</div>''')
0 """Test how footnotes in columns are drawn."""
1
2 import pytest
3
4 from ..testing_utils import assert_no_logs
5
6
7 @assert_no_logs
8 def test_footnote_column_margin_top(assert_pixels):
9 assert_pixels('''
10 RRRR_RRRR
11 RRRR_RRRR
12 _________
13 _________
14 _________
15 RRRRRRRR_
16 RRRRRRRR_
17 RRRR_RRRR
18 RRRR_RRRR
19 RRRR_RRRR
20 RRRR_RRRR
21 RRRR_____
22 RRRR_____
23 _________
24 ''', '''
25 <style>
26 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
27 @page {
28 size: 9px 7px;
29 @footnote {
30 margin-top: 2px;
31 }
32 }
33 div {
34 color: red;
35 columns: 2;
36 column-gap: 1px;
37 font-family: weasyprint;
38 font-size: 2px;
39 line-height: 1;
40 }
41 span {
42 float: footnote;
43 }
44 </style>
45 <div>a<span>de</span> ab ab ab ab ab ab</div>''')
46
47
48 @assert_no_logs
49 def test_footnote_column_fill_auto(assert_pixels):
50 assert_pixels('''
51 RRRR_____
52 RRRR_____
53 RRRR_____
54 RRRR_____
55 RRRR_____
56 RRRR_____
57 _________
58 RRRRRRRR_
59 RRRRRRRR_
60 RRRRRRRR_
61 RRRRRRRR_
62 RRRRRRRR_
63 RRRRRRRR_
64 ''', '''
65 <style>
66 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
67 @page {
68 size: 9px 13px;
69 }
70 div {
71 color: red;
72 columns: 2;
73 column-fill: auto;
74 column-gap: 1px;
75 font-family: weasyprint;
76 font-size: 2px;
77 line-height: 1;
78 }
79 span {
80 float: footnote;
81 }
82 </style>
83 <div>a<span>de</span> a<span>de</span> a<span>de</span></div>''')
84
85
86 @assert_no_logs
87 def test_footnote_column_fill_auto_break_inside_avoid(assert_pixels):
88 assert_pixels('''
89 RRRR_RRRR
90 RRRR_RRRR
91 RRRR_RRRR
92 RRRR_RRRR
93 RRRR_RRRR
94 RRRR_RRRR
95 _________
96 RRRRRRRR_
97 RRRRRRRR_
98 RRRRRRRR_
99 RRRRRRRR_
100 RRRRRRRR_
101 RRRRRRRR_
102 ''', '''
103 <style>
104 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
105 @page {
106 size: 9px 13px;
107 }
108 div {
109 color: red;
110 columns: 2;
111 column-fill: auto;
112 column-gap: 1px;
113 font-family: weasyprint;
114 font-size: 2px;
115 line-height: 1;
116 }
117 article {
118 break-inside: avoid;
119 }
120 span {
121 float: footnote;
122 }
123 </style>
124 <div>
125 <article>a<span>de</span> a<span>de</span></article>
126 <article>ab</article>
127 <article>a<span>de</span> ab</article>
128 <article>ab</article>
129 </div>''')
130
131
132 @assert_no_logs
133 def test_footnote_column_p_after(assert_pixels):
134 assert_pixels('''
135 RRRR_RRRR
136 RRRR_RRRR
137 RRRR_RRRR
138 RRRR_RRRR
139 KK__KK___
140 KK__KK___
141 _________
142 RRRRRRRR_
143 RRRRRRRR_
144 RRRRRRRR_
145 RRRRRRRR_
146 KK__KK___
147 KK__KK___
148 _________
149 _________
150 _________
151 _________
152 _________
153 _________
154 _________
155 _________
156 _________
157 ''', '''
158 <style>
159 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
160 @page {
161 size: 9px 11px;
162 }
163 body {
164 font-family: weasyprint;
165 font-size: 2px;
166 line-height: 1;
167 }
168 div {
169 color: red;
170 columns: 2;
171 column-gap: 1px;
172 }
173 span {
174 float: footnote;
175 }
176 </style>
177 <div>a<span>de</span> a<span>de</span> ab ab</div>
178 <p>a a a a</p>''')
179
180
181 @assert_no_logs
182 def test_footnote_column_p_before(assert_pixels):
183 assert_pixels('''
184 KKKK_____
185 KKKK_____
186 RRRR_RRRR
187 RRRR_RRRR
188 RRRR_RR__
189 RRRR_RR__
190 _________
191 RRRRRRRR_
192 RRRRRRRR_
193 RRRRRRRR_
194 RRRRRRRR_
195 RRRRRRRR_
196 RRRRRRRR_
197 RRRR_RR__
198 RRRR_RR__
199 _________
200 _________
201 _________
202 _________
203 _________
204 _________
205 _________
206 _________
207 _________
208 _________
209 _________
210 ''', '''
211 <style>
212 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
213 @page {
214 size: 9px 13px;
215 }
216 body {
217 font-family: weasyprint;
218 font-size: 2px;
219 line-height: 1;
220 }
221 div {
222 color: red;
223 columns: 2;
224 column-gap: 1px;
225 }
226 span {
227 float: footnote;
228 }
229 </style>
230 <p>ab</p>
231 <div>
232 a<span>de</span> a<span>de</span>
233 a<span>de</span> a ab a </div>''')
234
235
236 @assert_no_logs
237 def test_footnote_column_3(assert_pixels):
238 assert_pixels('''
239 RRRR_RRRR_RRRR
240 RRRR_RRRR_RRRR
241 ______________
242 RRRRRRRR______
243 RRRRRRRR______
244 RRRR_RRRR_____
245 RRRR_RRRR_____
246 ______________
247 ______________
248 ______________
249 ''', '''
250 <style>
251 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
252 @page {
253 size: 14px 5px;
254 }
255 body {
256 font-family: weasyprint;
257 font-size: 2px;
258 line-height: 1;
259 }
260 div {
261 color: red;
262 columns: 3;
263 column-gap: 1px;
264 }
265 span {
266 float: footnote;
267 }
268 </style>
269 <div>ab ab a<span>de</span> ab ab </div>''')
270
271
272 @assert_no_logs
273 def test_footnote_column_3_p_before(assert_pixels):
274 assert_pixels('''
275 KKKK__________
276 KKKK__________
277 RRRR_RRRR_RRRR
278 RRRR_RRRR_RRRR
279 RRRR_RRRR_RRRR
280 RRRR_RRRR_RRRR
281 ______________
282 RRRRRRRR______
283 RRRRRRRR______
284 RRRR_RRRR_____
285 RRRR_RRRR_____
286 ______________
287 ______________
288 ______________
289 ______________
290 ______________
291 RRRRRRRR______
292 RRRRRRRR______
293 ''', '''
294 <style>
295 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
296 @page {
297 size: 14px 9px;
298 }
299 body {
300 font-family: weasyprint;
301 font-size: 2px;
302 line-height: 1;
303 }
304 div {
305 color: red;
306 columns: 3;
307 column-gap: 1px;
308 }
309 span {
310 float: footnote;
311 }
312 </style>
313 <p>ab</p>
314 <div>ab ab a<span>de</span> ab ab ab a<span>de</span> ab </div>''')
315
316
317 @assert_no_logs
318 def test_footnote_column_clone_decoration(assert_pixels):
319 assert_pixels('''
320 _________
321 RRRR_RRRR
322 RRRR_RRRR
323 _________
324 _________
325 RRRRRRRR_
326 RRRRRRRR_
327 _________
328 RRRR_RRRR
329 RRRR_RRRR
330 _________
331 _________
332 _________
333 _________
334 ''', '''
335 <style>
336 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
337 @page {
338 size: 9px 7px;
339 }
340 div {
341 box-decoration-break: clone;
342 color: red;
343 columns: 2;
344 column-gap: 1px;
345 font-family: weasyprint;
346 font-size: 2px;
347 line-height: 1;
348 padding: 1px 0;
349 }
350 span {
351 float: footnote;
352 }
353 </style>
354 <div>a<span>de</span> ab ab ab</div>''')
355
356
357 @assert_no_logs
358 def test_footnote_column_max_height(assert_pixels):
359 assert_pixels('''
360 RRRR_RRRR
361 RRRR_RRRR
362 RRRR_RRRR
363 RRRR_RRRR
364 _________
365 RRRRRRRR_
366 RRRRRRRR_
367 RRRRRRRR_
368 RRRRRRRR_
369 RRRR_RRRR
370 RRRR_RRRR
371 _________
372 _________
373 _________
374 _________
375 _________
376 RRRRRRRR_
377 RRRRRRRR_
378 ''', '''
379 <style>
380 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
381 @page {
382 size: 9px 9px;
383 @footnote {
384 max-height: 2em;
385 }
386 }
387 div {
388 color: red;
389 columns: 2;
390 column-gap: 1px;
391 font-family: weasyprint;
392 font-size: 2px;
393 line-height: 1;
394 }
395 span {
396 float: footnote;
397 }
398 </style>
399 <div>
400 a<span>de</span> a<span>de</span>
401 a<span>de</span> ab
402 ab ab
403 </div>''')
404
405
406 @pytest.mark.xfail
407 @assert_no_logs
408 def test_footnote_column_reported_split(assert_pixels):
409 # When calling block_container_layout() in remove_placeholders(), we should
410 # use the whole skip stack and not just [skip:]
411 assert_pixels('''
412 RRRR_RRRR
413 RRRR_RRRR
414 RRRR_RRRR
415 RRRR_RRRR
416 _________
417 RRRRRRRR_
418 RRRRRRRR_
419 RRRRRRRR_
420 RRRRRRRR_
421 RRRR_____
422 RRRR_____
423 _________
424 _________
425 _________
426 _________
427 _________
428 RRRRRRRR_
429 RRRRRRRR_
430 ''', '''
431 <style>
432 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
433 @page {
434 size: 9px 9px;
435 }
436 div {
437 color: red;
438 columns: 2;
439 column-gap: 1px;
440 font-family: weasyprint;
441 font-size: 2px;
442 line-height: 1;
443 }
444 span {
445 float: footnote;
446 }
447 </style>
448 <div>
449 <article>a<span>de</span> a<span>de</span></article>
450 <article>a<span>de</span> ab ab</article>
451 </div>''')
+0
-366
tests/draw/test_footnotes.py less more
0 """Test how footnotes are drawn."""
1
2 from ..testing_utils import assert_no_logs
3
4
5 @assert_no_logs
6 def test_inline_footnote(assert_pixels):
7 assert_pixels('''
8 RRRRRRRR_
9 RRRRRRRR_
10 _________
11 _________
12 _________
13 RRRRRRRR_
14 RRRRRRRR_
15 ''', '''
16 <style>
17 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
18 @page {
19 size: 9px 7px;
20 }
21 div {
22 color: red;
23 font-family: weasyprint;
24 font-size: 2px;
25 line-height: 1;
26 }
27 span {
28 float: footnote;
29 }
30 </style>
31 <div>abc<span>de</span></div>''')
32
33
34 @assert_no_logs
35 def test_block_footnote(assert_pixels):
36 assert_pixels('''
37 RRRRRRRR_
38 RRRRRRRR_
39 _________
40 _________
41 _________
42 RRRRRRRR_
43 RRRRRRRR_
44 ''', '''
45 <style>
46 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
47 @page {
48 size: 9px 7px;
49 }
50 div {
51 color: red;
52 font-family: weasyprint;
53 font-size: 2px;
54 line-height: 1;
55 }
56 div.footnote {
57 float: footnote;
58 }
59 </style>
60 <div>abc<div class="footnote">de</div></div>''')
61
62
63 @assert_no_logs
64 def test_long_footnote(assert_pixels):
65 assert_pixels('''
66 RRRRRRRR_
67 RRRRRRRR_
68 _________
69 RRRRRRRR_
70 RRRRRRRR_
71 RR_______
72 RR_______
73 ''', '''
74 <style>
75 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
76 @page {
77 size: 9px 7px;
78 }
79 div {
80 color: red;
81 font-family: weasyprint;
82 font-size: 2px;
83 line-height: 1;
84 }
85 span {
86 float: footnote;
87 }
88 </style>
89 <div>abc<span>de f</span></div>''')
90
91
92 @assert_no_logs
93 def test_footnote_margin(assert_pixels):
94 assert_pixels('''
95 RRRRRRRR_
96 RRRRRRRR_
97 _________
98 _________
99 _RRRRRR__
100 _RRRRRR__
101 _________
102 ''', '''
103 <style>
104 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
105 @page {
106 size: 9px 7px;
107
108 @footnote {
109 margin: 1px;
110 }
111 }
112 div {
113 color: red;
114 font-family: weasyprint;
115 font-size: 2px;
116 line-height: 1;
117 }
118 span {
119 float: footnote;
120 }
121 </style>
122 <div>abc<span>d</span></div>''')
123
124
125 @assert_no_logs
126 def test_footnote_with_absolute(assert_pixels):
127 assert_pixels('''
128 _RRRR____
129 _RRRR____
130 _________
131 _RRRR____
132 _RRRR____
133 BB_______
134 BB_______
135 ''', '''
136 <style>
137 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
138 @page {
139 size: 9px 7px;
140 margin: 0 1px 2px;
141 }
142 div {
143 color: red;
144 font-family: weasyprint;
145 font-size: 2px;
146 line-height: 1;
147 }
148 span {
149 float: footnote;
150 }
151 mark {
152 display: block;
153 position: absolute;
154 left: -1px;
155 color: blue;
156 }
157 </style>
158 <div>a<span><mark>d</mark></span></div>''')
159
160
161 @assert_no_logs
162 def test_footnote_max_height_1(assert_pixels):
163 assert_pixels('''
164 RRRRRRRR_
165 RRRRRRRR_
166 RRRR_____
167 RRRR_____
168 _BBBBBB__
169 _BBBBBB__
170 _________
171 _________
172 _________
173 _________
174 _BBBBBB__
175 _BBBBBB__
176 ''', '''
177 <style>
178 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
179 @page {
180 size: 9px 6px;
181
182 @footnote {
183 margin-left: 1px;
184 max-height: 3px;
185 }
186 }
187 div {
188 color: red;
189 font-family: weasyprint;
190 font-size: 2px;
191 line-height: 1;
192 }
193 div.footnote {
194 float: footnote;
195 color: blue;
196 }
197 </style>
198 <div>ab<div class="footnote">c</div><div class="footnote">d</div></div>
199 <div>ef</div>''')
200
201
202 @assert_no_logs
203 def test_footnote_max_height_2(assert_pixels):
204 assert_pixels('''
205 RRRRRRRR_
206 RRRRRRRR_
207 _________
208 _________
209 _BBBBBB__
210 _BBBBBB__
211 _________
212 _________
213 _________
214 _________
215 _BBBBBB__
216 _BBBBBB__
217 ''', '''
218 <style>
219 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
220 @page {
221 size: 9px 6px;
222
223 @footnote {
224 margin-left: 1px;
225 max-height: 3px;
226 }
227 }
228 div {
229 color: red;
230 font-family: weasyprint;
231 font-size: 2px;
232 line-height: 1;
233 }
234 div.footnote {
235 float: footnote;
236 color: blue;
237 }
238 </style>
239 <div>ab<div class="footnote">c</div><div class="footnote">d</div></div>''')
240
241
242 @assert_no_logs
243 def test_footnote_max_height_3(assert_pixels):
244 # This case is crazy and the rendering is not really defined, but this test
245 # is useful to check that there’s no endless loop.
246 assert_pixels('''
247 RRRRRRRR_
248 RRRRRRRR_
249 _________
250 _________
251 _________
252 _________
253 _________
254 _________
255 _________
256 _________
257 _________
258 _BBBBBB__
259 ''', '''
260 <style>
261 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
262 @page {
263 size: 9px 6px;
264
265 @footnote {
266 margin-left: 1px;
267 max-height: 1px;
268 }
269 }
270 div {
271 color: red;
272 font-family: weasyprint;
273 font-size: 2px;
274 line-height: 1;
275 }
276 div.footnote {
277 float: footnote;
278 color: blue;
279 }
280 </style>
281 <div>ab<div class="footnote">c</div><div class="footnote">d</div></div>''')
282
283
284 @assert_no_logs
285 def test_footnote_max_height_4(assert_pixels):
286 assert_pixels('''
287 RRRRRRRR_
288 RRRRRRRR_
289 RRRR_____
290 RRRR_____
291 _BBBBBB__
292 _BBBBBB__
293 RRRR_____
294 RRRR_____
295 _________
296 _________
297 _BBBBBB__
298 _BBBBBB__
299 ''', '''
300 <style>
301 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
302 @page {
303 size: 9px 6px;
304
305 @footnote {
306 margin-left: 1px;
307 max-height: 3px;
308 }
309 }
310 div {
311 color: red;
312 font-family: weasyprint;
313 font-size: 2px;
314 line-height: 1;
315 }
316 div.footnote {
317 float: footnote;
318 color: blue;
319 }
320 </style>
321 <div>ab<div class="footnote">c</div><div class="footnote">d</div></div>
322 <div>ef</div>
323 <div>gh</div>''')
324
325
326 @assert_no_logs
327 def test_footnote_max_height_5(assert_pixels):
328 assert_pixels('''
329 RRRRRRRR__RR
330 RRRRRRRR__RR
331 _BBBBBB_____
332 _BBBBBB_____
333 _BBBBBB_____
334 _BBBBBB_____
335 RRRR________
336 RRRR________
337 ____________
338 ____________
339 _BBBBBB_____
340 _BBBBBB_____
341 ''', '''
342 <style>
343 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
344 @page {
345 size: 12px 6px;
346
347 @footnote {
348 margin-left: 1px;
349 max-height: 4px;
350 }
351 }
352 div {
353 color: red;
354 font-family: weasyprint;
355 font-size: 2px;
356 line-height: 1;
357 }
358 div.footnote {
359 float: footnote;
360 color: blue;
361 }
362 </style>
363 <div>ab<div class="footnote">c</div><div class="footnote">d</div>
364 <div class="footnote">e</div></div>
365 <div>fg</div>''')
00 """Test how gradients are drawn."""
1
2 import pytest
31
42 from ..testing_utils import assert_no_logs
53
6462 )''')
6563
6664
67 @pytest.mark.xfail
6865 @assert_no_logs
6966 def test_linear_gradients_5(assert_pixels):
70 # See https://bugs.ghostscript.com/show_bug.cgi?id=705225
7167 assert_pixels('''
7268 rBrrrBrrrB
7369 rBrrrBrrrB
10197 hhhhhhhhh
10298 hhhhhhhhh
10399 hhhhhhhhh
104 ''', '''<style>@page { size: 9px 5px; background: repeating-linear-gradient(
100 ''', '''<style>@page { size: 9px 5px; background:
101 repeating-linear-gradient(
105102 to right, black 3px, black 3px, #800080 3px, #800080 3px
106103 )''')
107104
114111 BBBBBBBBB
115112 BBBBBBBBB
116113 BBBBBBBBB
117 ''', '''<style>@page { size: 9px 5px; background: repeating-linear-gradient(
118 to right, blue 3px
119 )''')
114 ''', '''<style>@page { size: 9px 5px; background:
115 repeating-linear-gradient(to right, blue 3px)''')
120116
121117
122118 @assert_no_logs
127123 BBBBBBBBB
128124 BBBBBBBBB
129125 BBBBBBBBB
130 ''', '''<style>@page { size: 9px 5px; background: repeating-linear-gradient(
131 45deg, blue 3px
132 )''')
126 ''', '''<style>@page { size: 9px 5px; background:
127 repeating-linear-gradient(45deg, blue 3px)''')
133128
134129
135130 @assert_no_logs
158153 )''')
159154
160155
161 @pytest.mark.xfail
162156 @assert_no_logs
163157 def test_linear_gradients_12(assert_pixels):
164 # See https://bugs.ghostscript.com/show_bug.cgi?id=705225
165 assert_pixels('''
166 BBBBBBBBB
167 BBBBBBBBB
168 BBBBBBBBB
169 BBBBBBBBB
170 BBBBBBBBB
171 ''', '''<style>@page { size: 9px 5px; background: repeating-linear-gradient(
172 to right, red 3px, blue 3px, blue 4px, red 4px
158 assert_pixels('''
159 BBBBBBBBB
160 BBBBBBBBB
161 BBBBBBBBB
162 BBBBBBBBB
163 BBBBBBBBB
164 ''', '''<style>@page { size: 9px 5px; background:
165 repeating-linear-gradient(to right, red 3px, blue 3px, blue 4px, red 4px
173166 )''')
174167
175168
546546 img { margin: 1px; border: 1px solid lime; position: absolute }
547547 </style>
548548 <div><img src="pattern.png"></div>''')
549
550
551 @assert_no_logs
552 def test_image_exif(assert_same_renderings):
553 assert_same_renderings(
554 '''
555 <style>@page { size: 10px }</style>
556 <img style="display: block" src="not-optimized.jpg">
557 ''',
558 '''
559 <style>@page { size: 10px }</style>
560 <img style="display: block" src="not-optimized-exif.jpg">
561 ''',
562 tolerance=25,
563 )
564
565
566 @assert_no_logs
567 def test_image_exif_image_orientation(assert_same_renderings):
568 assert_same_renderings(
569 '''
570 <style>@page { size: 10px }</style>
571 <img style="display: block; image-orientation: 180deg"
572 src="not-optimized-exif.jpg">
573 ''',
574 '''
575 <style>@page { size: 10px }</style>
576 <img style="display: block" src="not-optimized-exif.jpg">
577 ''',
578 tolerance=25,
579 )
751751 <div>a<span>b</span></div>''')
752752
753753
754 def test_text_decoration_var(assert_pixels):
755 # Test regression: https://github.com/Kozea/WeasyPrint/issues/1697
756 assert_pixels('''
757 _____________
758 _zzzzzzzzzzz_
759 _zRRRRRRRRRz_
760 _zBBBBBBBBBz_
761 _zRRRRRRRRRz_
762 _zzzzzzzzzzz_
763 _____________
764 ''', '''
765 <style>
766 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
767 @page {
768 size: 13px 7px;
769 margin: 2px;
770 }
771 body {
772 --blue: blue;
773 color: red;
774 font-family: weasyprint;
775 font-size: 3px;
776 text-decoration-color: var(--blue);
777 text-decoration-line: line-through;
778 }
779 </style>
780 <div>abc</div>''')
781
782
754783 def test_zero_width_character(assert_pixels):
755784 # Test regression: https://github.com/Kozea/WeasyPrint/issues/1508
756785 assert_pixels('''
0 """Test how white spaces collapse."""
1
2 from ..testing_utils import assert_no_logs
3
4
5 @assert_no_logs
6 def test_whitespace_inline(assert_pixels):
7 assert_pixels('''
8 RRRR__RRRR____
9 RRRR__RRRR____
10 ______________
11 ______________
12 ''', '''
13 <style>
14 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
15 @page {size: 14px 4px}
16 body {
17 color: red;
18 font-family: weasyprint;
19 font-size: 2px;
20 line-height: 1;
21 }
22 </style>
23 <span>aa </span><span> aa</span>
24 ''')
25
26
27 @assert_no_logs
28 def test_whitespace_nested_inline(assert_pixels):
29 assert_pixels('''
30 RRRR__RRRR____
31 RRRR__RRRR____
32 ______________
33 ______________
34 ''', '''
35 <style>
36 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
37 @page {size: 14px 4px}
38 body {
39 color: red;
40 font-family: weasyprint;
41 font-size: 2px;
42 line-height: 1;
43 }
44 </style>
45 <span><span>aa </span></span><span><span> aa</span></span>
46 ''')
47
48
49 @assert_no_logs
50 def test_whitespace_inline_space_between(assert_pixels):
51 assert_pixels('''
52 RRRR__RRRR____
53 RRRR__RRRR____
54 ______________
55 ______________
56 ''', '''
57 <style>
58 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
59 @page {size: 14px 4px}
60 body {
61 color: red;
62 font-family: weasyprint;
63 font-size: 2px;
64 line-height: 1;
65 }
66 </style>
67 <span>aa </span> <span> aa</span>
68 ''')
69
70
71 @assert_no_logs
72 def test_whitespace_float_between(assert_pixels):
73 assert_pixels('''
74 RRRR__RRRR__BB
75 RRRR__RRRR__BB
76 ______________
77 ______________
78 ''', '''
79 <style>
80 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
81 @page {size: 14px 4px}
82 body {
83 color: red;
84 font-family: weasyprint;
85 font-size: 2px;
86 line-height: 1;
87 }
88 div {float: right; color: blue}
89 </style>
90 <span>aa </span><div>a</div><span> aa</span>
91 ''')
92
93
94 @assert_no_logs
95 def test_whitespace_in_float(assert_pixels):
96 assert_pixels('''
97 RRRRRRRR____BB
98 RRRRRRRR____BB
99 ______________
100 ______________
101 ''', '''
102 <style>
103 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
104 @page {size: 14px 4px}
105 body {
106 color: red;
107 font-family: weasyprint;
108 font-size: 2px;
109 line-height: 1;
110 }
111 div {
112 color: blue;
113 float: right;
114 }
115 </style>
116 <span>aa</span><div> a </div><span>aa</span>
117 ''')
118
119
120 @assert_no_logs
121 def test_whitespace_absolute_between(assert_pixels):
122 assert_pixels('''
123 RRRR__RRRR__BB
124 RRRR__RRRR__BB
125 ______________
126 ______________
127 ''', '''
128 <style>
129 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
130 @page {size: 14px 4px}
131 body {
132 color: red;
133 font-family: weasyprint;
134 font-size: 2px;
135 line-height: 1;
136 }
137 div {
138 color: blue;
139 position: absolute;
140 right: 0;
141 top: 0;
142 }
143 </style>
144 <span>aa </span><div>a</div><span> aa</span>
145 ''')
146
147
148 @assert_no_logs
149 def test_whitespace_in_absolute(assert_pixels):
150 assert_pixels('''
151 RRRRRRRR____BB
152 RRRRRRRR____BB
153 ______________
154 ______________
155 ''', '''
156 <style>
157 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
158 @page {size: 14px 4px}
159 body {
160 color: red;
161 font-family: weasyprint;
162 font-size: 2px;
163 line-height: 1;
164 }
165 div {
166 color: blue;
167 position: absolute;
168 right: 0;
169 top: 0;
170 }
171 </style>
172 <span>aa</span><div> a </div><span>aa</span>
173 ''')
174
175
176 @assert_no_logs
177 def test_whitespace_running_between(assert_pixels):
178 assert_pixels('''
179 RRRR__RRRR____
180 RRRR__RRRR____
181 ______BB______
182 ______BB______
183 ''', '''
184 <style>
185 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
186 @page {
187 size: 14px 4px;
188 margin: 0 0 2px;
189 @bottom-center {
190 content: element(test);
191 }
192 }
193 body {
194 color: red;
195 font-family: weasyprint;
196 font-size: 2px;
197 line-height: 1;
198 }
199 div {
200 background: green;
201 color: blue;
202 position: running(test);
203 }
204 </style>
205 <span>aa </span><div>a</div><span> aa</span>
206 ''')
207
208
209 @assert_no_logs
210 def test_whitespace_in_running(assert_pixels):
211 assert_pixels('''
212 RRRRRRRR______
213 RRRRRRRR______
214 ______BB______
215 ______BB______
216 ''', '''
217 <style>
218 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
219 @page {
220 size: 14px 4px;
221 margin: 0 0 2px;
222 @bottom-center {
223 content: element(test);
224 }
225 }
226 body {
227 color: red;
228 font-family: weasyprint;
229 font-size: 2px;
230 line-height: 1;
231 }
232 div {
233 background: green;
234 color: blue;
235 position: running(test);
236 }
237 </style>
238 <span>aa</span><div> a </div><span>aa</span>
239 ''')
256256 ('width: 10%; height: 1000px; min-width: auto; max-height: none',),
257257 ))
258258 def test_box_sizing(size):
259 # http://www.w3.org/TR/css3-ui/#box-sizing
259 # https://www.w3.org/TR/css-ui-3/#box-sizing
260260 page, = render_pages('''
261261 <style>
262262 @page { size: 100000px }
311311 ('min-width: 0; min-height: 0; width: 0; height: 0'),
312312 ))
313313 def test_box_sizing_zero(size):
314 # http://www.w3.org/TR/css3-ui/#box-sizing
314 # https://www.w3.org/TR/css-ui-3/#box-sizing
315315 page, = render_pages('''
316316 <style>
317317 @page { size: 100000px }
564564
565565 @assert_no_logs
566566 def test_box_decoration_break_block_slice():
567 # http://www.w3.org/TR/css3-background/#the-box-decoration-break
567 # https://www.w3.org/TR/css-backgrounds-3/#the-box-decoration-break
568568 page_1, page_2 = render_pages('''
569569 <style>
570570 @page { size: 100px }
613613
614614 @assert_no_logs
615615 def test_box_decoration_break_block_clone():
616 # http://www.w3.org/TR/css3-background/#the-box-decoration-break
616 # https://www.w3.org/TR/css-backgrounds-3/#the-box-decoration-break
617617 page_1, page_2 = render_pages('''
618618 <style>
619619 @page { size: 100px }
642642 assert footnote_marker.children[0].text == '1.'
643643 assert footnote_textbox.text == 'de'
644644 assert footnote_area.position_y == 5
645
646
647 @assert_no_logs
648 def test_reported_footnote_repagination():
649 # Regression test for https://github.com/Kozea/WeasyPrint/issues/1700
650 page1, page2 = render_pages('''
651 <style>
652 @font-face {src: url(weasyprint.otf); font-family: weasyprint}
653 @page {
654 size: 5px;
655 }
656 div {
657 font-family: weasyprint;
658 font-size: 2px;
659 line-height: 1;
660 }
661 span {
662 float: footnote;
663 }
664 a::after {
665 content: target-counter(attr(href), page);
666 }
667 </style>
668 <div><a href="#i">a</a> bb<span>de</span> <i id="i">fg</i></div>''')
669 html, = page1.children
670 body, = html.children
671 div, = body.children
672 line1, line2 = div.children
673 a, = line1.children
674 assert a.children[0].text == 'a'
675 assert a.children[1].children[0].text == '2'
676 b, footnote_call, _ = line2.children
677 assert b.text == 'bb'
678 assert footnote_call.children[0].text == '1'
679
680 html, footnote_area = page2.children
681 body, = html.children
682 div, = body.children
683 line1, = div.children
684 i, = line1.children
685 assert i.children[0].text == 'fg'
686
687 footnote_marker, footnote_textbox = (
688 footnote_area.children[0].children[0].children)
689 assert footnote_marker.children[0].text == '1.'
690 assert footnote_textbox.text == 'de'
691 assert footnote_area.position_y == 3
645692
646693
647694 @assert_no_logs
129129
130130 @assert_no_logs
131131 def test_breaking_linebox_regression_1():
132 # See http://unicode.org/reports/tr14/
132 # See https://unicode.org/reports/tr14/
133133 page, = render_pages('<pre>a\nb\rc\r\nd\u2029e</pre>')
134134 html, = page.children
135135 body, = html.children
10041004
10051005 @assert_no_logs
10061006 def test_box_decoration_break_inline_slice():
1007 # http://www.w3.org/TR/css3-background/#the-box-decoration-break
1007 # https://www.w3.org/TR/css-backgrounds-3/#the-box-decoration-break
10081008 page_1, = render_pages('''
10091009 <style>
10101010 @font-face { src: url(weasyprint.otf); font-family: weasyprint }
10351035
10361036 @assert_no_logs
10371037 def test_box_decoration_break_inline_clone():
1038 # http://www.w3.org/TR/css3-background/#the-box-decoration-break
1038 # https://www.w3.org/TR/css-backgrounds-3/#the-box-decoration-break
10391039 page_1, = render_pages('''
10401040 <style>
10411041 @font-face { src: url(weasyprint.otf); font-family: weasyprint }
102102 assert len(ul.children) == 1
103103 for li in ul.children:
104104 assert len(li.children) == 2
105
106
107 def test_lists_page_break_margin():
108 # Regression test for https://github.com/Kozea/WeasyPrint/issues/1058
109 page1, page2 = render_pages('''
110 <style>
111 @font-face { src: url(weasyprint.otf); font-family: weasyprint }
112 @page { size: 300px 100px }
113 ul { font-size: 30px; font-family: weasyprint; margin: 0 }
114 p { margin: 10px 0 }
115 </style>
116 <ul>
117 <li><p>a</p></li>
118 <li><p>a</p></li>
119 <li><p>a</p></li>
120 <li><p>a</p></li>
121 </ul>
122 ''')
123 for page in (page1, page2):
124 html, = page.children
125 body, = html.children
126 ul, = body.children
127 assert len(ul.children) == 2
128 for li in ul.children:
129 assert len(li.children) == 2
130 assert (
131 li.children[0].position_y ==
132 li.children[1].children[0].position_y)
14701470 </style>
14711471 <footer>Hello!<p>Bonjour!</p></footer>
14721472 ''')
1473
1474
1475 @assert_no_logs
1476 def test_running_flex():
1477 # Test regression
1478 render_pages('''
1479 <style>
1480 footer {
1481 display: flex;
1482 position: running(footer);
1483 }
1484 @page {
1485 @bottom-center {
1486 content: element(footer);
1487 }
1488 }
1489 </style>
1490 <footer>
1491 Hello!
1492 </footer>
1493 ''')
1494
1495
1496 @assert_no_logs
1497 def test_running_float():
1498 # Test regression
1499 render_pages('''
1500 <style>
1501 footer {
1502 float: left;
1503 position: running(footer);
1504 }
1505 @page {
1506 @bottom-center {
1507 content: element(footer);
1508 }
1509 }
1510 </style>
1511 <footer>
1512 Hello!
1513 </footer>
1514 ''')
369369
370370 @assert_no_logs
371371 def test_fixed_positioning_regression_1():
372 # Regression test for https://github.com/Kozea/WeasyPrint/issues/641
372 # Regression test for https://github.com/Kozea/WeasyPrint/pull/641
373373 page_1, page_2 = render_pages('''
374374 <style>
375375 @page:first { size: 100px 200px }
14121412 @assert_no_logs
14131413 def test_layout_table_auto_46():
14141414 # Test regression:
1415 # http://test.weasyprint.org/suite-css21/chapter8/section2/test56/
1415 # https://test.weasyprint.org/suite-css21/chapter8/section2/test56/
14161416 page, = render_pages('''
14171417 <div style="position: absolute">
14181418 <table style="margin: 50px; border: 20px solid black">
18411841
18421842 @assert_no_logs
18431843 def test_table_row_height_3():
1844 # Test regression: https://github.com/Kozea/WeasyPrint/issues/
1844 # Test regression: https://github.com/Kozea/WeasyPrint/issues/937
18451845 page, = render_pages('''
18461846 <style>
18471847 @font-face { src: url(weasyprint.otf); font-family: weasyprint }
1717 return HTML(resource_filename(filename)).render()
1818
1919 with capture_logs():
20 # This is a copy of http://www.webstandards.org/files/acid2/test.html
20 # This is a copy of https://www.webstandards.org/files/acid2/test.html
2121 document = render('acid2-test.html')
2222 intro_page, test_page = document.pages
2323 # Ignore the intro page: it is not in the reference
2424 test_png = document.copy([test_page]).write_png()
2525 test_pixels = Image.open(io.BytesIO(test_png)).getdata()
2626
27 # This is a copy of http://www.webstandards.org/files/acid2/reference.html
27 # This is a copy of https://www.webstandards.org/files/acid2/reference.html
2828 ref_png = render('acid2-reference.html').write_png()
2929 ref_image = Image.open(io.BytesIO(ref_png))
3030 ref_pixels = ref_image.getdata()
109109 anchors[anchor_name] = round(pos_x, 6), round(pos_y, 6)
110110 links = page.links
111111 for i, link in enumerate(links):
112 link_type, target, rectangle, download_name = link
112 link_type, target, rectangle, box = link
113113 pos_x, pos_y, width, height = rectangle
114114 link = (
115115 link_type, target,
116116 (round(pos_x, 6), round(pos_y, 6),
117117 round(width, 6), round(height, 6)),
118 download_name)
118 box)
119119 links[i] = link
120120 bookmarks = page.bookmarks
121121 for i, (level, label, (pos_x, pos_y), state) in enumerate(bookmarks):
425425 stdout = _run(f'--pdf-variant=pdf/a-{version}b - -', b'test')
426426 assert f'PDF-{pdf_version}'.encode() in stdout
427427 assert f'part="{version}"'.encode() in stdout
428
429
430 def test_pdfua():
431 stdout = _run('--pdf-variant=pdf/ua-1 - -', b'test')
432 assert b'part="1"' in stdout
428433
429434
430435 def test_pdf_identifier():
709714 assert document.make_bookmark_tree() == expected_tree
710715
711716
712 def assert_links(html, expected_links_by_page, expected_anchors_by_page,
713 expected_resolved_links,
717 def simplify_links(links):
718 return [
719 (link_type, link_target, rectangle)
720 for link_type, link_target, rectangle, box in links]
721
722
723 def assert_links(html, links, anchors, resolved_links,
714724 base_url=resource_filename('<inline HTML>'), warnings=(),
715725 round=False):
716726 with capture_logs() as logs:
717727 document = FakeHTML(string=html, base_url=base_url).render()
718728 if round:
719729 _round_meta(document.pages)
720 resolved_links = list(resolve_links(document.pages))
730 document_resolved_links = [
731 (simplify_links(page_links), page_anchors)
732 for page_links, page_anchors in resolve_links(document.pages)]
721733 assert len(logs) == len(warnings)
722734 for message, expected in zip(logs, warnings):
723735 assert expected in message
724 assert [p.links for p in document.pages] == expected_links_by_page
725 assert [p.anchors for p in document.pages] == expected_anchors_by_page
726 assert resolved_links == expected_resolved_links
736 document_links = [simplify_links(page.links) for page in document.pages]
737 document_anchors = [page.anchors for page in document.pages]
738 assert document_links == links
739 assert document_anchors == anchors
740 assert document_resolved_links == resolved_links
727741
728742
729743 @assert_no_logs
734748 p { height: 90px; margin: 0 0 10px 0 }
735749 img { width: 30px; vertical-align: top }
736750 </style>
737 <p><a href="http://weasyprint.org"><img src=pattern.png></a></p>
751 <p><a href="https://weasyprint.org"><img src=pattern.png></a></p>
738752 <p style="padding: 0 10px"><a
739753 href="#lipsum"><img style="border: solid 1px"
740754 src=pattern.png></a></p>
745759 </p>
746760 ''', [
747761 [
748 ('external', 'http://weasyprint.org', (0, 0, 30, 20), None),
749 ('external', 'http://weasyprint.org', (0, 0, 30, 30), None),
750 ('internal', 'lipsum', (10, 100, 42, 120), None),
751 ('internal', 'lipsum', (10, 100, 42, 132), None)
762 ('external', 'https://weasyprint.org', (0, 0, 30, 20)),
763 ('external', 'https://weasyprint.org', (0, 0, 30, 30)),
764 ('internal', 'lipsum', (10, 100, 42, 120)),
765 ('internal', 'lipsum', (10, 100, 42, 132))
752766 ],
753 [('internal', 'hello', (0, 0, 200, 30), None)],
767 [('internal', 'hello', (0, 0, 200, 30))],
754768 ], [
755769 {'hello': (0, 200)},
756770 {'lipsum': (0, 0)}
757771 ], [
758772 (
759773 [
760 ('external', 'http://weasyprint.org', (0, 0, 30, 20), None),
761 ('external', 'http://weasyprint.org', (0, 0, 30, 30), None),
762 ('internal', 'lipsum', (10, 100, 42, 120), None),
763 ('internal', 'lipsum', (10, 100, 42, 132), None)
774 ('external', 'https://weasyprint.org', (0, 0, 30, 20)),
775 ('external', 'https://weasyprint.org', (0, 0, 30, 30)),
776 ('internal', 'lipsum', (10, 100, 42, 120)),
777 ('internal', 'lipsum', (10, 100, 42, 132))
764778 ],
765779 [('hello', 0, 200)],
766780 ),
767781 (
768 [
769 ('internal', 'hello', (0, 0, 200, 30), None)
770 ],
782 [('internal', 'hello', (0, 0, 200, 30))],
771783 [('lipsum', 0, 0)]),
772784 ])
773785
778790 '''
779791 <body style="width: 200px">
780792 <a href="../lipsum/é_%E9" style="display: block; margin: 10px 5px">
781 ''', [[('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9',
782 (5, 10, 195, 10), None)]],
783 [{}], [([('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9',
784 (5, 10, 195, 10), None)], [])],
785 base_url='http://weasyprint.org/foo/bar/')
793 ''', [[('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9',
794 (5, 10, 195, 10))]],
795 [{}], [([('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9',
796 (5, 10, 195, 10))], [])],
797 base_url='https://weasyprint.org/foo/bar/')
786798
787799
788800 @assert_no_logs
792804 <body style="width: 200px">
793805 <div style="display: block; margin: 10px 5px;
794806 -weasy-link: url(../lipsum/é_%E9)">
795 ''', [[('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9',
796 (5, 10, 195, 10), None)]],
797 [{}], [([('external', 'http://weasyprint.org/foo/lipsum/%C3%A9_%E9',
798 (5, 10, 195, 10), None)], [])],
799 base_url='http://weasyprint.org/foo/bar/')
807 ''', [[('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9',
808 (5, 10, 195, 10))]],
809 [{}], [([('external', 'https://weasyprint.org/foo/lipsum/%C3%A9_%E9',
810 (5, 10, 195, 10))], [])],
811 base_url='https://weasyprint.org/foo/bar/')
800812
801813
802814 @assert_no_logs
806818 '''
807819 <body style="width: 200px">
808820 <a href="../lipsum" style="display: block; margin: 10px 5px">
809 ''', [[('external', '../lipsum', (5, 10, 195, 10), None)]], [{}],
810 [([('external', '../lipsum', (5, 10, 195, 10), None)], [])],
821 ''', [[('external', '../lipsum', (5, 10, 195, 10))]], [{}],
822 [([('external', '../lipsum', (5, 10, 195, 10))], [])],
811823 base_url=None)
812824
813825
832844 <body style="width: 200px">
833845 <a href="#lipsum" id="lipsum"
834846 style="display: block; margin: 10px 5px"></a>
835 <a href="http://weasyprint.org/" style="display: block"></a>
847 <a href="https://weasyprint.org/" style="display: block"></a>
836848 ''', [[
837 ('internal', 'lipsum', (5, 10, 195, 10), None),
838 ('external', 'http://weasyprint.org/', (0, 10, 200, 10), None)]],
849 ('internal', 'lipsum', (5, 10, 195, 10)),
850 ('external', 'https://weasyprint.org/', (0, 10, 200, 10))]],
839851 [{'lipsum': (5, 10)}],
840 [([('internal', 'lipsum', (5, 10, 195, 10), None),
841 ('external', 'http://weasyprint.org/', (0, 10, 200, 10), None)],
852 [([('internal', 'lipsum', (5, 10, 195, 10)),
853 ('external', 'https://weasyprint.org/', (0, 10, 200, 10))],
842854 [('lipsum', 5, 10)])],
843855 base_url=None)
844856
851863 <div style="-weasy-link: url(#lipsum);
852864 margin: 10px 5px" id="lipsum">
853865 ''',
854 [[('internal', 'lipsum', (5, 10, 195, 10), None)]],
866 [[('internal', 'lipsum', (5, 10, 195, 10))]],
855867 [{'lipsum': (5, 10)}],
856 [([('internal', 'lipsum', (5, 10, 195, 10), None)],
857 [('lipsum', 5, 10)])],
868 [([('internal', 'lipsum', (5, 10, 195, 10))], [('lipsum', 5, 10)])],
858869 base_url=None)
859870
860871
867878 <a href="#lipsum"></a>
868879 <a href="#missing" id="lipsum"></a>
869880 ''',
870 [[('internal', 'lipsum', (0, 0, 200, 15), None),
871 ('internal', 'missing', (0, 15, 200, 30), None)]],
881 [[('internal', 'lipsum', (0, 0, 200, 15)),
882 ('internal', 'missing', (0, 15, 200, 30))]],
872883 [{'lipsum': (0, 15)}],
873 [([('internal', 'lipsum', (0, 0, 200, 15), None)],
874 [('lipsum', 0, 15)])],
884 [([('internal', 'lipsum', (0, 0, 200, 15))], [('lipsum', 0, 15)])],
875885 base_url=None,
876886 warnings=[
877887 'ERROR: No anchor #missing for internal URI reference'])
885895 <a href="#lipsum" id="lipsum" style="display: block; height: 20px;
886896 transform: rotate(90deg) scale(2)">
887897 ''',
888 [[('internal', 'lipsum', (30, 10, 70, 210), None)]],
898 [[('internal', 'lipsum', (30, 10, 70, 210))]],
889899 [{'lipsum': (70, 10)}],
890 [([('internal', 'lipsum', (30, 10, 70, 210), None)],
891 [('lipsum', 70, 10)])],
900 [([('internal', 'lipsum', (30, 10, 70, 210))], [('lipsum', 70, 10)])],
892901 round=True)
893902
894903
900909 <body style="width: 200px">
901910 <a rel=attachment href="pattern.png" download="wow.png"
902911 style="display: block; margin: 10px 5px">
903 ''', [[('attachment', 'pattern.png',
904 (5, 10, 195, 10), 'wow.png')]],
905 [{}], [([('attachment', 'pattern.png',
906 (5, 10, 195, 10), 'wow.png')], [])],
912 ''', [[('attachment', 'pattern.png', (5, 10, 195, 10))]],
913 [{}], [([('attachment', 'pattern.png', (5, 10, 195, 10))], [])],
907914 base_url=None)
908915
909916
9971004 meta.setdefault('created', None)
9981005 meta.setdefault('modified', None)
9991006 meta.setdefault('attachments', [])
1007 meta.setdefault('lang', None)
10001008 meta.setdefault('custom', {})
10011009 assert vars(FakeHTML(string=html).render().metadata) == meta
10021010
10101018 def test_html_meta_2():
10111019 assert_meta(
10121020 '''
1021 <html lang="en"><head>
10131022 <meta name=author content="I Me &amp; Myself">
10141023 <meta name=author content="Smith, John">
10151024 <title>Test document</title>
10161025 <h1>Another title</h1>
10171026 <meta name=generator content="Human after all">
1027 <meta name=generator content="Human">
10181028 <meta name=dummy content=ignored>
10191029 <meta name=dummy>
10201030 <meta content=ignored>
10261036 <meta name=dcterms.modified content=2013>
10271037 <meta name=keywords content="Python; pydyf">
10281038 <meta name=description content="Blah… ">
1039 <meta name=description content="*Oh-no/">
1040 <meta name=dcterms.modified content=2012>
1041 </head></html>
10291042 ''',
10301043 authors=['I Me & Myself', 'Smith, John'],
10311044 title='Test document',
10341047 description="Blah… ",
10351048 created='2011-04',
10361049 modified='2013',
1050 lang='en',
10371051 custom={'dummy': 'ignored'})
10381052
10391053
221221
222222 @assert_no_logs
223223 def test_whitespace():
224 # TODO: test more cases
225 # http://www.w3.org/TR/CSS21/text.html#white-space-model
226224 assert_tree(parse_all('''
227225 <p>Lorem \t\r\n ipsum\t<strong> dolor
228226 <img src=pattern.png> sit
337335
338336 @assert_no_logs
339337 def test_tables_1():
340 # Rules in http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
338 # Rules in https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
341339
342340 # Rule 1.3
343 # Also table model: http://www.w3.org/TR/CSS21/tables.html#model
341 # Also table model: https://www.w3.org/TR/CSS21/tables.html#model
344342 assert_tree(parse_all('''
345343 <x-table>
346344 <x-tr>
407405
408406 @assert_no_logs
409407 def test_tables_3():
410 # http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
408 # https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
411409 # Rules 1.1 and 1.2
412410 # Rule XXX (not in the spec): column groups have at least one column child
413411 assert_tree(parse_all('''
233233 ('p::before', 'Text', 'a')])])])])])
234234
235235
236 @pytest.mark.xfail
237 @assert_no_logs
238 def test_counters_9():
239 document = HTML(string='''
240 <ol>
241 <li></li>
242 <li>
243 <ol style="counter-reset: a">
244 <li></li>
245 <li></li>
246 </ol>
247 </li>
248 <li></li>
249 </ol>
250 ''').render()
251 page, = document.pages
252 html, = page._page_box.children
253 body, = html.children
254 ol1, = body.children
255 oli1, oli2, oli3 = ol1.children
256 marker, ol2 = oli2.children
257 oli21, oli22 = ol2.children
258 assert oli1.children[0].children[0].children[0].text == '1. '
259 assert oli2.children[0].children[0].children[0].text == '2. '
260 assert oli21.children[0].children[0].children[0].text == '1. '
261 assert oli22.children[0].children[0].children[0].text == '2. '
262 assert oli3.children[0].children[0].children[0].text == '3. '
263
264
236265 @assert_no_logs
237266 def test_counter_styles_1():
238267 assert_tree(parse_all('''
1212 stylesheet = tinycss2.parse_stylesheet(
1313 '@font-face {'
1414 ' font-family: Gentium Hard;'
15 ' src: url(http://example.com/fonts/Gentium.woff);'
15 ' src: url(https://example.com/fonts/Gentium.woff);'
1616 '}')
1717 at_rule, = stylesheet
1818 assert at_rule.at_keyword == 'font-face'
1919 font_family, src = list(preprocess_descriptors(
20 'font-face', 'http://weasyprint.org/foo/',
20 'font-face', 'https://weasyprint.org/foo/',
2121 tinycss2.parse_declaration_list(at_rule.content)))
2222 assert font_family == ('font_family', 'Gentium Hard')
2323 assert src == (
24 'src', (('external', 'http://example.com/fonts/Gentium.woff'),))
24 'src', (('external', 'https://example.com/fonts/Gentium.woff'),))
2525
2626
2727 @assert_no_logs
3838 assert at_rule.at_keyword == 'font-face'
3939 font_family, src, font_style, font_weight, font_stretch = list(
4040 preprocess_descriptors(
41 'font-face', 'http://weasyprint.org/foo/',
41 'font-face', 'https://weasyprint.org/foo/',
4242 tinycss2.parse_declaration_list(at_rule.content)))
4343 assert font_family == ('font_family', 'Fonty Smiley')
4444 assert src == (
45 'src', (('external', 'http://weasyprint.org/foo/Fonty-Smiley.woff'),))
45 'src', (('external', 'https://weasyprint.org/foo/Fonty-Smiley.woff'),))
4646 assert font_style == ('font_style', 'italic')
4747 assert font_weight == ('font_weight', 200)
4848 assert font_stretch == ('font_stretch', 'condensed')
5858 at_rule, = stylesheet
5959 assert at_rule.at_keyword == 'font-face'
6060 font_family, src = list(preprocess_descriptors(
61 'font-face', 'http://weasyprint.org/foo/',
61 'font-face', 'https://weasyprint.org/foo/',
6262 tinycss2.parse_declaration_list(at_rule.content)))
6363 assert font_family == ('font_family', 'Gentium Hard')
6464 assert src == ('src', (('local', None),))
7575 at_rule, = stylesheet
7676 assert at_rule.at_keyword == 'font-face'
7777 font_family, src = list(preprocess_descriptors(
78 'font-face', 'http://weasyprint.org/foo/',
78 'font-face', 'https://weasyprint.org/foo/',
7979 tinycss2.parse_declaration_list(at_rule.content)))
8080 assert font_family == ('font_family', 'Gentium Hard')
8181 assert src == ('src', (('local', 'Gentium Hard'),))
9494 assert at_rule.at_keyword == 'font-face'
9595 with capture_logs() as logs:
9696 font_family, src = list(preprocess_descriptors(
97 'font-face', 'http://weasyprint.org/foo/',
97 'font-face', 'https://weasyprint.org/foo/',
9898 tinycss2.parse_declaration_list(at_rule.content)))
9999 assert font_family == ('font_family', 'Gentium Hard')
100100 assert src == ('src', (('local', 'Gentium Hard'),))
117117 with capture_logs() as logs:
118118 font_family, src, font_stretch = list(
119119 preprocess_descriptors(
120 'font-face', 'http://weasyprint.org/foo/',
120 'font-face', 'https://weasyprint.org/foo/',
121121 tinycss2.parse_declaration_list(at_rule.content)))
122122 assert font_family == ('font_family', 'Bad Font')
123123 assert src == (
124 'src', (('external', 'http://weasyprint.org/foo/BadFont.woff'),))
124 'src', (('external', 'https://weasyprint.org/foo/BadFont.woff'),))
125125 assert font_stretch == ('font_stretch', 'expanded')
126126 assert logs == [
127127 'WARNING: Ignored `font-style: wrong` at 1:91, invalid value.',
133133 stylesheet = tinycss2.parse_stylesheet('@font-face{}')
134134 with capture_logs() as logs:
135135 preprocess_stylesheet(
136 'print', 'http://wp.org/foo/', stylesheet, None, None, None,
136 'print', 'https://wp.org/foo/', stylesheet, None, None, None,
137137 None, None)
138138 assert logs == [
139139 "WARNING: Missing src descriptor in '@font-face' rule at 1:1"]
143143 stylesheet = tinycss2.parse_stylesheet('@font-face{src: url(test.woff)}')
144144 with capture_logs() as logs:
145145 preprocess_stylesheet(
146 'print', 'http://wp.org/foo/', stylesheet, None, None, None,
146 'print', 'https://wp.org/foo/', stylesheet, None, None, None,
147147 None, None)
148148 assert logs == [
149149 "WARNING: Missing font-family descriptor in '@font-face' rule at 1:1"]
153153 stylesheet = tinycss2.parse_stylesheet('@font-face{font-family: test}')
154154 with capture_logs() as logs:
155155 preprocess_stylesheet(
156 'print', 'http://wp.org/foo/', stylesheet, None, None, None,
156 'print', 'https://wp.org/foo/', stylesheet, None, None, None,
157157 None, None)
158158 assert logs == [
159159 "WARNING: Missing src descriptor in '@font-face' rule at 1:1"]
164164 '@font-face { font-family: test; src: wrong }')
165165 with capture_logs() as logs:
166166 preprocess_stylesheet(
167 'print', 'http://wp.org/foo/', stylesheet, None, None, None,
167 'print', 'https://wp.org/foo/', stylesheet, None, None, None,
168168 None, None)
169169 assert logs == [
170170 'WARNING: Ignored `src: wrong ` at 1:33, invalid value.',
176176 '@font-face { font-family: good, bad; src: url(test.woff) }')
177177 with capture_logs() as logs:
178178 preprocess_stylesheet(
179 'print', 'http://wp.org/foo/', stylesheet, None, None, None,
179 'print', 'https://wp.org/foo/', stylesheet, None, None, None,
180180 None, None)
181181 assert logs == [
182182 'WARNING: Ignored `font-family: good, bad` at 1:14, invalid value.',
188188 '@font-face { font-family: good, bad; src: really bad }')
189189 with capture_logs() as logs:
190190 preprocess_stylesheet(
191 'print', 'http://wp.org/foo/', stylesheet, None, None, None,
191 'print', 'https://wp.org/foo/', stylesheet, None, None, None,
192192 None, None)
193193 assert logs == [
194194 'WARNING: Ignored `font-family: good, bad` at 1:14, invalid value.',
206206 stylesheet = tinycss2.parse_stylesheet(rule)
207207 with capture_logs() as logs:
208208 preprocess_stylesheet(
209 'print', 'http://wp.org/foo/', stylesheet, None, None, None,
209 'print', 'https://wp.org/foo/', stylesheet, None, None, None,
210210 None, {})
211211 assert len(logs) >= 1
00 """Test expanders for shorthand properties."""
11
2 import math
2 from math import pi
33
44 import pytest
55 import tinycss2
1717 declarations = tinycss2.parse_declaration_list(css)
1818
1919 with capture_logs() as logs:
20 base_url = 'http://weasyprint.org/foo/'
20 base_url = 'https://weasyprint.org/foo/'
2121 declarations = list(preprocess_declarations(base_url, declarations))
2222
2323 if expected_error:
222222 ('transform: none', {'transform': ()}),
223223 ('transform: translate(6px) rotate(90deg)', {
224224 'transform': (
225 ('translate', ((6, 'px'), (0, 'px'))),
226 ('rotate', math.pi / 2))}),
225 ('translate', ((6, 'px'), (0, 'px'))), ('rotate', pi / 2))}),
227226 ('transform: translate(-4px, 0)', {
228227 'transform': (('translate', ((-4, 'px'), (0, None))),)}),
229228 ('transform: translate(6px, 20%)', {
359358 'list_style_type': 'inherit',
360359 }),
361360 ('list-style: url(../bar/lipsum.png)', {
362 'list_style_image': ('url', 'http://weasyprint.org/bar/lipsum.png'),
361 'list_style_image': ('url', 'https://weasyprint.org/bar/lipsum.png'),
363362 }),
364363 ('list-style: square', {
365364 'list_style_type': 'square',
421420 assert_background('red', background_color=(1, 0, 0, 1))
422421 assert_background(
423422 'url(lipsum.png)',
424 background_image=[('url', 'http://weasyprint.org/foo/lipsum.png')])
423 background_image=[('url', 'https://weasyprint.org/foo/lipsum.png')])
425424 assert_background(
426425 'no-repeat',
427426 background_repeat=[('no-repeat', 'no-repeat')])
467466 assert_background(
468467 'url(bar) #f00 repeat-y center left fixed',
469468 background_color=(1, 0, 0, 1),
470 background_image=[('url', 'http://weasyprint.org/foo/bar')],
469 background_image=[('url', 'https://weasyprint.org/foo/bar')],
471470 background_repeat=[('no-repeat', 'repeat')],
472471 background_attachment=['fixed'],
473472 background_position=[('left', (0, '%'), 'top', (50, '%'))])
514513 assert_background(
515514 'url(bar) center, no-repeat',
516515 background_color=(0, 0, 0, 0),
517 background_image=[('url', 'http://weasyprint.org/foo/bar'),
516 background_image=[('url', 'https://weasyprint.org/foo/bar'),
518517 ('none', None)],
519518 background_position=[('left', (50, '%'), 'top', (50, '%')),
520519 ('left', (0, '%'), 'top', (0, '%'))],
817816 red = (1, 0, 0, 1)
818817 lime = (0, 1, 0, 1)
819818 blue = (0, 0, 1, 1)
820 pi = math.pi
821819
822820 def gradient(css, direction, colors=(blue,), stop_positions=(None,)):
823821 for repeating, prefix in ((False, ''), (True, 'repeating-')):
12171215 ))
12181216 def test_text_align_invalid(rule, reason):
12191217 assert_invalid(rule, reason)
1218
1219
1220 @assert_no_logs
1221 @pytest.mark.parametrize('rule, result', (
1222 ('image-orientation: none', {'image_orientation': 'none'}),
1223 ('image-orientation: from-image', {'image_orientation': 'from-image'}),
1224 ('image-orientation: 90deg', {'image_orientation': (pi / 2, False)}),
1225 ('image-orientation: 30deg', {'image_orientation': (pi / 6, False)}),
1226 ('image-orientation: 180deg flip', {'image_orientation': (pi, True)}),
1227 ('image-orientation: 0deg flip', {'image_orientation': (0, True)}),
1228 ('image-orientation: flip 90deg', {'image_orientation': (pi / 2, True)}),
1229 ('image-orientation: flip', {'image_orientation': (0, True)}),
1230 ))
1231 def test_image_orientation(rule, result):
1232 assert expand_to_dict(rule) == result
1233
1234
1235 @assert_no_logs
1236 @pytest.mark.parametrize('rule, reason', (
1237 ('image-orientation: none none', 'invalid'),
1238 ('image-orientation: unknown', 'invalid'),
1239 ('image-orientation: none flip', 'invalid'),
1240 ('image-orientation: from-image flip', 'invalid'),
1241 ('image-orientation: 10', 'invalid'),
1242 ('image-orientation: 10 flip', 'invalid'),
1243 ('image-orientation: flip 10', 'invalid'),
1244 ('image-orientation: flip flip', 'invalid'),
1245 ('image-orientation: 90deg flop', 'invalid'),
1246 ('image-orientation: 90deg 180deg', 'invalid'),
1247 ))
1248 def test_image_orientation_invalid(rule, reason):
1249 assert_invalid(rule, reason)
302302 p { display: block; height: 90pt; margin: 0 0 10pt 0 }
303303 img { width: 30pt; vertical-align: top }
304304 </style>
305 <p><a href="http://weasyprint.org"><img src=pattern.png></a></p>
305 <p><a href="https://weasyprint.org"><img src=pattern.png></a></p>
306306 <p style="padding: 0 10pt"><a
307307 href="#lipsum"><img style="border: solid 1pt"
308308 src=pattern.png></a></p>
321321 b'/Rect \\[ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+ [\\d\\.]+) \\]', pdf)]
322322
323323 # 30pt wide (like the image), 20pt high (like line-height)
324 assert uris.pop(0) == b'http://weasyprint.org'
324 assert uris.pop(0) == b'https://weasyprint.org'
325325 assert subtypes.pop(0) == b'/Link'
326326 assert types.pop(0) == b'/URI'
327327 assert rects.pop(0) == [0, TOP, 30, TOP - 20]
328328
329329 # The image itself: 30*30pt
330 assert uris.pop(0) == b'http://weasyprint.org'
330 assert uris.pop(0) == b'https://weasyprint.org'
331331 assert subtypes.pop(0) == b'/Link'
332332 assert types.pop(0) == b'/URI'
333333 assert rects.pop(0) == [0, TOP, 30, TOP - 30]
372372 # 100% wide (block), 0pt high
373373 pdf = FakeHTML(
374374 string='<a href="../lipsum" style="display: block"></a>a',
375 base_url='http://weasyprint.org/foo/bar/').write_pdf()
376 assert b'/S /URI\n/URI (http://weasyprint.org/foo/lipsum)'
375 base_url='https://weasyprint.org/foo/bar/').write_pdf()
376 assert b'/S /URI\n/URI (https://weasyprint.org/foo/lipsum)'
377377 assert f'/Rect [ 0 {TOP} {RIGHT} {TOP} ]'.encode() in pdf
378378
379379
437437 def test_relative_links_different_base():
438438 pdf = FakeHTML(
439439 string='<a href="/test/lipsum"></a>a',
440 base_url='http://weasyprint.org/foo/bar/').write_pdf()
441 assert b'http://weasyprint.org/test/lipsum' in pdf
440 base_url='https://weasyprint.org/foo/bar/').write_pdf()
441 assert b'https://weasyprint.org/test/lipsum' in pdf
442442
443443
444444 @assert_no_logs
445445 def test_relative_links_same_base():
446446 pdf = FakeHTML(
447447 string='<a id="test" href="/foo/bar/#test"></a>a',
448 base_url='http://weasyprint.org/foo/bar/').write_pdf()
448 base_url='https://weasyprint.org/foo/bar/').write_pdf()
449449 assert b'/Dest (test)' in pdf
450450
451451
619619 ''').write_pdf()
620620 md5 = '<{}>'.format(hashlib.md5(b'some data').hexdigest()).encode()
621621 assert md5 in pdf
622 assert b'EmbeddedFiles' in pdf
623
624
625 @assert_no_logs
626 def test_attachments_data_with_anchor():
627 pdf = FakeHTML(string='''
628 <title>Test document 2</title>
629 <meta charset="utf-8">
630 <link rel="attachment" href="data:,some data">
631 <h1 id="title">Title</h1>
632 <a href="#title">example</a>
633 ''').write_pdf()
634 md5 = '<{}>'.format(hashlib.md5(b'some data').hexdigest()).encode()
635 assert md5 in pdf
636 assert b'EmbeddedFiles' in pdf
622637
623638
624639 @assert_no_logs
11
22 import pytest
33 from weasyprint.css.properties import INITIAL_VALUES
4 from weasyprint.formatting_structure.build import capitalize
45 from weasyprint.text.line_break import split_first_line
56
67 from .testing_utils import MONO_FONTS, SANS_FONTS, assert_no_logs, render_pages
958959 body, = html.children
959960 lines = body.children
960961 lines = []
961 print(body.children)
962962 for line in body.children:
963963 line_text = ''
964964 for span_box in line.children:
12281228 p1, p2, p3, p4, p5 = body.children
12291229 line1, = p1.children
12301230 text1, = line1.children
1231 assert text1.text == 'Hé Lo1'
1231 assert text1.text == 'Hé LO1'
12321232 line2, = p2.children
12331233 text2, = line2.children
12341234 assert text2.text == 'HÉ LO1'
12441244
12451245
12461246 @assert_no_logs
1247 @pytest.mark.parametrize(
1248 'original, transformed', (
1249 ('abc def ghi', 'Abc Def Ghi'),
1250 ('AbC def ghi', 'AbC Def Ghi'),
1251 ('I’m SO cool', 'I’m SO Cool'),
1252 ('Wow.wow!wow', 'Wow.wow!wow'),
1253 ('!now not tomorrow', '!Now Not Tomorrow'),
1254 ('SUPER cool', 'SUPER Cool'),
1255 ('i 😻 non‑breaking characters', 'I 😻 Non‑breaking Characters'),
1256 ('3lite 3lite', '3lite 3lite'),
1257 ('one/two/three', 'One/two/three'),
1258 ('supernatural,super', 'Supernatural,super'),
1259 ('éternel αιώνια', 'Éternel Αιώνια'),
1260 )
1261 )
1262 def test_text_transform_capitalize(original, transformed):
1263 # Results are different for different browsers, we almost get the same
1264 # results as Firefox, that’s good enough!
1265 assert capitalize(original) == transformed
1266
1267
1268 @assert_no_logs
12471269 def test_text_floating_pre_line():
12481270 # Test regression: https://github.com/Kozea/WeasyPrint/issues/610
12491271 page, = render_pages('''
126126 return [response]
127127
128128 # Port 0: let the OS pick an available port number
129 # http://stackoverflow.com/a/1365284/1162888
129 # https://stackoverflow.com/a/1365284/1162888
130130 server = wsgiref.simple_server.make_server('127.0.0.1', 0, wsgi_app)
131131 _host, port = server.socket.getsockname()
132132 thread = threading.Thread(target=server.serve_forever)
1212 import html5lib
1313 import tinycss2
1414
15 VERSION = __version__ = '56.1'
15 VERSION = __version__ = '57.0'
1616
1717 __all__ = [
1818 'HTML', 'CSS', 'Attachment', 'Document', 'Page', 'default_url_fetcher',
3030 def _find_base_url(html_document, fallback_base_url):
3131 """Return the base URL for the document.
3232
33 See http://www.w3.org/TR/html5/urls.html#document-base-url
33 See https://www.w3.org/TR/html5/urls.html#document-base-url
3434
3535 """
3636 first_base_element = next(iter(html_document.iter('base')), None)
33 stylesheets associated with a document and annotate every element with a value
44 for every CSS property.
55
6 http://www.w3.org/TR/CSS21/intro.html#processing-model
6 https://www.w3.org/TR/CSS21/intro.html#processing-model
77
88 This module does this in more than two steps. The
99 :func:`get_all_computed_styles` function does everything, but it is itsef based
4747 # values: (values, weight)
4848 # values: a PropertyValue-like object
4949 # weight: values with a greater weight take precedence, see
50 # http://www.w3.org/TR/CSS21/cascade.html#cascading-order
50 # https://www.w3.org/TR/CSS21/cascade.html#cascading-order
5151 self._cascaded_styles = cascaded_styles = {}
5252
5353 # keys: (element, pseudo_element_type), like cascaded_styles
122122 if ('table' in style['display'] and
123123 style['border_collapse'] == 'collapse'):
124124 # Padding do not apply
125 for side in ['top', 'bottom', 'left', 'right']:
125 for side in ('top', 'bottom', 'left', 'right'):
126126 style[f'padding_{side}'] = computed_values.ZERO_PIXELS
127127 if (len(style['display']) == 1 and
128128 style['display'][0].startswith('table-') and
129129 style['display'][0] != 'table-caption'):
130130 # Margins do not apply
131 for side in ['top', 'bottom', 'left', 'right']:
131 for side in ('top', 'bottom', 'left', 'right'):
132132 style[f'margin_{side}'] = computed_values.ZERO_PIXELS
133133
134134 return style
585585 and ``'user agent'``.
586586
587587 """
588 # See http://www.w3.org/TR/CSS21/cascade.html#cascading-order
588 # See https://www.w3.org/TR/CSS21/cascade.html#cascading-order
589589 if origin == 'user agent':
590590 return 1
591591 elif origin == 'user' and not importance:
625625 return copy
626626
627627 def __missing__(self, key):
628 if key in INHERITED or key.startswith('__'):
629 self[key] = self.parent_style[key]
628 if key in INHERITED or key[:2] == '__':
629 value = self[key] = self.parent_style[key]
630630 elif key == 'page':
631631 # page is not inherited but taken from the ancestor if 'auto'
632 self[key] = self.parent_style[key]
633 elif key.startswith('text_decoration_'):
634 self[key] = text_decoration(
632 value = self[key] = self.parent_style[key]
633 elif key[:16] == 'text_decoration_':
634 value = self[key] = text_decoration(
635635 key, INITIAL_VALUES[key], self.parent_style[key],
636636 cascaded=False)
637637 else:
638 self[key] = INITIAL_VALUES[key]
639 return self[key]
638 value = self[key] = INITIAL_VALUES[key]
639 return value
640640
641641
642642 class ComputedStyle(dict):
675675 if key in self.cascaded:
676676 value = keyword = self.cascaded[key][0]
677677 else:
678 if key in INHERITED or key.startswith('__'):
678 if key in INHERITED or key[:2] == '__':
679679 keyword = 'inherit'
680680 else:
681681 keyword = 'initial'
685685 keyword = 'initial'
686686
687687 if keyword == 'initial':
688 value = None if key.startswith('__') else INITIAL_VALUES[key]
688 value = None if key[:2] == '__' else INITIAL_VALUES[key]
689689 if key not in INITIAL_NOT_COMPUTED:
690690 # The value is the same as when computed
691691 self[key] = value
693693 # Values in parent_style are already computed.
694694 self[key] = value = self.parent_style[key]
695695
696 if key.startswith('text_decoration_') and self.parent_style:
697 self[key] = text_decoration(
696 if key[:16] == 'text_decoration_' and self.parent_style:
697 value = text_decoration(
698698 key, value, self.parent_style[key], key in self.cascaded)
699
700 if key == 'page' and value == 'auto':
699 if key in self:
700 del self[key]
701 elif key == 'page' and value == 'auto':
701702 # The page property does not inherit. However, if the page
702703 # value on an element is auto, then its used value is the value
703704 # specified on its nearest ancestor with a non-auto value. When
704705 # specified on the root element, the used value for auto is the
705706 # empty string.
706 self['page'] = value = (
707 value = (
707708 '' if self.parent_style is None else self.parent_style['page'])
708
709 if key in ('position', 'float', 'display'):
709 if key in self:
710 del self[key]
711 elif key in ('position', 'float', 'display'):
710712 self.specified[key] = value
711713
712714 if key in self:
00 """Convert specified property values into computed values."""
11
22 from collections import OrderedDict
3 from contextlib import suppress
4 from math import pi
35 from urllib.parse import unquote
46
57 from tinycss2.color3 import parse_color
2022
2123 # Value in pixels of font-size for <absolute-size> keywords: 12pt (16px) for
2224 # medium, and scaling factors given in CSS3 for others:
23 # http://www.w3.org/TR/css3-fonts/#font-size-prop
25 # https://www.w3.org/TR/css-fonts-3/#font-size-prop
2426 FONT_SIZE_KEYWORDS = OrderedDict(
2527 # medium is 16px, others are a ratio of medium
2628 (name, INITIAL_VALUES['font_size'] * a / b)
4446 }
4547 assert INITIAL_VALUES['border_top_width'] == BORDER_WIDTH_KEYWORDS['medium']
4648
47 # http://www.w3.org/TR/CSS21/fonts.html#propdef-font-weight
49 # https://www.w3.org/TR/CSS21/fonts.html#propdef-font-weight
4850 FONT_WEIGHT_RELATIVE = dict(
4951 bolder={
5052 100: 400,
7072 },
7173 )
7274
73 # http://www.w3.org/TR/css3-page/#size
75 # https://www.w3.org/TR/css-page-3/#size
7476 # name=(width in pixels, height in pixels)
7577 PAGE_SIZES = {
7678 'a10': (Dimension(26, 'mm'), Dimension(37, 'mm'),),
193195 style['font_variant_alternates'],
194196 style['font_variant_east_asian'],
195197 style['font_feature_settings'],
198 style['font_variation_settings'],
196199 ))
197200
198201
225228
226229 # See https://drafts.csswg.org/css-variables/#invalid-variables
227230 if new_value is None:
228 try:
231 with suppress(BaseException):
229232 computed_value = ''.join(
230233 token.serialize() for token in computed_value)
231 except BaseException:
232 pass
233234 LOGGER.warning(
234235 'Unsupported computed value "%s" set in variable %r '
235236 'for property %r.', computed_value,
383384 value if value in ('contain', 'cover') else
384385 length_or_percentage_tuple(style, name, value)
385386 for value in values)
387
388
389 @register_computer('image-orientation')
390 def image_orientation(style, name, values):
391 """Compute the ``image-orientation`` properties."""
392 if values in ('none', 'from-image'):
393 return values
394 angle, flip = values
395 return (int(round(angle / pi * 2)) % 4 * 90, flip)
386396
387397
388398 @register_computer('border-top-width')
537547 def display(style, name, value):
538548 """Compute the ``display`` property.
539549
540 See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
550 See https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
541551
542552 """
543553 float_ = style.specified['float']
560570 def compute_float(style, name, value):
561571 """Compute the ``float`` property.
562572
563 See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
573 See https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
564574
565575 """
566 if style.specified['position'] in ('absolute', 'fixed'):
576 position = style.specified['position']
577 if position in ('absolute', 'fixed') or position[0] == 'running()':
567578 return 'none'
568579 else:
569580 return value
77
88 */
99
10 /* http://www.w3.org/TR/html5/Overview#scroll-to-the-fragment-identifier */
10 /* https://www.w3.org/TR/html5/Overview#scroll-to-the-fragment-identifier */
1111 *[id] { -weasy-anchor: attr(id); }
1212 a[name] { -weasy-anchor: attr(name); }
1313
88
99
1010 INITIAL_VALUES = {
11 # CSS 2.1: http://www.w3.org/TR/CSS21/propidx.html
11 # CSS 2.1: https://www.w3.org/TR/CSS21/propidx.html
1212 'bottom': 'auto',
1313 'caption_side': 'top',
1414 'clear': 'none',
4141 'width': 'auto',
4242 'z_index': 'auto',
4343
44 # Backgrounds and Borders 3 (CR): https://www.w3.org/TR/css3-background/
44 # Backgrounds and Borders 3 (CR): https://www.w3.org/TR/css-backgrounds-3/
4545 'background_attachment': ('scroll',),
4646 'background_clip': ('border-box',),
4747 'background_color': parse_color('transparent'),
7070 'border_top_style': 'none',
7171 'border_top_width': 3, # computed value for 'medium'
7272
73 # Color 3 (REC): https://www.w3.org/TR/css3-color/
73 # Color 3 (REC): https://www.w3.org/TR/css-color-3/
7474 'opacity': 1,
7575
7676 # Multi-column Layout (WD): https://www.w3.org/TR/css-multicol-1/
100100 'font_variant_position': 'normal',
101101 'font_weight': 400,
102102
103 # Fonts 4 (WD): https://www.w3.org/TR/css-fonts-4/
104 'font_variation_settings': 'normal',
105
103106 # Fragmentation 3/4 (CR/WD): https://www.w3.org/TR/css-break-4/
104107 'box_decoration_break': 'slice',
105108 'break_after': 'auto',
119122 'quotes': list('“”‘’'), # chosen by the user agent
120123 'string_set': 'none',
121124
122 # Images 3/4 (CR/WD): https://www.w3.org/TR/css4-images/
125 # Images 3/4 (CR/WD): https://www.w3.org/TR/css-images-4/
123126 'image_resolution': 1, # dppx
124127 'image_rendering': 'auto',
125 # https://drafts.csswg.org/css-images-3/
128 'image_orientation': 'from-image',
126129 'object_fit': 'fill',
127130 'object_position': (('left', Dimension(50, '%'),
128131 'top', Dimension(50, '%')),),
213216 # Values inherited but not applicable to print are not included.
214217 #
215218 # text_decoration is not a really inherited, see
216 # http://www.w3.org/TR/CSS2/text.html#propdef-text-decoration
219 # https://www.w3.org/TR/CSS2/text.html#propdef-text-decoration
217220 #
218221 # link: click events normally bubble up to link ancestors
219 # See http://lists.w3.org/Archives/Public/www-style/2012Jun/0315.html
222 # See https://lists.w3.org/Archives/Public/www-style/2012Jun/0315.html
220223 INHERITED = {
221224 'block_ellipsis',
222225 'border_collapse',
239242 'font_variant_ligatures',
240243 'font_variant_numeric',
241244 'font_variant_position',
245 'font_variation_settings',
242246 'font_weight',
243247 'hyphens',
244248 'hyphenate_character',
269273 }
270274
271275
272 # http://www.w3.org/TR/CSS21/tables.html#model
273 # See also http://lists.w3.org/Archives/Public/www-style/2012Jun/0066.html
276 # https://www.w3.org/TR/CSS21/tables.html#model
277 # See also https://lists.w3.org/Archives/Public/www-style/2012Jun/0066.html
274278 # Only non-inherited properties need to be included here.
275279 TABLE_WRAPPER_BOX_PROPERTIES = {
276280 'bottom',
88 from ..urls import iri_to_uri, url_is_absolute
99 from .properties import Dimension
1010
11 # http://dev.w3.org/csswg/css3-values/#angles
11 # https://drafts.csswg.org/css-values-3/#angles
1212 # 1<unit> is this many radians.
1313 ANGLE_TO_RADIANS = {
1414 'rad': 1,
1818 }
1919
2020 # How many CSS pixels is one <unit>?
21 # http://www.w3.org/TR/CSS21/syndata.html#length-units
21 # https://www.w3.org/TR/CSS21/syndata.html#length-units
2222 LENGTHS_TO_PIXELS = {
2323 'px': 1,
2424 'pt': 1. / 0.75,
2929 'q': 96. / 25.4 / 4, # LENGTHS_TO_PIXELS['mm'] / 4
3030 }
3131
32 # http://dev.w3.org/csswg/css-values/#resolution
32 # https://drafts.csswg.org/css-values/#resolution
3333 RESOLUTION_TO_DPPX = {
3434 'dppx': 1,
3535 'dpi': 1 / LENGTHS_TO_PIXELS['in'],
256256 def parse_position(tokens):
257257 """Parse background-position and object-position.
258258
259 See http://dev.w3.org/csswg/css3-background/#the-background-position
259 See https://drafts.csswg.org/css-backgrounds-3/#the-background-position
260260 https://drafts.csswg.org/css-images-3/#propdef-object-position
261261
262262 """
166166 if keyword in ('normal', 'bold'):
167167 return keyword
168168 if token.type == 'number' and token.int_value is not None:
169 if token.int_value in [100, 200, 300, 400, 500, 600, 700, 800, 900]:
169 if token.int_value in (100, 200, 300, 400, 500, 600, 700, 800, 900):
170170 return token.int_value
171171
172172
166166 def expand_list_style(name, tokens, base_url):
167167 """Expand the ``list-style`` shorthand property.
168168
169 See http://www.w3.org/TR/CSS21/generate.html#propdef-list-style
169 See https://www.w3.org/TR/CSS21/generate.html#propdef-list-style
170170
171171 """
172172 type_specified = image_specified = False
208208 def expand_border(base_url, name, tokens):
209209 """Expand the ``border`` shorthand property.
210210
211 See http://www.w3.org/TR/CSS21/box.html#propdef-border
211 See https://www.w3.org/TR/CSS21/box.html#propdef-border
212212
213213 """
214214 for suffix in ('-top', '-right', '-bottom', '-left'):
226226 def expand_border_side(name, tokens):
227227 """Expand the ``border-*`` shorthand properties.
228228
229 See http://www.w3.org/TR/CSS21/box.html#propdef-border-top
229 See https://www.w3.org/TR/CSS21/box.html#propdef-border-top
230230
231231 """
232232 for token in tokens:
245245 def expand_background(base_url, name, tokens):
246246 """Expand the ``background`` shorthand property.
247247
248 See http://dev.w3.org/csswg/css3-background/#the-background
248 See https://drafts.csswg.org/css-backgrounds-3/#the-background
249249
250250 """
251251 properties = [
545545 def expand_word_wrap(name, tokens):
546546 """Expand the ``word-wrap`` legacy property.
547547
548 See http://www.w3.org/TR/css3-text/#overflow-wrap
548 See https://www.w3.org/TR/css-text-3/#overflow-wrap
549549
550550 """
551551 keyword = overflow_wrap(tokens)
00 """Validate properties.
11
2 See http://www.w3.org/TR/CSS21/propidx.html and various CSS3 modules.
2 See https://www.w3.org/TR/CSS21/propidx.html and various CSS3 modules.
33
44 """
55
838838 return None
839839 if values:
840840 return tuple(values)
841
842
843 @property()
844 def font_variation_settings(tokens):
845 """``font-variation-settings`` property validation."""
846 if len(tokens) == 1 and get_keyword(tokens[0]) == 'normal':
847 return 'normal'
848
849 @comma_separated_list
850 def font_variation_settings_list(tokens):
851 if len(tokens) == 2:
852 key, value = tokens
853 if key.type == 'string' and value.type == 'number':
854 return key.value, value.value
855
856 return font_variation_settings_list(tokens)
841857
842858
843859 @property()
12641280
12651281
12661282 @property(unstable=True)
1283 def image_orientation(tokens):
1284 """Validation for ``image-orientation``."""
1285 keyword = get_single_keyword(tokens)
1286 if keyword in ('none', 'from-image'):
1287 return keyword
1288 angle, flip = None, None
1289 for token in tokens:
1290 keyword = get_keyword(token)
1291 if keyword == 'flip':
1292 if flip is not None:
1293 return
1294 flip = True
1295 continue
1296 if angle is None:
1297 angle = get_angle(token)
1298 if angle is not None:
1299 continue
1300 return
1301 angle = 0 if angle is None else angle
1302 flip = False if flip is None else flip
1303 return (angle, flip)
1304
1305
1306 @property(unstable=True)
12671307 def size(tokens):
12681308 """``size`` property validation.
12691309
1270 See http://www.w3.org/TR/css3-page/#page-size-prop
1310 See https://www.w3.org/TR/css-page-3/#page-size-prop
12711311
12721312 """
12731313 lengths = [get_length(token, negative=False) for token in tokens]
15091549 length = get_length(args[0], percentage=True)
15101550 if name == 'rotate' and angle is not None:
15111551 transforms.append((name, angle))
1512 elif name == 'skewx' and angle is not None:
1552 elif name in ('skewx', 'skew') and angle is not None:
15131553 transforms.append(('skew', (angle, 0)))
15141554 elif name == 'skewy' and angle is not None:
15151555 transforms.append(('skew', (0, angle)))
103103 """
104104 def __init__(self, title=None, authors=None, description=None,
105105 keywords=None, generator=None, created=None, modified=None,
106 attachments=None, custom=None):
106 attachments=None, lang=None, custom=None):
107107 #: The title of the document, as a string or :obj:`None`.
108108 #: Extracted from the ``<title>`` element in HTML
109109 #: and written to the ``/Title`` info field in PDF.
129129 self.generator = generator
130130 #: The creation date of the document, as a string or :obj:`None`.
131131 #: Dates are in one of the six formats specified in
132 #: `W3C’s profile of ISO 8601 <http://www.w3.org/TR/NOTE-datetime>`_.
132 #: `W3C’s profile of ISO 8601 <https://www.w3.org/TR/NOTE-datetime>`_.
133133 #: Extracted from the ``<meta name=dcterms.created>`` element in HTML
134134 #: and written to the ``/CreationDate`` info field in PDF.
135135 self.created = created
136136 #: The modification date of the document, as a string or :obj:`None`.
137137 #: Dates are in one of the six formats specified in
138 #: `W3C’s profile of ISO 8601 <http://www.w3.org/TR/NOTE-datetime>`_.
138 #: `W3C’s profile of ISO 8601 <https://www.w3.org/TR/NOTE-datetime>`_.
139139 #: Extracted from the ``<meta name=dcterms.modified>`` element in HTML
140140 #: and written to the ``/ModDate`` info field in PDF.
141141 self.modified = modified
144144 #: Extracted from the ``<link rel=attachment>`` elements in HTML
145145 #: and written to the ``/EmbeddedFiles`` dictionary in PDF.
146146 self.attachments = attachments or []
147 #: Document language as BCP 47 language tags.
148 #: Extracted from ``<html lang=lang>`` in HTML.
149 self.lang = lang
147150 #: Custom metadata, as a dict whose keys are the metadata names and
148151 #: values are the metadata values.
149152 self.custom = custom or {}
215218 [Page(page_box) for page_box in page_boxes],
216219 DocumentMetadata(**get_html_metadata(html)),
217220 html.url_fetcher, font_config, optimize_size)
221 rendering._html = html
218222 return rendering
219223
220224 def __init__(self, pages, metadata, url_fetcher, font_config,
240244 # Set of flags for PDF size optimization. Can contain "images" and
241245 # "fonts".
242246 self._optimize_size = optimize_size
247
248 def build_element_structure(self, structure, etree_element=None):
249 if etree_element is None:
250 etree_element = self._html.etree_element
251 structure[etree_element] = {'parent': None}
252 for child in etree_element:
253 structure[child] = {'parent': etree_element}
254 self.build_element_structure(structure, child)
243255
244256 def copy(self, pages='all'):
245257 """Take a subset of the pages.
332344
333345 """
334346 pdf = generate_pdf(
335 self.pages, self.url_fetcher, self.metadata, self.fonts, target,
336 zoom, attachments, finisher, self._optimize_size, identifier,
347 self, target, zoom, attachments, self._optimize_size, identifier,
337348 variant, version, custom_metadata)
338349
339350 if finisher:
7777
7878 def draw_stacking_context(stream, stacking_context):
7979 """Draw a ``stacking_context`` on ``stream``."""
80 # See http://www.w3.org/TR/CSS2/zindex.html
80 # See https://www.w3.org/TR/CSS2/zindex.html
8181 with stacked(stream):
8282 box = stacking_context.box
83
84 stream.begin_marked_content(box, mcid=True)
8385
8486 # apply the viewport_overflow to the html box, see #35
8587 if box.is_for_root_element and (
113115 if box.transformation_matrix.determinant:
114116 stream.transform(*box.transformation_matrix.values)
115117 else:
118 stream.end_marked_content()
116119 return
117120
118121 # Point 1 is done in draw_page
157160 if isinstance(block, boxes.ReplacedBox):
158161 draw_border(stream, block)
159162 draw_replacedbox(stream, block)
160 else:
161 for child in block.children:
162 if isinstance(child, boxes.LineBox):
163 elif block.children:
164 if block != box:
165 stream.begin_marked_content(block, mcid=True)
166 if isinstance(block.children[-1], boxes.LineBox):
167 for child in block.children:
163168 draw_inline_level(
164169 stream, stacking_context.page, child)
170 if block != box:
171 stream.end_marked_content()
165172
166173 # Point 8
167174 for child_context in stacking_context.zero_z_contexts:
181188 stream.set_alpha(box.style['opacity'], stroke=True, fill=True)
182189 stream.draw_x_object(group_id)
183190 stream.pop_state()
191
192 stream.end_marked_content()
184193
185194
186195 def rounded_box_path(stream, radii):
236245 # Background color
237246 if bg.color.alpha > 0:
238247 with stacked(stream):
248 stream.set_color_rgb(*bg.color[:3])
249 stream.set_alpha(bg.color.alpha)
239250 painting_area = bg.layers[-1].painting_area
240251 if painting_area:
241252 if bleed:
249260 stream.clip()
250261 stream.end()
251262 stream.rectangle(*stream.page_rectangle)
252 stream.set_color_rgb(*bg.color[:3])
253 stream.set_alpha(bg.color.alpha)
254263 stream.fill()
255264
256265 if bleed and marks:
263272 svg = f'''
264273 <svg height="{height}" width="{width}"
265274 fill="transparent" stroke="black" stroke-width="1"
266 xmlns="http://www.w3.org/2000/svg">
275 xmlns="https://www.w3.org/2000/svg">
267276 '''
268277 if 'crop' in marks:
269278 svg += f'''
414423 # We need a plan to draw beautiful borders, and that's difficult, no need
415424 # to lie. Let's try to find the cases that we can handle in a smart way.
416425
426 def get_columns_with_rule():
427 """Yield columns that have a rule drawn on the left."""
428 skip_next = True
429 for child in box.children:
430 if child.style['column_span'] == 'all':
431 skip_next = True
432 elif skip_next:
433 skip_next = False
434 else:
435 yield child
436
417437 def draw_column_border():
418438 """Draw column borders."""
419439 columns = (
422442 box.style['column_count'] != 'auto'))
423443 if columns and box.style['column_rule_width']:
424444 border_widths = (0, 0, 0, box.style['column_rule_width'])
425 for child in box.children[1:]:
445 for child in get_columns_with_rule():
426446 with stacked(stream):
427447 position_x = (child.position_x - (
428448 box.style['column_rule_width'] +
429449 box.style['column_gap']) / 2)
430450 border_box = (
431451 position_x, child.position_y,
432 box.style['column_rule_width'], box.height)
452 box.style['column_rule_width'], child.height)
433453 clip_border_segment(
434454 stream, box.style['column_rule_style'],
435455 box.style['column_rule_width'], 'left', border_box,
516536 pi" Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372],
517537 wonderfully explained by Dr Rob.
518538
519 http://mathforum.org/dr.math/faq/formulas/
539 https://mathforum.org/dr.math/faq/formulas/
520540
521541 """
522542 x = (a - b) / (a + b)
665685
666686
667687 def draw_rounded_border(stream, box, style, color):
668 rounded_box_path(stream, box.rounded_padding_box())
669688 if style in ('ridge', 'groove'):
670 rounded_box_path(stream, box.rounded_box_ratio(1 / 2))
671689 stream.set_color_rgb(*color[0][:3])
672690 stream.set_alpha(color[0][3])
691 rounded_box_path(stream, box.rounded_padding_box())
692 rounded_box_path(stream, box.rounded_box_ratio(1 / 2))
673693 stream.fill(even_odd=True)
694 stream.set_color_rgb(*color[1][:3])
695 stream.set_alpha(color[1][3])
674696 rounded_box_path(stream, box.rounded_box_ratio(1 / 2))
675697 rounded_box_path(stream, box.rounded_border_box())
676 stream.set_color_rgb(*color[1][:3])
677 stream.set_alpha(color[1][3])
678698 stream.fill(even_odd=True)
679699 return
700 stream.set_color_rgb(*color[:3])
701 stream.set_alpha(color[3])
702 rounded_box_path(stream, box.rounded_padding_box())
680703 if style == 'double':
681704 rounded_box_path(stream, box.rounded_box_ratio(1 / 3))
682705 rounded_box_path(stream, box.rounded_box_ratio(2 / 3))
683706 rounded_box_path(stream, box.rounded_border_box())
684 stream.set_color_rgb(*color[:3])
685 stream.set_alpha(color[3])
686707 stream.fill(even_odd=True)
687708
688709
689710 def draw_rect_border(stream, box, widths, style, color):
690711 bbx, bby, bbw, bbh = box
691712 bt, br, bb, bl = widths
692 stream.rectangle(*box)
693713 if style in ('ridge', 'groove'):
714 stream.set_color_rgb(*color[0][:3])
715 stream.set_alpha(color[0][3])
716 stream.rectangle(*box)
694717 stream.rectangle(
695718 bbx + bl / 2, bby + bt / 2,
696719 bbw - (bl + br) / 2, bbh - (bt + bb) / 2)
697 stream.set_color_rgb(*color[0][:3])
698 stream.set_alpha(color[0][3])
699720 stream.fill(even_odd=True)
700721 stream.rectangle(
701722 bbx + bl / 2, bby + bt / 2,
705726 stream.set_alpha(color[1][3])
706727 stream.fill(even_odd=True)
707728 return
729 stream.set_color_rgb(*color[:3])
730 stream.set_alpha(color[3])
731 stream.rectangle(*box)
708732 if style == 'double':
709733 stream.rectangle(
710734 bbx + bl / 3, bby + bt / 3,
713737 bbx + bl * 2 / 3, bby + bt * 2 / 3,
714738 bbw - (bl + br) * 2 / 3, bbh - (bt + bb) * 2 / 3)
715739 stream.rectangle(bbx + bl, bby + bt, bbw - bl - br, bbh - bt - bb)
716 stream.set_color_rgb(*color[:3])
717 stream.set_alpha(color[3])
718740 stream.fill(even_odd=True)
719741
720742
935957 draw_background(stream, box.background)
936958 draw_border(stream, box)
937959 if isinstance(box, (boxes.InlineBox, boxes.LineBox)):
960 link_annotation = None
938961 if isinstance(box, boxes.LineBox):
939962 text_overflow = box.text_overflow
940963 block_ellipsis = box.block_ellipsis
964 else:
965 link_annotation = box.link_annotation
941966 ellipsis = 'none'
967 if link_annotation:
968 stream.begin_marked_content(box, mcid=True, tag='Link')
942969 for i, child in enumerate(box.children):
943970 if i == len(box.children) - 1:
944971 # Last child
955982 draw_inline_level(
956983 stream, page, child, child_offset_x, text_overflow,
957984 ellipsis)
985 if link_annotation:
986 stream.end_marked_content()
958987 elif isinstance(box, boxes.InlineReplacedBox):
959988 draw_replacedbox(stream, box)
960989 else:
10711100 utf8_text = textbox.pango_layout.text.encode()
10721101 previous_utf8_position = 0
10731102
1074 runs = [first_line.runs[0]]
1075 while runs[-1].next != ffi.NULL:
1076 runs.append(runs[-1].next)
1077
10781103 matrix = Matrix(1, 0, 0, -1, x, y)
10791104 if angle:
1080 matrix = Matrix(a=cos(angle), b=-sin(angle),
1081 c=sin(angle), d=cos(angle)) @ matrix
1105 a, c = cos(angle), sin(angle)
1106 b, d = -c, a
1107 matrix = Matrix(a, b, c, d) @ matrix
10821108 stream.text_matrix(*matrix.values)
10831109 last_font = None
10841110 string = ''
10851111 x_advance = 0
10861112 emojis = []
1087 for run in runs:
1113 run = first_line.runs[0]
1114 while run != ffi.NULL:
10881115 # Pango objects
1089 glyph_item = ffi.cast('PangoGlyphItem *', run.data)
1116 glyph_item = run.data
1117 run = run.next
10901118 glyph_string = glyph_item.glyphs
10911119 glyphs = glyph_string.glyphs
10921120 num_glyphs = glyph_string.num_glyphs
11061134 if string:
11071135 stream.show_text(string)
11081136 string = ''
1137 if last_font is None or font.bitmap != last_font.bitmap:
1138 stream.set_font_size(
1139 font.hash, 1 if font.bitmap else font_size)
11091140 last_font = font
1110 stream.set_font_size(font.hash, 1 if font.bitmap else font_size)
11111141 string += '<'
11121142 for i in range(num_glyphs):
11131143 glyph_info = glyphs[i]
00 """Classes for all types of boxes in the CSS formatting structure / box model.
11
2 See http://www.w3.org/TR/CSS21/visuren.html
2 See https://www.w3.org/TR/CSS21/visuren.html
33
44 Names are the same as in CSS 2.1 with the exception of ``TextBox``. In
55 WeasyPrint, any text is in a ``TextBox``. What CSS calls anonymous inline boxes
66 are text boxes but not all text boxes are anonymous inline boxes.
77
8 See http://www.w3.org/TR/CSS21/visuren.html#anonymous
8 See https://www.w3.org/TR/CSS21/visuren.html#anonymous
99
1010 Abstract classes, should not be instantiated:
1111
5555 class Box:
5656 """Abstract base class for all boxes."""
5757 # Definitions for the rules generating anonymous table boxes
58 # http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
58 # https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
5959 proper_table_child = False
6060 internal_table_or_caption = False
6161 tabular_container = False
7676 transformation_matrix = None
7777 bookmark_label = None
7878 string_set = None
79 download_name = None
8079 footnote = None
8180 cached_counter_values = None
8281 missing_link = None
122121
123122 """
124123 # Overridden in ParentBox to also translate children, if any.
125 if dx == 0 and dy == 0:
124 if dx == dy == 0:
126125 return
127126 self.position_x += dx
128127 self.position_y += dy
189188 def hit_area(self):
190189 """Return the (x, y, w, h) rectangle where the box is clickable."""
191190 # "Border area. That's the area that hit-testing is done on."
192 # http://lists.w3.org/Archives/Public/www-style/2012Jun/0318.html
191 # https://lists.w3.org/Archives/Public/www-style/2012Jun/0318.html
193192 # TODO: manage the border radii, use outer_border_radii instead
194193 return (self.border_box_x(), self.border_box_y(),
195194 self.border_width(), self.border_height())
221220 height = self.border_height() - bt - bb
222221
223222 # Fix overlapping curves
224 # See http://www.w3.org/TR/css3-background/#corner-overlap
223 # See https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
225224 ratio = min([1] + [
226225 extent / sum_radii
227 for extent, sum_radii in [
226 for extent, sum_radii in (
228227 (width, tlrx + trrx),
229228 (width, blrx + brrx),
230229 (height, tlry + blry),
231230 (height, trry + brry),
232 ]
231 )
233232 if sum_radii > 0
234233 ])
235234 return (
453452 inline box.
454453
455454 """
455 link_annotation = None
456
456457 def hit_area(self):
457458 """Return the (x, y, w, h) rectangle where the box is clickable."""
458459 # Use line-height (margin_height) rather than border_height
535536 class TableBox(BlockLevelBox, ParentBox):
536537 """Box for elements with ``display: table``"""
537538 # Definitions for the rules generating anonymous table boxes
538 # http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
539 # https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
539540 tabular_container = True
540541
541542 def all_children(self):
3737 ('table-caption',): boxes.TableCaptionBox,
3838 }
3939
40 # http://stackoverflow.com/questions/16317534/
40 # https://stackoverflow.com/questions/16317534/
4141 ASCII_TO_WIDE = {i: chr(i + 0xfee0) for i in range(0x21, 0x7f)}
4242 ASCII_TO_WIDE.update({0x20: '\u3000', 0x2D: '\u2212'})
4343
114114 ]
115115
116116 ``TextBox``es are anonymous inline boxes:
117 See http://www.w3.org/TR/CSS21/visuren.html#anonymous
117 See https://www.w3.org/TR/CSS21/visuren.html#anonymous
118118
119119 """
120120 if not isinstance(element.tag, str):
332332 else:
333333 if image_type == 'url':
334334 # image may be None here too, in case the image is not available.
335 image = get_image_from_uri(url=image)
335 image = get_image_from_uri(
336 url=image, orientation=style['image_orientation'])
336337 if image is not None:
337338 box = boxes.InlineReplacedBox.anonymous_from(box, image)
338339 children.append(box)
420421 if origin != 'external':
421422 # Embedding internal references is impossible
422423 continue
423 image = get_image_from_uri(url=uri)
424 image = get_image_from_uri(
425 url=uri, orientation=parent_box.style['image_orientation'])
424426 if image is not None:
425427 content_boxes.append(
426428 boxes.InlineReplacedBox.anonymous_from(parent_box, image))
699701 # 'auto' is the initial value but is not valid in stylesheet:
700702 # there was no counter-increment declaration for this element.
701703 # (Or the winning value was 'initial'.)
702 # http://dev.w3.org/csswg/css3-lists/#declaring-a-list-item
704 # https://drafts.csswg.org/css-lists-3/#declaring-a-list-item
703705 if 'list-item' in style['display']:
704706 counter_increment = [('list-item', 1)]
705707 else:
761763
762764 Take and return a ``Box`` object.
763765
764 See http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
766 See https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
765767
766768 """
767769 if not isinstance(box, boxes.ParentBox) or box.is_running():
798800 # TODO: Maybe only remove text if internal is also
799801 # a proper table descendant of box.
800802 # This is what the spec says, but maybe not what browsers do:
801 # http://lists.w3.org/Archives/Public/www-style/2011Oct/0567
803 # https://lists.w3.org/Archives/Public/www-style/2011Oct/0567
802804
803805 # Last child
804806 internal, text = children[-2:]
874876 Because of colspan/rowspan works, grid_y is implicitly the index of a row,
875877 but grid_x is an explicit attribute on cells, columns and column group.
876878
877 http://www.w3.org/TR/CSS21/tables.html#model
878 http://www.w3.org/TR/CSS21/tables.html#table-layout
879 https://www.w3.org/TR/CSS21/tables.html#model
880 https://www.w3.org/TR/CSS21/tables.html#table-layout
879881
880882 """
881883 # Group table children by type
936938 # Assign a (x,y) position in the grid to each cell.
937939 # rowspan can not extend beyond a row group, so each row group
938940 # is independent.
939 # http://www.w3.org/TR/CSS21/tables.html#table-layout
941 # https://www.w3.org/TR/CSS21/tables.html#table-layout
940942 # Column 0 is on the left if direction is ltr, right if rtl.
941943 # This algorithm does not change.
942944 grid_height = 0
954956 grid_x += 1
955957 cell.grid_x = grid_x
956958 new_grid_x = grid_x + cell.colspan
957 # http://www.w3.org/TR/html401/struct/tables.html#adef-rowspan
959 # https://www.w3.org/TR/html401/struct/tables.html#adef-rowspan
958960 if cell.rowspan != 1:
959961 max_rowspan = len(occupied_cells_by_row) + 1
960962 if cell.rowspan == 0:
10291031 width = box_style[f'border_{side}_width']
10301032 color = get_color(box_style, f'border_{side}_color')
10311033
1032 # http://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution
1034 # https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution
10331035 score = ((1 if style == 'hidden' else 0), width, style_scores[style])
10341036
10351037 style = style_map.get(style, style)
10501052 # The order is important here:
10511053 # "A style set on a cell wins over one on a row, which wins over a
10521054 # row group, column, column group and, lastly, table"
1053 # See http://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution
1055 # See https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution
10541056 strong_null_border = (
10551057 (1, 0, style_scores['hidden']), ('hidden', 0, TRANSPARENT))
10561058 grid_y = 0
11411143 x=0, y=grid_height, w=grid_width))
11421144 # "UAs must compute an initial left and right border width for the table
11431145 # by examining the first and last cells in the first row of the table."
1144 # http://www.w3.org/TR/CSS21/tables.html#collapsing-borders
1146 # https://www.w3.org/TR/CSS21/tables.html#collapsing-borders
11451147 # ... so h=1, not grid_height:
11461148 set_transparent_border(table, 'left', max_vertical_width(
11471149 x=0, y=0, h=1))
11561158
11571159 Take and return a ``Box`` object.
11581160
1159 See http://www.w3.org/TR/css-flexbox-1/#flex-items
1161 See https://www.w3.org/TR/css-flexbox-1/#flex-items
11601162
11611163 """
11621164 if not isinstance(box, boxes.ParentBox) or box.is_running():
11931195 def process_whitespace(box, following_collapsible_space=False):
11941196 """First part of "The 'white-space' processing model".
11951197
1196 See http://www.w3.org/TR/CSS21/text.html#white-space-model
1197 http://dev.w3.org/csswg/css3-text/#white-space-rules
1198 See https://www.w3.org/TR/CSS21/text.html#white-space-model
1199 https://drafts.csswg.org/css-text-3/#white-space-rules
11981200
11991201 """
12001202 if isinstance(box, boxes.TextBox):
12171219 # TODO: this should be language-specific
12181220 # Could also replace with a zero width space character (U+200B),
12191221 # or no character
1220 # CSS3: http://www.w3.org/TR/css3-text/#line-break-transform
1222 # CSS3: https://www.w3.org/TR/css-text-3/#overflow-wrap
12211223 text = text.replace('\n', ' ')
12221224
12231225 if space_collapse:
12311233
12321234 box.text = text
12331235
1234 elif isinstance(box, boxes.ParentBox) and not box.is_running():
1236 elif isinstance(box, boxes.ParentBox):
12351237 for child in box.children:
12361238 if isinstance(child, (boxes.TextBox, boxes.InlineBox)):
1237 following_collapsible_space = process_whitespace(
1239 child_collapsible_space = process_whitespace(
12381240 child, following_collapsible_space)
1239 else:
1240 process_whitespace(child)
1241 if child.is_in_normal_flow():
1242 following_collapsible_space = False
1243
1244 return following_collapsible_space
1241 if box.is_in_normal_flow() and child.is_in_normal_flow():
1242 following_collapsible_space = child_collapsible_space
1243 elif child.is_in_normal_flow():
1244 following_collapsible_space = False
1245
1246 return following_collapsible_space and not box.is_running()
12451247
12461248
12471249 def process_text_transform(box):
12511253 box.text = {
12521254 'uppercase': lambda text: text.upper(),
12531255 'lowercase': lambda text: text.lower(),
1254 # Python’s unicode.captitalize is not the same.
1255 'capitalize': lambda text: text.title(),
1256 'capitalize': capitalize,
12561257 'full-width': lambda text: text.translate(ASCII_TO_WIDE),
12571258 }[text_transform](box.text)
12581259 if box.style['hyphens'] == 'none':
12641265 process_text_transform(child)
12651266
12661267
1268 def capitalize(text):
1269 """Capitalize words according to CSS’s "text-transform: capitalize"."""
1270 letter_found = False
1271 output = ''
1272 for letter in text:
1273 category = unicodedata.category(letter)[0]
1274 if not letter_found and category in ('L', 'N'):
1275 letter_found = True
1276 letter = letter.upper()
1277 elif category == 'Z':
1278 letter_found = False
1279 output += letter
1280 return output
1281
1282
12671283 def inline_in_block(box):
12681284 """Build the structure of lines inside blocks and return a new box tree.
12691285
12731289 This line box will be broken into multiple lines later.
12741290
12751291 This is the first case in
1276 http://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
1292 https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
12771293
12781294 Eg.::
12791295
13841400 in an anonymous block-level box.
13851401
13861402 This is the second case in
1387 http://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
1403 https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
13881404
13891405 Eg. if this is given::
13901406
15381554 Like backgrounds, ``overflow`` on the root element must be propagated
15391555 to the viewport.
15401556
1541 See http://www.w3.org/TR/CSS21/visufx.html#overflow
1557 See https://www.w3.org/TR/CSS21/visufx.html#overflow
15421558 """
15431559 chosen_box = root_box
15441560 if (root_box.element_tag.lower() == 'html' and
2424 string=HTML5_UA, counter_style=HTML5_UA_COUNTER_STYLE)
2525 HTML5_PH_STYLESHEET = CSS(string=HTML5_PH)
2626
27 # http://whatwg.org/C#space-character
27 # https://html.spec.whatwg.org/multipage/#space-character
2828 HTML_WHITESPACE = ' \t\n\f\r'
2929 HTML_SPACE_SEPARATED_TOKENS_RE = re.compile(f'[^{HTML_WHITESPACE}]+')
3030
3636 :returns: A new Unicode string.
3737
3838 This is used for `ASCII case-insensitive
39 <http://whatwg.org/C#ascii-case-insensitive>`_ matching.
39 <https://whatwg.org/C#ascii-case-insensitive>`_ matching.
4040
4141 This is different from the :meth:`str.lower` method of Unicode strings
4242 which also affect non-ASCII characters,
4343 sometimes mapping them into the ASCII range:
4444
45 >>> keyword = u'Bac\N{KELVIN SIGN}ground'
46 >>> assert keyword.lower() == u'background'
45 >>> keyword = 'Bac\N{KELVIN SIGN}ground'
46 >>> assert keyword.lower() == 'background'
4747 >>> assert ascii_lower(keyword) != keyword.lower()
48 >>> assert ascii_lower(keyword) == u'bac\N{KELVIN SIGN}ground'
48 >>> assert ascii_lower(keyword) == 'bac\N{KELVIN SIGN}ground'
4949
5050 """
5151 # This turns out to be faster than unicode.translate()
113113
114114 Return either an image or the alt-text.
115115
116 See: http://www.w3.org/TR/html5/embedded-content-1.html#the-img-element
116 See: https://www.w3.org/TR/html5/embedded-content-1.html#the-img-element
117117
118118 """
119119 src = get_url_attribute(element, 'src', base_url)
120120 alt = element.get('alt')
121121 if src:
122 image = get_image_from_uri(url=src)
122 image = get_image_from_uri(
123 url=src, orientation=box.style['image_orientation'])
123124 if image is not None:
124125 return [make_replaced_box(element, box, image)]
125126 else:
153154 src = get_url_attribute(element, 'src', base_url)
154155 type_ = element.get('type', '').strip()
155156 if src:
156 image = get_image_from_uri(url=src, forced_mime_type=type_)
157 image = get_image_from_uri(
158 url=src, forced_mime_type=type_,
159 orientation=box.style['image_orientation'])
157160 if image is not None:
158161 return [make_replaced_box(element, box, image)]
159162 # No fallback.
170173 data = get_url_attribute(element, 'data', base_url)
171174 type_ = element.get('type', '').strip()
172175 if data:
173 image = get_image_from_uri(url=data, forced_mime_type=type_)
176 image = get_image_from_uri(
177 url=data, forced_mime_type=type_,
178 orientation=box.style['image_orientation'])
174179 if image is not None:
175180 return [make_replaced_box(element, box, image)]
176181 # The element’s children are the fallback.
193198 """Handle the ``span`` attribute."""
194199 if isinstance(box, boxes.TableColumnBox) and box.span > 1:
195200 # Generate multiple boxes
196 # http://lists.w3.org/Archives/Public/www-style/2011Nov/0293.html
201 # https://lists.w3.org/Archives/Public/www-style/2011Nov/0293.html
197202 return [box.copy() for _i in range(box.span)]
198203 return [box]
199204
204209 """Handle the ``colspan``, ``rowspan`` attributes."""
205210 if isinstance(box, boxes.TableCellBox):
206211 # HTML 4.01 gives special meaning to colspan=0
207 # http://www.w3.org/TR/html401/struct/tables.html#adef-rowspan
212 # https://www.w3.org/TR/html401/struct/tables.html#adef-rowspan
208213 # but HTML 5 removed it
209 # http://www.w3.org/TR/html5/tabular-data.html#attr-tdth-colspan
214 # https://html.spec.whatwg.org/multipage/tables.html#attr-tdth-colspan
210215 # rowspan=0 is still there though.
211216 try:
212217 box.colspan = max(int(element.get('colspan', '').strip()), 1)
223228 def handle_a(element, box, _get_image_from_uri, base_url):
224229 """Handle the ``rel`` attribute."""
225230 box.is_attachment = element_has_link_type(element, 'attachment')
226 box.download_name = element.get('download')
227231 return [box]
228232
229233
251255
252256 Relevant specs:
253257
254 http://www.whatwg.org/html#the-title-element
255 http://www.whatwg.org/html#standard-metadata-names
256 http://wiki.whatwg.org/wiki/MetaExtensions
257 http://microformats.org/wiki/existing-rel-values#HTML5_link_type_extensions
258 https://www.whatwg.org/html#the-title-element
259 https://www.whatwg.org/html#standard-metadata-names
260 https://wiki.whatwg.org/wiki/MetaExtensions
261 https://microformats.org/wiki/existing-rel-values#HTML5_link_type_extensions
258262
259263 """
260264 title = None
266270 modified = None
267271 attachments = []
268272 custom = {}
273 lang = html.etree_element.attrib.get('lang', None)
269274 for element in html.wrapper_element.query_all('title', 'meta', 'link'):
270275 element = element.etree_element
271276 if element.tag == 'title' and title is None:
304309 return dict(title=title, description=description, generator=generator,
305310 keywords=keywords, authors=authors,
306311 created=created, modified=modified,
307 attachments=attachments, custom=custom)
312 attachments=attachments, lang=lang, custom=custom)
308313
309314
310315 def strip_whitespace(string):
311316 """Use the HTML definition of "space character",
312317 not all Unicode Whitespace.
313318
314 http://www.whatwg.org/html#strip-leading-and-trailing-whitespace
315 http://www.whatwg.org/html#space-character
319 https://www.whatwg.org/html#strip-leading-and-trailing-whitespace
320 https://www.whatwg.org/html#space-character
316321
317322 """
318323 return string.strip(HTML_WHITESPACE)
66 from math import inf
77 from xml.etree import ElementTree
88
9 from PIL import Image
9 from PIL import Image, ImageFile, ImageOps
1010
1111 from .layout.percent import percentage
1212 from .logger import LOGGER
1313 from .svg import SVG
1414 from .urls import URLFetchingError, fetch
15
16 # Don’t crash when converting truncated images
17 ImageFile.LOAD_TRUNCATED_IMAGES = True
1518
1619
1720 class ImageLoadingError(ValueError):
8891
8992
9093 def get_image_from_uri(cache, url_fetcher, optimize_size, url,
91 forced_mime_type=None, context=None):
94 forced_mime_type=None, context=None,
95 orientation='from-image'):
9296 """Get an Image instance from an image URI."""
9397 if url in cache:
9498 return cache[url]
130134 else:
131135 # Store image id to enable cache in Stream.add_image
132136 image_id = md5(url.encode()).hexdigest()
137 if orientation == 'from-image':
138 if 'exif' in pillow_image.info:
139 pillow_image = ImageOps.exif_transpose(
140 pillow_image)
141 elif orientation != 'none':
142 angle, flip = orientation
143 if angle > 0:
144 rotation = getattr(
145 Image.Transpose, f'ROTATE_{angle}')
146 pillow_image = pillow_image.transpose(rotation)
147 if flip:
148 pillow_image = pillow_image.transpose(
149 Image.Transpose.FLIP_LEFT_RIGHT)
133150 image = RasterImage(pillow_image, image_id, optimize_size)
134151
135152 except (URLFetchingError, ImageLoadingError) as exception:
148165 ``positions`` is a list of ``None``, or ``Dimension`` in px or %. 0 is the
149166 starting point, 1 the ending point.
150167
151 See http://dev.w3.org/csswg/css-images-3/#color-stop-syntax.
168 See https://drafts.csswg.org/css-images-3/#color-stop-syntax.
152169
153170 Return processed color stops, as a list of floats in px.
154171
205222
206223 def gradient_average_color(colors, positions):
207224 """
208 http://dev.w3.org/csswg/css-images-3/#gradient-average-color
225 https://drafts.csswg.org/css-images-3/#gradient-average-color
209226 """
210227 nb_stops = len(positions)
211228 assert nb_stops > 1
55 Boxes in the new tree have *used values* in their ``position_x``,
66 ``position_y``, ``width`` and ``height`` attributes, amongst others.
77
8 See http://www.w3.org/TR/CSS21/cascade.html#used-value
8 See https://www.w3.org/TR/CSS21/cascade.html#used-value
99
1010 """
1111
3333 page_break = root_box.style['break_before']
3434
3535 # TODO: take care of text direction and writing mode
36 # https://www.w3.org/TR/css3-page/#progression
36 # https://www.w3.org/TR/css-page-3/#progression
3737 if page_break == 'right':
3838 right_page = True
3939 elif page_break == 'left':
232232 self.running_elements = defaultdict(lambda: defaultdict(lambda: []))
233233 self.current_page = None
234234 self.forced_break = False
235 self.broken_out_of_flow = []
235 self.broken_out_of_flow = {}
236236 self.in_column = False
237237
238238 # Cache
251251 self._excluded_shapes_lists.append(self.excluded_shapes)
252252
253253 def finish_block_formatting_context(self, root_box):
254 # See http://www.w3.org/TR/CSS2/visudet.html#root-height
254 # See https://www.w3.org/TR/CSS2/visudet.html#root-height
255255 if root_box.style['height'] == 'auto' and self.excluded_shapes:
256256 box_bottom = root_box.content_box_y() + root_box.height
257257 max_shape_bottom = max([
284284 4: ['Third Header', '3.5th Header']}
285285
286286 Value depends on current page.
287 http://dev.w3.org/csswg/css-gcpm/#funcdef-string
287 https://drafts.csswg.org/css-gcpm/#funcdef-string
288288
289289 :param store: dictionary where the resolved value is stored.
290290 :param page: current page.
330330
331331 def unlayout_footnote(self, footnote):
332332 """Remove a footnote from the layout and return it to the waitlist."""
333
334 # Handle unlayouting a footnote that hasn't been laid out yet (or has
335 # already been unlayout'd):
333 # TODO: Handle unlayouting a footnote that hasn't been laid out yet or
334 # has already been unlayouted
336335 if footnote not in self.footnotes:
337336 self.footnotes.append(footnote)
338337 if footnote in self.current_page_footnotes:
349348
350349 def _update_footnote_area(self):
351350 """Update the page bottom size and our footnote area height."""
352 if self.current_footnote_area.height != 'auto':
351 if self.current_footnote_area.height != 'auto' and not self.in_column:
353352 self.page_bottom += self.current_footnote_area.margin_height()
354353 self.current_footnote_area.children = self.current_page_footnotes
355354 if self.current_footnote_area.children:
356355 footnote_area = build.create_anonymous_boxes(
357356 self.current_footnote_area.deepcopy())
358 footnote_area, _, _, _, _, _ = block_level_layout(
357 footnote_area = block_level_layout(
359358 self, footnote_area, -inf, None,
360 self.current_footnote_area.page, True, [], [], [], False, None)
359 self.current_footnote_area.page)[0]
361360 self.current_footnote_area.height = footnote_area.height
362 self.page_bottom -= footnote_area.margin_height()
361 if not self.in_column:
362 self.page_bottom -= footnote_area.margin_height()
363363 last_child = footnote_area.children[-1]
364364 overflow = (
365365 last_child.position_y + last_child.margin_height() >
366 footnote_area.position_y + footnote_area.margin_height())
366 footnote_area.position_y + footnote_area.margin_height() -
367 footnote_area.margin_bottom)
367368 return overflow
368369 else:
369370 self.current_footnote_area.height = 0
2020 object.__setattr__(self, '_layout_done', True)
2121
2222 def translate(self, dx=0, dy=0, ignore_floats=False):
23 if dx == 0 and dy == 0:
23 if dx == dy == 0:
2424 return
2525 if self._layout_done:
2626 self._box.translate(dx, dy, ignore_floats)
4747
4848 @handle_min_max_width
4949 def absolute_width(box, context, cb_x, cb_y, cb_width, cb_height):
50 # http://www.w3.org/TR/CSS2/visudet.html#abs-replaced-width
50 # https://www.w3.org/TR/CSS2/visudet.html#abs-replaced-width
5151 ltr = (
5252 box.style.parent_style is None or
5353 box.style.parent_style['direction'] == 'ltr')
121121
122122
123123 def absolute_height(box, context, cb_x, cb_y, cb_width, cb_height):
124 # http://www.w3.org/TR/CSS2/visudet.html#abs-non-replaced-height
124 # https://www.w3.org/TR/CSS2/visudet.html#abs-non-replaced-height
125125 paddings_borders = (
126126 box.padding_top + box.padding_bottom +
127127 box.border_top_width + box.border_bottom_width)
228228 context, box, containing_block, fixed_boxes, bottom_space, skip_stack)
229229 placeholder.set_laid_out_box(new_box)
230230 if resume_at:
231 context.broken_out_of_flow.append((box, containing_block, resume_at))
231 context.broken_out_of_flow[placeholder] = (
232 box, containing_block, resume_at)
232233
233234
234235 def absolute_box_layout(context, box, containing_block, fixed_boxes,
235236 bottom_space, skip_stack):
236237 # TODO: handle inline boxes (point 10.1.4.1)
237 # http://www.w3.org/TR/CSS2/visudet.html#containing-block-details
238 # https://www.w3.org/TR/CSS2/visudet.html#containing-block-details
238239 if isinstance(containing_block, boxes.PageBox):
239240 cb_x = containing_block.content_box_x()
240241 cb_y = containing_block.content_box_y()
270271 box.style.parent_style is None or
271272 box.style.parent_style['direction'] == 'ltr')
272273
273 # http://www.w3.org/TR/CSS21/visudet.html#abs-replaced-width
274 # https://www.w3.org/TR/CSS21/visudet.html#abs-replaced-width
274275 if box.left == box.right == 'auto':
275276 # static position:
276277 if ltr:
306307 else:
307308 box.left = cb_width - (box.margin_width() + box.right)
308309
309 # http://www.w3.org/TR/CSS21/visudet.html#abs-replaced-height
310 # https://www.w3.org/TR/CSS21/visudet.html#abs-replaced-height
310311 if box.top == box.bottom == 'auto':
311312 box.top = box.position_y - cb_y
312313 if 'auto' in (box.top, box.bottom):
5050 images = []
5151 color = parse_color('transparent')
5252 else:
53 orientation = style['image_orientation']
5354 images = [
54 get_image_from_uri(url=value) if type_ == 'url' else value
55 get_image_from_uri(url=value, orientation=orientation)
56 if type_ == 'url' else value
5557 for type_, value in style['background_image']]
5658 color = get_color(style, 'background_color')
5759
1414
1515
1616 def block_level_layout(context, box, bottom_space, skip_stack,
17 containing_block, page_is_empty, absolute_boxes,
18 fixed_boxes, adjoining_margins, discard, max_lines):
17 containing_block, page_is_empty=True,
18 absolute_boxes=None, fixed_boxes=None,
19 adjoining_margins=None, discard=False, max_lines=None):
1920 """Lay out the block-level ``box``."""
21 absolute_boxes = [] if absolute_boxes is None else absolute_boxes
22 fixed_boxes = [] if fixed_boxes is None else fixed_boxes
23 adjoining_margins = [] if adjoining_margins is None else adjoining_margins
24
2025 if not isinstance(box, boxes.TableBox):
2126 resolve_percentages(box, containing_block)
2227
2631 box.margin_bottom = 0
2732
2833 if context.current_page > 1 and page_is_empty:
34 # When an unforced break occurs before or after a block-level box,
35 # any margins adjoining the break are truncated to zero.
2936 # TODO: this condition is wrong, it only works for blocks whose
3037 # parent breaks collapsing margins. It should work for blocks whose
3138 # one of the ancestors breaks collapsing margins.
96103 new_box.margin_bottom + new_box.padding_bottom +
97104 new_box.border_bottom_width)
98105 if columns_bottom_space:
106 remove_placeholders(
107 context, [new_box], absolute_boxes, fixed_boxes)
99108 bottom_space += columns_bottom_space
100109 result = columns_layout(
101110 context, box, bottom_space, skip_stack,
113122 new_box = result[0]
114123 if new_box and new_box.is_table_wrapper:
115124 # Don't collide with floats
116 # http://www.w3.org/TR/CSS21/visuren.html#floats
125 # https://www.w3.org/TR/CSS21/visuren.html#floats
117126 position_x, position_y, _ = avoid_collisions(
118127 context, new_box, containing_block, outer=False)
119128 new_box.translate(
133142 # TODO: what is the real text direction?
134143 direction = 'ltr'
135144
136 # http://www.w3.org/TR/CSS21/visudet.html#blockwidth
145 # https://www.w3.org/TR/CSS21/visudet.html#blockwidth
137146
138147 # These names are waaay too long
139148 margin_l = box.margin_left
175184 width = box.width = cb_width - (
176185 paddings_plus_borders + margin_l + margin_r)
177186 margin_sum = cb_width - paddings_plus_borders - width
178 if margin_l == 'auto' and margin_r == 'auto':
187 if margin_l == margin_r == 'auto':
179188 box.margin_left = margin_sum / 2
180189 box.margin_right = margin_sum / 2
181190 elif margin_l == 'auto' and margin_r != 'auto':
220229 adjoining_margins, bottom_space):
221230 stop = False
222231 resume_at = None
232 new_child = None
223233 out_of_flow_resume_at = None
224234
225235 child.position_y += collapse_margin(adjoining_margins)
226236 if child.is_absolutely_positioned():
227 placeholder = AbsolutePlaceholder(child)
237 new_child = placeholder = AbsolutePlaceholder(child)
228238 placeholder.index = index
229239 new_children.append(placeholder)
230240 if child.style['position'] == 'absolute':
256266 page = context.current_page
257267 context.running_elements[running_name][page].append(child)
258268
259 return stop, resume_at, out_of_flow_resume_at
269 return stop, resume_at, new_child, out_of_flow_resume_at
260270
261271
262272 def _break_line(context, box, line, new_children, lines_iterator,
339349 break
340350
341351 # TODO: this is incomplete.
342 # See http://dev.w3.org/csswg/css3-page/#allowed-pg-brk
352 # See https://drafts.csswg.org/css-page-3/#allowed-pg-brk
343353 # "When an unforced page break occurs here, both the adjoining
344354 # ‘margin-top’ and ‘margin-bottom’ are set to zero."
345355 # See https://github.com/Kozea/WeasyPrint/issues/115
422432
423433 if not new_containing_block.is_table_wrapper:
424434 resolve_percentages(child, new_containing_block)
425 if (child.is_in_normal_flow() and last_in_flow_child is None and
426 collapsing_with_children):
435 if last_in_flow_child is None and collapsing_with_children:
427436 # TODO: add the adjoining descendants' margin top to
428437 # [child.margin_top]
429438 old_collapsed_margin = collapse_margin(adjoining_margins)
430 if child.margin_top == 'auto':
439 # TODO: the margin-top value is set afterwards in
440 # block_level_layout, we shouldn’t duplicate this code
441 child_margin_top = child.margin_top
442 if child_margin_top == 'auto':
431443 child_margin_top = 0
432 else:
433 child_margin_top = child.margin_top
444 elif context.current_page > 1 and page_is_empty:
445 if box.style['margin_break'] == 'discard':
446 child_margin_top = 0
447 elif box.style['margin_break'] == 'auto':
448 if not context.forced_break:
449 child_margin_top = 0
434450 new_collapsed_margin = collapse_margin(
435451 adjoining_margins + [child_margin_top])
436452 collapsed_margin_difference = (
624640 new_children = []
625641 next_page = {'break': 'any', 'page': None}
626642 all_footnotes = []
627 broken_out_of_flow = []
643 broken_out_of_flow = {}
628644
629645 last_in_flow_child = None
630646
644660
645661 if not child.is_in_normal_flow():
646662 abort = False
647 stop, resume_at, out_of_flow_resume_at = _out_of_flow_layout(
648 context, box, index, child, new_children, page_is_empty,
649 absolute_boxes, fixed_boxes, adjoining_margins,
650 bottom_space)
663 stop, resume_at, new_child, out_of_flow_resume_at = (
664 _out_of_flow_layout(
665 context, box, index, child, new_children, page_is_empty,
666 absolute_boxes, fixed_boxes, adjoining_margins,
667 bottom_space))
651668 if out_of_flow_resume_at:
652 broken_out_of_flow.append((child, box, out_of_flow_resume_at))
669 broken_out_of_flow[new_child] = (
670 child, box, out_of_flow_resume_at)
653671
654672 elif isinstance(child, boxes.LineBox):
655673 (abort, stop, resume_at, position_y,
691709
692710 if abort:
693711 page = child.page_values()[0]
712 remove_placeholders(
713 context, box.children[skip:], absolute_boxes, fixed_boxes)
694714 for footnote in new_footnotes:
695715 context.unlayout_footnote(footnote)
696716 return (
715735 return (
716736 None, None, {'break': 'any', 'page': None}, [], False, max_lines)
717737
718 context.broken_out_of_flow.extend(broken_out_of_flow)
738 for key, value in broken_out_of_flow.items():
739 context.broken_out_of_flow[key] = value
719740
720741 if collapsing_with_children:
721742 box.position_y += (
728749 # Top and bottom margins of this box
729750 if (box.height in ('auto', 0) and
730751 get_clearance(context, box, collapsed_margin) is None and
731 all(value == 0 for value in [
752 all(value == 0 for value in (
732753 box.min_height, box.border_top_width, box.padding_top,
733 box.border_bottom_width, box.padding_bottom])):
754 box.border_bottom_width, box.padding_bottom))):
734755 collapsing_through = True
735756 else:
736757 position_y += collapsed_margin
760781 start=not is_start, end=box_is_fragmented and not discard)
761782
762783 # TODO: See corner cases in
763 # http://www.w3.org/TR/CSS21/visudet.html#normal-block
784 # https://www.w3.org/TR/CSS21/visudet.html#normal-block
764785 # TODO: See float.float_layout
765786 if new_box.height == 'auto':
766787 if context.excluded_shapes and new_box.style['overflow'] != 'visible':
791812 elif bottom_space > -inf and not new_box.is_column:
792813 # Make the box fill the blank space at the bottom of the page
793814 # https://www.w3.org/TR/css-break-3/#box-splitting
794 new_box.height = (
815 new_box_height = (
795816 context.page_bottom - bottom_space - new_box.position_y -
796817 (new_box.margin_height() - new_box.height))
797 if draw_bottom_decoration:
798 new_box.height += (
799 box.padding_bottom + box.border_bottom_width +
800 box.margin_bottom)
818 if new_box_height > new_box.height:
819 new_box.height = new_box_height
820 if draw_bottom_decoration:
821 new_box.height += (
822 box.padding_bottom + box.border_bottom_width +
823 box.margin_bottom)
801824
802825 if next_page['page'] is None:
803826 next_page['page'] = new_box.page_values()[1]
819842 def establishes_formatting_context(box):
820843 """Return whether a box establishes a block formatting context.
821844
822 See http://www.w3.org/TR/CSS2/visuren.html#block-formatting
845 See https://www.w3.org/TR/CSS2/visuren.html#block-formatting
823846
824847 """
825848 return (
10001023 For boxes that have been removed in find_earlier_page_break(), remove the
10011024 matching placeholders in absolute_boxes and fixed_boxes.
10021025
1026 Also takes care of removed footnotes and floats.
1027
10031028 """
10041029 for box in box_list:
10051030 if isinstance(box, boxes.ParentBox):
10121037 fixed_boxes.remove(box)
10131038 if box.footnote:
10141039 context.unlayout_footnote(box.footnote)
1040 if box in context.broken_out_of_flow:
1041 context.broken_out_of_flow.pop(box)
10151042
10161043
10171044 def avoid_page_break(page_break, context):
1111 """Lay out a multi-column ``box``."""
1212 from .block import (
1313 block_box_layout, block_level_layout, block_level_width,
14 collapse_margin)
15
16 # Implementation of the multi-column pseudo-algorithm:
17 # https://www.w3.org/TR/css3-multicol/#pseudo-algorithm
18 width = None
14 collapse_margin, remove_placeholders)
15
1916 style = box.style
17 width = style['column_width']
18 count = style['column_count']
19 gap = style['column_gap']
20 height = style['height']
2021 original_bottom_space = bottom_space
2122 context.in_column = True
2223
23 if box.style['position'] == 'relative':
24 if style['position'] == 'relative':
2425 # New containing block, use a new absolute list
2526 absolute_boxes = []
2627
2728 box = box.copy_with_children(box.children)
2829 box.position_y += collapse_margin(adjoining_margins) - box.margin_top
2930
30 height = box.style['height']
31 # Set height if defined
3132 if height != 'auto' and height.unit != '%':
3233 assert height.unit == 'px'
33 known_height = True
34 bottom_space = max(
35 bottom_space,
36 context.page_bottom - box.content_box_y() - height.value)
34 height_defined = True
35 empty_space = context.page_bottom - box.content_box_y() - height.value
36 bottom_space = max(bottom_space, empty_space)
3737 else:
38 known_height = False
39
40 # TODO: the available width can be unknown if the containing block needs
41 # the size of this block to know its own size.
38 height_defined = False
39
40 # TODO: the columns container width can be unknown if the containing block
41 # needs the size of this block to know its own size
4242 block_level_width(box, containing_block)
43 available_width = box.width
44 if style['column_width'] == 'auto' and style['column_count'] != 'auto':
45 count = style['column_count']
46 width = max(
47 0, available_width - (count - 1) * style['column_gap']) / count
48 elif (style['column_width'] != 'auto' and
49 style['column_count'] == 'auto'):
50 count = max(1, int(floor(
51 (available_width + style['column_gap']) /
52 (style['column_width'] + style['column_gap']))))
53 width = (
54 (available_width + style['column_gap']) / count -
55 style['column_gap'])
56 else:
57 count = min(style['column_count'], int(floor(
58 (available_width + style['column_gap']) /
59 (style['column_width'] + style['column_gap']))))
60 width = (
61 (available_width + style['column_gap']) / count -
62 style['column_gap'])
63
64 def create_column_box(children):
65 column_box = box.anonymous_from(box, children=children)
66 resolve_percentages(column_box, containing_block)
67 column_box.is_column = True
68 column_box.width = width
69 column_box.position_x = box.content_box_x()
70 column_box.position_y = box.content_box_y()
71 return column_box
72
73 # Handle column-span property.
74 # We want to get the following structure:
43
44 # Define the number of columns and their widths
45 if width == 'auto' and count != 'auto':
46 width = max(0, box.width - (count - 1) * gap) / count
47 elif width != 'auto' and count == 'auto':
48 count = max(1, int(floor((box.width + gap) / (width + gap))))
49 width = (box.width + gap) / count - gap
50 else: # overconstrained, with width != 'auto' and count != 'auto'
51 count = min(count, int(floor((box.width + gap) / (width + gap))))
52 width = (box.width + gap) / count - gap
53
54 # Handle column-span property with the following structure:
7555 # columns_and_blocks = [
7656 # [column_child_1, column_child_2],
7757 # spanning_block,
7959 # ]
8060 columns_and_blocks = []
8161 column_children = []
82
83 if skip_stack:
84 skip, = skip_stack.keys()
85 else:
86 skip = 0
87
88 for index, child in enumerate(box.children[skip:], start=skip):
62 skip, = skip_stack.keys() if skip_stack else (0,)
63 for i, child in enumerate(box.children[skip:], start=skip):
8964 if child.style['column_span'] == 'all':
9065 if column_children:
9166 columns_and_blocks.append(
92 (index - len(column_children), column_children))
93 columns_and_blocks.append((index, child.copy()))
67 (i - len(column_children), column_children))
68 columns_and_blocks.append((i, child.copy()))
9469 column_children = []
9570 continue
9671 column_children.append(child.copy())
9772 if column_children:
9873 columns_and_blocks.append(
99 (index + 1 - len(column_children), column_children))
74 (i + 1 - len(column_children), column_children))
10075
10176 if skip_stack:
10277 skip_stack = {0: skip_stack[skip]}
10580 next_page = {'break': 'any', 'page': None}
10681 skip_stack = None
10782
108 # Balance.
83 # Find height and balance.
10984 #
11085 # The current algorithm starts from the total available height, to check
11186 # whether the whole content can fit. If it doesn’t fit, we keep the partial
12297 current_position_y = box.content_box_y()
12398 new_children = []
12499 column_skip_stack = None
125 forced_end_probing = False
100 last_loop = False
126101 break_page = False
102 footnote_area_heights = [
103 0 if context.current_footnote_area.height == 'auto'
104 else context.current_footnote_area.margin_height()]
105 last_footnotes_height = 0
127106 for index, column_children_or_block in columns_and_blocks:
128107 if not isinstance(column_children_or_block, list):
129 # We get a spanning block, we display it like other blocks.
108 # We have a spanning block, we display it like other blocks
130109 block = column_children_or_block
131110 resolve_percentages(block, containing_block)
132111 block.position_x = box.content_box_x()
135114 block_level_layout(
136115 context, block, original_bottom_space, skip_stack,
137116 containing_block, page_is_empty, absolute_boxes,
138 fixed_boxes, adjoining_margins, discard=False,
139 max_lines=None))
117 fixed_boxes, adjoining_margins))
140118 skip_stack = None
141119 if new_child is None:
142 forced_end_probing = True
120 last_loop = True
143121 break_page = True
144122 break
145123 new_children.append(new_child)
147125 new_child.border_height() + new_child.border_box_y())
148126 adjoining_margins.append(new_child.margin_bottom)
149127 if resume_at:
150 forced_end_probing = True
128 last_loop = True
151129 break_page = True
152130 column_skip_stack = resume_at
153131 break
154132 page_is_empty = False
155133 continue
156134
157 excluded_shapes = context.excluded_shapes[:]
158
159 # We have a list of children that we have to balance between columns.
135 # We have a list of children that we have to balance between columns
160136 column_children = column_children_or_block
161137
162 # Find the total height available for the first run.
138 # Find the total height available for the first run
163139 current_position_y += collapse_margin(adjoining_margins)
164140 adjoining_margins = []
165 column_box = create_column_box(column_children)
166 column_box.position_y = current_position_y
167 max_height = context.page_bottom - current_position_y
168 height = max_height
141 column_box = _create_column_box(
142 box, containing_block, column_children, width, current_position_y)
143 height = max_height = (
144 context.page_bottom - current_position_y - original_bottom_space)
169145
170146 # Try to render columns until the content fits, increase the column
171 # height step by step.
147 # height step by step
172148 column_skip_stack = skip_stack
173149 lost_space = inf
174 first_probe_run = True
150 original_excluded_shapes = context.excluded_shapes[:]
175151 original_page_is_empty = page_is_empty
176 page_is_empty = False
177 stop_rendering = False
152 page_is_empty = stop_rendering = balancing = False
178153 while True:
154 # Remove extra excluded shapes introduced during the previous loop
155 while len(context.excluded_shapes) > len(original_excluded_shapes):
156 context.excluded_shapes.pop()
157
158 # Render the columns
179159 column_skip_stack = skip_stack
180
181 # Remove extra excluded shapes introduced during previous loop
182 new_excluded_shapes = (
183 len(context.excluded_shapes) - len(excluded_shapes))
184 for i in range(new_excluded_shapes):
185 context.excluded_shapes.pop()
186
187160 consumed_heights = []
161 new_boxes = []
188162 for i in range(count):
189 # Render the column
163 # Render one column
190164 new_box, resume_at, next_page, _, _, _ = block_box_layout(
191165 context, column_box,
192166 context.page_bottom - current_position_y - height,
193167 column_skip_stack, containing_block,
194 page_is_empty or first_probe_run, [], [], [],
168 page_is_empty or not balancing, [], [], [],
195169 discard=False, max_lines=None)
196170 if new_box is None:
197 # We didn't render anything, retry.
171 # We didn't render anything, retry
198172 column_skip_stack = {0: None}
199173 break
174 new_boxes.append(new_box)
200175 column_skip_stack = resume_at
201176
177 # Calculate consumed height, empty space and next box height
202178 in_flow_children = [
203179 child for child in new_box.children
204180 if child.is_in_normal_flow()]
205
206181 if in_flow_children:
207182 # Get the empty space at the bottom of the column box
208183 consumed_height = (
211186 empty_space = height - consumed_height
212187
213188 # Get the minimum size needed to render the next box
214 next_box, _, _, _, _, _ = block_box_layout(
215 context, column_box,
216 context.page_bottom - box.content_box_y(),
217 column_skip_stack, containing_block, True, [], [], [],
218 discard=False, max_lines=None)
219 for child in next_box.children:
220 if child.is_in_normal_flow():
221 next_box_size = child.margin_height()
222 break
189 if column_skip_stack:
190 next_box = block_box_layout(
191 context, column_box, inf, column_skip_stack,
192 containing_block, True, [], [], [],
193 discard=False, max_lines=None)[0]
194 for child in next_box.children:
195 if child.is_in_normal_flow():
196 next_box_height = child.margin_height()
197 break
198 remove_placeholders(context, [next_box], [], [])
199 else:
200 next_box_height = 0
223201 else:
224 consumed_height = empty_space = next_box_size = 0
202 consumed_height = empty_space = next_box_height = 0
225203
226204 consumed_heights.append(consumed_height)
227205
237215 # introduced by rounding errors. As the workaround below at
238216 # least adds 1 pixel for each loop, we can ignore lost spaces
239217 # lower than 1px.
240 if next_box_size - empty_space > 1:
241 lost_space = min(lost_space, next_box_size - empty_space)
218 if next_box_height - empty_space > 1:
219 lost_space = min(lost_space, next_box_height - empty_space)
242220
243221 # Stop if we already rendered the whole content
244222 if resume_at is None:
245223 break
246224
247 if forced_end_probing:
248 break
249
250 if first_probe_run:
251 # This is the first loop through, we might bail here.
252 if column_skip_stack or max(consumed_heights) > max_height:
253 # Even at maximum height, not everything fits. Stop now and
254 # let the columns continue on the next page.
255 stop_rendering = True
256 break
257 else:
258 # Everything fit, start expanding columns at the average of
259 # the column heights.
260 height = sum(consumed_heights)
261 if style['column_fill'] == 'balance':
262 height /= count
263 else:
225 # Remove placeholders but keep the current footnote area height
226 last_footnotes_height = (
227 0 if context.current_footnote_area.height == 'auto'
228 else context.current_footnote_area.margin_height())
229 remove_placeholders(context, new_boxes, [], [])
230
231 if last_loop:
232 break
233
234 if balancing:
264235 if column_skip_stack is None:
265236 # We rendered the whole content, stop
266237 break
238
239 # Increase the column heights and render them again
240 add_height = 1 if lost_space == inf else lost_space
241 height += add_height
242
243 if height > max_height:
244 # We reached max height, stop rendering
245 height = max_height
246 stop_rendering = True
247 break
248 else:
249 if last_footnotes_height not in footnote_area_heights:
250 # Footnotes have been rendered, try to re-render with the
251 # new footnote area height
252 height -= last_footnotes_height - footnote_area_heights[-1]
253 footnote_area_heights.append(last_footnotes_height)
254 continue
255
256 everything_fits = (
257 not column_skip_stack and
258 max(consumed_heights) <= max_height)
259 if everything_fits:
260 # Everything fits, start expanding columns at the average
261 # of the column heights
262 max_height -= last_footnotes_height
263 if style['column_fill'] == 'balance':
264 balancing = True
265 height = sum(consumed_heights) / count
266 else:
267 break
267268 else:
268 if lost_space == inf:
269 # We didn't find the extra size needed to render a
270 # child in the previous column, increase height by the
271 # minimal value.
272 add_height = 1
273 else:
274 # Increase the column heights and render them again
275 add_height = lost_space
276
277 if height + add_height > max_height:
278 height = max_height
279 stop_rendering = True
280 break
281
282 height += add_height
283 first_probe_run = False
284
285 # TODO: check box.style['max']-height
269 # Content overflows even at maximum height, stop now and
270 # let the columns continue on the next page
271 height += footnote_area_heights[-1]
272 if len(footnote_area_heights) > 2:
273 last_footnotes_height = min(
274 last_footnotes_height, footnote_area_heights[-1])
275 height -= last_footnotes_height
276 stop_rendering = True
277 break
278
279 # TODO: check style['max']-height
286280 bottom_space = max(
287281 bottom_space, context.page_bottom - current_position_y - height)
288282
289 # Replace the current box children with columns
283 # Replace the current box children with real columns
290284 i = 0
291285 max_column_height = 0
292286 columns = []
293287 while True:
294 if i == count - 1:
295 bottom_space = original_bottom_space
296 column_box = create_column_box(column_children)
297 column_box.position_y = current_position_y
288 column_box = _create_column_box(
289 box, containing_block, column_children, width,
290 current_position_y)
298291 if style['direction'] == 'rtl':
299 column_box.position_x += (
300 box.width - (i + 1) * width - i * style['column_gap'])
292 column_box.position_x += box.width - (i + 1) * width - i * gap
301293 else:
302 column_box.position_x += i * (width + style['column_gap'])
294 column_box.position_x += i * (width + gap)
303295 new_child, column_skip_stack, column_next_page, _, _, _ = (
304296 block_box_layout(
305297 context, column_box, bottom_space, skip_stack,
317309 bottom_space = original_bottom_space
318310 break
319311 i += 1
320 if i == count and not known_height:
312 if i == count and not height_defined:
321313 # [If] a declaration that constrains the column height
322314 # (e.g., using height or max-height). In this case,
323315 # additional column boxes are created in the inline
324316 # direction.
325317 break
326318
327 current_position_y += max_column_height
319 # Update the current y position and set the columns’ height
320 current_position_y += min(max_height, max_column_height)
328321 for column in columns:
329322 column.height = max_column_height
330323 new_children.append(column)
334327
335328 if stop_rendering:
336329 break
330
331 # Report footnotes above the defined footnotes height
332 _report_footnotes(context, last_footnotes_height)
337333
338334 if box.children and not new_children:
339335 # The box has children but none can be drawn, let's skip the whole box
340336 context.in_column = False
341337 return None, (0, None), {'break': 'any', 'page': None}, [], False
342338
343 # Set the height of box and the columns
339 # Set the height of the containing box
344340 box.children = new_children
345341 current_position_y += collapse_margin(adjoining_margins)
346342 height = current_position_y - box.content_box_y()
349345 height_difference = 0
350346 else:
351347 height_difference = box.height - height
348
349 # Update the latest columns’ height to respect min-height
352350 if box.min_height != 'auto' and box.min_height > box.height:
353351 height_difference += box.min_height - box.height
354352 box.height = box.min_height
358356 else:
359357 break
360358
361 if box.style['position'] == 'relative':
359 if style['position'] == 'relative':
362360 # New containing block, resolve the layout of the absolute descendants
363361 for absolute_box in absolute_boxes:
364362 absolute_layout(
365363 context, absolute_box, box, fixed_boxes, bottom_space,
366364 skip_stack=None)
367365
366 # Calculate skip stack
368367 if column_skip_stack:
369368 skip, = column_skip_stack.keys()
370369 skip_stack = {index + skip: column_skip_stack[skip]}
371370 elif break_page:
372371 skip_stack = {index: None}
372
373 # Update page bottom according to the new footnotes
374 if context.current_footnote_area.height != 'auto':
375 context.page_bottom += footnote_area_heights[0]
376 context.page_bottom -= context.current_footnote_area.margin_height()
377
373378 context.in_column = False
374379 return box, skip_stack, next_page, [], False
380
381
382 def _report_footnotes(context, footnotes_height):
383 """Report footnotes above the defined footnotes height."""
384 if not context.current_page_footnotes:
385 return
386
387 # Report and count footnotes
388 reported_footnotes = 0
389 while context.current_footnote_area.margin_height() > footnotes_height:
390 context.report_footnote(context.current_page_footnotes[-1])
391 reported_footnotes += 1
392
393 # Revert reported footnotes, as they’ve been reported starting from the
394 # last one
395 if reported_footnotes >= 2:
396 extra = context.reported_footnotes[-1:-reported_footnotes-1:-1]
397 context.reported_footnotes[-reported_footnotes:] = extra
398
399
400 def _create_column_box(box, containing_block, children, width, position_y):
401 """Create a column box including given children."""
402 column_box = box.anonymous_from(box, children=children)
403 resolve_percentages(column_box, containing_block)
404 column_box.is_column = True
405 column_box.width = width
406 column_box.position_x = box.content_box_x()
407 column_box.position_y = position_y
408 return column_box
146146 new_child.style['max_height'] = Dimension(inf, 'px')
147147 new_child = block.block_level_layout(
148148 context, new_child, -inf, child_skip_stack, parent_box,
149 page_is_empty, [], [], [], False, None)[0]
149 page_is_empty)[0]
150150 content_size = new_child.height
151151 child.min_height = min(specified_size, content_size)
152152
205205 new_child.width = inf
206206 new_child = block.block_level_layout(
207207 context, new_child, -inf, child_skip_stack, parent_box,
208 page_is_empty, absolute_boxes, fixed_boxes,
209 adjoining_margins=[], discard=False, max_lines=None)[0]
208 page_is_empty, absolute_boxes, fixed_boxes)[0]
210209 child.flex_base_size = new_child.margin_height()
211210 elif child.style[axis] == 'min-content':
212211 child.style[axis] = 'auto'
220219 new_child.width = 0
221220 new_child = block.block_level_layout(
222221 context, new_child, -inf, child_skip_stack, parent_box,
223 page_is_empty, absolute_boxes, fixed_boxes,
224 adjoining_margins=[], discard=False, max_lines=None)[0]
222 page_is_empty, absolute_boxes, fixed_boxes)[0]
225223 child.flex_base_size = new_child.margin_height()
226224 else:
227225 assert child.style[axis].unit == 'px'
678676 box.content_box_y() if cross == 'height'
679677 else box.content_box_x())
680678 for line in flex_lines:
681 line.lower_baseline = 0
679 line.lower_baseline = -inf
682680 # TODO: don't duplicate this loop
683681 for i, child in line:
684682 align_self = child.style['align_self']
688686 # TODO: handle vertical text
689687 child.baseline = child._baseline - position_cross
690688 line.lower_baseline = max(line.lower_baseline, child.baseline)
689 if line.lower_baseline == -inf:
690 line.lower_baseline = line[0][1]._baseline if line else 0
691691 for i, child in line:
692692 cross_margins = (
693693 (child.margin_top, child.margin_bottom) if cross == 'height'
2525 resolve_percentages(box, (cb_width, cb_height))
2626
2727 # TODO: This is only handled later in blocks.block_container_layout
28 # http://www.w3.org/TR/CSS21/visudet.html#normal-block
28 # https://www.w3.org/TR/CSS21/visudet.html#normal-block
2929 if cb_height == 'auto':
3030 cb_height = (
3131 containing_block.position_y - containing_block.content_box_y())
8080
8181 def find_float_position(context, box, containing_block):
8282 """Get the right position of the float ``box``."""
83 # See http://www.w3.org/TR/CSS2/visuren.html#float-position
83 # See https://www.w3.org/TR/CSS2/visuren.html#float-position
8484
8585 # Point 4 is already handled as box.position_y is set according to the
8686 # containing box top position, with collapsing margins handled
168168 fixed_boxes, bottom_space, skip_stack=None)
169169 float_children.append(new_waiting_float)
170170 if waiting_float_resume_at:
171 context.broken_out_of_flow.append(
172 (waiting_float, containing_block, waiting_float_resume_at))
171 context.broken_out_of_flow[new_waiting_float] = (
172 waiting_float, containing_block, waiting_float_resume_at)
173173 if float_children:
174174 line.children += tuple(float_children)
175175
179179 def skip_first_whitespace(box, skip_stack):
180180 """Return ``skip_stack`` to start just after removable leading spaces.
181181
182 See http://www.w3.org/TR/CSS21/text.html#white-space-model
182 See https://www.w3.org/TR/CSS21/text.html#white-space-model
183183
184184 """
185185 if skip_stack is None:
381381
382382 resolve_percentages(box, containing_block)
383383
384 # http://www.w3.org/TR/CSS21/visudet.html#inlineblock-width
384 # https://www.w3.org/TR/CSS21/visudet.html#inlineblock-width
385385 if box.margin_left == 'auto':
386386 box.margin_left = 0
387387 if box.margin_right == 'auto':
388388 box.margin_right = 0
389 # http://www.w3.org/TR/CSS21/visudet.html#block-root-margin
389 # https://www.w3.org/TR/CSS21/visudet.html#block-root-margin
390390 if box.margin_top == 'auto':
391391 box.margin_top = 0
392392 if box.margin_bottom == 'auto':
410410
411411 Position is taken from the top of its margin box.
412412
413 http://www.w3.org/TR/CSS21/visudet.html#propdef-vertical-align
413 https://www.w3.org/TR/CSS21/visudet.html#propdef-vertical-align
414414
415415 """
416416 if box.is_table_wrapper:
505505 elif isinstance(box, boxes.InlineFlexBox):
506506 box.position_x = position_x
507507 box.position_y = 0
508 for side in ['top', 'right', 'bottom', 'left']:
508 for side in ('top', 'right', 'bottom', 'left'):
509509 if getattr(box, f'margin_{side}') == 'auto':
510510 setattr(box, f'margin_{side}', 0)
511511 new_box, resume_at, _, _, _ = flex_layout(
558558 context, child, containing_block, absolute_boxes, fixed_boxes,
559559 bottom_space, skip_stack=None)
560560 if float_resume_at:
561 context.broken_out_of_flow.append(
562 (child, containing_block, float_resume_at))
561 context.broken_out_of_flow[child] = (
562 child, containing_block, float_resume_at)
563563 waiting_children.append((index, new_child, child))
564564 child = new_child
565565
896896 box.pango_layout = layout
897897 # "The height of the content area should be based on the font,
898898 # but this specification does not specify how."
899 # http://www.w3.org/TR/CSS21/visudet.html#inline-non-replaced
899 # https://www.w3.org/TR/CSS21/visudet.html#inline-non-replaced
900900 # We trust Pango and use the height of the LayoutLine.
901901 box.height = height
902902 # "only the 'line-height' is used when calculating the height
919919 preserved_line_break = (
920920 (length != resume_index) and between.strip(' '))
921921 if preserved_line_break:
922 # See http://unicode.org/reports/tr14/
922 # See https://unicode.org/reports/tr14/
923923 # \r is already handled by process_whitespace
924924 line_breaks = ('\n', '\t', '\f', '\u0085', '\u2028', '\u2029')
925925 assert between in line_breaks, (
11581158
11591159
11601160 def is_phantom_linebox(linebox):
1161 # See http://www.w3.org/TR/CSS21/visuren.html#phantom-line-box
1161 # See https://www.w3.org/TR/CSS21/visuren.html#phantom-line-box
11621162 for child in linebox.children:
11631163 if isinstance(child, boxes.InlineBox):
11641164 if not is_phantom_linebox(child):
101101
102102
103103 def compute_fixed_dimension(context, box, outer, vertical, top_or_left):
104 """
105 Compute and set a margin box fixed dimension on ``box``, as described in:
106 http://dev.w3.org/csswg/css3-page/#margin-constraints
104 """Compute and set a margin box fixed dimension on ``box``.
105
106 Described in: https://drafts.csswg.org/css-page-3/#margin-constraints
107107
108108 :param box:
109109 The margin box to work on
122122
123123 # Rule 2
124124 total = box.padding_plus_border + sum(
125 value for value in [box.margin_a, box.margin_b, box.inner]
125 value for value in (box.margin_a, box.margin_b, box.inner)
126126 if value != 'auto')
127127 if total > outer:
128128 if box.margin_a == 'auto':
133133 # XXX this is not in the spec, but without it box.inner
134134 # would end up with a negative value.
135135 # Instead, this will trigger rule 3 below.
136 # http://lists.w3.org/Archives/Public/www-style/2012Jul/0006.html
136 # https://lists.w3.org/Archives/Public/www-style/2012Jul/0006.html
137137 box.inner = 0
138138 # Rule 3
139139 if 'auto' not in [box.margin_a, box.margin_b, box.inner]:
162162 box.inner = (outer - box.padding_plus_border -
163163 box.margin_a - box.margin_b)
164164 # Rule 6
165 if box.margin_a == 'auto' and box.margin_b == 'auto':
165 if box.margin_a == box.margin_b == 'auto':
166166 box.margin_a = box.margin_b = (
167167 outer - box.padding_plus_border - box.inner) / 2
168168
174174 def compute_variable_dimension(context, side_boxes, vertical, outer_sum):
175175 """
176176 Compute and set a margin box fixed dimension on ``box``, as described in:
177 http://dev.w3.org/csswg/css3-page/#margin-dimension
177 https://drafts.csswg.org/css-page-3/#margin-dimension
178178
179179 :param side_boxes: Three boxes on a same side (as opposed to a corner.)
180180 A list of:
358358 page_end_y = margin_top + max_box_height
359359
360360 # Margin box dimensions, described in
361 # http://dev.w3.org/csswg/css3-page/#margin-box-dimensions
361 # https://drafts.csswg.org/css-page-3/#margin-box-dimensions
362362 generated_boxes = []
363363
364 for prefix, vertical, containing_block, position_x, position_y in [
364 for prefix, vertical, containing_block, position_x, position_y in (
365365 ('top', False, (max_box_width, margin_top),
366366 margin_left, 0),
367367 ('bottom', False, (max_box_width, margin_bottom),
370370 0, margin_top),
371371 ('right', True, (margin_right, max_box_height),
372372 page_end_x, margin_top),
373 ]:
373 ):
374374 if vertical:
375375 suffixes = ['top', 'middle', 'bottom']
376376 fixed_outer, variable_outer = containing_block
398398 variable_outer - box.margin_width())
399399 compute_fixed_dimension(
400400 context, box, fixed_outer, not vertical,
401 prefix in ['top', 'left'])
401 prefix in ('top', 'left'))
402402 generated_boxes.append(box)
403403
404404 # Corner boxes
405405
406 for at_keyword, cb_width, cb_height, position_x, position_y in [
406 for at_keyword, cb_width, cb_height, position_x, position_y in (
407407 ('@top-left-corner', margin_left, margin_top, 0, 0),
408408 ('@top-right-corner', margin_right, margin_top, page_end_x, 0),
409409 ('@bottom-left-corner', margin_left, margin_bottom, 0, page_end_y),
410410 ('@bottom-right-corner', margin_right, margin_bottom,
411411 page_end_x, page_end_y),
412 ]:
412 ):
413413 box = make_box(at_keyword, (cb_width, cb_height))
414414 if not box.is_generated:
415415 continue
467467 over-constrained, instead of ignoring any margins, the containing block
468468 is resized to coincide with the margin edges of the page box."
469469
470 http://dev.w3.org/csswg/css3-page/#page-box-page-rule
471 http://www.w3.org/TR/CSS21/visudet.html#blockwidth
470 https://drafts.csswg.org/css-page-3/#page-box-page-rule
471 https://www.w3.org/TR/CSS21/visudet.html#blockwidth
472472
473473 """
474474 remaining = containing_block_size - box.padding_plus_border
542542 root_box = root_box.copy_with_children([])
543543
544544 # TODO: handle cases where the root element is something else.
545 # See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
545 # See https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
546546 assert isinstance(root_box, (boxes.BlockBox, boxes.FlexContainerBox))
547547 context.create_block_formatting_context()
548548 context.current_page = page_number
549 context.current_page_footnotes = context.reported_footnotes.copy()
549 context.current_page_footnotes = []
550550 context.current_footnote_area = footnote_area
551551
552 if context.reported_footnotes:
553 footnote_area.children = tuple(context.reported_footnotes)
554 context.reported_footnotes = []
555 reported_footnote_area = build.create_anonymous_boxes(
556 footnote_area.deepcopy())
557 reported_footnote_area, _, _, _, _, _ = block_level_layout(
558 context, reported_footnote_area, -inf, None, footnote_area.page,
559 True, [], [], [], False, None)
560 footnote_area.height = reported_footnote_area.height
561 context.page_bottom -= reported_footnote_area.margin_height()
552 reported_footnotes = context.reported_footnotes
553 context.reported_footnotes = []
554 for i, reported_footnote in enumerate(reported_footnotes):
555 context.footnotes.append(reported_footnote)
556 overflow = context.layout_footnote(reported_footnote)
557 if overflow and i != 0:
558 context.report_footnote(reported_footnote)
559 context.reported_footnotes = reported_footnotes[i:]
560 break
562561
563562 page_is_empty = True
564563 adjoining_margins = []
565564 positioned_boxes = [] # Mixed absolute and fixed
566565 out_of_flow_boxes = []
567 broken_out_of_flow = []
568 for box, containing_block, skip_stack in context.broken_out_of_flow:
566 broken_out_of_flow = {}
567 context_out_of_flow = context.broken_out_of_flow.values()
568 for box, containing_block, skip_stack in context_out_of_flow:
569569 box.position_y = root_box.content_box_y()
570570 if box.is_floated():
571571 out_of_flow_box, out_of_flow_resume_at = float_layout(
578578 skip_stack)
579579 out_of_flow_boxes.append(out_of_flow_box)
580580 if out_of_flow_resume_at:
581 broken_out_of_flow.append(
582 (box, containing_block, out_of_flow_resume_at))
581 broken_out_of_flow[out_of_flow_box] = (
582 box, containing_block, out_of_flow_resume_at)
583583 context.broken_out_of_flow = broken_out_of_flow
584584 root_box, resume_at, next_page, _, _, _ = block_level_layout(
585585 context, root_box, 0, resume_at, initial_containing_block,
586 page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins,
587 False, None)
586 page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins)
588587 assert root_box
589588 root_box.children = out_of_flow_boxes + root_box.children
590589
591590 footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy())
592 footnote_area, _, _, _, _, _ = block_level_layout(
591 footnote_area = block_level_layout(
593592 context, footnote_area, -inf, None, footnote_area.page, True,
594 positioned_boxes, positioned_boxes, [], False, None)
593 positioned_boxes, positioned_boxes)[0]
595594 footnote_area.translate(dy=-footnote_area.margin_height())
596595
597596 page.fixed_boxes = [
722721 style_for.set_computed_styles(
723722 page_type,
724723 # @page inherits from the root element:
725 # http://lists.w3.org/Archives/Public/www-style/2012Jan/1164.html
724 # https://lists.w3.org/Archives/Public/www-style/2012Jan/1164.html
726725 root=html.etree_element, parent=html.etree_element,
727726 base_url=html.base_url)
728727
834833
835834 """
836835 i = 0
836 reported_footnotes = None
837837 while True:
838838 remake_state = context.page_maker[i][-1]
839839 if (len(pages) == 0 or
846846 remake_state['anchors'] = []
847847 remake_state['content_lookups'] = []
848848 page, resume_at = remake_page(i, context, root_box, html)
849 reported_footnotes = context.reported_footnotes
849850 yield page
850851 else:
851852 PROGRESS_LOGGER.info(
852853 'Step 5 - Creating layout - Page %d (up-to-date)', i + 1)
853854 resume_at = context.page_maker[i + 1][0]
855 reported_footnotes = None
854856 yield pages[i]
855857
856858 i += 1
857 if resume_at is None and not context.reported_footnotes:
858 # Throw away obsolete pages and broken out-of-flow boxes
859 if resume_at is None and not reported_footnotes:
860 # Throw away obsolete pages and content
859861 context.page_maker = context.page_maker[:i + 1]
860862 context.broken_out_of_flow.clear()
863 context.reported_footnotes.clear()
861864 return
9292 box, 'max_height', cb_height, main_flex_direction)
9393
9494 # Used value == computed value
95 for side in ['top', 'right', 'bottom', 'left']:
95 for side in ('top', 'right', 'bottom', 'left'):
9696 prop = f'border_{side}_width'
9797 setattr(box, prop, box.style[prop])
9898
33 shrink-to-fit algorithm.
44
55 Terms used (max-content width, min-content width) are defined in David
6 Baron's unofficial draft (http://dbaron.org/css/intrinsic/).
6 Baron's unofficial draft (https://dbaron.org/css/intrinsic/).
77
88 """
99
2121 *Warning:* both available_content_width and the return value are
2222 for width of the *content area*, not margin area.
2323
24 http://www.w3.org/TR/CSS21/visudet.html#float-width
24 https://www.w3.org/TR/CSS21/visudet.html#float-width
2525
2626 """
2727 return min(
9191 if width == 'auto' or width.unit == '%':
9292 # "percentages on the following properties are treated instead as
9393 # though they were the following: width: auto"
94 # http://dbaron.org/css/intrinsic/#outer-intrinsic
94 # https://dbaron.org/css/intrinsic/#outer-intrinsic
9595 children_widths = [
9696 function(context, child, outer=True) for child in box.children
9797 if not child.is_absolutely_positioned()]
225225 def table_cell_min_content_width(context, box, outer):
226226 """Return the min-content width for a ``TableCellBox``."""
227227 children_widths = [
228 min_content_width(context, child, outer=True)
229 for child in box.children
228 min_content_width(context, child) for child in box.children
230229 if not child.is_absolutely_positioned()]
231230 children_min_width = margin_width(
232231 box, max(children_widths) if children_widths else 0)
312311 current_line += lines[0]
313312 break
314313 else:
315 # http://www.w3.org/TR/css3-text/#line-break-details
314 # https://www.w3.org/TR/css-text-3/#overflow-wrap
316315 # "The line breaking behavior of a replaced element
317316 # or other atomic inline is equivalent to that
318317 # of the Object Replacement Character (U+FFFC)."
319 # http://www.unicode.org/reports/tr14/#DescriptionOfProperties
318 # https://www.unicode.org/reports/tr14/#DescriptionOfProperties
320319 # "By default, there is a break opportunity
321320 # both before and after any inline object."
322321 if minimum:
341340 def _percentage_contribution(box):
342341 """Return the percentage contribution of a cell, column or column group.
343342
344 http://dbaron.org/css/intrinsic/#pct-contrib
343 https://dbaron.org/css/intrinsic/#pct-contrib
345344
346345 """
347346 min_width = (
365364 column_intrinsic_percentages, constrainedness,
366365 total_horizontal_border_spacing, grid)``
367366
368 http://dbaron.org/css/intrinsic/
367 https://dbaron.org/css/intrinsic/
369368
370369 """
371370 from .table import distribute_excess_width
407406 min_width = block_min_content_width(context, table, outer=False)
408407 max_width = block_max_content_width(context, table, outer=False)
409408 outer_min_width = adjust(
410 box, outer=True, width=block_min_content_width(
411 context, table, outer=True))
409 box, outer=True, width=block_min_content_width(context, table))
412410 outer_max_width = adjust(
413 box, outer=True, width=block_max_content_width(
414 context, table, outer=True))
411 box, outer=True, width=block_max_content_width(context, table))
415412 result = ([], [], [], [], total_horizontal_border_spacing, [])
416413 context.tables[table] = result = {
417414 False: (min_width, max_width) + result,
585582 # infinitely large number if the numerator is nonzero [and] the
586583 # denominator of that ratio is 0."
587584 #
588 # http://dbaron.org/css/intrinsic/#autotableintrinsic
585 # https://dbaron.org/css/intrinsic/#autotableintrinsic
589586 #
590587 # Please note that "an infinitely large number" is not "infinite",
591588 # and that's probably not a coincindence: putting 'inf' here breaks
606603 if table.style['width'] != 'auto' and table.style['width'].unit == 'px':
607604 # "percentages on the following properties are treated instead as
608605 # though they were the following: width: auto"
609 # http://dbaron.org/css/intrinsic/#outer-intrinsic
606 # https://dbaron.org/css/intrinsic/#outer-intrinsic
610607 table_min_width = table_max_width = table.style['width'].value
611608 else:
612609 table_min_width = table_min_content_width
697694 # TODO: use real values, see
698695 # https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes
699696 min_contents = [
700 min_content_width(context, child, outer=True)
697 min_content_width(context, child)
701698 for child in box.children if child.is_flex_item]
702699 if not min_contents:
703700 return adjust(box, outer, 0)
713710 # TODO: use real values, see
714711 # https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes
715712 max_contents = [
716 max_content_width(context, child, outer=True)
713 max_content_width(context, child)
717714 for child in box.children if child.is_flex_item]
718715 if not max_contents:
719716 return adjust(box, outer, 0)
00 """Layout for images and other replaced elements.
11
2 See http://dev.w3.org/csswg/css-images-3/#sizing
2 See https://drafts.csswg.org/css-images-3/#sizing
33
44 """
55
1414
1515 Return a ``(concrete_width, concrete_height)`` tuple.
1616
17 See http://dev.w3.org/csswg/css-images-3/#default-sizing
17 See https://drafts.csswg.org/css-images-3/#default-sizing
1818
1919 """
2020 if specified_width == 'auto':
5252
5353 Return a ``(concrete_width, concrete_height)`` tuple.
5454
55 See http://dev.w3.org/csswg/css-images-3/#contain-constraint
55 See https://drafts.csswg.org/css-images-3/#contain-constraint
5656
5757 """
5858 return _constraint_image_sizing(
6565
6666 Return a ``(concrete_width, concrete_height)`` tuple.
6767
68 See http://dev.w3.org/csswg/css-images-3/#cover-constraint
68 See https://drafts.csswg.org/css-images-3/#cover-constraint
6969
7070 """
7171 return _constraint_image_sizing(
9898 if object_fit == 'fill':
9999 draw_width, draw_height = box.width, box.height
100100 else:
101 if object_fit == 'contain' or object_fit == 'scale-down':
101 if object_fit in ('contain', 'scale-down'):
102102 draw_width, draw_height = contain_constraint_image_sizing(
103103 box.width, box.height, intrinsic_ratio)
104104 elif object_fit == 'cover':
138138 box.style['image_resolution'], box.style['font_size'])
139139
140140 # This algorithm simply follows the different points of the specification:
141 # http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
142 if box.height == 'auto' and box.width == 'auto':
141 # https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
142 if box.height == box.width == 'auto':
143143 if width is not None:
144144 # Point #1
145145 box.width = width
167167 @handle_min_max_height
168168 def replaced_box_height(box):
169169 """Compute and set the used height for replaced boxes."""
170 # http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
170 # https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
171171 width, height, ratio = box.replacement.get_intrinsic_size(
172172 box.style['image_resolution'], box.style['font_size'])
173173
174174 # Test 'auto' on the computed width, not the used width
175 if box.height == 'auto' and box.width == 'auto':
175 if box.height == box.width == 'auto':
176176 box.height = height
177177 elif box.height == 'auto' and ratio:
178178 box.height = box.width / ratio
179179
180 if box.height == 'auto' and box.width == 'auto' and height is not None:
180 if box.height == box.width == 'auto' and height is not None:
181181 box.height = height
182182 elif ratio is not None and box.height == 'auto':
183183 box.height = box.width / ratio
190190
191191 def inline_replaced_box_layout(box, containing_block):
192192 """Lay out an inline :class:`boxes.ReplacedBox` ``box``."""
193 for side in ['top', 'right', 'bottom', 'left']:
193 for side in ('top', 'right', 'bottom', 'left'):
194194 if getattr(box, f'margin_{side}') == 'auto':
195195 setattr(box, f'margin_{side}', 0)
196196 inline_replaced_box_width_height(box, containing_block)
197197
198198
199199 def inline_replaced_box_width_height(box, containing_block):
200 if box.style['width'] == 'auto' and box.style['height'] == 'auto':
200 if box.style['width'] == box.style['height'] == 'auto':
201201 replaced_box_width.without_min_max(box, containing_block)
202202 replaced_box_height.without_min_max(box)
203203 min_max_auto_replaced(box)
268268 from .float import avoid_collisions
269269
270270 box = box.copy()
271 if box.style['width'] == 'auto' and box.style['height'] == 'auto':
271 if box.style['width'] == box.style['height'] == 'auto':
272272 computed_margins = box.margin_left, box.margin_right
273273 block_replaced_width.without_min_max(
274274 box, containing_block)
281281 replaced_box_height(box)
282282
283283 # Don't collide with floats
284 # http://www.w3.org/TR/CSS21/visuren.html#floats
284 # https://www.w3.org/TR/CSS21/visuren.html#floats
285285 box.position_x, box.position_y, _ = avoid_collisions(
286286 context, box, containing_block, outer=False)
287287 resume_at = None
295295 def block_replaced_width(box, containing_block):
296296 from .block import block_level_width
297297
298 # http://www.w3.org/TR/CSS21/visudet.html#block-replaced-width
298 # https://www.w3.org/TR/CSS21/visudet.html#block-replaced-width
299299 replaced_box_width.without_min_max(box, containing_block)
300300 block_level_width.without_min_max(box, containing_block)
211211 row = row.copy_with_children(new_row_children)
212212
213213 # Table height algorithm
214 # http://www.w3.org/TR/CSS21/tables.html#height-layout
214 # https://www.w3.org/TR/CSS21/tables.html#height-layout
215215
216216 # cells with vertical-align: baseline
217217 baseline_cells = []
611611 def fixed_table_layout(box):
612612 """Run the fixed table layout and return a list of column widths.
613613
614 http://www.w3.org/TR/CSS21/tables.html#fixed-table-layout
614 https://www.w3.org/TR/CSS21/tables.html#fixed-table-layout
615615
616616 """
617617 table = box.get_wrapped_table()
700700 def auto_table_layout(context, box, containing_block):
701701 """Run the auto table layout and return a list of column widths.
702702
703 http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
703 https://www.w3.org/TR/CSS21/tables.html#auto-table-layout
704704
705705 """
706706 table = box.get_wrapped_table()
816816 def cell_baseline(cell):
817817 """Return the y position of a cell baseline from the top of its border box.
818818
819 See http://www.w3.org/TR/CSS21/tables.html#height-layout
819 See https://www.w3.org/TR/CSS21/tables.html#height-layout
820820
821821 """
822822 result = find_in_flow_baseline(
856856
857857 Return excess width left when it's impossible without breaking rules.
858858
859 See http://dbaron.org/css/intrinsic/#distributetocols
859 See https://dbaron.org/css/intrinsic/#distributetocols
860860
861861 """
862862 # First group
3333 for page in pages:
3434 page_links = []
3535 for link in page.links:
36 link_type, anchor_name, rectangle, _ = link
36 link_type, anchor_name, _, _ = link
3737 if link_type == 'internal':
3838 if anchor_name not in anchors:
3939 LOGGER.error(
4040 'No anchor #%s for internal URI reference',
4141 anchor_name)
4242 else:
43 page_links.append(
44 (link_type, anchor_name, rectangle, None))
43 page_links.append(link)
4544 else:
4645 # External link
4746 page_links.append(link)
7473 # "Transforms apply to block-level and atomic inline-level elements,
7574 # but do not apply to elements which may be split into
7675 # multiple inline-level boxes."
77 # http://www.w3.org/TR/css3-2d-transforms/#introduction
76 # https://www.w3.org/TR/css-transforms-1/#introduction
7877 if box.style['transform'] and not isinstance(box, boxes.InlineBox):
7978 border_width = box.border_width()
8079 border_height = box.border_height()
135134 if link_type == 'external' and box.is_attachment:
136135 link_type = 'attachment'
137136 rectangle = rectangle_aabb(matrix, pos_x, pos_y, width, height)
138 link = (link_type, target, rectangle, box.download_name)
137 link = (link_type, target, rectangle, box)
139138 links.append(link)
140139 if matrix and (has_bookmark or has_anchor):
141140 pos_x, pos_y = matrix.transform_point(pos_x, pos_y)
1313 from ..logger import LOGGER, PROGRESS_LOGGER
1414 from ..matrix import Matrix
1515 from ..urls import URLFetchingError
16 from . import pdfa
16 from . import pdfa, pdfua
1717 from .fonts import build_fonts_dictionary
1818 from .stream import Stream
1919
2020 VARIANTS = {
21 name: function for variants in (pdfa.VARIANTS,)
22 for (name, function) in variants.items()}
21 name: data for variants in (pdfa.VARIANTS, pdfua.VARIANTS)
22 for (name, data) in variants.items()}
2323
2424
2525 def _w3c_date_to_pdf(string, attr_name):
169169 alpha['SMask']['G'] = alpha['SMask']['G'].reference
170170
171171
172 def _add_links(links, anchors, matrix, pdf, page, names):
172 def _add_links(links, anchors, matrix, pdf, page, names, mark):
173173 """Include hyperlinks in given PDF page."""
174 for link in links:
175 link_type, link_target, rectangle, _ = link
174 for link_type, link_target, rectangle, box in links:
176175 x1, y1 = matrix.transform_point(*rectangle[:2])
177176 x2, y2 = matrix.transform_point(*rectangle[2:])
178177 if link_type in ('internal', 'external'):
179 annot = pydyf.Dictionary({
178 box.link_annotation = pydyf.Dictionary({
180179 'Type': '/Annot',
181180 'Subtype': '/Link',
182181 'Rect': pydyf.Array([x1, y1, x2, y2]),
183182 'BS': pydyf.Dictionary({'W': 0}),
184183 })
184 if mark:
185 box.link_annotation['Contents'] = pydyf.String(link_target)
185186 if link_type == 'internal':
186 annot['Dest'] = pydyf.String(link_target)
187 box.link_annotation['Dest'] = pydyf.String(link_target)
187188 else:
188 annot['A'] = pydyf.Dictionary({
189 box.link_annotation['A'] = pydyf.Dictionary({
189190 'Type': '/Action',
190191 'S': '/URI',
191192 'URI': pydyf.String(link_target),
192193 })
193 pdf.add_object(annot)
194 pdf.add_object(box.link_annotation)
194195 if 'Annots' not in page:
195196 page['Annots'] = pydyf.Array()
196 page['Annots'].append(annot.reference)
197 page['Annots'].append(box.link_annotation.reference)
197198
198199 for anchor in anchors:
199200 anchor_name, x, y = anchor
206207 count = len(bookmarks)
207208 outlines = []
208209 for title, (page, x, y), children, state in bookmarks:
209 destination = pydyf.Array((
210 pdf.objects[pdf.pages['Kids'][page*3]].reference, '/XYZ', x, y, 0))
210 destination = pydyf.Array((pdf.page_references[page], '/XYZ', x, y, 0))
211211 outline = pydyf.Dictionary({
212212 'Title': pydyf.String(title), 'Dest': destination})
213213 pdf.add_object(outline)
230230 return outlines, count
231231
232232
233 def generate_pdf(pages, url_fetcher, metadata, fonts, target, zoom,
234 attachments, finisher, optimize_size, identifier, variant,
235 version, custom_metadata):
233 def generate_pdf(document, target, zoom, attachments, optimize_size,
234 identifier, variant, version, custom_metadata):
236235 # 0.75 = 72 PDF point per inch / 96 CSS pixel per inch
237236 scale = zoom * 0.75
238237
239238 PROGRESS_LOGGER.info('Step 6 - Creating PDF')
240239
241 pdf = pydyf.PDF()
242 pdf.version = str(version or '1.7').encode()
240 # Set properties according to PDF variants
241 mark = False
242 if variant:
243 variant_function, properties = VARIANTS[variant]
244 if 'version' in properties:
245 version = properties['version']
246 if 'mark' in properties:
247 mark = properties['mark']
248
249 pdf = pydyf.PDF((version or '1.7'), identifier)
243250 states = pydyf.Dictionary()
244251 x_objects = pydyf.Dictionary()
245252 patterns = pydyf.Dictionary()
254261 pdf.add_object(resources)
255262 pdf_names = []
256263
257 # Variants
258 if variant:
259 VARIANTS[variant](pdf, metadata)
260
261264 # Links and anchors
262 page_links_and_anchors = list(resolve_links(pages))
265 page_links_and_anchors = list(resolve_links(document.pages))
263266 attachment_links = [
264267 [link for link in page_links if link[0] == 'attachment']
265268 for page_links, page_anchors in page_links_and_anchors]
276279 # above about multiple regions won't always be correct, because
277280 # two links might have the same href, but different titles.
278281 annot_files[annot_target] = _write_pdf_attachment(
279 pdf, (annot_target, None), url_fetcher)
282 pdf, (annot_target, None), document.url_fetcher)
280283
281284 # Bookmarks
282285 root = []
287290 skipped_levels = []
288291 last_by_depth = [root]
289292 previous_level = 0
293 page_streams = []
290294
291295 for page_number, (page, links_and_anchors, page_links) in enumerate(
292 zip(pages, page_links_and_anchors, attachment_links)):
296 zip(document.pages, page_links_and_anchors, attachment_links)):
293297 # Draw from the top-left corner
294298 matrix = Matrix(scale, 0, 0, -scale, 0, page.height * scale)
295299
309313 left / scale, top / scale,
310314 (right - left) / scale, (bottom - top) / scale)
311315 stream = Stream(
312 fonts, page_rectangle, states, x_objects, patterns, shadings,
313 images)
316 document.fonts, page_rectangle, states, x_objects, patterns,
317 shadings, images, mark)
314318 stream.transform(d=-1, f=(page.height * scale))
315 page.paint(stream, scale=scale)
316319 pdf.add_object(stream)
320 page_streams.append(stream)
317321
318322 pdf_page = pydyf.Dictionary({
319323 'Type': '/Page',
322326 'Contents': stream.reference,
323327 'Resources': resources.reference,
324328 })
329 if mark:
330 pdf_page['Tabs'] = '/S'
331 pdf_page['StructParents'] = page_number
325332 pdf.add_page(pdf_page)
326333
327 _add_links(links, anchors, matrix, pdf, pdf_page, pdf_names)
334 _add_links(links, anchors, matrix, pdf, pdf_page, pdf_names, mark)
335 page.paint(stream, scale=scale)
328336
329337 # Bleed
330338 bleed = {key: value * 0.75 for key, value in page.bleed.items()}
400408
401409 # PDF information
402410 pdf.info['Producer'] = pydyf.String(f'WeasyPrint {__version__}')
411 metadata = document.metadata
403412 if metadata.title:
404413 pdf.info['Title'] = pydyf.String(metadata.title)
405414 if metadata.authors:
416425 if metadata.modified:
417426 pdf.info['ModDate'] = pydyf.String(
418427 _w3c_date_to_pdf(metadata.modified, 'modified'))
428 if metadata.lang:
429 pdf.catalog['Lang'] = pydyf.String(metadata.lang)
419430 if custom_metadata:
420431 for key, value in metadata.custom.items():
421432 key = ''.join(char for char in key if char.isalnum())
427438 attachments = metadata.attachments + (attachments or [])
428439 pdf_attachments = []
429440 for attachment in attachments:
430 pdf_attachment = _write_pdf_attachment(pdf, attachment, url_fetcher)
441 pdf_attachment = _write_pdf_attachment(
442 pdf, attachment, document.url_fetcher)
431443 if pdf_attachment is not None:
432444 pdf_attachments.append(pdf_attachment)
433445 if pdf_attachments:
441453 pdf.catalog['Names']['EmbeddedFiles'] = content.reference
442454
443455 # Embedded fonts
444 pdf_fonts = build_fonts_dictionary(pdf, fonts, optimize_size)
456 pdf_fonts = build_fonts_dictionary(pdf, document.fonts, optimize_size)
445457 pdf.add_object(pdf_fonts)
446458 resources['Font'] = pdf_fonts.reference
447459 _use_references(pdf, resources, images)
454466 name_array.append(pydyf.String(anchor[0]))
455467 name_array.append(anchor[1])
456468 dests = pydyf.Dictionary({'Names': name_array})
457 pdf.catalog['Names'] = pydyf.Dictionary({'Dests': dests})
469 if 'Names' in pdf.catalog:
470 pdf.catalog['Names']['Dests'] = dests
471 else:
472 pdf.catalog['Names'] = pydyf.Dictionary({'Dests': dests})
473
474 # Apply PDF variants functions
475 if variant:
476 variant_function(pdf, metadata, document, page_streams)
458477
459478 return pdf
200200 }
201201
202202 # Decode bitmaps
203 if glyph_format in (1, 6):
203 if 0 in (width, height) or not data:
204 glyph_info['bitmap'] = b''
205 elif glyph_format in (1, 6):
204206 glyph_info['bitmap'] = data
205207 elif glyph_format in (2, 5, 7):
206208 padding = (8 - (width % 8)) % 8
0 """PDF metadata stream generation."""
1
2 from xml.etree.ElementTree import (
3 Element, SubElement, register_namespace, tostring)
4
5 import pydyf
6
7 from .. import __version__
8
9 # XML namespaces used for metadata
10 NS = {
11 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
12 'dc': 'http://purl.org/dc/elements/1.1/',
13 'xmp': 'http://ns.adobe.com/xap/1.0/',
14 'pdf': 'http://ns.adobe.com/pdf/1.3/',
15 'pdfaid': 'http://www.aiim.org/pdfa/ns/id/',
16 'pdfuaid': 'http://www.aiim.org/pdfua/ns/id/',
17 }
18 for key, value in NS.items():
19 register_namespace(key, value)
20
21
22 def add_metadata(pdf, metadata, variant, version, conformance):
23 """Add PDF stream of metadata.
24
25 Described in ISO-32000-1:2008, 14.3.2.
26
27 """
28 # Add metadata
29 namespace = f'pdf{variant}id'
30 rdf = Element(f'{{{NS["rdf"]}}}RDF')
31
32 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
33 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
34 element.attrib[f'{{{NS[namespace]}}}part'] = str(version)
35 if conformance:
36 element.attrib[f'{{{NS[namespace]}}}conformance'] = conformance
37
38 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
39 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
40 element.attrib[f'{{{NS["pdf"]}}}Producer'] = f'WeasyPrint {__version__}'
41
42 if metadata.title:
43 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
44 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
45 element = SubElement(element, f'{{{NS["dc"]}}}title')
46 element = SubElement(element, f'{{{NS["rdf"]}}}Alt')
47 element = SubElement(element, f'{{{NS["rdf"]}}}li')
48 element.attrib['xml:lang'] = 'x-default'
49 element.text = metadata.title
50 if metadata.authors:
51 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
52 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
53 element = SubElement(element, f'{{{NS["dc"]}}}creator')
54 element = SubElement(element, f'{{{NS["rdf"]}}}Seq')
55 for author in metadata.authors:
56 author_element = SubElement(element, f'{{{NS["rdf"]}}}li')
57 author_element.text = author
58 if metadata.description:
59 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
60 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
61 element = SubElement(element, f'{{{NS["dc"]}}}subject')
62 element = SubElement(element, f'{{{NS["rdf"]}}}Bag')
63 element = SubElement(element, f'{{{NS["rdf"]}}}li')
64 element.text = metadata.description
65 if metadata.keywords:
66 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
67 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
68 element = SubElement(element, f'{{{NS["pdf"]}}}Keywords')
69 element.text = ', '.join(metadata.keywords)
70 if metadata.generator:
71 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
72 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
73 element = SubElement(element, f'{{{NS["xmp"]}}}CreatorTool')
74 element.text = metadata.generator
75 if metadata.created:
76 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
77 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
78 element = SubElement(element, f'{{{NS["xmp"]}}}CreateDate')
79 element.text = metadata.created
80 if metadata.modified:
81 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
82 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
83 element = SubElement(element, f'{{{NS["xmp"]}}}ModifyDate')
84 element.text = metadata.modified
85 xml = tostring(rdf, encoding='utf-8')
86 header = b'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>'
87 footer = b'<?xpacket end="r"?>'
88 stream_content = b'\n'.join((header, xml, footer))
89 extra = {'Type': '/Metadata', 'Subtype': '/XML'}
90 metadata = pydyf.Stream([stream_content], extra=extra)
91 pdf.add_object(metadata)
92 pdf.catalog['Metadata'] = metadata.reference
00 """PDF/A generation."""
11
2 from functools import partial
23 from importlib.resources import read_binary
3 from xml.etree.ElementTree import (
4 Element, SubElement, register_namespace, tostring)
54
65 import pydyf
76
8 from .. import __version__
97 from ..logger import LOGGER
10
11 # XML namespaces used for metadata
12 NS = {
13 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
14 'dc': 'http://purl.org/dc/elements/1.1/',
15 'xmp': 'http://ns.adobe.com/xap/1.0/',
16 'pdf': 'http://ns.adobe.com/pdf/1.3/',
17 'pdfaid': 'http://www.aiim.org/pdfa/ns/id/',
18 }
19 for key, value in NS.items():
20 register_namespace(key, value)
8 from .metadata import add_metadata
219
2210
23 def pdfa(pdf, metadata, version):
11 def pdfa(pdf, metadata, document, page_streams, version):
2412 """Set metadata for PDF/A documents."""
2513 LOGGER.warning(
2614 'PDF/A support is experimental, '
2715 'generated PDF files are not guaranteed to be valid. '
2816 'Please open an issue if you have problems generating PDF/A files.')
29
30 # Set PDF version
31 if version == 1:
32 pdf.version = b'1.4'
33 elif version in (2, 3):
34 pdf.version = b'1.7'
35 else:
36 pdf.version = b'2.0'
3717
3818 # Add ICC profile
3919 profile = pydyf.Stream(
5030 }),
5131 ])
5232
53 # Add metadata
54 rdf = Element(f'{{{NS["rdf"]}}}RDF')
55
56 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
57 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
58 element.attrib[f'{{{NS["pdfaid"]}}}part'] = str(version)
59 element.attrib[f'{{{NS["pdfaid"]}}}conformance'] = 'B'
60
61 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
62 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
63 element.attrib[f'{{{NS["pdf"]}}}Producer'] = f'WeasyPrint {__version__}'
64
65 if metadata.title:
66 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
67 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
68 element = SubElement(element, f'{{{NS["dc"]}}}title')
69 element = SubElement(element, f'{{{NS["rdf"]}}}Alt')
70 element = SubElement(element, f'{{{NS["rdf"]}}}li')
71 element.attrib['xml:lang'] = 'x-default'
72 element.text = metadata.title
73 if metadata.authors:
74 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
75 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
76 element = SubElement(element, f'{{{NS["dc"]}}}creator')
77 element = SubElement(element, f'{{{NS["rdf"]}}}Seq')
78 for author in metadata.authors:
79 author_element = SubElement(element, f'{{{NS["rdf"]}}}li')
80 author_element.text = author
81 if metadata.description:
82 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
83 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
84 element = SubElement(element, f'{{{NS["dc"]}}}subject')
85 element = SubElement(element, f'{{{NS["rdf"]}}}Bag')
86 element = SubElement(element, f'{{{NS["rdf"]}}}li')
87 element.text = metadata.description
88 if metadata.keywords:
89 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
90 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
91 element = SubElement(element, f'{{{NS["pdf"]}}}Keywords')
92 element.text = ', '.join(metadata.keywords)
93 if metadata.generator:
94 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
95 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
96 element = SubElement(element, f'{{{NS["xmp"]}}}CreatorTool')
97 element.text = metadata.generator
98 if metadata.created:
99 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
100 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
101 element = SubElement(element, f'{{{NS["xmp"]}}}CreateDate')
102 element.text = metadata.created
103 if metadata.modified:
104 element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
105 element.attrib[f'{{{NS["rdf"]}}}about'] = ''
106 element = SubElement(element, f'{{{NS["xmp"]}}}ModifyDate')
107 element.text = metadata.modified
108 xml = tostring(rdf, encoding='utf-8')
109 header = b'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>'
110 footer = b'<?xpacket end="r"?>'
111 stream_content = b'\n'.join((header, xml, footer))
112 extra = {'Type': '/Metadata', 'Subtype': '/XML'}
113 metadata = pydyf.Stream([stream_content], extra=extra)
114 pdf.add_object(metadata)
115 pdf.catalog['Metadata'] = metadata.reference
33 # Common PDF metadata stream
34 add_metadata(pdf, metadata, 'a', version, 'B')
11635
11736
11837 VARIANTS = {
119 'pdf/a-1b': lambda pdf, metadata: pdfa(pdf, metadata, 1),
120 'pdf/a-2b': lambda pdf, metadata: pdfa(pdf, metadata, 2),
121 'pdf/a-3b': lambda pdf, metadata: pdfa(pdf, metadata, 3),
122 'pdf/a-4b': lambda pdf, metadata: pdfa(pdf, metadata, 4),
123 }
38 f'pdf/a-{i}b': (partial(pdfa, version=i), {'version': pdf_version})
39 for i, pdf_version in enumerate(('1.4', '1.7', '1.7', '2.0'), start=1)}
0 """PDF/UA generation."""
1
2 import pydyf
3
4 from ..logger import LOGGER
5 from .metadata import add_metadata
6
7
8 def pdfua(pdf, metadata, document, page_streams):
9 """Set metadata for PDF/UA documents."""
10 LOGGER.warning(
11 'PDF/UA support is experimental, '
12 'generated PDF files are not guaranteed to be valid. '
13 'Please open an issue if you have problems generating PDF/UA files.')
14
15 # Structure for PDF tagging
16 content_mapping = pydyf.Dictionary({})
17 pdf.add_object(content_mapping)
18 structure_root = pydyf.Dictionary({
19 'Type': '/StructTreeRoot',
20 'ParentTree': content_mapping.reference,
21 })
22 pdf.add_object(structure_root)
23 structure_document = pydyf.Dictionary({
24 'Type': '/StructElem',
25 'S': '/Document',
26 'P': structure_root.reference,
27 })
28 pdf.add_object(structure_document)
29 structure_root['K'] = pydyf.Array([structure_document.reference])
30 pdf.catalog['StructTreeRoot'] = structure_root.reference
31
32 document_children = []
33 content_mapping['Nums'] = pydyf.Array()
34 links = []
35 for page_number, page_stream in enumerate(page_streams):
36 structure = {}
37 document.build_element_structure(structure)
38 parents = [None] * len(page_stream.marked)
39 for mcid, (key, box) in enumerate(page_stream.marked):
40 # Build structure elements
41 kids = [mcid]
42 if key == 'Link':
43 object_reference = pydyf.Dictionary({
44 'Type': '/OBJR',
45 'Obj': box.link_annotation.reference,
46 'Pg': pdf.page_references[page_number],
47 })
48 pdf.add_object(object_reference)
49 links.append((object_reference.reference, box.link_annotation))
50 etree_element = box.element
51 child_structure_data_element = None
52 while True:
53 if etree_element is None:
54 structure_data = structure.setdefault(
55 box, {'parent': None})
56 else:
57 structure_data = structure[etree_element]
58 new_element = 'element' not in structure_data
59 if new_element:
60 child = structure_data['element'] = pydyf.Dictionary({
61 'Type': '/StructElem',
62 'S': f'/{key}',
63 'K': pydyf.Array(kids),
64 'Pg': pdf.page_references[page_number],
65 })
66 pdf.add_object(child)
67 if key == 'LI':
68 if etree_element.tag == 'dt':
69 sub_key = 'Lbl'
70 else:
71 sub_key = 'LBody'
72 real_child = pydyf.Dictionary({
73 'Type': '/StructElem',
74 'S': f'/{sub_key}',
75 'K': pydyf.Array(kids),
76 'Pg': pdf.page_references[page_number],
77 'P': child.reference,
78 })
79 pdf.add_object(real_child)
80 for kid in kids:
81 if isinstance(kid, int):
82 parents[kid] = real_child.reference
83 child['K'] = pydyf.Array([real_child.reference])
84 structure_data['element'] = real_child
85 else:
86 for kid in kids:
87 if isinstance(kid, int):
88 parents[kid] = child.reference
89 else:
90 child = structure_data['element']
91 child['K'].extend(kids)
92 for kid in kids:
93 if isinstance(kid, int):
94 parents[kid] = child.reference
95 kid = child.reference
96 if child_structure_data_element is not None:
97 child_structure_data_element['P'] = kid
98 if not new_element:
99 break
100 kids = [kid]
101 child_structure_data_element = child
102 if structure_data['parent'] is None:
103 child['P'] = structure_document.reference
104 document_children.append(child.reference)
105 break
106 else:
107 etree_element = structure_data['parent']
108 key = page_stream.get_marked_content_tag(etree_element.tag)
109 content_mapping['Nums'].append(page_number)
110 content_mapping['Nums'].append(pydyf.Array(parents))
111 structure_document['K'] = pydyf.Array(document_children)
112 for i, (link, annotation) in enumerate(links, start=page_number + 1):
113 content_mapping['Nums'].append(i)
114 content_mapping['Nums'].append(link)
115 annotation['StructParent'] = i
116
117 # Common PDF metadata stream
118 add_metadata(pdf, metadata, 'ua', version=1, conformance=None)
119
120 # PDF document extra metadata
121 if 'Lang' not in pdf.catalog:
122 pdf.catalog['Lang'] = pydyf.String()
123 pdf.catalog['ViewerPreferences'] = pydyf.Dictionary({
124 'DisplayDocTitle': 'true',
125 })
126 pdf.catalog['MarkInfo'] = pydyf.Dictionary({'Marked': 'true'})
127
128
129 VARIANTS = {'pdf/ua-1': (pdfua, {'mark': True})}
00 """PDF stream."""
11
2 import hashlib
32 import io
43 import struct
4 from functools import lru_cache
55
66 import pydyf
77 from fontTools import subset
88 from fontTools.ttLib import TTFont, TTLibError, ttFont
9 from fontTools.varLib.mutator import instantiateVariableFont
910
1011 from ..logger import LOGGER
1112 from ..matrix import Matrix
12 from ..text.ffi import ffi, harfbuzz, pango
13 from ..text.ffi import ffi, harfbuzz, pango, units_to_double
1314
1415
1516 class Font:
16 def __init__(self, pango_font, hb_face):
17 def __init__(self, pango_font):
18 hb_font = pango.pango_font_get_hb_font(pango_font)
19 hb_face = harfbuzz.hb_font_get_face(hb_font)
1720 hb_blob = ffi.gc(
1821 harfbuzz.hb_face_reference_blob(hb_face),
1922 harfbuzz.hb_blob_destroy)
2326 self.index = harfbuzz.hb_face_get_index(hb_face)
2427
2528 pango_metrics = pango.pango_font_get_metrics(pango_font, ffi.NULL)
26 description = pango.pango_font_describe(pango_font)
27 font_size = pango.pango_font_description_get_size(description)
29 self.description = description = ffi.gc(
30 pango.pango_font_describe(pango_font),
31 pango.pango_font_description_free)
32 self.font_size = pango.pango_font_description_get_size(description)
2833 self.style = pango.pango_font_description_get_style(description)
2934 self.family = ffi.string(
3035 pango.pango_font_description_get_family(description))
31 digest = hashlib.sha1(self.file_content + bytes(self.index)).digest()
32 self.hash = ''.join(chr(65 + letter % 26) for letter in digest[:6])
36 digest = pango.pango_font_description_hash(description)
37 self.hash = ''.join(
38 chr(65 + letter % 26) + chr(65 + (letter << 6) % 26) for letter
39 in (hash(str(digest)) % (2 ** (3 * 8))).to_bytes(3, 'big'))
3340
3441 # Name
3542 description_string = ffi.string(
4451 self.name = b'/' + self.hash.encode() + b'+' + b'-'.join(fields)
4552
4653 # Ascent & descent
47 if font_size:
54 if self.font_size:
4855 self.ascent = int(
4956 pango.pango_font_metrics_get_ascent(pango_metrics) /
50 font_size * 1000)
57 self.font_size * 1000)
5158 self.descent = -int(
5259 pango.pango_font_metrics_get_descent(pango_metrics) /
53 font_size * 1000)
60 self.font_size * 1000)
5461 else:
5562 self.ascent = self.descent = 0
5663
6370 self.ttfont = None
6471 self.bitmap = False
6572 else:
66 self.bitmap = 'EBDT' in self.ttfont and 'EBLC' in self.ttfont
73 self.bitmap = (
74 'EBDT' in self.ttfont and
75 'EBLC' in self.ttfont and
76 not self.ttfont['glyf'].glyphs)
6777
6878 # Various properties
6979 self.italic_angle = 0 # TODO: this should be different
8090 self.flags = 2 ** (3 - 1) # Symbolic, custom character set
8191 if self.style:
8292 self.flags += 2 ** (7 - 1) # Italic
83 if b'Serif' in self.family.split():
93 if b'Serif' in fields:
8494 self.flags += 2 ** (2 - 1) # Serif
8595 widths = self.widths.values()
8696 if len(widths) > 1 and len(set(widths)) == 1:
90100 if self.ttfont is None:
91101 return
92102
103 # Subset font
93104 if cmap:
94105 optimized_font = io.BytesIO()
95106 options = subset.Options(
96107 retain_gids=True, passthrough_tables=True,
97 ignore_missing_glyphs=True, hinting=False)
108 ignore_missing_glyphs=True, hinting=False,
109 desubroutinize=True)
98110 options.drop_tables += ['GSUB', 'GPOS', 'SVG']
99111 subsetter = subset.Subsetter(options)
100112 subsetter.populate(gids=cmap)
105117 else:
106118 self.ttfont.save(optimized_font)
107119 self.file_content = optimized_font.getvalue()
120
121 # Transform variable into static font
122 if 'fvar' in self.ttfont:
123 variations = pango.pango_font_description_get_variations(
124 self.description)
125 if variations == ffi.NULL:
126 variations = {}
127 else:
128 variations = {
129 part.split('=')[0]: float(part.split('=')[1])
130 for part in ffi.string(variations).decode().split(',')}
131 if 'wght' not in variations:
132 variations['wght'] = pango.pango_font_description_get_weight(
133 self.description)
134 if 'opsz' not in variations:
135 variations['opsz'] = units_to_double(self.font_size)
136 if 'slnt' not in variations:
137 slnt = 0
138 if self.style == 1:
139 for axe in self.ttfont['fvar'].axes:
140 if axe.axisTag == 'slnt':
141 if axe.maxValue == 0:
142 slnt = axe.minValue
143 else:
144 slnt = axe.maxValue
145 break
146 variations['slnt'] = slnt
147 if 'ital' not in variations:
148 variations['ital'] = int(self.style == 2)
149 partial_font = io.BytesIO()
150 try:
151 ttfont = instantiateVariableFont(self.ttfont, variations)
152 for key, (advance, bearing) in ttfont['hmtx'].metrics.items():
153 if advance < 0:
154 ttfont['hmtx'].metrics[key] = (0, bearing)
155 ttfont.save(partial_font)
156 except Exception:
157 LOGGER.warning('Unable to mutate variable font')
158 else:
159 self.ttfont = ttfont
160 self.file_content = partial_font.getvalue()
108161
109162 if not (self.png or self.svg):
110163 return
139192 class Stream(pydyf.Stream):
140193 """PDF stream object with extra features."""
141194 def __init__(self, fonts, page_rectangle, states, x_objects, patterns,
142 shadings, images, *args, **kwargs):
195 shadings, images, mark, *args, **kwargs):
143196 super().__init__(*args, **kwargs)
144197 self.compress = True
145198 self.page_rectangle = page_rectangle
199 self.marked = []
146200 self._fonts = fonts
147201 self._states = states
148202 self._x_objects = x_objects
149203 self._patterns = patterns
150204 self._shadings = shadings
151205 self._images = images
206 self._mark = mark
152207 self._current_color = self._current_color_stroke = None
153208 self._current_alpha = self._current_alpha_stroke = None
154209 self._current_font = self._current_font_size = None
169224 self._ctm_stack.append(self.ctm)
170225
171226 def pop_state(self):
172 super().pop_state()
227 if self.stream and self.stream[-1] == b'q':
228 self.stream.pop()
229 else:
230 super().pop_state()
173231 self._current_color = self._current_color_stroke = None
174232 self._current_alpha = self._current_alpha_stroke = None
175233 self._current_font = None
257315 'BM': f'/{mode}',
258316 }))
259317
318 @lru_cache()
260319 def add_font(self, pango_font):
261 hb_font = pango.pango_font_get_hb_font(pango_font)
262 hb_face = harfbuzz.hb_font_get_face(hb_font)
263 if hb_face not in self._fonts:
264 self._fonts[hb_face] = Font(pango_font, hb_face)
265 return self._fonts[hb_face]
320 if pango.pango_version() > 14600:
321 pango_face = pango.pango_font_get_face(pango_font)
322 description = pango.pango_font_face_describe(pango_face)
323 else:
324 description = pango.pango_font_describe(pango_font)
325 key = pango.pango_font_description_hash(description)
326 pango.pango_font_description_free(description)
327 if key not in self._fonts:
328 self._fonts[key] = Font(pango_font)
329 return self._fonts[key]
266330
267331 def add_group(self, bounding_box):
268332 states = pydyf.Dictionary()
290354 })
291355 group = Stream(
292356 self._fonts, self.page_rectangle, states, x_objects, patterns,
293 shadings, self._images, extra=extra)
357 shadings, self._images, self._mark, extra=extra)
294358 group.id = f'x{len(self._x_objects)}'
295359 self._x_objects[group.id] = group
296360 return group
425489 })
426490 pattern = Stream(
427491 self._fonts, self.page_rectangle, states, x_objects, patterns,
428 shadings, self._images, extra=extra)
492 shadings, self._images, self._mark, extra=extra)
429493 pattern.id = f'p{len(self._patterns)}'
430494 self._patterns[pattern.id] = pattern
431495 return pattern
445509 self._shadings[shading.id] = shading
446510 return shading
447511
512 def begin_marked_content(self, box, mcid=False, tag=None):
513 if not self._mark:
514 return
515 property_list = None
516 if tag is None:
517 tag = self.get_marked_content_tag(box.element_tag)
518 if mcid:
519 property_list = pydyf.Dictionary({'MCID': len(self.marked)})
520 self.marked.append((tag, box))
521 super().begin_marked_content(tag, property_list)
522
523 def end_marked_content(self):
524 if not self._mark:
525 return
526 super().end_marked_content()
527
448528 @staticmethod
449529 def create_interpolation_function(domain, c0, c1, n):
450530 return pydyf.Dictionary({
464544 'Bounds': pydyf.Array(bounds),
465545 'Functions': pydyf.Array(sub_functions),
466546 })
547
548 def get_marked_content_tag(self, element_tag):
549 if element_tag == 'div':
550 return 'Div'
551 elif element_tag == 'span':
552 return 'Span'
553 elif element_tag == 'article':
554 return 'Art'
555 elif element_tag == 'section':
556 return 'Sect'
557 elif element_tag == 'blockquote':
558 return 'BlockQuote'
559 elif element_tag == 'p':
560 return 'P'
561 elif element_tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
562 return element_tag.upper()
563 elif element_tag in ('dl', 'ul', 'ol'):
564 return 'L'
565 elif element_tag == 'li':
566 return 'LI'
567 elif element_tag == 'dt':
568 return 'LI'
569 elif element_tag == 'dd':
570 return 'LI'
571 elif element_tag == 'table':
572 return 'Table'
573 elif element_tag in ('tr', 'th', 'td'):
574 return element_tag.upper()
575 elif element_tag in ('thead', 'tbody', 'tfoot'):
576 return element_tag[:2].upper() + element_tag[2:]
577 else:
578 return 'NonStruct'
00 """Stacking contexts management."""
1
2 import operator
31
42 from .formatting_structure import boxes
53 from .layout.absolute import AbsolutePlaceholder
6
7 _Z_INDEX_GETTER = operator.attrgetter('z_index')
84
95
106 class StackingContext:
117 """Stacking contexts define the paint order of all pieces of a document.
128
13 http://www.w3.org/TR/CSS21/visuren.html#x43
14 http://www.w3.org/TR/CSS21/zindex.html
9 https://www.w3.org/TR/CSS21/visuren.html#x43
10 https://www.w3.org/TR/CSS21/zindex.html
1511
1612 """
1713 def __init__(self, box, child_contexts, blocks, floats, blocks_and_cells,
3228 self.zero_z_contexts.append(context)
3329 else: # context.z_index > 0
3430 self.positive_z_contexts.append(context)
35 self.negative_z_contexts.sort(key=_Z_INDEX_GETTER)
36 self.positive_z_contexts.sort(key=_Z_INDEX_GETTER)
31 self.negative_z_contexts.sort(key=lambda context: context.z_index)
32 self.positive_z_contexts.sort(key=lambda context: context.z_index)
3733 # sort() is stable, so the lists are now storted
3834 # by z-index, then tree order.
3935
5753 child_contexts = children
5854 # child_contexts: where to put sub-contexts that we find here.
5955 # May not be the same as children for:
60 # "treat the element as if it created a new stacking context,
61 # but any positioned descendants and descendants which actually
62 # create a new stacking context should be considered part of the
63 # parent stacking context, not this new one."
56 # "treat the element as if it created a new stacking context, but any
57 # positioned descendants and descendants which actually create a new
58 # stacking context should be considered part of the parent stacking
59 # context, not this new one."
6460 blocks = []
6561 floats = []
6662 blocks_and_cells = []
63 box = _dispatch_children(
64 box, page, child_contexts, blocks, floats, blocks_and_cells)
65 return cls(box, children, blocks, floats, blocks_and_cells, page)
6766
68 def dispatch(box):
69 if isinstance(box, AbsolutePlaceholder):
70 box = box._box
71 style = box.style
72 absolute_and_z_index = (
73 style['position'] != 'static' and style['z_index'] != 'auto')
74 if (absolute_and_z_index or
75 style['opacity'] < 1 or
76 # 'transform: none' gives a "falsy" empty list here
77 style['transform'] or
78 style['overflow'] != 'visible'):
79 # This box defines a new stacking context, remove it
80 # from the "normal" children list.
81 child_contexts.append(
82 StackingContext.from_box(box, page))
83 else:
84 if style['position'] != 'static':
85 assert style['z_index'] == 'auto'
86 # "Fake" context: sub-contexts will go in this
87 # `child_contexts` list.
88 # Insert at the position before creating the sub-context.
89 index = len(child_contexts)
90 child_contexts.insert(
91 index,
92 StackingContext.from_box(box, page, child_contexts))
93 elif box.is_floated():
94 floats.append(StackingContext.from_box(
95 box, page, child_contexts))
96 elif isinstance(
97 box, (boxes.InlineBlockBox, boxes.InlineFlexBox)):
98 # Have this fake stacking context be part of the "normal"
99 # box tree, because we need its position in the middle
100 # of a tree of inline boxes.
101 return StackingContext.from_box(box, page, child_contexts)
102 else:
103 if isinstance(box, boxes.BlockLevelBox):
104 blocks_index = len(blocks)
105 blocks_and_cells_index = len(blocks_and_cells)
106 elif isinstance(box, boxes.TableCellBox):
107 blocks_index = None
108 blocks_and_cells_index = len(blocks_and_cells)
109 else:
110 blocks_index = None
111 blocks_and_cells_index = None
11267
113 box = dispatch_children(box)
68 def _dispatch(box, page, child_contexts, blocks, floats, blocks_and_cells):
69 if isinstance(box, AbsolutePlaceholder):
70 box = box._box
71 style = box.style
11472
115 # Insert at the positions before dispatch the children.
116 if blocks_index is not None:
117 blocks.insert(blocks_index, box)
118 if blocks_and_cells_index is not None:
119 blocks_and_cells.insert(blocks_and_cells_index, box)
73 # Remove boxes defining a new stacking context from the children list.
74 defines_stacking_context = (
75 (style['position'] != 'static' and style['z_index'] != 'auto') or
76 style['opacity'] < 1 or
77 style['transform'] or # 'transform: none' gives a "falsy" empty list
78 style['overflow'] != 'visible')
79 if defines_stacking_context:
80 child_contexts.append(StackingContext.from_box(box, page))
81 return
12082
121 return box
83 if style['position'] != 'static':
84 assert style['z_index'] == 'auto'
85 # "Fake" context: sub-contexts will go in this `child_contexts` list.
86 # Insert at the position before creating the sub-context.
87 index = len(child_contexts)
88 stacking_context = StackingContext.from_box(box, page, child_contexts)
89 child_contexts.insert(index, stacking_context)
90 elif box.is_floated():
91 floats.append(StackingContext.from_box(box, page, child_contexts))
92 elif isinstance(box, (boxes.InlineBlockBox, boxes.InlineFlexBox)):
93 # Have this fake stacking context be part of the "normal" box tree,
94 # because we need its position in the middle of a tree of inline boxes.
95 return StackingContext.from_box(box, page, child_contexts)
96 else:
97 if isinstance(box, boxes.BlockLevelBox):
98 blocks_index = len(blocks)
99 blocks_and_cells_index = len(blocks_and_cells)
100 elif isinstance(box, boxes.TableCellBox):
101 blocks_index = None
102 blocks_and_cells_index = len(blocks_and_cells)
103 else:
104 blocks_index = None
105 blocks_and_cells_index = None
122106
123 def dispatch_children(box):
124 if not isinstance(box, boxes.ParentBox):
125 return box
107 box = _dispatch_children(
108 box, page, child_contexts, blocks, floats, blocks_and_cells)
126109
127 new_children = []
128 for child in box.children:
129 result = dispatch(child)
130 if result is not None:
131 new_children.append(result)
132 box = box.copy_with_children(new_children)
133 return box
110 # Insert at the positions before dispatch the children.
111 if blocks_index is not None:
112 blocks.insert(blocks_index, box)
113 if blocks_and_cells_index is not None:
114 blocks_and_cells.insert(blocks_and_cells_index, box)
134115
135 box = dispatch_children(box)
116 return box
136117
137 return cls(box, children, blocks, floats, blocks_and_cells, page)
118
119 def _dispatch_children(box, page, child_contexts, blocks, floats,
120 blocks_and_cells):
121 if not isinstance(box, boxes.ParentBox):
122 return box
123
124 new_children = []
125 for child in box.children:
126 result = _dispatch(
127 child, page, child_contexts, blocks, floats, blocks_and_cells)
128 if result is not None:
129 new_children.append(result)
130 return box.copy_with_children(new_children)
00 """Render SVG images."""
11
22 import re
3 from contextlib import suppress
34 from math import cos, hypot, pi, radians, sin, sqrt
45 from xml.etree import ElementTree
56
286287 self.tree = Node(wrapper, style)
287288 self.url = url
288289
290 # Replace 'currentColor' value
291 for key in COLOR_ATTRIBUTES:
292 if self.tree.get(key) == 'currentColor':
293 self.tree.attrib[key] = self.tree.get('color', 'black')
294
289295 self.filters = {}
290296 self.gradients = {}
291297 self.images = {}
414420
415421 # Draw node
416422 if visible and node.tag in TAGS:
417 try:
423 with suppress(PointError):
418424 TAGS[node.tag](self, node, font_size)
419 except PointError:
420 pass
421425
422426 # Draw node children
423427 if display and node.tag not in DEF_TYPES:
251251 cx = cxprime * cos(phi) - cyprime * sin(phi) + (x1 + x) / 2
252252 cy = cxprime * sin(phi) + cyprime * cos(phi) + (y1 + y) / 2
253253
254 if phi == 0 or phi == pi:
254 if phi in (0, pi):
255255 minx = cx - rx
256256 tminx = atan2(0, -rx)
257257 maxx = cx + rx
260260 tminy = atan2(-ry, 0)
261261 maxy = cy + ry
262262 tmaxy = atan2(ry, 0)
263 elif phi == pi / 2 or phi == 3 * pi / 2:
263 elif phi in (pi / 2, 3 * pi / 2):
264264 minx = cx - ry
265265 tminx = atan2(0, -ry)
266266 maxx = cx + ry
5757 # TODO: support contentStyleType on <svg>
5858 stylesheets = []
5959 for element in tree.etree_element.iter():
60 # http://www.w3.org/TR/SVG/styling.html#StyleElement
60 # https://www.w3.org/TR/SVG/styling.html#StyleElement
6161 if (element.tag == '{http://www.w3.org/2000/svg}style' and
6262 element.get('type', 'text/css') == 'text/css' and
6363 element.text):
4747
4848 if tree.tag in ('svg', 'symbol'):
4949 # Explicitely specified
50 # http://www.w3.org/TR/SVG11/struct.html#UseElement
50 # https://www.w3.org/TR/SVG11/struct.html#UseElement
5151 tree._etree_node.tag = 'svg'
5252 if 'width' in node.attrib and 'height' in node.attrib:
5353 tree.attrib['width'] = node.attrib['width']
244244 alpha_stream.stream = [f'/{alpha_shading.id} sh']
245245
246246 group.shading(shading.id)
247 pattern.set_alpha(1)
247248 pattern.draw_x_object(group.id)
248249 svg.stream.color_space('Pattern', stroke=stroke)
249250 svg.stream.set_color_special(pattern.id, stroke=stroke)
6767 ry = height / 2
6868
6969 # Inspired by Cairo Cookbook
70 # http://cairographics.org/cookbook/roundedrectangles/
70 # https://cairographics.org/cookbook/roundedrectangles/
7171 ARC_TO_BEZIER = 4 * (2 ** .5 - 1) / 3
7272 c1, c2 = ARC_TO_BEZIER * rx, ARC_TO_BEZIER * ry
7373
00 """Util functions for SVG rendering."""
11
22 import re
3 from contextlib import suppress
34 from math import cos, radians, sin, tan
45 from urllib.parse import urlparse
56
2829 if not string:
2930 return 0
3031
31 try:
32 with suppress(ValueError):
3233 return float(string)
33 except ValueError:
34 # Not a float, try something else
35 pass
3634
35 # Not a float, try something else
3736 string = normalize(string).split(' ', 1)[0]
3837 if string.endswith('%'):
3938 assert percentage_reference is not None
123122 if url and url.startswith('url(') and url.endswith(')'):
124123 url = url[4:-1]
125124 if len(url) >= 2:
126 for quote in '\'"':
127 if url[0] == url[-1]:
125 for quote in ("'", '"'):
126 if url[0] == url[-1] == quote:
128127 url = url[1:-1]
129128 break
130129 return urlparse(url or '')
00 """Imports of dynamic libraries used for text layout."""
11
22 import os
3 from contextlib import suppress
34
45 import cffi
56
4344 typedef ... PangoAttrList;
4445 typedef ... PangoAttrClass;
4546 typedef ... PangoFont;
47 typedef ... PangoFontFace;
4648 typedef guint PangoGlyph;
4749 typedef gint PangoGlyphUnit;
4850
104106 } GSList;
105107
106108 typedef struct {
109 void *shape_engine;
110 void *lang_engine;
111 PangoFont *font;
112 guint level;
113 guint gravity;
114 guint flags;
115 guint script;
116 PangoLanguage *language;
117 GSList *extra_attrs;
118 } PangoAnalysis;
119
120 typedef struct {
121 gint offset;
122 gint length;
123 gint num_chars;
124 PangoAnalysis analysis;
125 } PangoItem;
126
127 typedef struct {
128 PangoGlyphUnit width;
129 PangoGlyphUnit x_offset;
130 PangoGlyphUnit y_offset;
131 } PangoGlyphGeometry;
132
133 typedef struct {
134 guint is_cluster_start : 1;
135 } PangoGlyphVisAttr;
136
137 typedef struct {
138 PangoGlyph glyph;
139 PangoGlyphGeometry geometry;
140 PangoGlyphVisAttr attr;
141 } PangoGlyphInfo;
142
143 typedef struct {
144 gint num_glyphs;
145 PangoGlyphInfo *glyphs;
146 gint *log_clusters;
147 } PangoGlyphString;
148
149 typedef struct {
150 PangoItem *item;
151 PangoGlyphString *glyphs;
152 } PangoGlyphItem;
153
154 typedef struct GSListRuns {
155 PangoGlyphItem *data;
156 struct GSListRuns *next;
157 } GSListRuns;
158
159 typedef struct {
107160 const PangoAttrClass *klass;
108161 guint start_index;
109162 guint end_index;
113166 PangoLayout *layout;
114167 gint start_index;
115168 gint length;
116 GSList *runs;
169 GSListRuns *runs;
117170 guint is_paragraph_start : 1;
118171 guint resolved_dir : 3;
119172 } PangoLayoutLine;
140193 guint is_expandable_space : 1;
141194 guint is_word_boundary : 1;
142195 } PangoLogAttr;
143
144 typedef struct {
145 void *shape_engine;
146 void *lang_engine;
147 PangoFont *font;
148 guint level;
149 guint gravity;
150 guint flags;
151 guint script;
152 PangoLanguage *language;
153 GSList *extra_attrs;
154 } PangoAnalysis;
155
156 typedef struct {
157 gint offset;
158 gint length;
159 gint num_chars;
160 PangoAnalysis analysis;
161 } PangoItem;
162
163 typedef struct {
164 PangoGlyphUnit width;
165 PangoGlyphUnit x_offset;
166 PangoGlyphUnit y_offset;
167 } PangoGlyphGeometry;
168
169 typedef struct {
170 guint is_cluster_start : 1;
171 } PangoGlyphVisAttr;
172
173 typedef struct {
174 PangoGlyph glyph;
175 PangoGlyphGeometry geometry;
176 PangoGlyphVisAttr attr;
177 } PangoGlyphInfo;
178
179 typedef struct {
180 gint num_glyphs;
181 PangoGlyphInfo *glyphs;
182 gint *log_clusters;
183 } PangoGlyphString;
184
185 typedef struct {
186 PangoItem *item;
187 PangoGlyphString *glyphs;
188 } PangoGlyphItem;
189196
190197 int pango_version (void);
191198
219226 void pango_font_description_free (PangoFontDescription *desc);
220227 PangoFontDescription * pango_font_description_copy (
221228 const PangoFontDescription *desc);
229
222230 void pango_font_description_set_family (
223231 PangoFontDescription *desc, const char *family);
224232 void pango_font_description_set_style (
225233 PangoFontDescription *desc, PangoStyle style);
226 PangoStyle pango_font_description_get_style (
227 const PangoFontDescription *desc);
228234 void pango_font_description_set_stretch (
229235 PangoFontDescription *desc, PangoStretch stretch);
230236 void pango_font_description_set_weight (
231237 PangoFontDescription *desc, PangoWeight weight);
232238 void pango_font_description_set_absolute_size (
233239 PangoFontDescription *desc, double size);
240 void pango_font_description_set_variations (
241 PangoFontDescription* desc, const char* variations);
242
243 PangoStyle pango_font_description_get_style (
244 const PangoFontDescription *desc);
245 const char* pango_font_description_get_variations (
246 const PangoFontDescription* desc);
247 PangoWeight pango_font_description_get_weight (
248 const PangoFontDescription* desc);
234249 int pango_font_description_get_size (PangoFontDescription *desc);
235250
236251 int pango_glyph_string_get_width (PangoGlyphString *glyphs);
240255 PangoFontDescription * pango_font_describe (PangoFont *font);
241256 const char * pango_font_description_get_family (
242257 const PangoFontDescription *desc);
243 int pango_font_description_hash (const PangoFontDescription *desc);
258 guint pango_font_description_hash (const PangoFontDescription *desc);
244259
245260 PangoContext * pango_context_new ();
246261 PangoContext * pango_font_map_create_context (PangoFontMap *fontmap);
248263 PangoFontMetrics * pango_context_get_metrics (
249264 PangoContext *context, const PangoFontDescription *desc,
250265 PangoLanguage *language);
266 PangoFontMetrics * pango_font_get_metrics (
267 PangoFont *font, PangoLanguage *language);
251268 void pango_font_metrics_unref (PangoFontMetrics *metrics);
252269 int pango_font_metrics_get_ascent (PangoFontMetrics *metrics);
253270 int pango_font_metrics_get_descent (PangoFontMetrics *metrics);
259276 PangoFontMetrics *metrics);
260277 int pango_font_metrics_get_strikethrough_position (
261278 PangoFontMetrics *metrics);
262
263 void pango_context_set_round_glyph_positions (
264 PangoContext *context, gboolean round_positions);
265
266 PangoFontMetrics * pango_font_get_metrics (
267 PangoFont *font, PangoLanguage *language);
268
269279 void pango_font_get_glyph_extents (
270280 PangoFont *font, PangoGlyph glyph, PangoRectangle *ink_rect,
271281 PangoRectangle *logical_rect);
282 PangoFontFace* pango_font_get_face (PangoFont* font);
283 PangoFontDescription* pango_font_face_describe (PangoFontFace* face);
284
285 void pango_context_set_round_glyph_positions (
286 PangoContext *context, gboolean round_positions);
272287
273288 PangoAttrList * pango_attr_list_new (void);
274289 void pango_attr_list_unref (PangoAttrList *list);
376391 def _dlopen(ffi, *names):
377392 """Try various names for the same library, for different platforms."""
378393 for name in names:
379 try:
394 with suppress(OSError):
380395 return ffi.dlopen(name)
381 except OSError:
382 pass
383396 # Re-raise the exception.
397 print(
398 '\n-----\n\n'
399 'WeasyPrint could not import some external libraries. Please '
400 'carefully follow the installation steps before reporting an issue:\n'
401 'https://doc.courtbouillon.org/weasyprint/stable/'
402 'first_steps.html#installation\n'
403 'https://doc.courtbouillon.org/weasyprint/stable/'
404 'first_steps.html#troubleshooting',
405 '\n\n-----\n') # pragma: no cover
384406 return ffi.dlopen(names[0]) # pragma: no cover
385407
386408
389411 'WEASYPRINT_DLL_DIRECTORIES',
390412 'C:\\Program Files\\GTK3-Runtime Win64\\bin').split(';')
391413 for dll_directory in dll_directories:
392 try:
414 with suppress((OSError, FileNotFoundError)):
393415 os.add_dll_directory(dll_directory)
394 except (OSError, FileNotFoundError):
395 pass
396416
397417 gobject = _dlopen(
398418 ffi, 'gobject-2.0-0', 'gobject-2.0', 'libgobject-2.0-0',
406426 'libharfbuzz-0.dll')
407427 fontconfig = _dlopen(
408428 ffi, 'fontconfig-1', 'fontconfig', 'libfontconfig', 'libfontconfig.so.1',
409 'libfontconfig-1.dylib', 'libfontconfig-1.dll')
429 'libfontconfig.1.dylib', 'libfontconfig-1.dll')
410430 pangoft2 = _dlopen(
411431 ffi, 'pangoft2-1.0-0', 'pangoft2-1.0', 'libpangoft2-1.0-0',
412432 'libpangoft2-1.0.so.0', 'libpangoft2-1.0.dylib', 'libpangoft2-1.0-0.dll')
109109 self.font, style['font_weight'])
110110 pango.pango_font_description_set_absolute_size(
111111 self.font, units_from_double(font_size))
112 if style['font_variation_settings'] != 'normal':
113 string = ','.join(
114 f'{key}={value}' for key, value in
115 style['font_variation_settings']).encode()
116 pango.pango_font_description_set_variations(self.font, string)
112117 pango.pango_layout_set_font_description(self.layout, self.font)
113118
114119 text_decoration = style['text_decoration_line']
140145 if features and context:
141146 features = ','.join(
142147 f'{key} {value}' for key, value in features.items()).encode()
143 # TODO: attributes should be freed.
144148 # In the meantime, keep a cache to avoid leaking too many of them.
145149 attr = context.font_features.setdefault(
146150 features, pango.pango_attr_font_features_new(features))
151155 def get_first_line(self):
152156 first_line = pango.pango_layout_get_line_readonly(self.layout, 0)
153157 second_line = pango.pango_layout_get_line_readonly(self.layout, 1)
154 if second_line != ffi.NULL:
155 index = second_line.start_index
156 else:
157 index = None
158 index = None if second_line == ffi.NULL else second_line.start_index
158159 self.first_line_direction = first_line.resolved_dir
159160 return first_line, index
160161
161162 def set_text(self, text, justify=False):
162 try:
163 index = text.find('\n')
164 if index != -1:
163165 # Keep only the first line plus one character, we don't need more
164 text = text[:text.index('\n') + 2]
165 except ValueError:
166 # End-of-line not found, keep the whole text
167 pass
166 text = text[:index+2]
167 self.text = text
168168 text, bytestring = unicode_to_char_p(text)
169 self.text = bytestring.decode()
170169 pango.pango_layout_set_text(self.layout, text, -1)
171170
172171 word_spacing = self.style['word_spacing']
184183
185184 if self.text and (word_spacing or letter_spacing or word_breaking):
186185 attr_list = pango.pango_layout_get_attributes(self.layout)
187 if not attr_list:
188 # TODO: list should be freed
189 attr_list = pango.pango_attr_list_new()
186 if attr_list == ffi.NULL:
187 attr_list = ffi.gc(
188 pango.pango_attr_list_new(),
189 pango.pango_attr_list_unref)
190190
191191 def add_attr(start, end, spacing):
192 # TODO: attributes should be freed
193192 attr = pango.pango_attr_letter_spacing_new(spacing)
194193 attr.start_index, attr.end_index = start, end
195194 pango.pango_attr_list_change(attr_list, attr)
207206 position = bytestring.find(b' ', position + 1)
208207
209208 if word_breaking:
210 # TODO: attributes should be freed
211209 attr = pango.pango_attr_insert_hyphens_new(False)
212210 attr.start_index, attr.end_index = 0, len(bytestring)
213211 pango.pango_attr_list_change(attr_list, attr)
294292 max_width = None
295293
296294 # Step #1: Get a draft layout with the first line
297 layout = None
298295 if max_width is not None and max_width != inf and style['font_size']:
296 short_text = text
299297 if max_width == 0:
300298 # Trying to find minimum size, let's naively split on spaces and
301299 # keep one word + one letter
302300 space_index = text.find(' ')
303 if space_index == -1:
304 expected_length = len(text)
305 else:
306 expected_length = space_index + 2 # index + space + one letter
301 if space_index != -1:
302 short_text = text[:space_index+2] # index + space + one letter
307303 else:
308 expected_length = int(max_width / style['font_size'] * 2.5)
309 if expected_length < len(text):
310 # Try to use a small amount of text instead of the whole text
311 layout = create_layout(
312 text[:expected_length], style, context, max_width,
313 justification_spacing)
314 first_line, index = layout.get_first_line()
315 if index is None:
316 # The small amount of text fits in one line, give up and use
317 # the whole text
318 layout = None
319 if layout is None:
304 short_text = text[:int(max_width / style['font_size'] * 2.5)]
305 # Try to use a small amount of text instead of the whole text
306 layout = create_layout(
307 short_text, style, context, max_width, justification_spacing)
308 first_line, resume_index = layout.get_first_line()
309 if resume_index is None and short_text != text:
310 # The small amount of text fits in one line, give up and use
311 # the whole text
312 layout.set_text(text)
313 first_line, resume_index = layout.get_first_line()
314 else:
320315 layout = create_layout(
321316 text, style, context, original_max_width, justification_spacing)
322 first_line, index = layout.get_first_line()
323 resume_index = index
317 first_line, resume_index = layout.get_first_line()
324318
325319 # Step #2: Don't split lines when it's not needed
326320 if max_width is None:
328322 return first_line_metrics(
329323 first_line, text, layout, resume_index, space_collapse, style)
330324 first_line_width, _ = line_size(first_line, style)
331 if index is None and first_line_width <= max_width:
325 if resume_index is None and first_line_width <= max_width:
332326 # The first line fits in the available width
333327 return first_line_metrics(
334328 first_line, text, layout, resume_index, space_collapse, style)
336330 # Step #3: Try to put the first word of the second line on the first line
337331 # https://mail.gnome.org/archives/gtk-i18n-list/2013-September/msg00006
338332 # is a good thread related to this problem.
339 first_line_text = text.encode()[:index].decode()
333 first_line_text = text.encode()[:resume_index].decode()
340334 first_line_fits = (
341335 first_line_width <= max_width or
342336 ' ' in first_line_text.strip() or
343337 can_break_text(first_line_text.strip(), style['lang']))
344338 if first_line_fits:
345339 # The first line fits but may have been cut too early by Pango
346 second_line_text = text.encode()[index:].decode()
340 second_line_text = text.encode()[resume_index:].decode()
347341 else:
348342 # The line can't be split earlier, try to hyphenate the first word.
349343 first_line_text = ''
356350 # only try when space collapsing is allowed
357351 new_first_line_text = first_line_text + next_word
358352 layout.set_text(new_first_line_text)
359 first_line, index = layout.get_first_line()
360 first_line_width, _ = line_size(first_line, style)
361 if index is None and first_line_text:
362 # The next word fits in the first line, keep the layout
363 resume_index = len(new_first_line_text.encode()) + 1
364 return first_line_metrics(
365 first_line, text, layout, resume_index, space_collapse,
366 style)
367 elif index:
368 # Text may have been split elsewhere by Pango earlier
369 resume_index = index
370 else:
371 # Second line is None
372 resume_index = first_line.length + 1
373 if resume_index >= len(text.encode()):
374 resume_index = None
353 first_line, resume_index = layout.get_first_line()
354 if resume_index is None:
355 if first_line_text:
356 # The next word fits in the first line, keep the layout
357 resume_index = len(new_first_line_text.encode()) + 1
358 return first_line_metrics(
359 first_line, text, layout, resume_index, space_collapse,
360 style)
361 else:
362 # Second line is None
363 resume_index = first_line.length + 1
364 if resume_index >= len(text.encode()):
365 resume_index = None
375366 elif first_line_text:
376367 # We found something on the first line but we did not find a word on
377368 # the next line, no need to hyphenate, we can keep the current layout
385376 hyphenated = False
386377 soft_hyphen = '\xad'
387378
388 try_hyphenate = False
379 auto_hyphenation = manual_hyphenation = False
389380 if hyphens != 'none':
381 manual_hyphenation = soft_hyphen in first_line_text + next_word
382 if hyphens == 'auto' and lang:
390383 next_word_boundaries = get_next_word_boundaries(second_line_text, lang)
391384 if next_word_boundaries:
392385 # We have a word to hyphenate
404397 if space > limit_zone or space < 0:
405398 # Available space is worth the try, or the line is even too
406399 # long to fit: try to hyphenate
407 try_hyphenate = True
408
409 if try_hyphenate:
410 # Automatic hyphenation possible and next word is long enough
411 auto_hyphenation = hyphens == 'auto' and lang
412 manual_hyphenation = False
413 if auto_hyphenation:
414 if soft_hyphen in first_line_text or soft_hyphen in next_word:
415 # Automatic hyphenation opportunities within a word must be
416 # ignored if the word contains a conditional hyphen, in favor
417 # of the conditional hyphen(s).
418 # See https://drafts.csswg.org/css-text-3/#valdef-hyphens-auto
419 manual_hyphenation = True
420 else:
421 manual_hyphenation = hyphens == 'manual'
422
423 if manual_hyphenation:
424 # Manual hyphenation: check that the line ends with a soft
425 # hyphen and add the missing hyphen
426 if first_line_text.endswith(soft_hyphen):
427 # The first line has been split on a soft hyphen
428 if ' ' in first_line_text:
429 first_line_text, next_word = (
430 first_line_text.rsplit(' ', 1))
431 next_word = f' {next_word}'
432 layout.set_text(first_line_text)
433 first_line, index = layout.get_first_line()
434 resume_index = len((f'{first_line_text} ').encode())
435 else:
436 first_line_text, next_word = '', first_line_text
437 soft_hyphen_indexes = [
438 match.start() for match in re.finditer(soft_hyphen, next_word)]
439 soft_hyphen_indexes.reverse()
440 dictionary_iterations = [
441 next_word[:i + 1] for i in soft_hyphen_indexes]
442 elif auto_hyphenation:
443 dictionary_key = (lang, left, right, total)
444 dictionary = context.dictionaries.get(dictionary_key)
445 if dictionary is None:
446 dictionary = pyphen.Pyphen(lang=lang, left=left, right=right)
447 context.dictionaries[dictionary_key] = dictionary
448 dictionary_iterations = [
449 start for start, end in dictionary.iterate(next_word)]
450 else:
451 dictionary_iterations = []
452
453 if dictionary_iterations:
454 for first_word_part in dictionary_iterations:
455 new_first_line_text = (
456 first_line_text +
457 second_line_text[:start_word] +
458 first_word_part)
459 hyphenated_first_line_text = (
460 new_first_line_text + style['hyphenate_character'])
461 new_layout = create_layout(
462 hyphenated_first_line_text, style, context, max_width,
463 justification_spacing)
464 new_first_line, new_index = new_layout.get_first_line()
465 new_first_line_width, _ = line_size(new_first_line, style)
466 new_space = max_width - new_first_line_width
467 if new_index is None and (
468 new_space >= 0 or
469 first_word_part == dictionary_iterations[-1]):
470 hyphenated = True
471 layout = new_layout
472 first_line = new_first_line
473 index = new_index
474 resume_index = len(new_first_line_text.encode())
475 if text[len(new_first_line_text)] == soft_hyphen:
476 # Recreate the layout with no max_width to be sure that
477 # we don't break before the soft hyphen
478 pango.pango_layout_set_width(
479 layout.layout, units_from_double(-1))
480 resume_index += len(soft_hyphen.encode())
481 break
482
483 if not hyphenated and not first_line_text:
484 # Recreate the layout with no max_width to be sure that
485 # we don't break before or inside the hyphenate character
486 hyphenated = True
487 layout.set_text(hyphenated_first_line_text)
488 pango.pango_layout_set_width(
489 layout.layout, units_from_double(-1))
490 first_line, index = layout.get_first_line()
400 auto_hyphenation = True
401
402 # Automatic hyphenation opportunities within a word must be ignored if the
403 # word contains a conditional hyphen, in favor of the conditional
404 # hyphen(s).
405 # See https://drafts.csswg.org/css-text-3/#valdef-hyphens-auto
406 if manual_hyphenation:
407 # Manual hyphenation: check that the line ends with a soft
408 # hyphen and add the missing hyphen
409 if first_line_text.endswith(soft_hyphen):
410 # The first line has been split on a soft hyphen
411 if ' ' in first_line_text:
412 first_line_text, next_word = first_line_text.rsplit(' ', 1)
413 next_word = f' {next_word}'
414 layout.set_text(first_line_text)
415 first_line, _ = layout.get_first_line()
416 resume_index = len((f'{first_line_text} ').encode())
417 else:
418 first_line_text, next_word = '', first_line_text
419 soft_hyphen_indexes = [
420 match.start() for match in re.finditer(soft_hyphen, next_word)]
421 soft_hyphen_indexes.reverse()
422 dictionary_iterations = [
423 next_word[:i + 1] for i in soft_hyphen_indexes]
424 start_word = 0
425 elif auto_hyphenation:
426 dictionary_key = (lang, left, right, total)
427 dictionary = context.dictionaries.get(dictionary_key)
428 if dictionary is None:
429 dictionary = pyphen.Pyphen(lang=lang, left=left, right=right)
430 context.dictionaries[dictionary_key] = dictionary
431 dictionary_iterations = [
432 start for start, end in dictionary.iterate(next_word)]
433 else:
434 dictionary_iterations = []
435
436 if dictionary_iterations:
437 for first_word_part in dictionary_iterations:
438 new_first_line_text = (
439 first_line_text +
440 second_line_text[:start_word] +
441 first_word_part)
442 hyphenated_first_line_text = (
443 new_first_line_text + style['hyphenate_character'])
444 new_layout = create_layout(
445 hyphenated_first_line_text, style, context, max_width,
446 justification_spacing)
447 new_first_line, index = new_layout.get_first_line()
448 new_first_line_width, _ = line_size(new_first_line, style)
449 new_space = max_width - new_first_line_width
450 hyphenated = (
451 index is None and (
452 new_space >= 0 or
453 first_word_part == dictionary_iterations[-1]))
454 if hyphenated:
455 layout = new_layout
456 first_line = new_first_line
491457 resume_index = len(new_first_line_text.encode())
492 if text[len(first_line_text)] == soft_hyphen:
458 if text[len(new_first_line_text)] == soft_hyphen:
459 # Recreate the layout with no max_width to be sure that
460 # we don't break before the soft hyphen
461 pango.pango_layout_set_width(layout.layout, -1)
493462 resume_index += len(soft_hyphen.encode())
463 break
464
465 if not hyphenated and not first_line_text:
466 # Recreate the layout with no max_width to be sure that
467 # we don't break before or inside the hyphenate character
468 hyphenated = True
469 layout.set_text(hyphenated_first_line_text)
470 pango.pango_layout_set_width(layout.layout, -1)
471 first_line, _ = layout.get_first_line()
472 resume_index = len(new_first_line_text.encode())
473 if text[len(first_line_text)] == soft_hyphen:
474 resume_index += len(soft_hyphen.encode())
494475
495476 if not hyphenated and first_line_text.endswith(soft_hyphen):
496477 # Recreate the layout with no max_width to be sure that
499480 hyphenated_first_line_text = (
500481 first_line_text + style['hyphenate_character'])
501482 layout.set_text(hyphenated_first_line_text)
502 pango.pango_layout_set_width(
503 layout.layout, units_from_double(-1))
504 first_line, index = layout.get_first_line()
483 pango.pango_layout_set_width(layout.layout, -1)
484 first_line, _ = layout.get_first_line()
505485 resume_index = len(first_line_text.encode())
506486
507487 # Step 5: Try to break word if it's too long for the line
1414 from . import __version__
1515 from .logger import LOGGER
1616
17 # See http://stackoverflow.com/a/11687993/1162888
17 # See https://stackoverflow.com/a/11687993/1162888
1818 # Both are needed in Python 3 as the re module does not like to mix
19 # http://tools.ietf.org/html/rfc3986#section-3.1
19 # https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
2020 UNICODE_SCHEME_RE = re.compile('^([a-zA-Z][a-zA-Z0-9.+-]+):')
2121 BYTES_SCHEME_RE = re.compile(b'^([a-zA-Z][a-zA-Z0-9.+-]+):')
2222