New upstream version 57.0
Scott Kitterman
1 year, 6 months ago
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: weasyprint |
2 | Version: 56.1 | |
2 | Version: 57.0 | |
3 | 3 | Summary: The Awesome Document Factory |
4 | 4 | Keywords: html,css,pdf,converter |
5 | 5 | Author-email: Simon Sapin <simon.sapin@exyr.org> |
23 | 23 | Classifier: Topic :: Text Processing :: Markup :: HTML |
24 | 24 | Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion |
25 | 25 | Classifier: Topic :: Printing |
26 | Requires-Dist: pydyf >=0.2.0 | |
26 | Requires-Dist: pydyf >=0.5.0 | |
27 | 27 | Requires-Dist: cffi >=0.6 |
28 | 28 | Requires-Dist: html5lib >=1.1 |
29 | 29 | Requires-Dist: tinycss2 >=1.0.0 |
34 | 34 | Requires-Dist: sphinx ; extra == "doc" |
35 | 35 | Requires-Dist: sphinx_rtd_theme ; extra == "doc" |
36 | 36 | 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" | |
42 | 39 | Project-URL: Changelog, https://github.com/Kozea/WeasyPrint/releases |
43 | 40 | Project-URL: Code, https://github.com/Kozea/WeasyPrint |
44 | 41 | Project-URL: Documentation, https://doc.courtbouillon.org/weasyprint/ |
84 | 84 | are currently not supported, although a custom :ref:`URL fetcher |
85 | 85 | <URL Fetchers>` can help. |
86 | 86 | |
87 | .. _data URIs: http://en.wikipedia.org/wiki/Data_URI_scheme | |
87 | .. _data URIs: https://en.wikipedia.org/wiki/Data_URI_scheme | |
88 | 88 | |
89 | 89 | |
90 | 90 | HTML |
120 | 120 | HTML, try to enable this option. |
121 | 121 | |
122 | 122 | .. _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 | |
124 | 124 | .. _Pillow: https://python-pillow.org/ |
125 | 125 | |
126 | 126 | Stylesheet Origins |
139 | 139 | to raise their priority. |
140 | 140 | |
141 | 141 | .. _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 | |
144 | 144 | |
145 | 145 | |
146 | 146 | |
172 | 172 | specifications. The major rules to follow are to include a PDF identifier, to |
173 | 173 | check the PDF version, and to avoid anti-aliasing for images using |
174 | 174 | ``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. | |
175 | 181 | |
176 | 182 | |
177 | 183 | Fonts |
221 | 227 | * `System colors`_ and `system fonts`_. The former are deprecated in `CSS Color |
222 | 228 | Module Level 3`_. |
223 | 229 | |
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 | |
235 | 241 | |
236 | 242 | To the best of our knowledge, everything else that applies to the |
237 | 243 | print media **is** supported. Please report a bug if you find this list |
238 | 244 | incomplete. |
239 | 245 | |
240 | Selectors Level 3 | |
241 | +++++++++++++++++ | |
246 | Selectors Level 3 / 4 | |
247 | +++++++++++++++++++++ | |
242 | 248 | |
243 | 249 | With the exceptions noted here, all `Selectors Level 3`_ are supported. |
244 | 250 | |
246 | 252 | ``:target`` and ``:visited`` pseudo-classes are accepted as valid but |
247 | 253 | never match anything. |
248 | 254 | |
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/ | |
250 | 263 | |
251 | 264 | CSS Text Module Level 3 / 4 |
252 | 265 | +++++++++++++++++++++++++++ |
264 | 277 | - the ``overflow-wrap`` property replacing ``word-wrap``; |
265 | 278 | - the ``break-all`` value of the ``word-break`` property (see `#1153`_); |
266 | 279 | - 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 | |
267 | 282 | - the ``tab-size`` property. |
268 | 283 | |
269 | Experimental_ properties controling hyphenation_ are supported by WeasyPrint: | |
284 | Properties controling hyphenation_ are supported by WeasyPrint: | |
270 | 285 | |
271 | 286 | - ``hyphens``, |
272 | 287 | - ``hyphenate-character``, |
297 | 312 | supported: |
298 | 313 | |
299 | 314 | - 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; | |
303 | 316 | - the ``text-indent`` and ``hanging-punctuation`` properties. |
304 | 317 | |
305 | 318 | The other features provided by `CSS Text Module Level 4`_ are **not** |
314 | 327 | |
315 | 328 | .. _#1153: https://github.com/Kozea/WeasyPrint/issues/1153 |
316 | 329 | .. _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 | |
318 | 331 | .. _CSS Text Module Level 3: https://www.w3.org/TR/css-text-3/ |
319 | 332 | .. _CSS Text Module Level 4: https://www.w3.org/TR/css-text-4/ |
320 | 333 | |
321 | CSS Fonts Module Level 3 | |
322 | ++++++++++++++++++++++++ | |
334 | CSS Fonts Module Level 3 / 4 | |
335 | ++++++++++++++++++++++++++++ | |
323 | 336 | |
324 | 337 | The `CSS Fonts Module Level 3`_ is a candidate recommendation describing "how |
325 | 338 | font properties are specified and how font resources are loaded dynamically". |
343 | 356 | |
344 | 357 | The shorthand ``font`` and ``font-variant`` properties are supported. |
345 | 358 | |
346 | WeasyPrint supports the ``@font-face`` rule, provided that Pango >= 1.38 is installed. | |
359 | WeasyPrint supports the ``@font-face`` rule. | |
347 | 360 | |
348 | 361 | WeasyPrint does **not** support the ``@font-feature-values`` rule and the |
349 | 362 | values of ``font-variant-alternates`` other than ``normal`` and |
352 | 365 | The ``font-variant-caps`` property is supported but needs the small-caps variant of |
353 | 366 | the font to be installed. WeasyPrint does **not** simulate missing small-caps |
354 | 367 | 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 | ||
355 | 375 | |
356 | 376 | CSS Paged Media Module Level 3 |
357 | 377 | ++++++++++++++++++++++++++++++ |
375 | 395 | - the page ``size``, ``bleed`` and ``marks`` properties; |
376 | 396 | - the named pages. |
377 | 397 | |
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/ | |
379 | 399 | .. _#93: https://github.com/Kozea/WeasyPrint/issues/93 |
380 | 400 | |
381 | 401 | CSS Generated Content for Paged Media Module |
405 | 425 | |
406 | 426 | Page groups (``:nth(X of pagename)`` pseudo-class) are not supported. |
407 | 427 | |
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/ | |
409 | 429 | .. _Page selectors: https://www.w3.org/TR/css-gcpm-3/#document-page-selectors |
410 | 430 | .. _running elements: https://www.w3.org/TR/css-gcpm-3/#running-elements |
411 | 431 | .. _Footnotes: https://www.w3.org/TR/css-gcpm-3/#footnotes |
444 | 464 | In particular, ``target-counter()`` and ``target-text()`` are useful when it |
445 | 465 | comes to tables of contents (see `an example`_). |
446 | 466 | |
447 | You can also control `PDF bookmarks`_ with WeasyPrint. Using the experimental_ | |
467 | You can also control `PDF bookmarks`_ with WeasyPrint. Using the | |
448 | 468 | ``bookmark-level``, ``bookmark-label`` and ``bookmark-state`` properties, you |
449 | 469 | can add bookmarks that will be available in your PDF reader. |
450 | 470 | |
462 | 482 | - quotes (``content: *-quote``); |
463 | 483 | - leaders (``content: leader()``). |
464 | 484 | |
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/ | |
466 | 486 | .. _Quotes: https://www.w3.org/TR/css-content-3/#quotes |
467 | 487 | .. _Named strings: https://www.w3.org/TR/css-content-3/#named-strings |
468 | 488 | .. _Cross-references: https://www.w3.org/TR/css-content-3/#cross-references |
469 | 489 | .. _an example: https://github.com/Kozea/WeasyPrint/pull/652#issuecomment-403276559 |
470 | 490 | .. _PDF bookmarks: https://www.w3.org/TR/css-content-3/#bookmark-generation |
471 | .. _experimental: http://www.w3.org/TR/css-2010/#experimental | |
472 | 491 | .. _user agent stylesheet: https://github.com/Kozea/WeasyPrint/blob/master/weasyprint/css/html5_ua.css |
473 | 492 | |
474 | 493 | CSS Color Module Level 3 |
484 | 503 | This recommendation is fully implemented in WeasyPrint, except the deprecated |
485 | 504 | System Colors. |
486 | 505 | |
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/ | |
488 | 507 | |
489 | 508 | CSS Transforms Module Level 1 |
490 | 509 | +++++++++++++++++++++++++++++ |
495 | 514 | rotated and scaled in two or three dimensional space." |
496 | 515 | |
497 | 516 | 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``). | |
500 | 520 | |
501 | 521 | WeasyPrint does **not** support the ``transform-style``, ``perspective``, |
502 | 522 | ``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/ | |
507 | 527 | |
508 | 528 | CSS Backgrounds and Borders Module Level 3 |
509 | 529 | ++++++++++++++++++++++++++++++++++++++++++ |
534 | 554 | `git branch`_ that is not released, as it relies on raster implementation of |
535 | 555 | shadows. |
536 | 556 | |
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 | |
543 | 563 | .. _git branch: https://github.com/Kozea/WeasyPrint/pull/149 |
544 | 564 | |
545 | 565 | CSS Image Values and Replaced Content Module Level 3 / 4 |
566 | 586 | The ``from-image`` and ``snap`` values of the ``image-resolution`` property are |
567 | 587 | **not** supported, but the ``resolution`` value is supported. |
568 | 588 | |
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/ | |
575 | 593 | |
576 | 594 | CSS Box Sizing Module Level 3 |
577 | 595 | +++++++++++++++++++++++++++++ |
654 | 672 | The ``column-fill`` property is supported, with a column balancing algorithm |
655 | 673 | that should be efficient with simple cases. |
656 | 674 | |
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/ | |
658 | 676 | |
659 | 677 | CSS Fragmentation Module Level 3 / 4 |
660 | 678 | ++++++++++++++++++++++++++++++++++++ |
0 | 0 | Changelog |
1 | 1 | ========= |
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 | |
2 | 144 | |
3 | 145 | |
4 | 146 | Version 56.1 |
2318 | 2460 | Release process: |
2319 | 2461 | |
2320 | 2462 | * Drop Python 3.1 support. |
2321 | * Set up [Travis CI](http://travis-ci.org/) | |
2463 | * Set up [Travis CI](https://travis-ci.org/) | |
2322 | 2464 | to automatically test all pushes and pull requests. |
2323 | 2465 | * Start testing on Python 3.4 locally. (Travis does not support 3.4 yet.) |
2324 | 2466 | |
2330 | 2472 | |
2331 | 2473 | New features: |
2332 | 2474 | |
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>`_ | |
2334 | 2476 | property, allowing line breaks inside otherwise-unbreakable words. |
2335 | 2477 | Thanks Frédérick Deslandes! |
2336 | 2478 | * 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, | |
2338 | 2480 | allowing images to be sized proportionally to their intrinsic size |
2339 | 2481 | at a resolution other than 96 image pixels per CSS ``in`` |
2340 | 2482 | (ie. one image pixel per CSS ``px``) |
2605 | 2747 | WeasyPrint for fetching linked stylesheets or images, eg. to generate them |
2606 | 2748 | on the fly without going through the network. |
2607 | 2749 | This enables the creation of `Flask-WeasyPrint |
2608 | <http://packages.python.org/Flask-WeasyPrint/>`_. | |
2750 | <https://packages.python.org/Flask-WeasyPrint/>`_. | |
2609 | 2751 | |
2610 | 2752 | |
2611 | 2753 | Version 0.11 |
2657 | 2799 | |
2658 | 2800 | Bookmarks can be controlled by the ``-weasy-bookmark-level`` and |
2659 | 2801 | ``-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>`_. | |
2661 | 2803 | |
2662 | 2804 | The default UA stylesheet sets a matching bookmark level on all ``<h1>`` |
2663 | 2805 | to ``<h6>`` elements. |
2677 | 2819 | * Speed improvements on big stylesheets / small documents thanks to tinycss. |
2678 | 2820 | * Many bug fixes |
2679 | 2821 | |
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/ | |
2682 | 2824 | |
2683 | 2825 | |
2684 | 2826 | Version 0.7.1 |
41 | 41 | ----- |
42 | 42 | |
43 | 43 | Tests are stored in the ``tests`` folder at the top of the repository. They use |
44 | the `pytest`_ library. | |
44 | the pytest_ library. | |
45 | 45 | |
46 | You can launch tests (with code coverage and lint) using the following command:: | |
46 | You can launch tests using the following command:: | |
47 | 47 | |
48 | 48 | venv/bin/python -m pytest |
49 | 49 | |
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 | ||
50 | 56 | .. _pytest: https://docs.pytest.org/ |
57 | .. _isort: https://pycqa.github.io/isort/ | |
58 | .. _flake8: https://flake8.pycqa.org/ | |
51 | 59 | |
52 | 60 | |
53 | 61 | Documentation |
10 | 10 | |
11 | 11 | * Python_ ≥ 3.7.0 |
12 | 12 | * Pango_ ≥ 1.44.0 |
13 | * pydyf_ ≥ 0.2.0 | |
13 | * pydyf_ ≥ 0.5.0 | |
14 | 14 | * CFFI_ ≥ 0.6 |
15 | 15 | * html5lib_ ≥ 1.1 |
16 | 16 | * tinycss2_ ≥ 1.0.0 |
19 | 19 | * Pillow_ ≥ 4.0.0 |
20 | 20 | * fontTools_ ≥ 4.0.0 |
21 | 21 | |
22 | .. _Python: http://www.python.org/ | |
23 | .. _Pango: http://pango.gnome.org/ | |
22 | .. _Python: https://www.python.org/ | |
23 | .. _Pango: https://pango.gnome.org/ | |
24 | 24 | .. _CFFI: https://cffi.readthedocs.io/ |
25 | 25 | .. _html5lib: https://html5lib.readthedocs.io/ |
26 | 26 | .. _pydyf: https://doc.courtbouillon.org/pydyf/ |
27 | 27 | .. _tinycss2: https://doc.courtbouillon.org/tinycss2/ |
28 | 28 | .. _cssselect2: https://doc.courtbouillon.org/cssselect2/ |
29 | .. _Pyphen: http://pyphen.org/ | |
29 | .. _Pyphen: https://pyphen.org/ | |
30 | 30 | .. _Pillow: https://python-pillow.org/ |
31 | 31 | .. _fontTools: https://github.com/fonttools/fonttools |
32 | 32 | |
48 | 48 | |
49 | 49 | If WeasyPrint is not available on your distribution, or if you want to use a |
50 | 50 | 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:: | |
53 | 54 | |
54 | 55 | python3 --version |
55 | 56 | pango-view --version |
162 | 163 | macOS |
163 | 164 | ~~~~~ |
164 | 165 | |
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 | |
177 | 169 | |
178 | 170 | .. _Homebrew: https://brew.sh/ |
179 | 171 | |
306 | 298 | |
307 | 299 | .. code-block:: sh |
308 | 300 | |
309 | weasyprint http://weasyprint.org /tmp/weasyprint-website.pdf | |
301 | weasyprint https://weasyprint.org /tmp/weasyprint-website.pdf | |
310 | 302 | |
311 | 303 | You may see warnings on the standard error output about unsupported CSS |
312 | 304 | properties. See :ref:`Command-Line API` for the details of all available |
319 | 311 | |
320 | 312 | .. code-block:: sh |
321 | 313 | |
322 | weasyprint http://weasyprint.org /tmp/weasyprint-website.pdf \ | |
314 | weasyprint https://weasyprint.org /tmp/weasyprint-website.pdf \ | |
323 | 315 | -s <(echo 'body { font-family: serif !important }') |
324 | 316 | |
325 | 317 | If you have many documents to convert you may prefer using the Python API |
342 | 334 | .. code-block:: python |
343 | 335 | |
344 | 336 | 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') | |
346 | 338 | |
347 | 339 | … or with the inline stylesheet: |
348 | 340 | |
349 | 341 | .. code-block:: python |
350 | 342 | |
351 | 343 | 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', | |
353 | 345 | stylesheets=[CSS(string='body { font-family: serif !important }')]) |
354 | 346 | |
355 | 347 | Instantiating HTML and CSS Objects |
366 | 358 | HTML('../foo.html') # Same as … |
367 | 359 | HTML(filename='../foo.html') |
368 | 360 | |
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') | |
371 | 363 | |
372 | 364 | HTML(sys.stdin) # Same as … |
373 | 365 | HTML(file_obj=sys.stdin) |
399 | 391 | css = CSS(string=''' |
400 | 392 | @font-face { |
401 | 393 | font-family: Gentium; |
402 | src: url(http://example.com/fonts/Gentium.otf); | |
394 | src: url(https://example.com/fonts/Gentium.otf); | |
403 | 395 | } |
404 | 396 | h1 { font-family: Gentium }''', font_config=font_config) |
405 | 397 | html.write_pdf( |
444 | 436 | .. code-block:: python |
445 | 437 | |
446 | 438 | # 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 | |
448 | 440 | # 1. Introduction to CSS 2.1 (page 2) |
449 | 441 | # 1. A brief CSS 2.1 tutorial for HTML (page 2) |
450 | 442 | # 2. A brief CSS 2.1 tutorial for XML (page 5) |
509 | 501 | the function internally used by WeasyPrint to retreive data. |
510 | 502 | |
511 | 503 | .. _Flask-Weasyprint: https://github.com/Kozea/Flask-WeasyPrint |
512 | .. _Flask: http://flask.pocoo.org/ | |
504 | .. _Flask: https://flask.pocoo.org/ | |
513 | 505 | .. _Django-WeasyPrint: https://github.com/fdemmer/django-weasyprint |
514 | 506 | .. _Django: https://www.djangoproject.com/ |
515 | 507 | |
526 | 518 | .. code-block:: python |
527 | 519 | |
528 | 520 | # No size optimization, faster, but generated PDF is larger |
529 | HTML('http://example.org/').write_pdf( | |
521 | HTML('https://example.org/').write_pdf( | |
530 | 522 | 'example.pdf', optimize_size=()) |
531 | 523 | |
532 | 524 | # Full size optimization, slower, but generated PDF is smaller |
533 | HTML('http://example.org/').write_pdf( | |
525 | HTML('https://example.org/').write_pdf( | |
534 | 526 | 'example.pdf', optimize_size=('fonts', 'images')) |
535 | 527 | |
536 | 528 | ``image_cache`` gives the possibility to use a cache for images, avoiding to |
544 | 536 | |
545 | 537 | cache = {} |
546 | 538 | 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( | |
548 | 540 | f'example-{i}.pdf', image_cache=cache) |
549 | 541 | |
550 | 542 | |
609 | 601 | Infinite Requests |
610 | 602 | ~~~~~~~~~~~~~~~~~ |
611 | 603 | |
612 | WeasyPrint can reach files on the network, for example using ``http://`` | |
604 | WeasyPrint can reach files on the network, for example using ``https://`` | |
613 | 605 | URIs. For various reasons, HTTP requests may take a long time and lead to |
614 | 606 | problems similar to :ref:`Long Renderings`. |
615 | 607 | |
686 | 678 | |
687 | 679 | - locally installed fonts (using ``font-family`` and ``@font-face``), |
688 | 680 | - 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 | |
690 | 682 | documents), |
691 | 683 | - Python, Pango and other libraries versions (implementation details |
692 | 684 | lead to different renderings). |
99 | 99 | 7. Metadata −such as document information, attachments, embedded files, |
100 | 100 | hyperlinks, and PDF trim and bleed boxes− are added to the PDF. |
101 | 101 | |
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 | |
103 | 103 | |
104 | 104 | |
105 | 105 | Parsing HTML |
146 | 146 | *inheritance* (from the parent element) or the property’s *initial value*, |
147 | 147 | so that every element has a *specified value* for every property. |
148 | 148 | |
149 | .. _cascade: http://www.w3.org/TR/CSS21/cascade.html | |
149 | .. _cascade: https://www.w3.org/TR/CSS21/cascade.html | |
150 | 150 | |
151 | 151 | These *specified values* are turned into *computed values* in the |
152 | 152 | ``css.computed_values`` module. Keywords and lengths in various units are |
173 | 173 | generally close but not identical to the ElementTree tree: some elements |
174 | 174 | generate more than one box or none. |
175 | 175 | |
176 | .. _visual formatting model: http://www.w3.org/TR/CSS21/visuren.html | |
176 | .. _visual formatting model: https://www.w3.org/TR/CSS21/visuren.html | |
177 | 177 | |
178 | 178 | Boxes are of a lot of different kinds. For example you should not confuse |
179 | 179 | *block-level boxes* and *block containers*, though *block boxes* are both. The |
216 | 216 | According to the `box model`_, each box has rectangular margin, border, |
217 | 217 | padding and content areas: |
218 | 218 | |
219 | .. _box model: http://www.w3.org/TR/CSS21/box.html | |
219 | .. _box model: https://www.w3.org/TR/CSS21/box.html | |
220 | 220 | |
221 | 221 | .. image:: https://www.w3.org/TR/CSS21/images/boxdim.png |
222 | 222 | :alt: CSS Box Model |
238 | 238 | .. [#] These are the coordinates *if* no `CSS transform`_ applies. |
239 | 239 | Transforms change the actual location of boxes, but they are applied |
240 | 240 | 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/ | |
243 | 243 | |
244 | 244 | |
245 | 245 | Stacking & Drawing |
256 | 256 | |
257 | 257 | The code lives in the ``draw`` module. |
258 | 258 | |
259 | .. _stacking rules: http://www.w3.org/TR/CSS21/zindex.html | |
259 | .. _stacking rules: https://www.w3.org/TR/CSS21/zindex.html | |
260 | 260 | |
261 | 261 | |
262 | 262 | Metadata |
11 | 11 | readme = {file = 'README.rst', content-type = 'text/x-rst'} |
12 | 12 | license = {file = 'LICENSE'} |
13 | 13 | dependencies = [ |
14 | 'pydyf >=0.2.0', | |
14 | 'pydyf >=0.5.0', | |
15 | 15 | 'cffi >=0.6', |
16 | 16 | 'html5lib >=1.1', |
17 | 17 | 'tinycss2 >=1.0.0', |
51 | 51 | |
52 | 52 | [project.optional-dependencies] |
53 | 53 | doc = ['sphinx', 'sphinx_rtd_theme'] |
54 | test = ['pytest', 'pytest-xdist', 'pytest-flake8', 'pytest-isort', 'pytest-cov', 'coverage[toml]'] | |
54 | test = ['pytest', 'isort', 'flake8'] | |
55 | 55 | |
56 | 56 | [project.scripts] |
57 | 57 | weasyprint = 'weasyprint.__main__:main' |
58 | 58 | |
59 | 59 | [tool.flit.sdist] |
60 | exclude = ['.*', 'tests/results'] | |
61 | ||
62 | [tool.pytest.ini_options] | |
63 | addopts = '--isort --flake8 --numprocesses=auto' | |
60 | exclude = ['.*'] | |
64 | 61 | |
65 | 62 | [tool.coverage.run] |
66 | 63 | branch = true |
33 | 33 | pngs = run(command, stdout=PIPE).stdout |
34 | 34 | os.remove(pdf.name) |
35 | 35 | |
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}' | |
39 | 38 | |
40 | 39 | if split_images: |
41 | 40 | assert target is None |
48 | 48 | expected_pixels) |
49 | 49 | width, height, pixels = html_to_pixels(html) |
50 | 50 | 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}') | |
52 | 54 | assert_pixels_equal(name, width, height, pixels, expected_pixels) |
53 | 55 | |
54 | 56 |
17 | 17 | @page { size: 5px } |
18 | 18 | svg { display: block } |
19 | 19 | </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"> | |
21 | 21 | <defs> |
22 | 22 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
23 | 23 | gradientUnits="objectBoundingBox"> |
48 | 48 | @page { size: 10px } |
49 | 49 | svg { display: block } |
50 | 50 | </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"> | |
52 | 52 | <defs> |
53 | 53 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
54 | 54 | gradientUnits="objectBoundingBox"> |
79 | 79 | @page { size: 10px } |
80 | 80 | svg { display: block } |
81 | 81 | </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"> | |
83 | 83 | <defs> |
84 | 84 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
85 | 85 | gradientUnits="objectBoundingBox"> |
105 | 105 | @page { size: 5px } |
106 | 106 | svg { display: block } |
107 | 107 | </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"> | |
109 | 109 | <defs> |
110 | 110 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
111 | 111 | gradientUnits="objectBoundingBox"> |
132 | 132 | @page { size: 5px } |
133 | 133 | svg { display: block } |
134 | 134 | </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"> | |
136 | 136 | <defs> |
137 | 137 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
138 | 138 | gradientUnits="objectBoundingBox"> |
158 | 158 | @page { size: 5px } |
159 | 159 | svg { display: block } |
160 | 160 | </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"> | |
162 | 162 | <defs> |
163 | 163 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
164 | 164 | gradientUnits="objectBoundingBox"> |
183 | 183 | @page { size: 2px } |
184 | 184 | svg { display: block } |
185 | 185 | </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"> | |
187 | 187 | <defs> |
188 | 188 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
189 | 189 | gradientUnits="objectBoundingBox"> |
212 | 212 | @page { size: 5px } |
213 | 213 | svg { display: block } |
214 | 214 | </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"> | |
216 | 216 | <defs> |
217 | 217 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
218 | 218 | gradientUnits="objectBoundingBox"> |
238 | 238 | @page { size: 5px } |
239 | 239 | svg { display: block } |
240 | 240 | </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"> | |
242 | 242 | <defs> |
243 | 243 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
244 | 244 | gradientUnits="objectBoundingBox"> |
265 | 265 | @page { size: 5px } |
266 | 266 | svg { display: block } |
267 | 267 | </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"> | |
269 | 269 | <defs> |
270 | 270 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
271 | 271 | gradientUnits="objectBoundingBox"> |
295 | 295 | @page { size: 5px } |
296 | 296 | svg { display: block } |
297 | 297 | </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"> | |
299 | 299 | <defs> |
300 | 300 | <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1" |
301 | 301 | gradientUnits="objectBoundingBox"> |
21 | 21 | @page { size: 9px } |
22 | 22 | svg { display: block } |
23 | 23 | </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"> | |
25 | 25 | <defs> |
26 | 26 | <clipPath id="clip"> |
27 | 27 | <rect x="2" y="2" width="5" height="5" /> |
50 | 50 | @page { size: 9px } |
51 | 51 | svg { display: block } |
52 | 52 | </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"> | |
54 | 54 | <defs> |
55 | 55 | <clipPath id="clip"> |
56 | 56 | <rect x="2" y="2" width="5" height="5" /> |
82 | 82 | @page { size: 9px } |
83 | 83 | svg { display: block } |
84 | 84 | </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"> | |
86 | 86 | <defs> |
87 | 87 | <clipPath id="clip"> |
88 | 88 | <rect x="2" y="2" width="2" height="2" /> |
4 | 4 | from ...testing_utils import assert_no_logs |
5 | 5 | |
6 | 6 | 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"> | |
8 | 8 | <defs> |
9 | 9 | <rect id="rectangle" width="5" height="2" fill="red" /> |
10 | 10 | </defs> |
22 | 22 | @page { size: 10px } |
23 | 23 | svg { display: block } |
24 | 24 | </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"> | |
26 | 26 | <defs> |
27 | 27 | <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1" |
28 | 28 | gradientUnits="objectBoundingBox"> |
53 | 53 | @page { size: 10px } |
54 | 54 | svg { display: block } |
55 | 55 | </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"> | |
57 | 57 | <defs> |
58 | 58 | <linearGradient id="grad" x1="0" y1="0" x2="0" y2="10" |
59 | 59 | gradientUnits="userSpaceOnUse"> |
82 | 82 | @page { size: 10px 8px } |
83 | 83 | svg { display: block } |
84 | 84 | </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"> | |
86 | 86 | <defs> |
87 | 87 | <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1" |
88 | 88 | gradientUnits="objectBoundingBox"> |
115 | 115 | @page { size: 10px 8px } |
116 | 116 | svg { display: block } |
117 | 117 | </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"> | |
119 | 119 | <defs> |
120 | 120 | <linearGradient id="grad" x1="0" y1="0" x2="0" y2="8" |
121 | 121 | gradientUnits="userSpaceOnUse"> |
149 | 149 | @page { size: 10px 8px} |
150 | 150 | svg { display: block } |
151 | 151 | </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"> | |
153 | 153 | <defs> |
154 | 154 | <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1" |
155 | 155 | gradientUnits="objectBoundingBox" gradientTransform="scale(0.5)"> |
190 | 190 | @page { size: 10px 16px } |
191 | 191 | svg { display: block } |
192 | 192 | </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"> | |
194 | 194 | <defs> |
195 | 195 | <linearGradient id="grad" x1="0" y1="0" x2="0" y2="0.5" |
196 | 196 | gradientUnits="objectBoundingBox" spreadMethod="repeat"> |
232 | 232 | @page { size: 10px 16px } |
233 | 233 | svg { display: block } |
234 | 234 | </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"> | |
236 | 236 | <defs> |
237 | 237 | <linearGradient id="grad" x1="0" y1="0" x2="0" y2="0.25" |
238 | 238 | gradientUnits="objectBoundingBox" spreadMethod="repeat"> |
273 | 273 | @page { size: 10px 16px } |
274 | 274 | svg { display: block } |
275 | 275 | </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"> | |
277 | 277 | <defs> |
278 | 278 | <linearGradient id="grad" x1="0" y1="0" x2="0" y2="0.5" |
279 | 279 | gradientUnits="objectBoundingBox" spreadMethod="reflect"> |
308 | 308 | @page { size: 10px } |
309 | 309 | svg { display: block } |
310 | 310 | </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"> | |
312 | 312 | <defs> |
313 | 313 | <radialGradient id="grad" cx="0.5" cy="0.5" r="0.5" |
314 | 314 | fx="0.5" fy="0.5" fr="0.2" |
340 | 340 | @page { size: 10px } |
341 | 341 | svg { display: block } |
342 | 342 | </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"> | |
344 | 344 | <defs> |
345 | 345 | <radialGradient id="grad" cx="5" cy="5" r="5" fx="5" fy="5" fr="2" |
346 | 346 | gradientUnits="userSpaceOnUse"> |
371 | 371 | @page { size: 10px } |
372 | 372 | svg { display: block } |
373 | 373 | </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"> | |
375 | 375 | <defs> |
376 | 376 | <radialGradient id="grad" cx="0.5" cy="0.5" r="0.5" |
377 | 377 | fx="0.5" fy="0.5" fr="0.2" |
405 | 405 | @page { size: 10px } |
406 | 406 | svg { display: block } |
407 | 407 | </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"> | |
409 | 409 | <defs> |
410 | 410 | <radialGradient id="grad" cx="5" cy="5" r="5" |
411 | 411 | fx="5" fy="5" fr="2" |
440 | 440 | @page { size: 10px } |
441 | 441 | svg { display: block } |
442 | 442 | </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"> | |
444 | 444 | <defs> |
445 | 445 | <radialGradient id="grad" cx="0.5" cy="0.5" r="0.5" |
446 | 446 | fx="0.5" fy="0.5" fr="0.2" |
475 | 475 | @page { size: 10px } |
476 | 476 | svg { display: block } |
477 | 477 | </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"> | |
479 | 479 | <defs> |
480 | 480 | <radialGradient id="grad" cx="0.5" cy="0.5" r="0.5" |
481 | 481 | fx="0.5" fy="0.5" fr="0.2" |
520 | 520 | <rect x="0" y="0" width="10" height="10" fill="url(#grad)" /> |
521 | 521 | </svg> |
522 | 522 | ''') |
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) |
17 | 17 | @page { size: 4px 4px } |
18 | 18 | svg { display: block } |
19 | 19 | </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"> | |
21 | 21 | <svg x="1" y="1" width="2" height="2" viewBox="0 0 10 10"> |
22 | 22 | <rect x="5" y="5" width="5" height="5" fill="blue" /> |
23 | 23 | </svg> |
37 | 37 | @page { size: 4px 4px } |
38 | 38 | svg { display: block } |
39 | 39 | </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"> | |
41 | 41 | <svg x="1" y="1" width="2" height="2" viewBox="10 10 10 10"> |
42 | 42 | <rect x="15" y="15" width="5" height="5" fill="blue" /> |
43 | 43 | </svg> |
62 | 62 | svg { display: block } |
63 | 63 | </style> |
64 | 64 | <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"> | |
66 | 66 | <rect width="4" height="4" fill="red" /> |
67 | 67 | <rect width="1" height="2" fill="blue" /> |
68 | 68 | <rect x="3" y="2" width="1" height="2" fill="lime" /> |
88 | 88 | </style> |
89 | 89 | <svg width="8px" height="4px" viewBox="0 0 4 4" |
90 | 90 | preserveAspectRatio="none" |
91 | xmlns="http://www.w3.org/2000/svg"> | |
91 | xmlns="https://www.w3.org/2000/svg"> | |
92 | 92 | <rect width="4" height="4" fill="red" /> |
93 | 93 | <rect width="1" height="2" fill="blue" /> |
94 | 94 | <rect x="3" y="2" width="1" height="2" fill="lime" /> |
114 | 114 | </style> |
115 | 115 | <svg width="8px" height="4px" viewBox="0 0 4 4" |
116 | 116 | preserveAspectRatio="xMaxYMax meet" |
117 | xmlns="http://www.w3.org/2000/svg"> | |
117 | xmlns="https://www.w3.org/2000/svg"> | |
118 | 118 | <rect width="4" height="4" fill="red" /> |
119 | 119 | <rect width="1" height="2" fill="blue" /> |
120 | 120 | <rect x="3" y="2" width="1" height="2" fill="lime" /> |
140 | 140 | </style> |
141 | 141 | <svg width="4px" height="8px" viewBox="0 0 4 4" |
142 | 142 | preserveAspectRatio="xMaxYMax meet" |
143 | xmlns="http://www.w3.org/2000/svg"> | |
143 | xmlns="https://www.w3.org/2000/svg"> | |
144 | 144 | <rect width="4" height="4" fill="red" /> |
145 | 145 | <rect width="1" height="2" fill="blue" /> |
146 | 146 | <rect x="3" y="2" width="1" height="2" fill="lime" /> |
166 | 166 | </style> |
167 | 167 | <svg width="8px" height="4px" viewBox="0 0 4 4" |
168 | 168 | preserveAspectRatio="xMinYMin slice" |
169 | xmlns="http://www.w3.org/2000/svg"> | |
169 | xmlns="https://www.w3.org/2000/svg"> | |
170 | 170 | <rect width="4" height="4" fill="red" /> |
171 | 171 | <rect width="1" height="2" fill="blue" /> |
172 | 172 | <rect x="3" y="2" width="1" height="2" fill="lime" /> |
192 | 192 | </style> |
193 | 193 | <svg width="4px" height="8px" viewBox="0 0 4 4" |
194 | 194 | preserveAspectRatio="xMinYMin slice" |
195 | xmlns="http://www.w3.org/2000/svg"> | |
195 | xmlns="https://www.w3.org/2000/svg"> | |
196 | 196 | <rect width="4" height="4" fill="red" /> |
197 | 197 | <rect width="1" height="2" fill="blue" /> |
198 | 198 | <rect x="3" y="2" width="1" height="2" fill="lime" /> |
213 | 213 | @page { size: 4px 4px } |
214 | 214 | svg { display: block } |
215 | 215 | </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"> | |
217 | 217 | <svg x="1" y="1" width="50%" height="50%" viewBox="0 0 10 10"> |
218 | 218 | <rect x="5" y="5" width="5" height="5" fill="blue" /> |
219 | 219 | </svg> |
232 | 232 | @page { size: 4px 4px } |
233 | 233 | svg { display: block } |
234 | 234 | </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"> | |
236 | 236 | <That’s bad! |
237 | 237 | </svg> |
238 | 238 | ''') |
250 | 250 | @page { size: 4px 4px } |
251 | 251 | svg { display: block } |
252 | 252 | </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"> | |
254 | 254 | <image xlink:href="%s" /> |
255 | 255 | </svg> |
256 | 256 | ''' % path2url(resource_filename('pattern.png'))) |
267 | 267 | @page { size: 4px 4px } |
268 | 268 | svg { display: block } |
269 | 269 | </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"> | |
271 | 271 | <image xlink:href="it doesn’t exist, mouhahahaha" /> |
272 | 272 | </svg> |
273 | 273 | ''') |
0 | 0 | """Test how opacity is handled for SVG.""" |
1 | ||
2 | import pytest | |
1 | 3 | |
2 | 4 | from ...testing_utils import assert_no_logs |
3 | 5 | |
6 | 8 | @page { size: 9px } |
7 | 9 | svg { display: block } |
8 | 10 | </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>''' | |
10 | 12 | |
11 | 13 | |
12 | 14 | @assert_no_logs |
13 | 15 | def test_opacity(assert_same_renderings): |
14 | 16 | assert_same_renderings( |
15 | ''' | |
17 | opacity_source % ''' | |
16 | 18 | <rect x="2" y="2" width="5" height="5" stroke-width="2" |
17 | 19 | stroke="rgb(127, 255, 127)" fill="rgb(127, 127, 255)" /> |
18 | 20 | ''', |
19 | ''' | |
21 | opacity_source % ''' | |
20 | 22 | <rect x="2" y="2" width="5" height="5" stroke-width="2" |
21 | 23 | stroke="lime" fill="blue" opacity="0.5" /> |
22 | 24 | ''', |
26 | 28 | @assert_no_logs |
27 | 29 | def test_fill_opacity(assert_same_renderings): |
28 | 30 | assert_same_renderings( |
29 | ''' | |
31 | opacity_source % ''' | |
30 | 32 | <rect x="2" y="2" width="5" height="5" |
31 | 33 | fill="blue" opacity="0.5" /> |
32 | 34 | <rect x="2" y="2" width="5" height="5" stroke-width="2" |
33 | 35 | stroke="lime" fill="transparent" /> |
34 | 36 | ''', |
35 | ''' | |
37 | opacity_source % ''' | |
36 | 38 | <rect x="2" y="2" width="5" height="5" stroke-width="2" |
37 | 39 | stroke="lime" fill="blue" fill-opacity="0.5" /> |
38 | 40 | ''', |
39 | 41 | ) |
40 | 42 | |
41 | 43 | |
44 | @pytest.mark.xfail | |
42 | 45 | @assert_no_logs |
43 | 46 | 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 | |
44 | 52 | assert_same_renderings( |
45 | 53 | ''' |
46 | 54 | <rect x="2" y="2" width="5" height="5" |
48 | 56 | <rect x="2" y="2" width="5" height="5" stroke-width="2" |
49 | 57 | stroke="lime" fill="transparent" opacity="0.5" /> |
50 | 58 | ''', |
51 | ''' | |
59 | opacity_source % ''' | |
52 | 60 | <rect x="2" y="2" width="5" height="5" stroke-width="2" |
53 | 61 | stroke="lime" fill="blue" stroke-opacity="0.5" /> |
54 | 62 | ''', |
55 | 63 | ) |
56 | 64 | |
57 | 65 | |
66 | @pytest.mark.xfail | |
58 | 67 | @assert_no_logs |
59 | 68 | def test_stroke_fill_opacity(assert_same_renderings): |
60 | 69 | assert_same_renderings( |
61 | ''' | |
70 | opacity_source % ''' | |
62 | 71 | <rect x="2" y="2" width="5" height="5" |
63 | 72 | fill="blue" opacity="0.5" /> |
64 | 73 | <rect x="2" y="2" width="5" height="5" stroke-width="2" |
65 | 74 | stroke="lime" fill="transparent" opacity="0.5" /> |
66 | 75 | ''', |
67 | ''' | |
76 | opacity_source % ''' | |
68 | 77 | <rect x="2" y="2" width="5" height="5" stroke-width="2" |
69 | 78 | stroke="lime" fill="blue" |
70 | 79 | stroke-opacity="0.5" fill-opacity="0.5" /> |
72 | 81 | ) |
73 | 82 | |
74 | 83 | |
84 | @pytest.mark.xfail | |
75 | 85 | @assert_no_logs |
76 | 86 | def test_pattern_gradient_stroke_fill_opacity(assert_same_renderings): |
77 | 87 | assert_same_renderings( |
78 | ''' | |
88 | opacity_source % ''' | |
79 | 89 | <defs> |
80 | 90 | <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1" |
81 | 91 | gradientUnits="objectBoundingBox"> |
96 | 106 | <rect x="2" y="2" width="5" height="5" stroke-width="2" |
97 | 107 | stroke="url(#grad)" fill="transparent" opacity="0.5" /> |
98 | 108 | ''', |
99 | ''' | |
109 | opacity_source % ''' | |
100 | 110 | <defs> |
101 | 111 | <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1" |
102 | 112 | gradientUnits="objectBoundingBox"> |
116 | 126 | stroke="url(#grad)" fill="url(#pat)" |
117 | 127 | stroke-opacity="0.5" fill-opacity="0.5" /> |
118 | 128 | ''', |
129 | tolerance=1, | |
119 | 130 | ) |
20 | 20 | @page { size: 10px } |
21 | 21 | svg { display: block } |
22 | 22 | </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"> | |
24 | 24 | <path d="M 0 1 H 8 H 1" |
25 | 25 | stroke="blue" stroke-width="2" fill="none"/> |
26 | 26 | <path d="M 0 4 H 8 4" |
51 | 51 | @page { size: 10px } |
52 | 52 | svg { display: block } |
53 | 53 | </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"> | |
55 | 55 | <path d="M 1 0 V 1 V 4" |
56 | 56 | stroke="blue" stroke-width="2" fill="none"/> |
57 | 57 | <path d="M 4 6 V 4 10" |
82 | 82 | @page { size: 10px } |
83 | 83 | svg { display: block } |
84 | 84 | </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"> | |
86 | 86 | <path d="M 4 3 L 4 10" |
87 | 87 | stroke="blue" stroke-width="2" fill="none"/> |
88 | 88 | <path d="M 7 0 l 0 6" |
109 | 109 | @page { size: 10px } |
110 | 110 | svg { display: block } |
111 | 111 | </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"> | |
113 | 113 | <path d="M 1 1 H 6 V 5 H 1 Z" |
114 | 114 | stroke="blue" stroke-width="2" fill="none"/> |
115 | 115 | <path d="M 9 10 V 7 H 5 V 10 z" |
136 | 136 | @page { size: 10px } |
137 | 137 | svg { display: block } |
138 | 138 | </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"> | |
140 | 140 | <path d="M 1 1 H 6 V 5 H 1 Z" |
141 | 141 | stroke="blue" stroke-width="2" fill="lime"/> |
142 | 142 | <path d="M 9 10 V 7 H 5 V 10 z" |
163 | 163 | @page { size: 10px } |
164 | 164 | svg { display: block } |
165 | 165 | </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"> | |
167 | 167 | <path d="M 2 5 C 2 5 3 5 5 5" |
168 | 168 | stroke="blue" stroke-width="2" fill="none"/> |
169 | 169 | <path d="M 2 8 c 0 0 1 0 3 0" |
190 | 190 | @page { size: 10px } |
191 | 191 | svg { display: block } |
192 | 192 | </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"> | |
194 | 194 | <path d="M 2 5 S 3 5 5 5" |
195 | 195 | stroke="blue" stroke-width="2" fill="none"/> |
196 | 196 | <path d="M 2 8 s 1 0 3 0" |
219 | 219 | @page { size: 10px 12px } |
220 | 220 | svg { display: block } |
221 | 221 | </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"> | |
223 | 223 | <path d="M 2 1 C 2 1 3 1 5 1 S 8 3 8 1" |
224 | 224 | stroke="blue" stroke-width="2" fill="none"/> |
225 | 225 | <path d="M 2 4 C 2 4 3 4 5 4 s 3 2 1 0" |
250 | 250 | @page { size: 10px } |
251 | 251 | svg { display: block } |
252 | 252 | </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"> | |
254 | 254 | <path d="M 2 5 Q 4 5 6 5" |
255 | 255 | stroke="blue" stroke-width="2" fill="none"/> |
256 | 256 | <path d="M 2 8 q 2 0 4 0" |
277 | 277 | @page { size: 10px } |
278 | 278 | svg { display: block } |
279 | 279 | </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"> | |
281 | 281 | <path d="M 2 5 T 6 5" |
282 | 282 | stroke="blue" stroke-width="2" fill="none"/> |
283 | 283 | <path d="M 2 8 t 4 0" |
306 | 306 | @page { size: 12px } |
307 | 307 | svg { display: block } |
308 | 308 | </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"> | |
310 | 310 | <path d="M 0 3 Q 3 0 6 3 T 12 3" |
311 | 311 | stroke="blue" stroke-width="2" fill="none"/> |
312 | 312 | <path d="M 0 9 Q 3 6 6 9 t 6 0" |
335 | 335 | @page { size: 12px } |
336 | 336 | svg { display: block } |
337 | 337 | </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"> | |
339 | 339 | <path d="M 0 3 q 3 -3 6 0 T 12 3" |
340 | 340 | stroke="blue" stroke-width="2" fill="none"/> |
341 | 341 | <path d="M 0 9 q 3 -3 6 0 t 6 0" |
364 | 364 | @page { size: 12px } |
365 | 365 | svg { display: block } |
366 | 366 | </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"> | |
368 | 368 | <path d="M 1 6 A 5 5 0 0 1 6 1" |
369 | 369 | stroke="blue" stroke-width="2" fill="none"/> |
370 | 370 | <path d="M 6 11 a 5 5 0 0 1 5 -5" |
393 | 393 | @page { size: 12px } |
394 | 394 | svg { display: block } |
395 | 395 | </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"> | |
397 | 397 | <path d="M 1 6 A 5 5 0 1 0 6 1" |
398 | 398 | stroke="lime" stroke-width="2" fill="none"/> |
399 | 399 | </svg> |
420 | 420 | @page { size: 12px } |
421 | 421 | svg { display: block } |
422 | 422 | </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"> | |
424 | 424 | <path d="M 1 6 a 5 5 0 1 0 5 -5" |
425 | 425 | stroke="lime" stroke-width="2" fill="none"/> |
426 | 426 | </svg> |
447 | 447 | @page { size: 12px } |
448 | 448 | svg { display: block } |
449 | 449 | </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"> | |
451 | 451 | <path d="M 1 6 A 5 5 0 0 0 6 1" |
452 | 452 | stroke="blue" stroke-width="2" fill="none"/> |
453 | 453 | <path d="M 6 11 a 5 5 0 0 0 5 -5" |
476 | 476 | @page { size: 12px } |
477 | 477 | svg { display: block } |
478 | 478 | </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"> | |
480 | 480 | <path d="M 6 11 A 5 5 0 1 1 11 6" |
481 | 481 | stroke="blue" stroke-width="2" fill="none"/> |
482 | 482 | </svg> |
503 | 503 | @page { size: 12px } |
504 | 504 | svg { display: block } |
505 | 505 | </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"> | |
507 | 507 | <path d="M 6 11 a 5 5 0 1 1 5 -5" |
508 | 508 | stroke="blue" stroke-width="2" fill="none"/> |
509 | 509 | </svg> |
530 | 530 | @page { size: 12px } |
531 | 531 | svg { display: block } |
532 | 532 | </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"> | |
534 | 534 | <path d="M 1 6 A 5 5 0 0 0 11 6" |
535 | 535 | stroke="lime" stroke-width="2" fill="none"/> |
536 | 536 | </svg> |
557 | 557 | @page { size: 12px } |
558 | 558 | svg { display: block } |
559 | 559 | </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"> | |
561 | 561 | <path d="M 1 1 L 1 5 L" |
562 | 562 | stroke="lime" stroke-width="2" fill="none"/> |
563 | 563 | </svg> |
584 | 584 | @page { size: 12px } |
585 | 585 | svg { display: block } |
586 | 586 | </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"> | |
588 | 588 | <marker id="line" |
589 | 589 | viewBox="0 0 1 2" refX="0.5" refY="1" |
590 | 590 | markerUnits="strokeWidth" |
618 | 618 | @page { size: 12px } |
619 | 619 | svg { display: block } |
620 | 620 | </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"> | |
622 | 622 | <marker id="line" |
623 | 623 | viewBox="0 0 1 2" refX="0.5" refY="1" |
624 | 624 | markerUnits="strokeWidth" |
18 | 18 | @page { size: 8px } |
19 | 19 | svg { display: block } |
20 | 20 | </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"> | |
22 | 22 | <defs> |
23 | 23 | <pattern id="pat" x="0" y="0" width="4" height="4" |
24 | 24 | patternUnits="userSpaceOnUse" |
50 | 50 | @page { size: 8px } |
51 | 51 | svg { display: block } |
52 | 52 | </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"> | |
54 | 54 | <defs> |
55 | 55 | <pattern id="pat" x="0" y="0" width="50%" height="50%" |
56 | 56 | patternUnits="objectBoundingBox" |
82 | 82 | @page { size: 8px } |
83 | 83 | svg { display: block } |
84 | 84 | </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"> | |
86 | 86 | <defs> |
87 | 87 | <pattern id="pat" x="0" y="0" width="4" height="4" |
88 | 88 | patternUnits="userSpaceOnUse" |
114 | 114 | @page { size: 8px } |
115 | 115 | svg { display: block } |
116 | 116 | </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"> | |
118 | 118 | <defs> |
119 | 119 | <pattern id="pat" x="0" y="0" width="4" height="4" |
120 | 120 | patternUnits="userSpaceOnUse" |
19 | 19 | @page { size: 9px } |
20 | 20 | svg { display: block } |
21 | 21 | </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"> | |
23 | 23 | <rect x="2" y="2" width="5" height="5" |
24 | 24 | stroke-width="2" stroke="red" fill="none" /> |
25 | 25 | </svg> |
43 | 43 | @page { size: 9px } |
44 | 44 | svg { display: block } |
45 | 45 | </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"> | |
47 | 47 | <rect x="2" y="2" width="5" height="5" fill="red" /> |
48 | 48 | </svg> |
49 | 49 | ''') |
66 | 66 | @page { size: 9px } |
67 | 67 | svg { display: block } |
68 | 68 | </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"> | |
70 | 70 | <rect x="2" y="2" width="5" height="5" |
71 | 71 | stroke-width="2" stroke="red" fill="blue" /> |
72 | 72 | </svg> |
90 | 90 | @page { size: 9px } |
91 | 91 | svg { display: block } |
92 | 92 | </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"> | |
94 | 94 | <rect width="9" height="9" fill="red" rx="4" ry="4" /> |
95 | 95 | </svg> |
96 | 96 | ''') |
113 | 113 | @page { size: 9px } |
114 | 114 | svg { display: block } |
115 | 115 | </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"> | |
117 | 117 | <rect width="9" height="9" fill="red" rx="0" ry="4" /> |
118 | 118 | </svg> |
119 | 119 | ''') |
136 | 136 | @page { size: 9px } |
137 | 137 | svg { display: block } |
138 | 138 | </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"> | |
140 | 140 | <line x1="0" y1="5" x2="6" y2="5" |
141 | 141 | stroke="red" stroke-width="2"/> |
142 | 142 | </svg> |
160 | 160 | @page { size: 9px } |
161 | 161 | svg { display: block } |
162 | 162 | </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"> | |
164 | 164 | <polyline points="1,6, 1,2, 5,2, 5,6" |
165 | 165 | stroke="red" stroke-width="2" fill="none"/> |
166 | 166 | </svg> |
184 | 184 | @page { size: 9px } |
185 | 185 | svg { display: block } |
186 | 186 | </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"> | |
188 | 188 | <polyline points="1,6, 1,2, 5,2, 5,6" |
189 | 189 | stroke="red" stroke-width="2" fill="blue"/> |
190 | 190 | </svg> |
208 | 208 | @page { size: 9px } |
209 | 209 | svg { display: block } |
210 | 210 | </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"> | |
212 | 212 | <polygon points="1,6, 1,2, 5,2, 5,6" |
213 | 213 | stroke="red" stroke-width="2" fill="none"/> |
214 | 214 | </svg> |
232 | 232 | @page { size: 9px } |
233 | 233 | svg { display: block } |
234 | 234 | </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"> | |
236 | 236 | <polygon points="1,6, 1,2, 5,2, 5,6" |
237 | 237 | stroke="red" stroke-width="2" fill="blue"/> |
238 | 238 | </svg> |
257 | 257 | @page { size: 10px } |
258 | 258 | svg { display: block } |
259 | 259 | </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"> | |
261 | 261 | <circle cx="5" cy="5" r="3" |
262 | 262 | stroke="red" stroke-width="2" fill="none"/> |
263 | 263 | </svg> |
282 | 282 | @page { size: 10px } |
283 | 283 | svg { display: block } |
284 | 284 | </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"> | |
286 | 286 | <circle cx="5" cy="5" r="3" |
287 | 287 | stroke="red" stroke-width="2" fill="blue"/> |
288 | 288 | </svg> |
307 | 307 | @page { size: 10px } |
308 | 308 | svg { display: block } |
309 | 309 | </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"> | |
311 | 311 | <ellipse cx="5" cy="5" rx="3" ry="3" |
312 | 312 | stroke="red" stroke-width="2" fill="none"/> |
313 | 313 | </svg> |
332 | 332 | @page { size: 10px } |
333 | 333 | svg { display: block } |
334 | 334 | </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"> | |
336 | 336 | <ellipse cx="5" cy="5" rx="3" ry="3" |
337 | 337 | stroke="red" stroke-width="2" fill="blue"/> |
338 | 338 | </svg> |
356 | 356 | @page { size: 9px } |
357 | 357 | svg { display: block } |
358 | 358 | </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"> | |
360 | 360 | <g x="5" y="5"> |
361 | 361 | <rect width="5" height="5" fill="red" /> |
362 | 362 | </g> |
381 | 381 | @page { size: 9px } |
382 | 382 | svg { display: block } |
383 | 383 | </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"> | |
385 | 385 | <g x="5" y="5"> |
386 | 386 | <rect x="2" y="2" width="5" height="5" fill="red" /> |
387 | 387 | </g> |
406 | 406 | @page { size: 9px } |
407 | 407 | svg { display: block } |
408 | 408 | </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"> | |
410 | 410 | <rect x="2" y="2" width="5" height="5" |
411 | 411 | stroke-width="0" stroke="red" fill="none" /> |
412 | 412 | </svg> |
430 | 430 | @page { size: 9px } |
431 | 431 | svg { display: block } |
432 | 432 | </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"> | |
434 | 434 | <rect x="2" y="2" width="5" height="5" fill="red" /> |
435 | 435 | </svg> |
436 | 436 | ''') |
453 | 453 | @page { size: 9px } |
454 | 454 | svg { display: block } |
455 | 455 | </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"> | |
457 | 457 | <rect x="2" y="2" width="5" height="5" fill="inherit" /> |
458 | 458 | </svg> |
459 | 459 | ''') |
13 | 13 | @page { size: 20px 2px } |
14 | 14 | svg { display: block } |
15 | 15 | </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"> | |
17 | 17 | <text x="0" y="1.5" font-family="weasyprint" font-size="2" fill="blue"> |
18 | 18 | ABC DEF |
19 | 19 | </text> |
34 | 34 | @page { font-size: 1px; size: 20em 8ex } |
35 | 35 | svg { display: block } |
36 | 36 | </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"> | |
38 | 38 | <text x="2" y="2.5" font-family="weasyprint" font-size="2" |
39 | 39 | fill="transparent" stroke="blue" stroke-width="1ex"> |
40 | 40 | A B C |
54 | 54 | @page { size: 20px 2px } |
55 | 55 | svg { display: block } |
56 | 56 | </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"> | |
58 | 58 | <text x="0 4 7" y="1.5" font-family="weasyprint" font-size="2" |
59 | 59 | fill="blue"> |
60 | 60 | ABCD |
82 | 82 | @page { size: 30px 10px } |
83 | 83 | svg { display: block } |
84 | 84 | </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"> | |
86 | 86 | <text x="0" y="9 9 4 9 4" font-family="weasyprint" font-size="5" |
87 | 87 | fill="blue"> |
88 | 88 | ABCDEF |
110 | 110 | @page { size: 30px 10px } |
111 | 111 | svg { display: block } |
112 | 112 | </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"> | |
114 | 114 | <text x="0 10" y="9 4 9 4" font-family="weasyprint" font-size="5" |
115 | 115 | fill="blue"> |
116 | 116 | ABCDE |
130 | 130 | @page { size: 20px 2px } |
131 | 131 | svg { display: block } |
132 | 132 | </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"> | |
134 | 134 | <text dx="0 2 1" y="1.5" font-family="weasyprint" font-size="2" |
135 | 135 | fill="blue"> |
136 | 136 | ABCD |
158 | 158 | @page { size: 30px 10px } |
159 | 159 | svg { display: block } |
160 | 160 | </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"> | |
162 | 162 | <text x="0" dy="9 0 -5 5 -5" font-family="weasyprint" font-size="5" |
163 | 163 | fill="blue"> |
164 | 164 | ABCDEF |
186 | 186 | @page { size: 30px 10px } |
187 | 187 | svg { display: block } |
188 | 188 | </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"> | |
190 | 190 | <text dx="0 5" dy="9 -5 5 -5" font-family="weasyprint" font-size="5" |
191 | 191 | fill="blue"> |
192 | 192 | ABCDE |
208 | 208 | @page { size: 20px 4px } |
209 | 209 | svg { display: block } |
210 | 210 | </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"> | |
212 | 212 | <text x="2" y="1.5" font-family="weasyprint" font-size="2" |
213 | 213 | fill="blue"> |
214 | 214 | ABC |
232 | 232 | @page { size: 20px 2px } |
233 | 233 | svg { display: block } |
234 | 234 | </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"> | |
236 | 236 | <text x="10" y="1.5" font-family="weasyprint" font-size="2" |
237 | 237 | fill="blue" text-anchor="middle"> |
238 | 238 | ABC |
252 | 252 | @page { size: 20px 2px } |
253 | 253 | svg { display: block } |
254 | 254 | </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"> | |
256 | 256 | <text x="18" y="1.5" font-family="weasyprint" font-size="2" |
257 | 257 | fill="blue" text-anchor="end"> |
258 | 258 | ABC |
272 | 272 | @page { size: 20px 2px } |
273 | 273 | svg { display: block } |
274 | 274 | </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"> | |
276 | 276 | <text x="10" y="10" font-family="weasyprint" font-size="2" fill="blue"> |
277 | 277 | <tspan x="0" y="1.5">ABC DEF</tspan> |
278 | 278 | </text> |
293 | 293 | @page { size: 20px 4px } |
294 | 294 | svg { display: block } |
295 | 295 | </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"> | |
297 | 297 | <text x="2" y="1.5" font-family="weasyprint" font-size="2" fill="red" |
298 | 298 | letter-spacing="2">abc</text> |
299 | 299 | <text x="2" y="1.5" font-family="weasyprint" font-size="2" fill="blue" |
19 | 19 | @page { size: 9px } |
20 | 20 | svg { display: block } |
21 | 21 | </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"> | |
23 | 23 | <rect x="0" y="4" width="5" height="5" transform="translate(2, -2)" |
24 | 24 | stroke-width="2" stroke="red" fill="none" /> |
25 | 25 | </svg> |
27 | 27 | |
28 | 28 | |
29 | 29 | @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 | |
30 | 54 | def test_transform_translatex(assert_pixels): |
31 | 55 | assert_pixels(''' |
32 | 56 | _________ |
43 | 67 | @page { size: 9px } |
44 | 68 | svg { display: block } |
45 | 69 | </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"> | |
47 | 71 | <rect x="0" y="2" width="5" height="5" transform="translateX(2)" |
48 | 72 | stroke-width="2" stroke="red" fill="none" /> |
49 | 73 | </svg> |
67 | 91 | @page { size: 9px } |
68 | 92 | svg { display: block } |
69 | 93 | </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"> | |
71 | 95 | <rect x="2" y="0" width="5" height="5" transform="translateY(2)" |
72 | 96 | stroke-width="2" stroke="red" fill="none" /> |
73 | 97 | </svg> |
91 | 115 | @page { size: 9px } |
92 | 116 | svg { display: block } |
93 | 117 | </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"> | |
95 | 119 | <rect x="2" y="-7" width="4" height="5" transform="rotate(90)" |
96 | 120 | stroke-width="2" stroke="red" fill="none" /> |
97 | 121 | </svg> |
115 | 139 | @page { size: 9px } |
116 | 140 | svg { display: block } |
117 | 141 | </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"> | |
119 | 143 | <rect x="7" y="2" width="4" height="5" transform="rotate(90 7 2)" |
120 | 144 | stroke-width="2" stroke="red" fill="none" /> |
121 | 145 | </svg> |
139 | 163 | @page { size: 9px } |
140 | 164 | svg { display: block } |
141 | 165 | </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"> | |
143 | 167 | <rect x="2" y="2" width="2" height="2" transform="skew(20 20)" |
144 | 168 | stroke-width="2" stroke="red" fill="none" /> |
145 | 169 | </svg> |
147 | 171 | |
148 | 172 | |
149 | 173 | @assert_no_logs |
150 | def test_transform_skewx(assert_pixels): | |
174 | def test_transform_skew_one(assert_pixels): | |
151 | 175 | assert_pixels(''' |
152 | 176 | _________ |
153 | 177 | _RRRRR___ |
163 | 187 | @page { size: 9px } |
164 | 188 | svg { display: block } |
165 | 189 | </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"> | |
167 | 215 | <rect x="2" y="2" width="2" height="2" transform="skewX(20)" |
168 | 216 | stroke-width="2" stroke="red" fill="none" /> |
169 | 217 | </svg> |
187 | 235 | @page { size: 9px } |
188 | 236 | svg { display: block } |
189 | 237 | </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"> | |
191 | 239 | <rect x="2" y="2" width="2" height="2" transform="skewY(20)" |
192 | 240 | stroke-width="2" stroke="red" fill="none" /> |
193 | 241 | </svg> |
211 | 259 | @page { size: 9px } |
212 | 260 | svg { display: block } |
213 | 261 | </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"> | |
215 | 263 | <rect x="2" y="2" width="2" height="2" transform="scale(1.5)" |
216 | 264 | stroke-width="2" stroke="red" fill="none" /> |
217 | 265 | </svg> |
235 | 283 | @page { size: 9px } |
236 | 284 | svg { display: block } |
237 | 285 | </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"> | |
239 | 287 | <rect x="2" y="2" width="2" height="2" transform="scale(1.5 1.5)" |
240 | 288 | stroke-width="2" stroke="red" fill="none" /> |
241 | 289 | </svg> |
259 | 307 | @page { size: 9px } |
260 | 308 | svg { display: block } |
261 | 309 | </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"> | |
263 | 311 | <rect x="2" y="2" width="2" height="2" transform="scaleX(1.5)" |
264 | 312 | stroke-width="2" stroke="red" fill="none" /> |
265 | 313 | </svg> |
283 | 331 | @page { size: 9px } |
284 | 332 | svg { display: block } |
285 | 333 | </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"> | |
287 | 335 | <rect x="2" y="2" width="2" height="2" transform="scaleY(1.5)" |
288 | 336 | stroke-width="2" stroke="red" fill="none" /> |
289 | 337 | </svg> |
307 | 355 | @page { size: 9px } |
308 | 356 | svg { display: block } |
309 | 357 | </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"> | |
311 | 359 | <rect x="0" y="0" width="2" height="2" |
312 | 360 | transform="matrix(1.5 0 0 1.5 3 3)" |
313 | 361 | stroke-width="2" stroke="red" fill="none" /> |
332 | 380 | @page { size: 9px } |
333 | 381 | svg { display: block } |
334 | 382 | </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"> | |
336 | 384 | <rect x="0" y="0" width="4" height="5" |
337 | 385 | transform="rotate(90) translateY(-7) translateX(2)" |
338 | 386 | stroke-width="2" stroke="red" fill="none" /> |
339 | 387 | </svg> |
340 | 388 | ''') |
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 | ''') |
19 | 19 | @page { size: 9px } |
20 | 20 | svg { display: block } |
21 | 21 | </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"> | |
23 | 23 | <rect visibility="visible" |
24 | 24 | x="2" y="2" width="5" height="5" fill="red" /> |
25 | 25 | </svg> |
43 | 43 | @page { size: 9px } |
44 | 44 | svg { display: block } |
45 | 45 | </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"> | |
47 | 47 | <rect visibility="hidden" |
48 | 48 | x="2" y="2" width="5" height="5" fill="red" /> |
49 | 49 | </svg> |
67 | 67 | @page { size: 9px } |
68 | 68 | svg { display: block } |
69 | 69 | </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"> | |
71 | 71 | <g visibility="hidden"> |
72 | 72 | <rect x="2" y="2" width="5" height="5" fill="red" /> |
73 | 73 | </g> |
92 | 92 | @page { size: 9px } |
93 | 93 | svg { display: block } |
94 | 94 | </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"> | |
96 | 96 | <g visibility="hidden"> |
97 | 97 | <rect visibility="visible" |
98 | 98 | x="2" y="2" width="5" height="5" fill="red" /> |
118 | 118 | @page { size: 9px } |
119 | 119 | svg { display: block } |
120 | 120 | </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"> | |
122 | 122 | <rect display="inline" |
123 | 123 | x="2" y="2" width="5" height="5" fill="red" /> |
124 | 124 | </svg> |
142 | 142 | @page { size: 9px } |
143 | 143 | svg { display: block } |
144 | 144 | </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"> | |
146 | 146 | <rect display="none" |
147 | 147 | x="2" y="2" width="5" height="5" fill="red" /> |
148 | 148 | </svg> |
166 | 166 | @page { size: 9px } |
167 | 167 | svg { display: block } |
168 | 168 | </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"> | |
170 | 170 | <g display="none"> |
171 | 171 | <rect x="2" y="2" width="5" height="5" fill="red" /> |
172 | 172 | </g> |
191 | 191 | @page { size: 9px } |
192 | 192 | svg { display: block } |
193 | 193 | </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"> | |
195 | 195 | <g display="none"> |
196 | 196 | <rect display="inline" |
197 | 197 | x="2" y="2" width="5" height="5" fill="red" /> |
14 | 14 | source = ''' |
15 | 15 | <style> |
16 | 16 | @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 } | |
19 | 18 | </style> |
20 | 19 | <body>''' |
21 | 20 | |
94 | 93 | |
95 | 94 | |
96 | 95 | @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 | |
97 | 123 | def test_margin_boxes(assert_pixels): |
98 | 124 | assert_pixels(''' |
99 | 125 | _______________ |
48 | 48 | <img src=blue.jpg> |
49 | 49 | <img src=blue.jpg> |
50 | 50 | </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>''') |
0 | 0 | """Test the currentColor value.""" |
1 | ||
2 | import pytest | |
1 | 3 | |
2 | 4 | from ..testing_utils import assert_no_logs |
3 | 5 | |
49 | 51 | color: lime; border: 1px solid; border-color: inherit } |
50 | 52 | </style> |
51 | 53 | <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>''') |
697 | 697 | </style> |
698 | 698 | <div class="split">aaaaa aaaaa aa</div> |
699 | 699 | 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 | """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>''') |
0 | 0 | """Test how gradients are drawn.""" |
1 | ||
2 | import pytest | |
3 | 1 | |
4 | 2 | from ..testing_utils import assert_no_logs |
5 | 3 | |
64 | 62 | )''') |
65 | 63 | |
66 | 64 | |
67 | @pytest.mark.xfail | |
68 | 65 | @assert_no_logs |
69 | 66 | def test_linear_gradients_5(assert_pixels): |
70 | # See https://bugs.ghostscript.com/show_bug.cgi?id=705225 | |
71 | 67 | assert_pixels(''' |
72 | 68 | rBrrrBrrrB |
73 | 69 | rBrrrBrrrB |
101 | 97 | hhhhhhhhh |
102 | 98 | hhhhhhhhh |
103 | 99 | hhhhhhhhh |
104 | ''', '''<style>@page { size: 9px 5px; background: repeating-linear-gradient( | |
100 | ''', '''<style>@page { size: 9px 5px; background: | |
101 | repeating-linear-gradient( | |
105 | 102 | to right, black 3px, black 3px, #800080 3px, #800080 3px |
106 | 103 | )''') |
107 | 104 | |
114 | 111 | BBBBBBBBB |
115 | 112 | BBBBBBBBB |
116 | 113 | 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)''') | |
120 | 116 | |
121 | 117 | |
122 | 118 | @assert_no_logs |
127 | 123 | BBBBBBBBB |
128 | 124 | BBBBBBBBB |
129 | 125 | 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)''') | |
133 | 128 | |
134 | 129 | |
135 | 130 | @assert_no_logs |
158 | 153 | )''') |
159 | 154 | |
160 | 155 | |
161 | @pytest.mark.xfail | |
162 | 156 | @assert_no_logs |
163 | 157 | 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 | |
173 | 166 | )''') |
174 | 167 | |
175 | 168 |
546 | 546 | img { margin: 1px; border: 1px solid lime; position: absolute } |
547 | 547 | </style> |
548 | 548 | <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 | ) |
751 | 751 | <div>a<span>b</span></div>''') |
752 | 752 | |
753 | 753 | |
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 | ||
754 | 783 | def test_zero_width_character(assert_pixels): |
755 | 784 | # Test regression: https://github.com/Kozea/WeasyPrint/issues/1508 |
756 | 785 | 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 | ''') |
256 | 256 | ('width: 10%; height: 1000px; min-width: auto; max-height: none',), |
257 | 257 | )) |
258 | 258 | 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 | |
260 | 260 | page, = render_pages(''' |
261 | 261 | <style> |
262 | 262 | @page { size: 100000px } |
311 | 311 | ('min-width: 0; min-height: 0; width: 0; height: 0'), |
312 | 312 | )) |
313 | 313 | 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 | |
315 | 315 | page, = render_pages(''' |
316 | 316 | <style> |
317 | 317 | @page { size: 100000px } |
564 | 564 | |
565 | 565 | @assert_no_logs |
566 | 566 | 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 | |
568 | 568 | page_1, page_2 = render_pages(''' |
569 | 569 | <style> |
570 | 570 | @page { size: 100px } |
613 | 613 | |
614 | 614 | @assert_no_logs |
615 | 615 | 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 | |
617 | 617 | page_1, page_2 = render_pages(''' |
618 | 618 | <style> |
619 | 619 | @page { size: 100px } |
642 | 642 | assert footnote_marker.children[0].text == '1.' |
643 | 643 | assert footnote_textbox.text == 'de' |
644 | 644 | 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 | |
645 | 692 | |
646 | 693 | |
647 | 694 | @assert_no_logs |
129 | 129 | |
130 | 130 | @assert_no_logs |
131 | 131 | def test_breaking_linebox_regression_1(): |
132 | # See http://unicode.org/reports/tr14/ | |
132 | # See https://unicode.org/reports/tr14/ | |
133 | 133 | page, = render_pages('<pre>a\nb\rc\r\nd\u2029e</pre>') |
134 | 134 | html, = page.children |
135 | 135 | body, = html.children |
1004 | 1004 | |
1005 | 1005 | @assert_no_logs |
1006 | 1006 | 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 | |
1008 | 1008 | page_1, = render_pages(''' |
1009 | 1009 | <style> |
1010 | 1010 | @font-face { src: url(weasyprint.otf); font-family: weasyprint } |
1035 | 1035 | |
1036 | 1036 | @assert_no_logs |
1037 | 1037 | 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 | |
1039 | 1039 | page_1, = render_pages(''' |
1040 | 1040 | <style> |
1041 | 1041 | @font-face { src: url(weasyprint.otf); font-family: weasyprint } |
102 | 102 | assert len(ul.children) == 1 |
103 | 103 | for li in ul.children: |
104 | 104 | 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) |
1470 | 1470 | </style> |
1471 | 1471 | <footer>Hello!<p>Bonjour!</p></footer> |
1472 | 1472 | ''') |
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 | ''') |
369 | 369 | |
370 | 370 | @assert_no_logs |
371 | 371 | 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 | |
373 | 373 | page_1, page_2 = render_pages(''' |
374 | 374 | <style> |
375 | 375 | @page:first { size: 100px 200px } |
1412 | 1412 | @assert_no_logs |
1413 | 1413 | def test_layout_table_auto_46(): |
1414 | 1414 | # Test regression: |
1415 | # http://test.weasyprint.org/suite-css21/chapter8/section2/test56/ | |
1415 | # https://test.weasyprint.org/suite-css21/chapter8/section2/test56/ | |
1416 | 1416 | page, = render_pages(''' |
1417 | 1417 | <div style="position: absolute"> |
1418 | 1418 | <table style="margin: 50px; border: 20px solid black"> |
1841 | 1841 | |
1842 | 1842 | @assert_no_logs |
1843 | 1843 | 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 | |
1845 | 1845 | page, = render_pages(''' |
1846 | 1846 | <style> |
1847 | 1847 | @font-face { src: url(weasyprint.otf); font-family: weasyprint } |
Binary diff not shown
17 | 17 | return HTML(resource_filename(filename)).render() |
18 | 18 | |
19 | 19 | 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 | |
21 | 21 | document = render('acid2-test.html') |
22 | 22 | intro_page, test_page = document.pages |
23 | 23 | # Ignore the intro page: it is not in the reference |
24 | 24 | test_png = document.copy([test_page]).write_png() |
25 | 25 | test_pixels = Image.open(io.BytesIO(test_png)).getdata() |
26 | 26 | |
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 | |
28 | 28 | ref_png = render('acid2-reference.html').write_png() |
29 | 29 | ref_image = Image.open(io.BytesIO(ref_png)) |
30 | 30 | ref_pixels = ref_image.getdata() |
109 | 109 | anchors[anchor_name] = round(pos_x, 6), round(pos_y, 6) |
110 | 110 | links = page.links |
111 | 111 | for i, link in enumerate(links): |
112 | link_type, target, rectangle, download_name = link | |
112 | link_type, target, rectangle, box = link | |
113 | 113 | pos_x, pos_y, width, height = rectangle |
114 | 114 | link = ( |
115 | 115 | link_type, target, |
116 | 116 | (round(pos_x, 6), round(pos_y, 6), |
117 | 117 | round(width, 6), round(height, 6)), |
118 | download_name) | |
118 | box) | |
119 | 119 | links[i] = link |
120 | 120 | bookmarks = page.bookmarks |
121 | 121 | for i, (level, label, (pos_x, pos_y), state) in enumerate(bookmarks): |
425 | 425 | stdout = _run(f'--pdf-variant=pdf/a-{version}b - -', b'test') |
426 | 426 | assert f'PDF-{pdf_version}'.encode() in stdout |
427 | 427 | 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 | |
428 | 433 | |
429 | 434 | |
430 | 435 | def test_pdf_identifier(): |
709 | 714 | assert document.make_bookmark_tree() == expected_tree |
710 | 715 | |
711 | 716 | |
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, | |
714 | 724 | base_url=resource_filename('<inline HTML>'), warnings=(), |
715 | 725 | round=False): |
716 | 726 | with capture_logs() as logs: |
717 | 727 | document = FakeHTML(string=html, base_url=base_url).render() |
718 | 728 | if round: |
719 | 729 | _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)] | |
721 | 733 | assert len(logs) == len(warnings) |
722 | 734 | for message, expected in zip(logs, warnings): |
723 | 735 | 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 | |
727 | 741 | |
728 | 742 | |
729 | 743 | @assert_no_logs |
734 | 748 | p { height: 90px; margin: 0 0 10px 0 } |
735 | 749 | img { width: 30px; vertical-align: top } |
736 | 750 | </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> | |
738 | 752 | <p style="padding: 0 10px"><a |
739 | 753 | href="#lipsum"><img style="border: solid 1px" |
740 | 754 | src=pattern.png></a></p> |
745 | 759 | </p> |
746 | 760 | ''', [ |
747 | 761 | [ |
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)) | |
752 | 766 | ], |
753 | [('internal', 'hello', (0, 0, 200, 30), None)], | |
767 | [('internal', 'hello', (0, 0, 200, 30))], | |
754 | 768 | ], [ |
755 | 769 | {'hello': (0, 200)}, |
756 | 770 | {'lipsum': (0, 0)} |
757 | 771 | ], [ |
758 | 772 | ( |
759 | 773 | [ |
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)) | |
764 | 778 | ], |
765 | 779 | [('hello', 0, 200)], |
766 | 780 | ), |
767 | 781 | ( |
768 | [ | |
769 | ('internal', 'hello', (0, 0, 200, 30), None) | |
770 | ], | |
782 | [('internal', 'hello', (0, 0, 200, 30))], | |
771 | 783 | [('lipsum', 0, 0)]), |
772 | 784 | ]) |
773 | 785 | |
778 | 790 | ''' |
779 | 791 | <body style="width: 200px"> |
780 | 792 | <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/') | |
786 | 798 | |
787 | 799 | |
788 | 800 | @assert_no_logs |
792 | 804 | <body style="width: 200px"> |
793 | 805 | <div style="display: block; margin: 10px 5px; |
794 | 806 | -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/') | |
800 | 812 | |
801 | 813 | |
802 | 814 | @assert_no_logs |
806 | 818 | ''' |
807 | 819 | <body style="width: 200px"> |
808 | 820 | <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))], [])], | |
811 | 823 | base_url=None) |
812 | 824 | |
813 | 825 | |
832 | 844 | <body style="width: 200px"> |
833 | 845 | <a href="#lipsum" id="lipsum" |
834 | 846 | 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> | |
836 | 848 | ''', [[ |
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))]], | |
839 | 851 | [{'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))], | |
842 | 854 | [('lipsum', 5, 10)])], |
843 | 855 | base_url=None) |
844 | 856 | |
851 | 863 | <div style="-weasy-link: url(#lipsum); |
852 | 864 | margin: 10px 5px" id="lipsum"> |
853 | 865 | ''', |
854 | [[('internal', 'lipsum', (5, 10, 195, 10), None)]], | |
866 | [[('internal', 'lipsum', (5, 10, 195, 10))]], | |
855 | 867 | [{'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)])], | |
858 | 869 | base_url=None) |
859 | 870 | |
860 | 871 | |
867 | 878 | <a href="#lipsum"></a> |
868 | 879 | <a href="#missing" id="lipsum"></a> |
869 | 880 | ''', |
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))]], | |
872 | 883 | [{'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)])], | |
875 | 885 | base_url=None, |
876 | 886 | warnings=[ |
877 | 887 | 'ERROR: No anchor #missing for internal URI reference']) |
885 | 895 | <a href="#lipsum" id="lipsum" style="display: block; height: 20px; |
886 | 896 | transform: rotate(90deg) scale(2)"> |
887 | 897 | ''', |
888 | [[('internal', 'lipsum', (30, 10, 70, 210), None)]], | |
898 | [[('internal', 'lipsum', (30, 10, 70, 210))]], | |
889 | 899 | [{'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)])], | |
892 | 901 | round=True) |
893 | 902 | |
894 | 903 | |
900 | 909 | <body style="width: 200px"> |
901 | 910 | <a rel=attachment href="pattern.png" download="wow.png" |
902 | 911 | 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))], [])], | |
907 | 914 | base_url=None) |
908 | 915 | |
909 | 916 | |
997 | 1004 | meta.setdefault('created', None) |
998 | 1005 | meta.setdefault('modified', None) |
999 | 1006 | meta.setdefault('attachments', []) |
1007 | meta.setdefault('lang', None) | |
1000 | 1008 | meta.setdefault('custom', {}) |
1001 | 1009 | assert vars(FakeHTML(string=html).render().metadata) == meta |
1002 | 1010 | |
1010 | 1018 | def test_html_meta_2(): |
1011 | 1019 | assert_meta( |
1012 | 1020 | ''' |
1021 | <html lang="en"><head> | |
1013 | 1022 | <meta name=author content="I Me & Myself"> |
1014 | 1023 | <meta name=author content="Smith, John"> |
1015 | 1024 | <title>Test document</title> |
1016 | 1025 | <h1>Another title</h1> |
1017 | 1026 | <meta name=generator content="Human after all"> |
1027 | <meta name=generator content="Human"> | |
1018 | 1028 | <meta name=dummy content=ignored> |
1019 | 1029 | <meta name=dummy> |
1020 | 1030 | <meta content=ignored> |
1026 | 1036 | <meta name=dcterms.modified content=2013> |
1027 | 1037 | <meta name=keywords content="Python; pydyf"> |
1028 | 1038 | <meta name=description content="Blah… "> |
1039 | <meta name=description content="*Oh-no/"> | |
1040 | <meta name=dcterms.modified content=2012> | |
1041 | </head></html> | |
1029 | 1042 | ''', |
1030 | 1043 | authors=['I Me & Myself', 'Smith, John'], |
1031 | 1044 | title='Test document', |
1034 | 1047 | description="Blah… ", |
1035 | 1048 | created='2011-04', |
1036 | 1049 | modified='2013', |
1050 | lang='en', | |
1037 | 1051 | custom={'dummy': 'ignored'}) |
1038 | 1052 | |
1039 | 1053 |
221 | 221 | |
222 | 222 | @assert_no_logs |
223 | 223 | def test_whitespace(): |
224 | # TODO: test more cases | |
225 | # http://www.w3.org/TR/CSS21/text.html#white-space-model | |
226 | 224 | assert_tree(parse_all(''' |
227 | 225 | <p>Lorem \t\r\n ipsum\t<strong> dolor |
228 | 226 | <img src=pattern.png> sit |
337 | 335 | |
338 | 336 | @assert_no_logs |
339 | 337 | 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 | |
341 | 339 | |
342 | 340 | # 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 | |
344 | 342 | assert_tree(parse_all(''' |
345 | 343 | <x-table> |
346 | 344 | <x-tr> |
407 | 405 | |
408 | 406 | @assert_no_logs |
409 | 407 | 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 | |
411 | 409 | # Rules 1.1 and 1.2 |
412 | 410 | # Rule XXX (not in the spec): column groups have at least one column child |
413 | 411 | assert_tree(parse_all(''' |
233 | 233 | ('p::before', 'Text', 'a')])])])])]) |
234 | 234 | |
235 | 235 | |
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 | ||
236 | 265 | @assert_no_logs |
237 | 266 | def test_counter_styles_1(): |
238 | 267 | assert_tree(parse_all(''' |
12 | 12 | stylesheet = tinycss2.parse_stylesheet( |
13 | 13 | '@font-face {' |
14 | 14 | ' font-family: Gentium Hard;' |
15 | ' src: url(http://example.com/fonts/Gentium.woff);' | |
15 | ' src: url(https://example.com/fonts/Gentium.woff);' | |
16 | 16 | '}') |
17 | 17 | at_rule, = stylesheet |
18 | 18 | assert at_rule.at_keyword == 'font-face' |
19 | 19 | font_family, src = list(preprocess_descriptors( |
20 | 'font-face', 'http://weasyprint.org/foo/', | |
20 | 'font-face', 'https://weasyprint.org/foo/', | |
21 | 21 | tinycss2.parse_declaration_list(at_rule.content))) |
22 | 22 | assert font_family == ('font_family', 'Gentium Hard') |
23 | 23 | assert src == ( |
24 | 'src', (('external', 'http://example.com/fonts/Gentium.woff'),)) | |
24 | 'src', (('external', 'https://example.com/fonts/Gentium.woff'),)) | |
25 | 25 | |
26 | 26 | |
27 | 27 | @assert_no_logs |
38 | 38 | assert at_rule.at_keyword == 'font-face' |
39 | 39 | font_family, src, font_style, font_weight, font_stretch = list( |
40 | 40 | preprocess_descriptors( |
41 | 'font-face', 'http://weasyprint.org/foo/', | |
41 | 'font-face', 'https://weasyprint.org/foo/', | |
42 | 42 | tinycss2.parse_declaration_list(at_rule.content))) |
43 | 43 | assert font_family == ('font_family', 'Fonty Smiley') |
44 | 44 | assert src == ( |
45 | 'src', (('external', 'http://weasyprint.org/foo/Fonty-Smiley.woff'),)) | |
45 | 'src', (('external', 'https://weasyprint.org/foo/Fonty-Smiley.woff'),)) | |
46 | 46 | assert font_style == ('font_style', 'italic') |
47 | 47 | assert font_weight == ('font_weight', 200) |
48 | 48 | assert font_stretch == ('font_stretch', 'condensed') |
58 | 58 | at_rule, = stylesheet |
59 | 59 | assert at_rule.at_keyword == 'font-face' |
60 | 60 | font_family, src = list(preprocess_descriptors( |
61 | 'font-face', 'http://weasyprint.org/foo/', | |
61 | 'font-face', 'https://weasyprint.org/foo/', | |
62 | 62 | tinycss2.parse_declaration_list(at_rule.content))) |
63 | 63 | assert font_family == ('font_family', 'Gentium Hard') |
64 | 64 | assert src == ('src', (('local', None),)) |
75 | 75 | at_rule, = stylesheet |
76 | 76 | assert at_rule.at_keyword == 'font-face' |
77 | 77 | font_family, src = list(preprocess_descriptors( |
78 | 'font-face', 'http://weasyprint.org/foo/', | |
78 | 'font-face', 'https://weasyprint.org/foo/', | |
79 | 79 | tinycss2.parse_declaration_list(at_rule.content))) |
80 | 80 | assert font_family == ('font_family', 'Gentium Hard') |
81 | 81 | assert src == ('src', (('local', 'Gentium Hard'),)) |
94 | 94 | assert at_rule.at_keyword == 'font-face' |
95 | 95 | with capture_logs() as logs: |
96 | 96 | font_family, src = list(preprocess_descriptors( |
97 | 'font-face', 'http://weasyprint.org/foo/', | |
97 | 'font-face', 'https://weasyprint.org/foo/', | |
98 | 98 | tinycss2.parse_declaration_list(at_rule.content))) |
99 | 99 | assert font_family == ('font_family', 'Gentium Hard') |
100 | 100 | assert src == ('src', (('local', 'Gentium Hard'),)) |
117 | 117 | with capture_logs() as logs: |
118 | 118 | font_family, src, font_stretch = list( |
119 | 119 | preprocess_descriptors( |
120 | 'font-face', 'http://weasyprint.org/foo/', | |
120 | 'font-face', 'https://weasyprint.org/foo/', | |
121 | 121 | tinycss2.parse_declaration_list(at_rule.content))) |
122 | 122 | assert font_family == ('font_family', 'Bad Font') |
123 | 123 | assert src == ( |
124 | 'src', (('external', 'http://weasyprint.org/foo/BadFont.woff'),)) | |
124 | 'src', (('external', 'https://weasyprint.org/foo/BadFont.woff'),)) | |
125 | 125 | assert font_stretch == ('font_stretch', 'expanded') |
126 | 126 | assert logs == [ |
127 | 127 | 'WARNING: Ignored `font-style: wrong` at 1:91, invalid value.', |
133 | 133 | stylesheet = tinycss2.parse_stylesheet('@font-face{}') |
134 | 134 | with capture_logs() as logs: |
135 | 135 | preprocess_stylesheet( |
136 | 'print', 'http://wp.org/foo/', stylesheet, None, None, None, | |
136 | 'print', 'https://wp.org/foo/', stylesheet, None, None, None, | |
137 | 137 | None, None) |
138 | 138 | assert logs == [ |
139 | 139 | "WARNING: Missing src descriptor in '@font-face' rule at 1:1"] |
143 | 143 | stylesheet = tinycss2.parse_stylesheet('@font-face{src: url(test.woff)}') |
144 | 144 | with capture_logs() as logs: |
145 | 145 | preprocess_stylesheet( |
146 | 'print', 'http://wp.org/foo/', stylesheet, None, None, None, | |
146 | 'print', 'https://wp.org/foo/', stylesheet, None, None, None, | |
147 | 147 | None, None) |
148 | 148 | assert logs == [ |
149 | 149 | "WARNING: Missing font-family descriptor in '@font-face' rule at 1:1"] |
153 | 153 | stylesheet = tinycss2.parse_stylesheet('@font-face{font-family: test}') |
154 | 154 | with capture_logs() as logs: |
155 | 155 | preprocess_stylesheet( |
156 | 'print', 'http://wp.org/foo/', stylesheet, None, None, None, | |
156 | 'print', 'https://wp.org/foo/', stylesheet, None, None, None, | |
157 | 157 | None, None) |
158 | 158 | assert logs == [ |
159 | 159 | "WARNING: Missing src descriptor in '@font-face' rule at 1:1"] |
164 | 164 | '@font-face { font-family: test; src: wrong }') |
165 | 165 | with capture_logs() as logs: |
166 | 166 | preprocess_stylesheet( |
167 | 'print', 'http://wp.org/foo/', stylesheet, None, None, None, | |
167 | 'print', 'https://wp.org/foo/', stylesheet, None, None, None, | |
168 | 168 | None, None) |
169 | 169 | assert logs == [ |
170 | 170 | 'WARNING: Ignored `src: wrong ` at 1:33, invalid value.', |
176 | 176 | '@font-face { font-family: good, bad; src: url(test.woff) }') |
177 | 177 | with capture_logs() as logs: |
178 | 178 | preprocess_stylesheet( |
179 | 'print', 'http://wp.org/foo/', stylesheet, None, None, None, | |
179 | 'print', 'https://wp.org/foo/', stylesheet, None, None, None, | |
180 | 180 | None, None) |
181 | 181 | assert logs == [ |
182 | 182 | 'WARNING: Ignored `font-family: good, bad` at 1:14, invalid value.', |
188 | 188 | '@font-face { font-family: good, bad; src: really bad }') |
189 | 189 | with capture_logs() as logs: |
190 | 190 | preprocess_stylesheet( |
191 | 'print', 'http://wp.org/foo/', stylesheet, None, None, None, | |
191 | 'print', 'https://wp.org/foo/', stylesheet, None, None, None, | |
192 | 192 | None, None) |
193 | 193 | assert logs == [ |
194 | 194 | 'WARNING: Ignored `font-family: good, bad` at 1:14, invalid value.', |
206 | 206 | stylesheet = tinycss2.parse_stylesheet(rule) |
207 | 207 | with capture_logs() as logs: |
208 | 208 | preprocess_stylesheet( |
209 | 'print', 'http://wp.org/foo/', stylesheet, None, None, None, | |
209 | 'print', 'https://wp.org/foo/', stylesheet, None, None, None, | |
210 | 210 | None, {}) |
211 | 211 | assert len(logs) >= 1 |
0 | 0 | """Test expanders for shorthand properties.""" |
1 | 1 | |
2 | import math | |
2 | from math import pi | |
3 | 3 | |
4 | 4 | import pytest |
5 | 5 | import tinycss2 |
17 | 17 | declarations = tinycss2.parse_declaration_list(css) |
18 | 18 | |
19 | 19 | with capture_logs() as logs: |
20 | base_url = 'http://weasyprint.org/foo/' | |
20 | base_url = 'https://weasyprint.org/foo/' | |
21 | 21 | declarations = list(preprocess_declarations(base_url, declarations)) |
22 | 22 | |
23 | 23 | if expected_error: |
222 | 222 | ('transform: none', {'transform': ()}), |
223 | 223 | ('transform: translate(6px) rotate(90deg)', { |
224 | 224 | 'transform': ( |
225 | ('translate', ((6, 'px'), (0, 'px'))), | |
226 | ('rotate', math.pi / 2))}), | |
225 | ('translate', ((6, 'px'), (0, 'px'))), ('rotate', pi / 2))}), | |
227 | 226 | ('transform: translate(-4px, 0)', { |
228 | 227 | 'transform': (('translate', ((-4, 'px'), (0, None))),)}), |
229 | 228 | ('transform: translate(6px, 20%)', { |
359 | 358 | 'list_style_type': 'inherit', |
360 | 359 | }), |
361 | 360 | ('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'), | |
363 | 362 | }), |
364 | 363 | ('list-style: square', { |
365 | 364 | 'list_style_type': 'square', |
421 | 420 | assert_background('red', background_color=(1, 0, 0, 1)) |
422 | 421 | assert_background( |
423 | 422 | 'url(lipsum.png)', |
424 | background_image=[('url', 'http://weasyprint.org/foo/lipsum.png')]) | |
423 | background_image=[('url', 'https://weasyprint.org/foo/lipsum.png')]) | |
425 | 424 | assert_background( |
426 | 425 | 'no-repeat', |
427 | 426 | background_repeat=[('no-repeat', 'no-repeat')]) |
467 | 466 | assert_background( |
468 | 467 | 'url(bar) #f00 repeat-y center left fixed', |
469 | 468 | background_color=(1, 0, 0, 1), |
470 | background_image=[('url', 'http://weasyprint.org/foo/bar')], | |
469 | background_image=[('url', 'https://weasyprint.org/foo/bar')], | |
471 | 470 | background_repeat=[('no-repeat', 'repeat')], |
472 | 471 | background_attachment=['fixed'], |
473 | 472 | background_position=[('left', (0, '%'), 'top', (50, '%'))]) |
514 | 513 | assert_background( |
515 | 514 | 'url(bar) center, no-repeat', |
516 | 515 | background_color=(0, 0, 0, 0), |
517 | background_image=[('url', 'http://weasyprint.org/foo/bar'), | |
516 | background_image=[('url', 'https://weasyprint.org/foo/bar'), | |
518 | 517 | ('none', None)], |
519 | 518 | background_position=[('left', (50, '%'), 'top', (50, '%')), |
520 | 519 | ('left', (0, '%'), 'top', (0, '%'))], |
817 | 816 | red = (1, 0, 0, 1) |
818 | 817 | lime = (0, 1, 0, 1) |
819 | 818 | blue = (0, 0, 1, 1) |
820 | pi = math.pi | |
821 | 819 | |
822 | 820 | def gradient(css, direction, colors=(blue,), stop_positions=(None,)): |
823 | 821 | for repeating, prefix in ((False, ''), (True, 'repeating-')): |
1217 | 1215 | )) |
1218 | 1216 | def test_text_align_invalid(rule, reason): |
1219 | 1217 | 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) |
302 | 302 | p { display: block; height: 90pt; margin: 0 0 10pt 0 } |
303 | 303 | img { width: 30pt; vertical-align: top } |
304 | 304 | </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> | |
306 | 306 | <p style="padding: 0 10pt"><a |
307 | 307 | href="#lipsum"><img style="border: solid 1pt" |
308 | 308 | src=pattern.png></a></p> |
321 | 321 | b'/Rect \\[ ([\\d\\.]+ [\\d\\.]+ [\\d\\.]+ [\\d\\.]+) \\]', pdf)] |
322 | 322 | |
323 | 323 | # 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' | |
325 | 325 | assert subtypes.pop(0) == b'/Link' |
326 | 326 | assert types.pop(0) == b'/URI' |
327 | 327 | assert rects.pop(0) == [0, TOP, 30, TOP - 20] |
328 | 328 | |
329 | 329 | # The image itself: 30*30pt |
330 | assert uris.pop(0) == b'http://weasyprint.org' | |
330 | assert uris.pop(0) == b'https://weasyprint.org' | |
331 | 331 | assert subtypes.pop(0) == b'/Link' |
332 | 332 | assert types.pop(0) == b'/URI' |
333 | 333 | assert rects.pop(0) == [0, TOP, 30, TOP - 30] |
372 | 372 | # 100% wide (block), 0pt high |
373 | 373 | pdf = FakeHTML( |
374 | 374 | 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)' | |
377 | 377 | assert f'/Rect [ 0 {TOP} {RIGHT} {TOP} ]'.encode() in pdf |
378 | 378 | |
379 | 379 | |
437 | 437 | def test_relative_links_different_base(): |
438 | 438 | pdf = FakeHTML( |
439 | 439 | 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 | |
442 | 442 | |
443 | 443 | |
444 | 444 | @assert_no_logs |
445 | 445 | def test_relative_links_same_base(): |
446 | 446 | pdf = FakeHTML( |
447 | 447 | 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() | |
449 | 449 | assert b'/Dest (test)' in pdf |
450 | 450 | |
451 | 451 | |
619 | 619 | ''').write_pdf() |
620 | 620 | md5 = '<{}>'.format(hashlib.md5(b'some data').hexdigest()).encode() |
621 | 621 | 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 | |
622 | 637 | |
623 | 638 | |
624 | 639 | @assert_no_logs |
1 | 1 | |
2 | 2 | import pytest |
3 | 3 | from weasyprint.css.properties import INITIAL_VALUES |
4 | from weasyprint.formatting_structure.build import capitalize | |
4 | 5 | from weasyprint.text.line_break import split_first_line |
5 | 6 | |
6 | 7 | from .testing_utils import MONO_FONTS, SANS_FONTS, assert_no_logs, render_pages |
958 | 959 | body, = html.children |
959 | 960 | lines = body.children |
960 | 961 | lines = [] |
961 | print(body.children) | |
962 | 962 | for line in body.children: |
963 | 963 | line_text = '' |
964 | 964 | for span_box in line.children: |
1228 | 1228 | p1, p2, p3, p4, p5 = body.children |
1229 | 1229 | line1, = p1.children |
1230 | 1230 | text1, = line1.children |
1231 | assert text1.text == 'Hé Lo1' | |
1231 | assert text1.text == 'Hé LO1' | |
1232 | 1232 | line2, = p2.children |
1233 | 1233 | text2, = line2.children |
1234 | 1234 | assert text2.text == 'HÉ LO1' |
1244 | 1244 | |
1245 | 1245 | |
1246 | 1246 | @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 | |
1247 | 1269 | def test_text_floating_pre_line(): |
1248 | 1270 | # Test regression: https://github.com/Kozea/WeasyPrint/issues/610 |
1249 | 1271 | page, = render_pages(''' |
126 | 126 | return [response] |
127 | 127 | |
128 | 128 | # Port 0: let the OS pick an available port number |
129 | # http://stackoverflow.com/a/1365284/1162888 | |
129 | # https://stackoverflow.com/a/1365284/1162888 | |
130 | 130 | server = wsgiref.simple_server.make_server('127.0.0.1', 0, wsgi_app) |
131 | 131 | _host, port = server.socket.getsockname() |
132 | 132 | thread = threading.Thread(target=server.serve_forever) |
12 | 12 | import html5lib |
13 | 13 | import tinycss2 |
14 | 14 | |
15 | VERSION = __version__ = '56.1' | |
15 | VERSION = __version__ = '57.0' | |
16 | 16 | |
17 | 17 | __all__ = [ |
18 | 18 | 'HTML', 'CSS', 'Attachment', 'Document', 'Page', 'default_url_fetcher', |
30 | 30 | def _find_base_url(html_document, fallback_base_url): |
31 | 31 | """Return the base URL for the document. |
32 | 32 | |
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 | |
34 | 34 | |
35 | 35 | """ |
36 | 36 | first_base_element = next(iter(html_document.iter('base')), None) |
3 | 3 | stylesheets associated with a document and annotate every element with a value |
4 | 4 | for every CSS property. |
5 | 5 | |
6 | http://www.w3.org/TR/CSS21/intro.html#processing-model | |
6 | https://www.w3.org/TR/CSS21/intro.html#processing-model | |
7 | 7 | |
8 | 8 | This module does this in more than two steps. The |
9 | 9 | :func:`get_all_computed_styles` function does everything, but it is itsef based |
47 | 47 | # values: (values, weight) |
48 | 48 | # values: a PropertyValue-like object |
49 | 49 | # 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 | |
51 | 51 | self._cascaded_styles = cascaded_styles = {} |
52 | 52 | |
53 | 53 | # keys: (element, pseudo_element_type), like cascaded_styles |
122 | 122 | if ('table' in style['display'] and |
123 | 123 | style['border_collapse'] == 'collapse'): |
124 | 124 | # Padding do not apply |
125 | for side in ['top', 'bottom', 'left', 'right']: | |
125 | for side in ('top', 'bottom', 'left', 'right'): | |
126 | 126 | style[f'padding_{side}'] = computed_values.ZERO_PIXELS |
127 | 127 | if (len(style['display']) == 1 and |
128 | 128 | style['display'][0].startswith('table-') and |
129 | 129 | style['display'][0] != 'table-caption'): |
130 | 130 | # Margins do not apply |
131 | for side in ['top', 'bottom', 'left', 'right']: | |
131 | for side in ('top', 'bottom', 'left', 'right'): | |
132 | 132 | style[f'margin_{side}'] = computed_values.ZERO_PIXELS |
133 | 133 | |
134 | 134 | return style |
585 | 585 | and ``'user agent'``. |
586 | 586 | |
587 | 587 | """ |
588 | # See http://www.w3.org/TR/CSS21/cascade.html#cascading-order | |
588 | # See https://www.w3.org/TR/CSS21/cascade.html#cascading-order | |
589 | 589 | if origin == 'user agent': |
590 | 590 | return 1 |
591 | 591 | elif origin == 'user' and not importance: |
625 | 625 | return copy |
626 | 626 | |
627 | 627 | 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] | |
630 | 630 | elif key == 'page': |
631 | 631 | # 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( | |
635 | 635 | key, INITIAL_VALUES[key], self.parent_style[key], |
636 | 636 | cascaded=False) |
637 | 637 | else: |
638 | self[key] = INITIAL_VALUES[key] | |
639 | return self[key] | |
638 | value = self[key] = INITIAL_VALUES[key] | |
639 | return value | |
640 | 640 | |
641 | 641 | |
642 | 642 | class ComputedStyle(dict): |
675 | 675 | if key in self.cascaded: |
676 | 676 | value = keyword = self.cascaded[key][0] |
677 | 677 | else: |
678 | if key in INHERITED or key.startswith('__'): | |
678 | if key in INHERITED or key[:2] == '__': | |
679 | 679 | keyword = 'inherit' |
680 | 680 | else: |
681 | 681 | keyword = 'initial' |
685 | 685 | keyword = 'initial' |
686 | 686 | |
687 | 687 | if keyword == 'initial': |
688 | value = None if key.startswith('__') else INITIAL_VALUES[key] | |
688 | value = None if key[:2] == '__' else INITIAL_VALUES[key] | |
689 | 689 | if key not in INITIAL_NOT_COMPUTED: |
690 | 690 | # The value is the same as when computed |
691 | 691 | self[key] = value |
693 | 693 | # Values in parent_style are already computed. |
694 | 694 | self[key] = value = self.parent_style[key] |
695 | 695 | |
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( | |
698 | 698 | 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': | |
701 | 702 | # The page property does not inherit. However, if the page |
702 | 703 | # value on an element is auto, then its used value is the value |
703 | 704 | # specified on its nearest ancestor with a non-auto value. When |
704 | 705 | # specified on the root element, the used value for auto is the |
705 | 706 | # empty string. |
706 | self['page'] = value = ( | |
707 | value = ( | |
707 | 708 | '' 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'): | |
710 | 712 | self.specified[key] = value |
711 | 713 | |
712 | 714 | if key in self: |
0 | 0 | """Convert specified property values into computed values.""" |
1 | 1 | |
2 | 2 | from collections import OrderedDict |
3 | from contextlib import suppress | |
4 | from math import pi | |
3 | 5 | from urllib.parse import unquote |
4 | 6 | |
5 | 7 | from tinycss2.color3 import parse_color |
20 | 22 | |
21 | 23 | # Value in pixels of font-size for <absolute-size> keywords: 12pt (16px) for |
22 | 24 | # 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 | |
24 | 26 | FONT_SIZE_KEYWORDS = OrderedDict( |
25 | 27 | # medium is 16px, others are a ratio of medium |
26 | 28 | (name, INITIAL_VALUES['font_size'] * a / b) |
44 | 46 | } |
45 | 47 | assert INITIAL_VALUES['border_top_width'] == BORDER_WIDTH_KEYWORDS['medium'] |
46 | 48 | |
47 | # http://www.w3.org/TR/CSS21/fonts.html#propdef-font-weight | |
49 | # https://www.w3.org/TR/CSS21/fonts.html#propdef-font-weight | |
48 | 50 | FONT_WEIGHT_RELATIVE = dict( |
49 | 51 | bolder={ |
50 | 52 | 100: 400, |
70 | 72 | }, |
71 | 73 | ) |
72 | 74 | |
73 | # http://www.w3.org/TR/css3-page/#size | |
75 | # https://www.w3.org/TR/css-page-3/#size | |
74 | 76 | # name=(width in pixels, height in pixels) |
75 | 77 | PAGE_SIZES = { |
76 | 78 | 'a10': (Dimension(26, 'mm'), Dimension(37, 'mm'),), |
193 | 195 | style['font_variant_alternates'], |
194 | 196 | style['font_variant_east_asian'], |
195 | 197 | style['font_feature_settings'], |
198 | style['font_variation_settings'], | |
196 | 199 | )) |
197 | 200 | |
198 | 201 | |
225 | 228 | |
226 | 229 | # See https://drafts.csswg.org/css-variables/#invalid-variables |
227 | 230 | if new_value is None: |
228 | try: | |
231 | with suppress(BaseException): | |
229 | 232 | computed_value = ''.join( |
230 | 233 | token.serialize() for token in computed_value) |
231 | except BaseException: | |
232 | pass | |
233 | 234 | LOGGER.warning( |
234 | 235 | 'Unsupported computed value "%s" set in variable %r ' |
235 | 236 | 'for property %r.', computed_value, |
383 | 384 | value if value in ('contain', 'cover') else |
384 | 385 | length_or_percentage_tuple(style, name, value) |
385 | 386 | 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) | |
386 | 396 | |
387 | 397 | |
388 | 398 | @register_computer('border-top-width') |
537 | 547 | def display(style, name, value): |
538 | 548 | """Compute the ``display`` property. |
539 | 549 | |
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 | |
541 | 551 | |
542 | 552 | """ |
543 | 553 | float_ = style.specified['float'] |
560 | 570 | def compute_float(style, name, value): |
561 | 571 | """Compute the ``float`` property. |
562 | 572 | |
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 | |
564 | 574 | |
565 | 575 | """ |
566 | if style.specified['position'] in ('absolute', 'fixed'): | |
576 | position = style.specified['position'] | |
577 | if position in ('absolute', 'fixed') or position[0] == 'running()': | |
567 | 578 | return 'none' |
568 | 579 | else: |
569 | 580 | return value |
7 | 7 | |
8 | 8 | */ |
9 | 9 | |
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 */ | |
11 | 11 | *[id] { -weasy-anchor: attr(id); } |
12 | 12 | a[name] { -weasy-anchor: attr(name); } |
13 | 13 |
8 | 8 | |
9 | 9 | |
10 | 10 | 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 | |
12 | 12 | 'bottom': 'auto', |
13 | 13 | 'caption_side': 'top', |
14 | 14 | 'clear': 'none', |
41 | 41 | 'width': 'auto', |
42 | 42 | 'z_index': 'auto', |
43 | 43 | |
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/ | |
45 | 45 | 'background_attachment': ('scroll',), |
46 | 46 | 'background_clip': ('border-box',), |
47 | 47 | 'background_color': parse_color('transparent'), |
70 | 70 | 'border_top_style': 'none', |
71 | 71 | 'border_top_width': 3, # computed value for 'medium' |
72 | 72 | |
73 | # Color 3 (REC): https://www.w3.org/TR/css3-color/ | |
73 | # Color 3 (REC): https://www.w3.org/TR/css-color-3/ | |
74 | 74 | 'opacity': 1, |
75 | 75 | |
76 | 76 | # Multi-column Layout (WD): https://www.w3.org/TR/css-multicol-1/ |
100 | 100 | 'font_variant_position': 'normal', |
101 | 101 | 'font_weight': 400, |
102 | 102 | |
103 | # Fonts 4 (WD): https://www.w3.org/TR/css-fonts-4/ | |
104 | 'font_variation_settings': 'normal', | |
105 | ||
103 | 106 | # Fragmentation 3/4 (CR/WD): https://www.w3.org/TR/css-break-4/ |
104 | 107 | 'box_decoration_break': 'slice', |
105 | 108 | 'break_after': 'auto', |
119 | 122 | 'quotes': list('“”‘’'), # chosen by the user agent |
120 | 123 | 'string_set': 'none', |
121 | 124 | |
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/ | |
123 | 126 | 'image_resolution': 1, # dppx |
124 | 127 | 'image_rendering': 'auto', |
125 | # https://drafts.csswg.org/css-images-3/ | |
128 | 'image_orientation': 'from-image', | |
126 | 129 | 'object_fit': 'fill', |
127 | 130 | 'object_position': (('left', Dimension(50, '%'), |
128 | 131 | 'top', Dimension(50, '%')),), |
213 | 216 | # Values inherited but not applicable to print are not included. |
214 | 217 | # |
215 | 218 | # 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 | |
217 | 220 | # |
218 | 221 | # 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 | |
220 | 223 | INHERITED = { |
221 | 224 | 'block_ellipsis', |
222 | 225 | 'border_collapse', |
239 | 242 | 'font_variant_ligatures', |
240 | 243 | 'font_variant_numeric', |
241 | 244 | 'font_variant_position', |
245 | 'font_variation_settings', | |
242 | 246 | 'font_weight', |
243 | 247 | 'hyphens', |
244 | 248 | 'hyphenate_character', |
269 | 273 | } |
270 | 274 | |
271 | 275 | |
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 | |
274 | 278 | # Only non-inherited properties need to be included here. |
275 | 279 | TABLE_WRAPPER_BOX_PROPERTIES = { |
276 | 280 | 'bottom', |
8 | 8 | from ..urls import iri_to_uri, url_is_absolute |
9 | 9 | from .properties import Dimension |
10 | 10 | |
11 | # http://dev.w3.org/csswg/css3-values/#angles | |
11 | # https://drafts.csswg.org/css-values-3/#angles | |
12 | 12 | # 1<unit> is this many radians. |
13 | 13 | ANGLE_TO_RADIANS = { |
14 | 14 | 'rad': 1, |
18 | 18 | } |
19 | 19 | |
20 | 20 | # 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 | |
22 | 22 | LENGTHS_TO_PIXELS = { |
23 | 23 | 'px': 1, |
24 | 24 | 'pt': 1. / 0.75, |
29 | 29 | 'q': 96. / 25.4 / 4, # LENGTHS_TO_PIXELS['mm'] / 4 |
30 | 30 | } |
31 | 31 | |
32 | # http://dev.w3.org/csswg/css-values/#resolution | |
32 | # https://drafts.csswg.org/css-values/#resolution | |
33 | 33 | RESOLUTION_TO_DPPX = { |
34 | 34 | 'dppx': 1, |
35 | 35 | 'dpi': 1 / LENGTHS_TO_PIXELS['in'], |
256 | 256 | def parse_position(tokens): |
257 | 257 | """Parse background-position and object-position. |
258 | 258 | |
259 | See http://dev.w3.org/csswg/css3-background/#the-background-position | |
259 | See https://drafts.csswg.org/css-backgrounds-3/#the-background-position | |
260 | 260 | https://drafts.csswg.org/css-images-3/#propdef-object-position |
261 | 261 | |
262 | 262 | """ |
166 | 166 | if keyword in ('normal', 'bold'): |
167 | 167 | return keyword |
168 | 168 | 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): | |
170 | 170 | return token.int_value |
171 | 171 | |
172 | 172 |
166 | 166 | def expand_list_style(name, tokens, base_url): |
167 | 167 | """Expand the ``list-style`` shorthand property. |
168 | 168 | |
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 | |
170 | 170 | |
171 | 171 | """ |
172 | 172 | type_specified = image_specified = False |
208 | 208 | def expand_border(base_url, name, tokens): |
209 | 209 | """Expand the ``border`` shorthand property. |
210 | 210 | |
211 | See http://www.w3.org/TR/CSS21/box.html#propdef-border | |
211 | See https://www.w3.org/TR/CSS21/box.html#propdef-border | |
212 | 212 | |
213 | 213 | """ |
214 | 214 | for suffix in ('-top', '-right', '-bottom', '-left'): |
226 | 226 | def expand_border_side(name, tokens): |
227 | 227 | """Expand the ``border-*`` shorthand properties. |
228 | 228 | |
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 | |
230 | 230 | |
231 | 231 | """ |
232 | 232 | for token in tokens: |
245 | 245 | def expand_background(base_url, name, tokens): |
246 | 246 | """Expand the ``background`` shorthand property. |
247 | 247 | |
248 | See http://dev.w3.org/csswg/css3-background/#the-background | |
248 | See https://drafts.csswg.org/css-backgrounds-3/#the-background | |
249 | 249 | |
250 | 250 | """ |
251 | 251 | properties = [ |
545 | 545 | def expand_word_wrap(name, tokens): |
546 | 546 | """Expand the ``word-wrap`` legacy property. |
547 | 547 | |
548 | See http://www.w3.org/TR/css3-text/#overflow-wrap | |
548 | See https://www.w3.org/TR/css-text-3/#overflow-wrap | |
549 | 549 | |
550 | 550 | """ |
551 | 551 | keyword = overflow_wrap(tokens) |
0 | 0 | """Validate properties. |
1 | 1 | |
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. | |
3 | 3 | |
4 | 4 | """ |
5 | 5 | |
838 | 838 | return None |
839 | 839 | if values: |
840 | 840 | 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) | |
841 | 857 | |
842 | 858 | |
843 | 859 | @property() |
1264 | 1280 | |
1265 | 1281 | |
1266 | 1282 | @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) | |
1267 | 1307 | def size(tokens): |
1268 | 1308 | """``size`` property validation. |
1269 | 1309 | |
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 | |
1271 | 1311 | |
1272 | 1312 | """ |
1273 | 1313 | lengths = [get_length(token, negative=False) for token in tokens] |
1509 | 1549 | length = get_length(args[0], percentage=True) |
1510 | 1550 | if name == 'rotate' and angle is not None: |
1511 | 1551 | transforms.append((name, angle)) |
1512 | elif name == 'skewx' and angle is not None: | |
1552 | elif name in ('skewx', 'skew') and angle is not None: | |
1513 | 1553 | transforms.append(('skew', (angle, 0))) |
1514 | 1554 | elif name == 'skewy' and angle is not None: |
1515 | 1555 | transforms.append(('skew', (0, angle))) |
103 | 103 | """ |
104 | 104 | def __init__(self, title=None, authors=None, description=None, |
105 | 105 | keywords=None, generator=None, created=None, modified=None, |
106 | attachments=None, custom=None): | |
106 | attachments=None, lang=None, custom=None): | |
107 | 107 | #: The title of the document, as a string or :obj:`None`. |
108 | 108 | #: Extracted from the ``<title>`` element in HTML |
109 | 109 | #: and written to the ``/Title`` info field in PDF. |
129 | 129 | self.generator = generator |
130 | 130 | #: The creation date of the document, as a string or :obj:`None`. |
131 | 131 | #: 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>`_. | |
133 | 133 | #: Extracted from the ``<meta name=dcterms.created>`` element in HTML |
134 | 134 | #: and written to the ``/CreationDate`` info field in PDF. |
135 | 135 | self.created = created |
136 | 136 | #: The modification date of the document, as a string or :obj:`None`. |
137 | 137 | #: 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>`_. | |
139 | 139 | #: Extracted from the ``<meta name=dcterms.modified>`` element in HTML |
140 | 140 | #: and written to the ``/ModDate`` info field in PDF. |
141 | 141 | self.modified = modified |
144 | 144 | #: Extracted from the ``<link rel=attachment>`` elements in HTML |
145 | 145 | #: and written to the ``/EmbeddedFiles`` dictionary in PDF. |
146 | 146 | self.attachments = attachments or [] |
147 | #: Document language as BCP 47 language tags. | |
148 | #: Extracted from ``<html lang=lang>`` in HTML. | |
149 | self.lang = lang | |
147 | 150 | #: Custom metadata, as a dict whose keys are the metadata names and |
148 | 151 | #: values are the metadata values. |
149 | 152 | self.custom = custom or {} |
215 | 218 | [Page(page_box) for page_box in page_boxes], |
216 | 219 | DocumentMetadata(**get_html_metadata(html)), |
217 | 220 | html.url_fetcher, font_config, optimize_size) |
221 | rendering._html = html | |
218 | 222 | return rendering |
219 | 223 | |
220 | 224 | def __init__(self, pages, metadata, url_fetcher, font_config, |
240 | 244 | # Set of flags for PDF size optimization. Can contain "images" and |
241 | 245 | # "fonts". |
242 | 246 | 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) | |
243 | 255 | |
244 | 256 | def copy(self, pages='all'): |
245 | 257 | """Take a subset of the pages. |
332 | 344 | |
333 | 345 | """ |
334 | 346 | 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, | |
337 | 348 | variant, version, custom_metadata) |
338 | 349 | |
339 | 350 | if finisher: |
77 | 77 | |
78 | 78 | def draw_stacking_context(stream, stacking_context): |
79 | 79 | """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 | |
81 | 81 | with stacked(stream): |
82 | 82 | box = stacking_context.box |
83 | ||
84 | stream.begin_marked_content(box, mcid=True) | |
83 | 85 | |
84 | 86 | # apply the viewport_overflow to the html box, see #35 |
85 | 87 | if box.is_for_root_element and ( |
113 | 115 | if box.transformation_matrix.determinant: |
114 | 116 | stream.transform(*box.transformation_matrix.values) |
115 | 117 | else: |
118 | stream.end_marked_content() | |
116 | 119 | return |
117 | 120 | |
118 | 121 | # Point 1 is done in draw_page |
157 | 160 | if isinstance(block, boxes.ReplacedBox): |
158 | 161 | draw_border(stream, block) |
159 | 162 | 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: | |
163 | 168 | draw_inline_level( |
164 | 169 | stream, stacking_context.page, child) |
170 | if block != box: | |
171 | stream.end_marked_content() | |
165 | 172 | |
166 | 173 | # Point 8 |
167 | 174 | for child_context in stacking_context.zero_z_contexts: |
181 | 188 | stream.set_alpha(box.style['opacity'], stroke=True, fill=True) |
182 | 189 | stream.draw_x_object(group_id) |
183 | 190 | stream.pop_state() |
191 | ||
192 | stream.end_marked_content() | |
184 | 193 | |
185 | 194 | |
186 | 195 | def rounded_box_path(stream, radii): |
236 | 245 | # Background color |
237 | 246 | if bg.color.alpha > 0: |
238 | 247 | with stacked(stream): |
248 | stream.set_color_rgb(*bg.color[:3]) | |
249 | stream.set_alpha(bg.color.alpha) | |
239 | 250 | painting_area = bg.layers[-1].painting_area |
240 | 251 | if painting_area: |
241 | 252 | if bleed: |
249 | 260 | stream.clip() |
250 | 261 | stream.end() |
251 | 262 | stream.rectangle(*stream.page_rectangle) |
252 | stream.set_color_rgb(*bg.color[:3]) | |
253 | stream.set_alpha(bg.color.alpha) | |
254 | 263 | stream.fill() |
255 | 264 | |
256 | 265 | if bleed and marks: |
263 | 272 | svg = f''' |
264 | 273 | <svg height="{height}" width="{width}" |
265 | 274 | fill="transparent" stroke="black" stroke-width="1" |
266 | xmlns="http://www.w3.org/2000/svg"> | |
275 | xmlns="https://www.w3.org/2000/svg"> | |
267 | 276 | ''' |
268 | 277 | if 'crop' in marks: |
269 | 278 | svg += f''' |
414 | 423 | # We need a plan to draw beautiful borders, and that's difficult, no need |
415 | 424 | # to lie. Let's try to find the cases that we can handle in a smart way. |
416 | 425 | |
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 | ||
417 | 437 | def draw_column_border(): |
418 | 438 | """Draw column borders.""" |
419 | 439 | columns = ( |
422 | 442 | box.style['column_count'] != 'auto')) |
423 | 443 | if columns and box.style['column_rule_width']: |
424 | 444 | 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(): | |
426 | 446 | with stacked(stream): |
427 | 447 | position_x = (child.position_x - ( |
428 | 448 | box.style['column_rule_width'] + |
429 | 449 | box.style['column_gap']) / 2) |
430 | 450 | border_box = ( |
431 | 451 | position_x, child.position_y, |
432 | box.style['column_rule_width'], box.height) | |
452 | box.style['column_rule_width'], child.height) | |
433 | 453 | clip_border_segment( |
434 | 454 | stream, box.style['column_rule_style'], |
435 | 455 | box.style['column_rule_width'], 'left', border_box, |
516 | 536 | pi" Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372], |
517 | 537 | wonderfully explained by Dr Rob. |
518 | 538 | |
519 | http://mathforum.org/dr.math/faq/formulas/ | |
539 | https://mathforum.org/dr.math/faq/formulas/ | |
520 | 540 | |
521 | 541 | """ |
522 | 542 | x = (a - b) / (a + b) |
665 | 685 | |
666 | 686 | |
667 | 687 | def draw_rounded_border(stream, box, style, color): |
668 | rounded_box_path(stream, box.rounded_padding_box()) | |
669 | 688 | if style in ('ridge', 'groove'): |
670 | rounded_box_path(stream, box.rounded_box_ratio(1 / 2)) | |
671 | 689 | stream.set_color_rgb(*color[0][:3]) |
672 | 690 | 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)) | |
673 | 693 | stream.fill(even_odd=True) |
694 | stream.set_color_rgb(*color[1][:3]) | |
695 | stream.set_alpha(color[1][3]) | |
674 | 696 | rounded_box_path(stream, box.rounded_box_ratio(1 / 2)) |
675 | 697 | rounded_box_path(stream, box.rounded_border_box()) |
676 | stream.set_color_rgb(*color[1][:3]) | |
677 | stream.set_alpha(color[1][3]) | |
678 | 698 | stream.fill(even_odd=True) |
679 | 699 | return |
700 | stream.set_color_rgb(*color[:3]) | |
701 | stream.set_alpha(color[3]) | |
702 | rounded_box_path(stream, box.rounded_padding_box()) | |
680 | 703 | if style == 'double': |
681 | 704 | rounded_box_path(stream, box.rounded_box_ratio(1 / 3)) |
682 | 705 | rounded_box_path(stream, box.rounded_box_ratio(2 / 3)) |
683 | 706 | rounded_box_path(stream, box.rounded_border_box()) |
684 | stream.set_color_rgb(*color[:3]) | |
685 | stream.set_alpha(color[3]) | |
686 | 707 | stream.fill(even_odd=True) |
687 | 708 | |
688 | 709 | |
689 | 710 | def draw_rect_border(stream, box, widths, style, color): |
690 | 711 | bbx, bby, bbw, bbh = box |
691 | 712 | bt, br, bb, bl = widths |
692 | stream.rectangle(*box) | |
693 | 713 | if style in ('ridge', 'groove'): |
714 | stream.set_color_rgb(*color[0][:3]) | |
715 | stream.set_alpha(color[0][3]) | |
716 | stream.rectangle(*box) | |
694 | 717 | stream.rectangle( |
695 | 718 | bbx + bl / 2, bby + bt / 2, |
696 | 719 | bbw - (bl + br) / 2, bbh - (bt + bb) / 2) |
697 | stream.set_color_rgb(*color[0][:3]) | |
698 | stream.set_alpha(color[0][3]) | |
699 | 720 | stream.fill(even_odd=True) |
700 | 721 | stream.rectangle( |
701 | 722 | bbx + bl / 2, bby + bt / 2, |
705 | 726 | stream.set_alpha(color[1][3]) |
706 | 727 | stream.fill(even_odd=True) |
707 | 728 | return |
729 | stream.set_color_rgb(*color[:3]) | |
730 | stream.set_alpha(color[3]) | |
731 | stream.rectangle(*box) | |
708 | 732 | if style == 'double': |
709 | 733 | stream.rectangle( |
710 | 734 | bbx + bl / 3, bby + bt / 3, |
713 | 737 | bbx + bl * 2 / 3, bby + bt * 2 / 3, |
714 | 738 | bbw - (bl + br) * 2 / 3, bbh - (bt + bb) * 2 / 3) |
715 | 739 | 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]) | |
718 | 740 | stream.fill(even_odd=True) |
719 | 741 | |
720 | 742 | |
935 | 957 | draw_background(stream, box.background) |
936 | 958 | draw_border(stream, box) |
937 | 959 | if isinstance(box, (boxes.InlineBox, boxes.LineBox)): |
960 | link_annotation = None | |
938 | 961 | if isinstance(box, boxes.LineBox): |
939 | 962 | text_overflow = box.text_overflow |
940 | 963 | block_ellipsis = box.block_ellipsis |
964 | else: | |
965 | link_annotation = box.link_annotation | |
941 | 966 | ellipsis = 'none' |
967 | if link_annotation: | |
968 | stream.begin_marked_content(box, mcid=True, tag='Link') | |
942 | 969 | for i, child in enumerate(box.children): |
943 | 970 | if i == len(box.children) - 1: |
944 | 971 | # Last child |
955 | 982 | draw_inline_level( |
956 | 983 | stream, page, child, child_offset_x, text_overflow, |
957 | 984 | ellipsis) |
985 | if link_annotation: | |
986 | stream.end_marked_content() | |
958 | 987 | elif isinstance(box, boxes.InlineReplacedBox): |
959 | 988 | draw_replacedbox(stream, box) |
960 | 989 | else: |
1071 | 1100 | utf8_text = textbox.pango_layout.text.encode() |
1072 | 1101 | previous_utf8_position = 0 |
1073 | 1102 | |
1074 | runs = [first_line.runs[0]] | |
1075 | while runs[-1].next != ffi.NULL: | |
1076 | runs.append(runs[-1].next) | |
1077 | ||
1078 | 1103 | matrix = Matrix(1, 0, 0, -1, x, y) |
1079 | 1104 | 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 | |
1082 | 1108 | stream.text_matrix(*matrix.values) |
1083 | 1109 | last_font = None |
1084 | 1110 | string = '' |
1085 | 1111 | x_advance = 0 |
1086 | 1112 | emojis = [] |
1087 | for run in runs: | |
1113 | run = first_line.runs[0] | |
1114 | while run != ffi.NULL: | |
1088 | 1115 | # Pango objects |
1089 | glyph_item = ffi.cast('PangoGlyphItem *', run.data) | |
1116 | glyph_item = run.data | |
1117 | run = run.next | |
1090 | 1118 | glyph_string = glyph_item.glyphs |
1091 | 1119 | glyphs = glyph_string.glyphs |
1092 | 1120 | num_glyphs = glyph_string.num_glyphs |
1106 | 1134 | if string: |
1107 | 1135 | stream.show_text(string) |
1108 | 1136 | 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) | |
1109 | 1140 | last_font = font |
1110 | stream.set_font_size(font.hash, 1 if font.bitmap else font_size) | |
1111 | 1141 | string += '<' |
1112 | 1142 | for i in range(num_glyphs): |
1113 | 1143 | glyph_info = glyphs[i] |
0 | 0 | """Classes for all types of boxes in the CSS formatting structure / box model. |
1 | 1 | |
2 | See http://www.w3.org/TR/CSS21/visuren.html | |
2 | See https://www.w3.org/TR/CSS21/visuren.html | |
3 | 3 | |
4 | 4 | Names are the same as in CSS 2.1 with the exception of ``TextBox``. In |
5 | 5 | WeasyPrint, any text is in a ``TextBox``. What CSS calls anonymous inline boxes |
6 | 6 | are text boxes but not all text boxes are anonymous inline boxes. |
7 | 7 | |
8 | See http://www.w3.org/TR/CSS21/visuren.html#anonymous | |
8 | See https://www.w3.org/TR/CSS21/visuren.html#anonymous | |
9 | 9 | |
10 | 10 | Abstract classes, should not be instantiated: |
11 | 11 | |
55 | 55 | class Box: |
56 | 56 | """Abstract base class for all boxes.""" |
57 | 57 | # 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 | |
59 | 59 | proper_table_child = False |
60 | 60 | internal_table_or_caption = False |
61 | 61 | tabular_container = False |
76 | 76 | transformation_matrix = None |
77 | 77 | bookmark_label = None |
78 | 78 | string_set = None |
79 | download_name = None | |
80 | 79 | footnote = None |
81 | 80 | cached_counter_values = None |
82 | 81 | missing_link = None |
122 | 121 | |
123 | 122 | """ |
124 | 123 | # Overridden in ParentBox to also translate children, if any. |
125 | if dx == 0 and dy == 0: | |
124 | if dx == dy == 0: | |
126 | 125 | return |
127 | 126 | self.position_x += dx |
128 | 127 | self.position_y += dy |
189 | 188 | def hit_area(self): |
190 | 189 | """Return the (x, y, w, h) rectangle where the box is clickable.""" |
191 | 190 | # "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 | |
193 | 192 | # TODO: manage the border radii, use outer_border_radii instead |
194 | 193 | return (self.border_box_x(), self.border_box_y(), |
195 | 194 | self.border_width(), self.border_height()) |
221 | 220 | height = self.border_height() - bt - bb |
222 | 221 | |
223 | 222 | # 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 | |
225 | 224 | ratio = min([1] + [ |
226 | 225 | extent / sum_radii |
227 | for extent, sum_radii in [ | |
226 | for extent, sum_radii in ( | |
228 | 227 | (width, tlrx + trrx), |
229 | 228 | (width, blrx + brrx), |
230 | 229 | (height, tlry + blry), |
231 | 230 | (height, trry + brry), |
232 | ] | |
231 | ) | |
233 | 232 | if sum_radii > 0 |
234 | 233 | ]) |
235 | 234 | return ( |
453 | 452 | inline box. |
454 | 453 | |
455 | 454 | """ |
455 | link_annotation = None | |
456 | ||
456 | 457 | def hit_area(self): |
457 | 458 | """Return the (x, y, w, h) rectangle where the box is clickable.""" |
458 | 459 | # Use line-height (margin_height) rather than border_height |
535 | 536 | class TableBox(BlockLevelBox, ParentBox): |
536 | 537 | """Box for elements with ``display: table``""" |
537 | 538 | # 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 | |
539 | 540 | tabular_container = True |
540 | 541 | |
541 | 542 | def all_children(self): |
37 | 37 | ('table-caption',): boxes.TableCaptionBox, |
38 | 38 | } |
39 | 39 | |
40 | # http://stackoverflow.com/questions/16317534/ | |
40 | # https://stackoverflow.com/questions/16317534/ | |
41 | 41 | ASCII_TO_WIDE = {i: chr(i + 0xfee0) for i in range(0x21, 0x7f)} |
42 | 42 | ASCII_TO_WIDE.update({0x20: '\u3000', 0x2D: '\u2212'}) |
43 | 43 | |
114 | 114 | ] |
115 | 115 | |
116 | 116 | ``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 | |
118 | 118 | |
119 | 119 | """ |
120 | 120 | if not isinstance(element.tag, str): |
332 | 332 | else: |
333 | 333 | if image_type == 'url': |
334 | 334 | # 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']) | |
336 | 337 | if image is not None: |
337 | 338 | box = boxes.InlineReplacedBox.anonymous_from(box, image) |
338 | 339 | children.append(box) |
420 | 421 | if origin != 'external': |
421 | 422 | # Embedding internal references is impossible |
422 | 423 | continue |
423 | image = get_image_from_uri(url=uri) | |
424 | image = get_image_from_uri( | |
425 | url=uri, orientation=parent_box.style['image_orientation']) | |
424 | 426 | if image is not None: |
425 | 427 | content_boxes.append( |
426 | 428 | boxes.InlineReplacedBox.anonymous_from(parent_box, image)) |
699 | 701 | # 'auto' is the initial value but is not valid in stylesheet: |
700 | 702 | # there was no counter-increment declaration for this element. |
701 | 703 | # (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 | |
703 | 705 | if 'list-item' in style['display']: |
704 | 706 | counter_increment = [('list-item', 1)] |
705 | 707 | else: |
761 | 763 | |
762 | 764 | Take and return a ``Box`` object. |
763 | 765 | |
764 | See http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes | |
766 | See https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes | |
765 | 767 | |
766 | 768 | """ |
767 | 769 | if not isinstance(box, boxes.ParentBox) or box.is_running(): |
798 | 800 | # TODO: Maybe only remove text if internal is also |
799 | 801 | # a proper table descendant of box. |
800 | 802 | # 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 | |
802 | 804 | |
803 | 805 | # Last child |
804 | 806 | internal, text = children[-2:] |
874 | 876 | Because of colspan/rowspan works, grid_y is implicitly the index of a row, |
875 | 877 | but grid_x is an explicit attribute on cells, columns and column group. |
876 | 878 | |
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 | |
879 | 881 | |
880 | 882 | """ |
881 | 883 | # Group table children by type |
936 | 938 | # Assign a (x,y) position in the grid to each cell. |
937 | 939 | # rowspan can not extend beyond a row group, so each row group |
938 | 940 | # is independent. |
939 | # http://www.w3.org/TR/CSS21/tables.html#table-layout | |
941 | # https://www.w3.org/TR/CSS21/tables.html#table-layout | |
940 | 942 | # Column 0 is on the left if direction is ltr, right if rtl. |
941 | 943 | # This algorithm does not change. |
942 | 944 | grid_height = 0 |
954 | 956 | grid_x += 1 |
955 | 957 | cell.grid_x = grid_x |
956 | 958 | 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 | |
958 | 960 | if cell.rowspan != 1: |
959 | 961 | max_rowspan = len(occupied_cells_by_row) + 1 |
960 | 962 | if cell.rowspan == 0: |
1029 | 1031 | width = box_style[f'border_{side}_width'] |
1030 | 1032 | color = get_color(box_style, f'border_{side}_color') |
1031 | 1033 | |
1032 | # http://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution | |
1034 | # https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution | |
1033 | 1035 | score = ((1 if style == 'hidden' else 0), width, style_scores[style]) |
1034 | 1036 | |
1035 | 1037 | style = style_map.get(style, style) |
1050 | 1052 | # The order is important here: |
1051 | 1053 | # "A style set on a cell wins over one on a row, which wins over a |
1052 | 1054 | # 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 | |
1054 | 1056 | strong_null_border = ( |
1055 | 1057 | (1, 0, style_scores['hidden']), ('hidden', 0, TRANSPARENT)) |
1056 | 1058 | grid_y = 0 |
1141 | 1143 | x=0, y=grid_height, w=grid_width)) |
1142 | 1144 | # "UAs must compute an initial left and right border width for the table |
1143 | 1145 | # 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 | |
1145 | 1147 | # ... so h=1, not grid_height: |
1146 | 1148 | set_transparent_border(table, 'left', max_vertical_width( |
1147 | 1149 | x=0, y=0, h=1)) |
1156 | 1158 | |
1157 | 1159 | Take and return a ``Box`` object. |
1158 | 1160 | |
1159 | See http://www.w3.org/TR/css-flexbox-1/#flex-items | |
1161 | See https://www.w3.org/TR/css-flexbox-1/#flex-items | |
1160 | 1162 | |
1161 | 1163 | """ |
1162 | 1164 | if not isinstance(box, boxes.ParentBox) or box.is_running(): |
1193 | 1195 | def process_whitespace(box, following_collapsible_space=False): |
1194 | 1196 | """First part of "The 'white-space' processing model". |
1195 | 1197 | |
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 | |
1198 | 1200 | |
1199 | 1201 | """ |
1200 | 1202 | if isinstance(box, boxes.TextBox): |
1217 | 1219 | # TODO: this should be language-specific |
1218 | 1220 | # Could also replace with a zero width space character (U+200B), |
1219 | 1221 | # 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 | |
1221 | 1223 | text = text.replace('\n', ' ') |
1222 | 1224 | |
1223 | 1225 | if space_collapse: |
1231 | 1233 | |
1232 | 1234 | box.text = text |
1233 | 1235 | |
1234 | elif isinstance(box, boxes.ParentBox) and not box.is_running(): | |
1236 | elif isinstance(box, boxes.ParentBox): | |
1235 | 1237 | for child in box.children: |
1236 | 1238 | if isinstance(child, (boxes.TextBox, boxes.InlineBox)): |
1237 | following_collapsible_space = process_whitespace( | |
1239 | child_collapsible_space = process_whitespace( | |
1238 | 1240 | 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() | |
1245 | 1247 | |
1246 | 1248 | |
1247 | 1249 | def process_text_transform(box): |
1251 | 1253 | box.text = { |
1252 | 1254 | 'uppercase': lambda text: text.upper(), |
1253 | 1255 | 'lowercase': lambda text: text.lower(), |
1254 | # Python’s unicode.captitalize is not the same. | |
1255 | 'capitalize': lambda text: text.title(), | |
1256 | 'capitalize': capitalize, | |
1256 | 1257 | 'full-width': lambda text: text.translate(ASCII_TO_WIDE), |
1257 | 1258 | }[text_transform](box.text) |
1258 | 1259 | if box.style['hyphens'] == 'none': |
1264 | 1265 | process_text_transform(child) |
1265 | 1266 | |
1266 | 1267 | |
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 | ||
1267 | 1283 | def inline_in_block(box): |
1268 | 1284 | """Build the structure of lines inside blocks and return a new box tree. |
1269 | 1285 | |
1273 | 1289 | This line box will be broken into multiple lines later. |
1274 | 1290 | |
1275 | 1291 | 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 | |
1277 | 1293 | |
1278 | 1294 | Eg.:: |
1279 | 1295 | |
1384 | 1400 | in an anonymous block-level box. |
1385 | 1401 | |
1386 | 1402 | 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 | |
1388 | 1404 | |
1389 | 1405 | Eg. if this is given:: |
1390 | 1406 | |
1538 | 1554 | Like backgrounds, ``overflow`` on the root element must be propagated |
1539 | 1555 | to the viewport. |
1540 | 1556 | |
1541 | See http://www.w3.org/TR/CSS21/visufx.html#overflow | |
1557 | See https://www.w3.org/TR/CSS21/visufx.html#overflow | |
1542 | 1558 | """ |
1543 | 1559 | chosen_box = root_box |
1544 | 1560 | if (root_box.element_tag.lower() == 'html' and |
24 | 24 | string=HTML5_UA, counter_style=HTML5_UA_COUNTER_STYLE) |
25 | 25 | HTML5_PH_STYLESHEET = CSS(string=HTML5_PH) |
26 | 26 | |
27 | # http://whatwg.org/C#space-character | |
27 | # https://html.spec.whatwg.org/multipage/#space-character | |
28 | 28 | HTML_WHITESPACE = ' \t\n\f\r' |
29 | 29 | HTML_SPACE_SEPARATED_TOKENS_RE = re.compile(f'[^{HTML_WHITESPACE}]+') |
30 | 30 | |
36 | 36 | :returns: A new Unicode string. |
37 | 37 | |
38 | 38 | 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. | |
40 | 40 | |
41 | 41 | This is different from the :meth:`str.lower` method of Unicode strings |
42 | 42 | which also affect non-ASCII characters, |
43 | 43 | sometimes mapping them into the ASCII range: |
44 | 44 | |
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' | |
47 | 47 | >>> 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' | |
49 | 49 | |
50 | 50 | """ |
51 | 51 | # This turns out to be faster than unicode.translate() |
113 | 113 | |
114 | 114 | Return either an image or the alt-text. |
115 | 115 | |
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 | |
117 | 117 | |
118 | 118 | """ |
119 | 119 | src = get_url_attribute(element, 'src', base_url) |
120 | 120 | alt = element.get('alt') |
121 | 121 | if src: |
122 | image = get_image_from_uri(url=src) | |
122 | image = get_image_from_uri( | |
123 | url=src, orientation=box.style['image_orientation']) | |
123 | 124 | if image is not None: |
124 | 125 | return [make_replaced_box(element, box, image)] |
125 | 126 | else: |
153 | 154 | src = get_url_attribute(element, 'src', base_url) |
154 | 155 | type_ = element.get('type', '').strip() |
155 | 156 | 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']) | |
157 | 160 | if image is not None: |
158 | 161 | return [make_replaced_box(element, box, image)] |
159 | 162 | # No fallback. |
170 | 173 | data = get_url_attribute(element, 'data', base_url) |
171 | 174 | type_ = element.get('type', '').strip() |
172 | 175 | 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']) | |
174 | 179 | if image is not None: |
175 | 180 | return [make_replaced_box(element, box, image)] |
176 | 181 | # The element’s children are the fallback. |
193 | 198 | """Handle the ``span`` attribute.""" |
194 | 199 | if isinstance(box, boxes.TableColumnBox) and box.span > 1: |
195 | 200 | # 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 | |
197 | 202 | return [box.copy() for _i in range(box.span)] |
198 | 203 | return [box] |
199 | 204 | |
204 | 209 | """Handle the ``colspan``, ``rowspan`` attributes.""" |
205 | 210 | if isinstance(box, boxes.TableCellBox): |
206 | 211 | # 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 | |
208 | 213 | # 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 | |
210 | 215 | # rowspan=0 is still there though. |
211 | 216 | try: |
212 | 217 | box.colspan = max(int(element.get('colspan', '').strip()), 1) |
223 | 228 | def handle_a(element, box, _get_image_from_uri, base_url): |
224 | 229 | """Handle the ``rel`` attribute.""" |
225 | 230 | box.is_attachment = element_has_link_type(element, 'attachment') |
226 | box.download_name = element.get('download') | |
227 | 231 | return [box] |
228 | 232 | |
229 | 233 | |
251 | 255 | |
252 | 256 | Relevant specs: |
253 | 257 | |
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 | |
258 | 262 | |
259 | 263 | """ |
260 | 264 | title = None |
266 | 270 | modified = None |
267 | 271 | attachments = [] |
268 | 272 | custom = {} |
273 | lang = html.etree_element.attrib.get('lang', None) | |
269 | 274 | for element in html.wrapper_element.query_all('title', 'meta', 'link'): |
270 | 275 | element = element.etree_element |
271 | 276 | if element.tag == 'title' and title is None: |
304 | 309 | return dict(title=title, description=description, generator=generator, |
305 | 310 | keywords=keywords, authors=authors, |
306 | 311 | created=created, modified=modified, |
307 | attachments=attachments, custom=custom) | |
312 | attachments=attachments, lang=lang, custom=custom) | |
308 | 313 | |
309 | 314 | |
310 | 315 | def strip_whitespace(string): |
311 | 316 | """Use the HTML definition of "space character", |
312 | 317 | not all Unicode Whitespace. |
313 | 318 | |
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 | |
316 | 321 | |
317 | 322 | """ |
318 | 323 | return string.strip(HTML_WHITESPACE) |
6 | 6 | from math import inf |
7 | 7 | from xml.etree import ElementTree |
8 | 8 | |
9 | from PIL import Image | |
9 | from PIL import Image, ImageFile, ImageOps | |
10 | 10 | |
11 | 11 | from .layout.percent import percentage |
12 | 12 | from .logger import LOGGER |
13 | 13 | from .svg import SVG |
14 | 14 | from .urls import URLFetchingError, fetch |
15 | ||
16 | # Don’t crash when converting truncated images | |
17 | ImageFile.LOAD_TRUNCATED_IMAGES = True | |
15 | 18 | |
16 | 19 | |
17 | 20 | class ImageLoadingError(ValueError): |
88 | 91 | |
89 | 92 | |
90 | 93 | 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'): | |
92 | 96 | """Get an Image instance from an image URI.""" |
93 | 97 | if url in cache: |
94 | 98 | return cache[url] |
130 | 134 | else: |
131 | 135 | # Store image id to enable cache in Stream.add_image |
132 | 136 | 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) | |
133 | 150 | image = RasterImage(pillow_image, image_id, optimize_size) |
134 | 151 | |
135 | 152 | except (URLFetchingError, ImageLoadingError) as exception: |
148 | 165 | ``positions`` is a list of ``None``, or ``Dimension`` in px or %. 0 is the |
149 | 166 | starting point, 1 the ending point. |
150 | 167 | |
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. | |
152 | 169 | |
153 | 170 | Return processed color stops, as a list of floats in px. |
154 | 171 | |
205 | 222 | |
206 | 223 | def gradient_average_color(colors, positions): |
207 | 224 | """ |
208 | http://dev.w3.org/csswg/css-images-3/#gradient-average-color | |
225 | https://drafts.csswg.org/css-images-3/#gradient-average-color | |
209 | 226 | """ |
210 | 227 | nb_stops = len(positions) |
211 | 228 | assert nb_stops > 1 |
5 | 5 | Boxes in the new tree have *used values* in their ``position_x``, |
6 | 6 | ``position_y``, ``width`` and ``height`` attributes, amongst others. |
7 | 7 | |
8 | See http://www.w3.org/TR/CSS21/cascade.html#used-value | |
8 | See https://www.w3.org/TR/CSS21/cascade.html#used-value | |
9 | 9 | |
10 | 10 | """ |
11 | 11 | |
33 | 33 | page_break = root_box.style['break_before'] |
34 | 34 | |
35 | 35 | # 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 | |
37 | 37 | if page_break == 'right': |
38 | 38 | right_page = True |
39 | 39 | elif page_break == 'left': |
232 | 232 | self.running_elements = defaultdict(lambda: defaultdict(lambda: [])) |
233 | 233 | self.current_page = None |
234 | 234 | self.forced_break = False |
235 | self.broken_out_of_flow = [] | |
235 | self.broken_out_of_flow = {} | |
236 | 236 | self.in_column = False |
237 | 237 | |
238 | 238 | # Cache |
251 | 251 | self._excluded_shapes_lists.append(self.excluded_shapes) |
252 | 252 | |
253 | 253 | 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 | |
255 | 255 | if root_box.style['height'] == 'auto' and self.excluded_shapes: |
256 | 256 | box_bottom = root_box.content_box_y() + root_box.height |
257 | 257 | max_shape_bottom = max([ |
284 | 284 | 4: ['Third Header', '3.5th Header']} |
285 | 285 | |
286 | 286 | Value depends on current page. |
287 | http://dev.w3.org/csswg/css-gcpm/#funcdef-string | |
287 | https://drafts.csswg.org/css-gcpm/#funcdef-string | |
288 | 288 | |
289 | 289 | :param store: dictionary where the resolved value is stored. |
290 | 290 | :param page: current page. |
330 | 330 | |
331 | 331 | def unlayout_footnote(self, footnote): |
332 | 332 | """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 | |
336 | 335 | if footnote not in self.footnotes: |
337 | 336 | self.footnotes.append(footnote) |
338 | 337 | if footnote in self.current_page_footnotes: |
349 | 348 | |
350 | 349 | def _update_footnote_area(self): |
351 | 350 | """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: | |
353 | 352 | self.page_bottom += self.current_footnote_area.margin_height() |
354 | 353 | self.current_footnote_area.children = self.current_page_footnotes |
355 | 354 | if self.current_footnote_area.children: |
356 | 355 | footnote_area = build.create_anonymous_boxes( |
357 | 356 | self.current_footnote_area.deepcopy()) |
358 | footnote_area, _, _, _, _, _ = block_level_layout( | |
357 | footnote_area = block_level_layout( | |
359 | 358 | self, footnote_area, -inf, None, |
360 | self.current_footnote_area.page, True, [], [], [], False, None) | |
359 | self.current_footnote_area.page)[0] | |
361 | 360 | 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() | |
363 | 363 | last_child = footnote_area.children[-1] |
364 | 364 | overflow = ( |
365 | 365 | 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) | |
367 | 368 | return overflow |
368 | 369 | else: |
369 | 370 | self.current_footnote_area.height = 0 |
20 | 20 | object.__setattr__(self, '_layout_done', True) |
21 | 21 | |
22 | 22 | def translate(self, dx=0, dy=0, ignore_floats=False): |
23 | if dx == 0 and dy == 0: | |
23 | if dx == dy == 0: | |
24 | 24 | return |
25 | 25 | if self._layout_done: |
26 | 26 | self._box.translate(dx, dy, ignore_floats) |
47 | 47 | |
48 | 48 | @handle_min_max_width |
49 | 49 | 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 | |
51 | 51 | ltr = ( |
52 | 52 | box.style.parent_style is None or |
53 | 53 | box.style.parent_style['direction'] == 'ltr') |
121 | 121 | |
122 | 122 | |
123 | 123 | 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 | |
125 | 125 | paddings_borders = ( |
126 | 126 | box.padding_top + box.padding_bottom + |
127 | 127 | box.border_top_width + box.border_bottom_width) |
228 | 228 | context, box, containing_block, fixed_boxes, bottom_space, skip_stack) |
229 | 229 | placeholder.set_laid_out_box(new_box) |
230 | 230 | 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) | |
232 | 233 | |
233 | 234 | |
234 | 235 | def absolute_box_layout(context, box, containing_block, fixed_boxes, |
235 | 236 | bottom_space, skip_stack): |
236 | 237 | # 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 | |
238 | 239 | if isinstance(containing_block, boxes.PageBox): |
239 | 240 | cb_x = containing_block.content_box_x() |
240 | 241 | cb_y = containing_block.content_box_y() |
270 | 271 | box.style.parent_style is None or |
271 | 272 | box.style.parent_style['direction'] == 'ltr') |
272 | 273 | |
273 | # http://www.w3.org/TR/CSS21/visudet.html#abs-replaced-width | |
274 | # https://www.w3.org/TR/CSS21/visudet.html#abs-replaced-width | |
274 | 275 | if box.left == box.right == 'auto': |
275 | 276 | # static position: |
276 | 277 | if ltr: |
306 | 307 | else: |
307 | 308 | box.left = cb_width - (box.margin_width() + box.right) |
308 | 309 | |
309 | # http://www.w3.org/TR/CSS21/visudet.html#abs-replaced-height | |
310 | # https://www.w3.org/TR/CSS21/visudet.html#abs-replaced-height | |
310 | 311 | if box.top == box.bottom == 'auto': |
311 | 312 | box.top = box.position_y - cb_y |
312 | 313 | if 'auto' in (box.top, box.bottom): |
50 | 50 | images = [] |
51 | 51 | color = parse_color('transparent') |
52 | 52 | else: |
53 | orientation = style['image_orientation'] | |
53 | 54 | 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 | |
55 | 57 | for type_, value in style['background_image']] |
56 | 58 | color = get_color(style, 'background_color') |
57 | 59 |
14 | 14 | |
15 | 15 | |
16 | 16 | 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): | |
19 | 20 | """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 | ||
20 | 25 | if not isinstance(box, boxes.TableBox): |
21 | 26 | resolve_percentages(box, containing_block) |
22 | 27 | |
26 | 31 | box.margin_bottom = 0 |
27 | 32 | |
28 | 33 | 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. | |
29 | 36 | # TODO: this condition is wrong, it only works for blocks whose |
30 | 37 | # parent breaks collapsing margins. It should work for blocks whose |
31 | 38 | # one of the ancestors breaks collapsing margins. |
96 | 103 | new_box.margin_bottom + new_box.padding_bottom + |
97 | 104 | new_box.border_bottom_width) |
98 | 105 | if columns_bottom_space: |
106 | remove_placeholders( | |
107 | context, [new_box], absolute_boxes, fixed_boxes) | |
99 | 108 | bottom_space += columns_bottom_space |
100 | 109 | result = columns_layout( |
101 | 110 | context, box, bottom_space, skip_stack, |
113 | 122 | new_box = result[0] |
114 | 123 | if new_box and new_box.is_table_wrapper: |
115 | 124 | # 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 | |
117 | 126 | position_x, position_y, _ = avoid_collisions( |
118 | 127 | context, new_box, containing_block, outer=False) |
119 | 128 | new_box.translate( |
133 | 142 | # TODO: what is the real text direction? |
134 | 143 | direction = 'ltr' |
135 | 144 | |
136 | # http://www.w3.org/TR/CSS21/visudet.html#blockwidth | |
145 | # https://www.w3.org/TR/CSS21/visudet.html#blockwidth | |
137 | 146 | |
138 | 147 | # These names are waaay too long |
139 | 148 | margin_l = box.margin_left |
175 | 184 | width = box.width = cb_width - ( |
176 | 185 | paddings_plus_borders + margin_l + margin_r) |
177 | 186 | margin_sum = cb_width - paddings_plus_borders - width |
178 | if margin_l == 'auto' and margin_r == 'auto': | |
187 | if margin_l == margin_r == 'auto': | |
179 | 188 | box.margin_left = margin_sum / 2 |
180 | 189 | box.margin_right = margin_sum / 2 |
181 | 190 | elif margin_l == 'auto' and margin_r != 'auto': |
220 | 229 | adjoining_margins, bottom_space): |
221 | 230 | stop = False |
222 | 231 | resume_at = None |
232 | new_child = None | |
223 | 233 | out_of_flow_resume_at = None |
224 | 234 | |
225 | 235 | child.position_y += collapse_margin(adjoining_margins) |
226 | 236 | if child.is_absolutely_positioned(): |
227 | placeholder = AbsolutePlaceholder(child) | |
237 | new_child = placeholder = AbsolutePlaceholder(child) | |
228 | 238 | placeholder.index = index |
229 | 239 | new_children.append(placeholder) |
230 | 240 | if child.style['position'] == 'absolute': |
256 | 266 | page = context.current_page |
257 | 267 | context.running_elements[running_name][page].append(child) |
258 | 268 | |
259 | return stop, resume_at, out_of_flow_resume_at | |
269 | return stop, resume_at, new_child, out_of_flow_resume_at | |
260 | 270 | |
261 | 271 | |
262 | 272 | def _break_line(context, box, line, new_children, lines_iterator, |
339 | 349 | break |
340 | 350 | |
341 | 351 | # 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 | |
343 | 353 | # "When an unforced page break occurs here, both the adjoining |
344 | 354 | # ‘margin-top’ and ‘margin-bottom’ are set to zero." |
345 | 355 | # See https://github.com/Kozea/WeasyPrint/issues/115 |
422 | 432 | |
423 | 433 | if not new_containing_block.is_table_wrapper: |
424 | 434 | 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: | |
427 | 436 | # TODO: add the adjoining descendants' margin top to |
428 | 437 | # [child.margin_top] |
429 | 438 | 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': | |
431 | 443 | 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 | |
434 | 450 | new_collapsed_margin = collapse_margin( |
435 | 451 | adjoining_margins + [child_margin_top]) |
436 | 452 | collapsed_margin_difference = ( |
624 | 640 | new_children = [] |
625 | 641 | next_page = {'break': 'any', 'page': None} |
626 | 642 | all_footnotes = [] |
627 | broken_out_of_flow = [] | |
643 | broken_out_of_flow = {} | |
628 | 644 | |
629 | 645 | last_in_flow_child = None |
630 | 646 | |
644 | 660 | |
645 | 661 | if not child.is_in_normal_flow(): |
646 | 662 | 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)) | |
651 | 668 | 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) | |
653 | 671 | |
654 | 672 | elif isinstance(child, boxes.LineBox): |
655 | 673 | (abort, stop, resume_at, position_y, |
691 | 709 | |
692 | 710 | if abort: |
693 | 711 | page = child.page_values()[0] |
712 | remove_placeholders( | |
713 | context, box.children[skip:], absolute_boxes, fixed_boxes) | |
694 | 714 | for footnote in new_footnotes: |
695 | 715 | context.unlayout_footnote(footnote) |
696 | 716 | return ( |
715 | 735 | return ( |
716 | 736 | None, None, {'break': 'any', 'page': None}, [], False, max_lines) |
717 | 737 | |
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 | |
719 | 740 | |
720 | 741 | if collapsing_with_children: |
721 | 742 | box.position_y += ( |
728 | 749 | # Top and bottom margins of this box |
729 | 750 | if (box.height in ('auto', 0) and |
730 | 751 | get_clearance(context, box, collapsed_margin) is None and |
731 | all(value == 0 for value in [ | |
752 | all(value == 0 for value in ( | |
732 | 753 | 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))): | |
734 | 755 | collapsing_through = True |
735 | 756 | else: |
736 | 757 | position_y += collapsed_margin |
760 | 781 | start=not is_start, end=box_is_fragmented and not discard) |
761 | 782 | |
762 | 783 | # 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 | |
764 | 785 | # TODO: See float.float_layout |
765 | 786 | if new_box.height == 'auto': |
766 | 787 | if context.excluded_shapes and new_box.style['overflow'] != 'visible': |
791 | 812 | elif bottom_space > -inf and not new_box.is_column: |
792 | 813 | # Make the box fill the blank space at the bottom of the page |
793 | 814 | # https://www.w3.org/TR/css-break-3/#box-splitting |
794 | new_box.height = ( | |
815 | new_box_height = ( | |
795 | 816 | context.page_bottom - bottom_space - new_box.position_y - |
796 | 817 | (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) | |
801 | 824 | |
802 | 825 | if next_page['page'] is None: |
803 | 826 | next_page['page'] = new_box.page_values()[1] |
819 | 842 | def establishes_formatting_context(box): |
820 | 843 | """Return whether a box establishes a block formatting context. |
821 | 844 | |
822 | See http://www.w3.org/TR/CSS2/visuren.html#block-formatting | |
845 | See https://www.w3.org/TR/CSS2/visuren.html#block-formatting | |
823 | 846 | |
824 | 847 | """ |
825 | 848 | return ( |
1000 | 1023 | For boxes that have been removed in find_earlier_page_break(), remove the |
1001 | 1024 | matching placeholders in absolute_boxes and fixed_boxes. |
1002 | 1025 | |
1026 | Also takes care of removed footnotes and floats. | |
1027 | ||
1003 | 1028 | """ |
1004 | 1029 | for box in box_list: |
1005 | 1030 | if isinstance(box, boxes.ParentBox): |
1012 | 1037 | fixed_boxes.remove(box) |
1013 | 1038 | if box.footnote: |
1014 | 1039 | context.unlayout_footnote(box.footnote) |
1040 | if box in context.broken_out_of_flow: | |
1041 | context.broken_out_of_flow.pop(box) | |
1015 | 1042 | |
1016 | 1043 | |
1017 | 1044 | def avoid_page_break(page_break, context): |
11 | 11 | """Lay out a multi-column ``box``.""" |
12 | 12 | from .block import ( |
13 | 13 | 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 | ||
19 | 16 | style = box.style |
17 | width = style['column_width'] | |
18 | count = style['column_count'] | |
19 | gap = style['column_gap'] | |
20 | height = style['height'] | |
20 | 21 | original_bottom_space = bottom_space |
21 | 22 | context.in_column = True |
22 | 23 | |
23 | if box.style['position'] == 'relative': | |
24 | if style['position'] == 'relative': | |
24 | 25 | # New containing block, use a new absolute list |
25 | 26 | absolute_boxes = [] |
26 | 27 | |
27 | 28 | box = box.copy_with_children(box.children) |
28 | 29 | box.position_y += collapse_margin(adjoining_margins) - box.margin_top |
29 | 30 | |
30 | height = box.style['height'] | |
31 | # Set height if defined | |
31 | 32 | if height != 'auto' and height.unit != '%': |
32 | 33 | 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) | |
37 | 37 | 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 | |
42 | 42 | 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: | |
75 | 55 | # columns_and_blocks = [ |
76 | 56 | # [column_child_1, column_child_2], |
77 | 57 | # spanning_block, |
79 | 59 | # ] |
80 | 60 | columns_and_blocks = [] |
81 | 61 | 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): | |
89 | 64 | if child.style['column_span'] == 'all': |
90 | 65 | if column_children: |
91 | 66 | 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())) | |
94 | 69 | column_children = [] |
95 | 70 | continue |
96 | 71 | column_children.append(child.copy()) |
97 | 72 | if column_children: |
98 | 73 | columns_and_blocks.append( |
99 | (index + 1 - len(column_children), column_children)) | |
74 | (i + 1 - len(column_children), column_children)) | |
100 | 75 | |
101 | 76 | if skip_stack: |
102 | 77 | skip_stack = {0: skip_stack[skip]} |
105 | 80 | next_page = {'break': 'any', 'page': None} |
106 | 81 | skip_stack = None |
107 | 82 | |
108 | # Balance. | |
83 | # Find height and balance. | |
109 | 84 | # |
110 | 85 | # The current algorithm starts from the total available height, to check |
111 | 86 | # whether the whole content can fit. If it doesn’t fit, we keep the partial |
122 | 97 | current_position_y = box.content_box_y() |
123 | 98 | new_children = [] |
124 | 99 | column_skip_stack = None |
125 | forced_end_probing = False | |
100 | last_loop = False | |
126 | 101 | 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 | |
127 | 106 | for index, column_children_or_block in columns_and_blocks: |
128 | 107 | 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 | |
130 | 109 | block = column_children_or_block |
131 | 110 | resolve_percentages(block, containing_block) |
132 | 111 | block.position_x = box.content_box_x() |
135 | 114 | block_level_layout( |
136 | 115 | context, block, original_bottom_space, skip_stack, |
137 | 116 | containing_block, page_is_empty, absolute_boxes, |
138 | fixed_boxes, adjoining_margins, discard=False, | |
139 | max_lines=None)) | |
117 | fixed_boxes, adjoining_margins)) | |
140 | 118 | skip_stack = None |
141 | 119 | if new_child is None: |
142 | forced_end_probing = True | |
120 | last_loop = True | |
143 | 121 | break_page = True |
144 | 122 | break |
145 | 123 | new_children.append(new_child) |
147 | 125 | new_child.border_height() + new_child.border_box_y()) |
148 | 126 | adjoining_margins.append(new_child.margin_bottom) |
149 | 127 | if resume_at: |
150 | forced_end_probing = True | |
128 | last_loop = True | |
151 | 129 | break_page = True |
152 | 130 | column_skip_stack = resume_at |
153 | 131 | break |
154 | 132 | page_is_empty = False |
155 | 133 | continue |
156 | 134 | |
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 | |
160 | 136 | column_children = column_children_or_block |
161 | 137 | |
162 | # Find the total height available for the first run. | |
138 | # Find the total height available for the first run | |
163 | 139 | current_position_y += collapse_margin(adjoining_margins) |
164 | 140 | 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) | |
169 | 145 | |
170 | 146 | # Try to render columns until the content fits, increase the column |
171 | # height step by step. | |
147 | # height step by step | |
172 | 148 | column_skip_stack = skip_stack |
173 | 149 | lost_space = inf |
174 | first_probe_run = True | |
150 | original_excluded_shapes = context.excluded_shapes[:] | |
175 | 151 | original_page_is_empty = page_is_empty |
176 | page_is_empty = False | |
177 | stop_rendering = False | |
152 | page_is_empty = stop_rendering = balancing = False | |
178 | 153 | 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 | |
179 | 159 | 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 | ||
187 | 160 | consumed_heights = [] |
161 | new_boxes = [] | |
188 | 162 | for i in range(count): |
189 | # Render the column | |
163 | # Render one column | |
190 | 164 | new_box, resume_at, next_page, _, _, _ = block_box_layout( |
191 | 165 | context, column_box, |
192 | 166 | context.page_bottom - current_position_y - height, |
193 | 167 | column_skip_stack, containing_block, |
194 | page_is_empty or first_probe_run, [], [], [], | |
168 | page_is_empty or not balancing, [], [], [], | |
195 | 169 | discard=False, max_lines=None) |
196 | 170 | if new_box is None: |
197 | # We didn't render anything, retry. | |
171 | # We didn't render anything, retry | |
198 | 172 | column_skip_stack = {0: None} |
199 | 173 | break |
174 | new_boxes.append(new_box) | |
200 | 175 | column_skip_stack = resume_at |
201 | 176 | |
177 | # Calculate consumed height, empty space and next box height | |
202 | 178 | in_flow_children = [ |
203 | 179 | child for child in new_box.children |
204 | 180 | if child.is_in_normal_flow()] |
205 | ||
206 | 181 | if in_flow_children: |
207 | 182 | # Get the empty space at the bottom of the column box |
208 | 183 | consumed_height = ( |
211 | 186 | empty_space = height - consumed_height |
212 | 187 | |
213 | 188 | # 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 | |
223 | 201 | else: |
224 | consumed_height = empty_space = next_box_size = 0 | |
202 | consumed_height = empty_space = next_box_height = 0 | |
225 | 203 | |
226 | 204 | consumed_heights.append(consumed_height) |
227 | 205 | |
237 | 215 | # introduced by rounding errors. As the workaround below at |
238 | 216 | # least adds 1 pixel for each loop, we can ignore lost spaces |
239 | 217 | # 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) | |
242 | 220 | |
243 | 221 | # Stop if we already rendered the whole content |
244 | 222 | if resume_at is None: |
245 | 223 | break |
246 | 224 | |
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: | |
264 | 235 | if column_skip_stack is None: |
265 | 236 | # We rendered the whole content, stop |
266 | 237 | 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 | |
267 | 268 | 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 | |
286 | 280 | bottom_space = max( |
287 | 281 | bottom_space, context.page_bottom - current_position_y - height) |
288 | 282 | |
289 | # Replace the current box children with columns | |
283 | # Replace the current box children with real columns | |
290 | 284 | i = 0 |
291 | 285 | max_column_height = 0 |
292 | 286 | columns = [] |
293 | 287 | 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) | |
298 | 291 | 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 | |
301 | 293 | else: |
302 | column_box.position_x += i * (width + style['column_gap']) | |
294 | column_box.position_x += i * (width + gap) | |
303 | 295 | new_child, column_skip_stack, column_next_page, _, _, _ = ( |
304 | 296 | block_box_layout( |
305 | 297 | context, column_box, bottom_space, skip_stack, |
317 | 309 | bottom_space = original_bottom_space |
318 | 310 | break |
319 | 311 | i += 1 |
320 | if i == count and not known_height: | |
312 | if i == count and not height_defined: | |
321 | 313 | # [If] a declaration that constrains the column height |
322 | 314 | # (e.g., using height or max-height). In this case, |
323 | 315 | # additional column boxes are created in the inline |
324 | 316 | # direction. |
325 | 317 | break |
326 | 318 | |
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) | |
328 | 321 | for column in columns: |
329 | 322 | column.height = max_column_height |
330 | 323 | new_children.append(column) |
334 | 327 | |
335 | 328 | if stop_rendering: |
336 | 329 | break |
330 | ||
331 | # Report footnotes above the defined footnotes height | |
332 | _report_footnotes(context, last_footnotes_height) | |
337 | 333 | |
338 | 334 | if box.children and not new_children: |
339 | 335 | # The box has children but none can be drawn, let's skip the whole box |
340 | 336 | context.in_column = False |
341 | 337 | return None, (0, None), {'break': 'any', 'page': None}, [], False |
342 | 338 | |
343 | # Set the height of box and the columns | |
339 | # Set the height of the containing box | |
344 | 340 | box.children = new_children |
345 | 341 | current_position_y += collapse_margin(adjoining_margins) |
346 | 342 | height = current_position_y - box.content_box_y() |
349 | 345 | height_difference = 0 |
350 | 346 | else: |
351 | 347 | height_difference = box.height - height |
348 | ||
349 | # Update the latest columns’ height to respect min-height | |
352 | 350 | if box.min_height != 'auto' and box.min_height > box.height: |
353 | 351 | height_difference += box.min_height - box.height |
354 | 352 | box.height = box.min_height |
358 | 356 | else: |
359 | 357 | break |
360 | 358 | |
361 | if box.style['position'] == 'relative': | |
359 | if style['position'] == 'relative': | |
362 | 360 | # New containing block, resolve the layout of the absolute descendants |
363 | 361 | for absolute_box in absolute_boxes: |
364 | 362 | absolute_layout( |
365 | 363 | context, absolute_box, box, fixed_boxes, bottom_space, |
366 | 364 | skip_stack=None) |
367 | 365 | |
366 | # Calculate skip stack | |
368 | 367 | if column_skip_stack: |
369 | 368 | skip, = column_skip_stack.keys() |
370 | 369 | skip_stack = {index + skip: column_skip_stack[skip]} |
371 | 370 | elif break_page: |
372 | 371 | 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 | ||
373 | 378 | context.in_column = False |
374 | 379 | 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 |
146 | 146 | new_child.style['max_height'] = Dimension(inf, 'px') |
147 | 147 | new_child = block.block_level_layout( |
148 | 148 | context, new_child, -inf, child_skip_stack, parent_box, |
149 | page_is_empty, [], [], [], False, None)[0] | |
149 | page_is_empty)[0] | |
150 | 150 | content_size = new_child.height |
151 | 151 | child.min_height = min(specified_size, content_size) |
152 | 152 | |
205 | 205 | new_child.width = inf |
206 | 206 | new_child = block.block_level_layout( |
207 | 207 | 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] | |
210 | 209 | child.flex_base_size = new_child.margin_height() |
211 | 210 | elif child.style[axis] == 'min-content': |
212 | 211 | child.style[axis] = 'auto' |
220 | 219 | new_child.width = 0 |
221 | 220 | new_child = block.block_level_layout( |
222 | 221 | 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] | |
225 | 223 | child.flex_base_size = new_child.margin_height() |
226 | 224 | else: |
227 | 225 | assert child.style[axis].unit == 'px' |
678 | 676 | box.content_box_y() if cross == 'height' |
679 | 677 | else box.content_box_x()) |
680 | 678 | for line in flex_lines: |
681 | line.lower_baseline = 0 | |
679 | line.lower_baseline = -inf | |
682 | 680 | # TODO: don't duplicate this loop |
683 | 681 | for i, child in line: |
684 | 682 | align_self = child.style['align_self'] |
688 | 686 | # TODO: handle vertical text |
689 | 687 | child.baseline = child._baseline - position_cross |
690 | 688 | 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 | |
691 | 691 | for i, child in line: |
692 | 692 | cross_margins = ( |
693 | 693 | (child.margin_top, child.margin_bottom) if cross == 'height' |
25 | 25 | resolve_percentages(box, (cb_width, cb_height)) |
26 | 26 | |
27 | 27 | # 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 | |
29 | 29 | if cb_height == 'auto': |
30 | 30 | cb_height = ( |
31 | 31 | containing_block.position_y - containing_block.content_box_y()) |
80 | 80 | |
81 | 81 | def find_float_position(context, box, containing_block): |
82 | 82 | """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 | |
84 | 84 | |
85 | 85 | # Point 4 is already handled as box.position_y is set according to the |
86 | 86 | # containing box top position, with collapsing margins handled |
168 | 168 | fixed_boxes, bottom_space, skip_stack=None) |
169 | 169 | float_children.append(new_waiting_float) |
170 | 170 | 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) | |
173 | 173 | if float_children: |
174 | 174 | line.children += tuple(float_children) |
175 | 175 | |
179 | 179 | def skip_first_whitespace(box, skip_stack): |
180 | 180 | """Return ``skip_stack`` to start just after removable leading spaces. |
181 | 181 | |
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 | |
183 | 183 | |
184 | 184 | """ |
185 | 185 | if skip_stack is None: |
381 | 381 | |
382 | 382 | resolve_percentages(box, containing_block) |
383 | 383 | |
384 | # http://www.w3.org/TR/CSS21/visudet.html#inlineblock-width | |
384 | # https://www.w3.org/TR/CSS21/visudet.html#inlineblock-width | |
385 | 385 | if box.margin_left == 'auto': |
386 | 386 | box.margin_left = 0 |
387 | 387 | if box.margin_right == 'auto': |
388 | 388 | 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 | |
390 | 390 | if box.margin_top == 'auto': |
391 | 391 | box.margin_top = 0 |
392 | 392 | if box.margin_bottom == 'auto': |
410 | 410 | |
411 | 411 | Position is taken from the top of its margin box. |
412 | 412 | |
413 | http://www.w3.org/TR/CSS21/visudet.html#propdef-vertical-align | |
413 | https://www.w3.org/TR/CSS21/visudet.html#propdef-vertical-align | |
414 | 414 | |
415 | 415 | """ |
416 | 416 | if box.is_table_wrapper: |
505 | 505 | elif isinstance(box, boxes.InlineFlexBox): |
506 | 506 | box.position_x = position_x |
507 | 507 | box.position_y = 0 |
508 | for side in ['top', 'right', 'bottom', 'left']: | |
508 | for side in ('top', 'right', 'bottom', 'left'): | |
509 | 509 | if getattr(box, f'margin_{side}') == 'auto': |
510 | 510 | setattr(box, f'margin_{side}', 0) |
511 | 511 | new_box, resume_at, _, _, _ = flex_layout( |
558 | 558 | context, child, containing_block, absolute_boxes, fixed_boxes, |
559 | 559 | bottom_space, skip_stack=None) |
560 | 560 | 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) | |
563 | 563 | waiting_children.append((index, new_child, child)) |
564 | 564 | child = new_child |
565 | 565 | |
896 | 896 | box.pango_layout = layout |
897 | 897 | # "The height of the content area should be based on the font, |
898 | 898 | # 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 | |
900 | 900 | # We trust Pango and use the height of the LayoutLine. |
901 | 901 | box.height = height |
902 | 902 | # "only the 'line-height' is used when calculating the height |
919 | 919 | preserved_line_break = ( |
920 | 920 | (length != resume_index) and between.strip(' ')) |
921 | 921 | if preserved_line_break: |
922 | # See http://unicode.org/reports/tr14/ | |
922 | # See https://unicode.org/reports/tr14/ | |
923 | 923 | # \r is already handled by process_whitespace |
924 | 924 | line_breaks = ('\n', '\t', '\f', '\u0085', '\u2028', '\u2029') |
925 | 925 | assert between in line_breaks, ( |
1158 | 1158 | |
1159 | 1159 | |
1160 | 1160 | 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 | |
1162 | 1162 | for child in linebox.children: |
1163 | 1163 | if isinstance(child, boxes.InlineBox): |
1164 | 1164 | if not is_phantom_linebox(child): |
101 | 101 | |
102 | 102 | |
103 | 103 | 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 | |
107 | 107 | |
108 | 108 | :param box: |
109 | 109 | The margin box to work on |
122 | 122 | |
123 | 123 | # Rule 2 |
124 | 124 | 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) | |
126 | 126 | if value != 'auto') |
127 | 127 | if total > outer: |
128 | 128 | if box.margin_a == 'auto': |
133 | 133 | # XXX this is not in the spec, but without it box.inner |
134 | 134 | # would end up with a negative value. |
135 | 135 | # 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 | |
137 | 137 | box.inner = 0 |
138 | 138 | # Rule 3 |
139 | 139 | if 'auto' not in [box.margin_a, box.margin_b, box.inner]: |
162 | 162 | box.inner = (outer - box.padding_plus_border - |
163 | 163 | box.margin_a - box.margin_b) |
164 | 164 | # Rule 6 |
165 | if box.margin_a == 'auto' and box.margin_b == 'auto': | |
165 | if box.margin_a == box.margin_b == 'auto': | |
166 | 166 | box.margin_a = box.margin_b = ( |
167 | 167 | outer - box.padding_plus_border - box.inner) / 2 |
168 | 168 | |
174 | 174 | def compute_variable_dimension(context, side_boxes, vertical, outer_sum): |
175 | 175 | """ |
176 | 176 | 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 | |
178 | 178 | |
179 | 179 | :param side_boxes: Three boxes on a same side (as opposed to a corner.) |
180 | 180 | A list of: |
358 | 358 | page_end_y = margin_top + max_box_height |
359 | 359 | |
360 | 360 | # 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 | |
362 | 362 | generated_boxes = [] |
363 | 363 | |
364 | for prefix, vertical, containing_block, position_x, position_y in [ | |
364 | for prefix, vertical, containing_block, position_x, position_y in ( | |
365 | 365 | ('top', False, (max_box_width, margin_top), |
366 | 366 | margin_left, 0), |
367 | 367 | ('bottom', False, (max_box_width, margin_bottom), |
370 | 370 | 0, margin_top), |
371 | 371 | ('right', True, (margin_right, max_box_height), |
372 | 372 | page_end_x, margin_top), |
373 | ]: | |
373 | ): | |
374 | 374 | if vertical: |
375 | 375 | suffixes = ['top', 'middle', 'bottom'] |
376 | 376 | fixed_outer, variable_outer = containing_block |
398 | 398 | variable_outer - box.margin_width()) |
399 | 399 | compute_fixed_dimension( |
400 | 400 | context, box, fixed_outer, not vertical, |
401 | prefix in ['top', 'left']) | |
401 | prefix in ('top', 'left')) | |
402 | 402 | generated_boxes.append(box) |
403 | 403 | |
404 | 404 | # Corner boxes |
405 | 405 | |
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 ( | |
407 | 407 | ('@top-left-corner', margin_left, margin_top, 0, 0), |
408 | 408 | ('@top-right-corner', margin_right, margin_top, page_end_x, 0), |
409 | 409 | ('@bottom-left-corner', margin_left, margin_bottom, 0, page_end_y), |
410 | 410 | ('@bottom-right-corner', margin_right, margin_bottom, |
411 | 411 | page_end_x, page_end_y), |
412 | ]: | |
412 | ): | |
413 | 413 | box = make_box(at_keyword, (cb_width, cb_height)) |
414 | 414 | if not box.is_generated: |
415 | 415 | continue |
467 | 467 | over-constrained, instead of ignoring any margins, the containing block |
468 | 468 | is resized to coincide with the margin edges of the page box." |
469 | 469 | |
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 | |
472 | 472 | |
473 | 473 | """ |
474 | 474 | remaining = containing_block_size - box.padding_plus_border |
542 | 542 | root_box = root_box.copy_with_children([]) |
543 | 543 | |
544 | 544 | # 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 | |
546 | 546 | assert isinstance(root_box, (boxes.BlockBox, boxes.FlexContainerBox)) |
547 | 547 | context.create_block_formatting_context() |
548 | 548 | context.current_page = page_number |
549 | context.current_page_footnotes = context.reported_footnotes.copy() | |
549 | context.current_page_footnotes = [] | |
550 | 550 | context.current_footnote_area = footnote_area |
551 | 551 | |
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 | |
562 | 561 | |
563 | 562 | page_is_empty = True |
564 | 563 | adjoining_margins = [] |
565 | 564 | positioned_boxes = [] # Mixed absolute and fixed |
566 | 565 | 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: | |
569 | 569 | box.position_y = root_box.content_box_y() |
570 | 570 | if box.is_floated(): |
571 | 571 | out_of_flow_box, out_of_flow_resume_at = float_layout( |
578 | 578 | skip_stack) |
579 | 579 | out_of_flow_boxes.append(out_of_flow_box) |
580 | 580 | 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) | |
583 | 583 | context.broken_out_of_flow = broken_out_of_flow |
584 | 584 | root_box, resume_at, next_page, _, _, _ = block_level_layout( |
585 | 585 | 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) | |
588 | 587 | assert root_box |
589 | 588 | root_box.children = out_of_flow_boxes + root_box.children |
590 | 589 | |
591 | 590 | footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy()) |
592 | footnote_area, _, _, _, _, _ = block_level_layout( | |
591 | footnote_area = block_level_layout( | |
593 | 592 | context, footnote_area, -inf, None, footnote_area.page, True, |
594 | positioned_boxes, positioned_boxes, [], False, None) | |
593 | positioned_boxes, positioned_boxes)[0] | |
595 | 594 | footnote_area.translate(dy=-footnote_area.margin_height()) |
596 | 595 | |
597 | 596 | page.fixed_boxes = [ |
722 | 721 | style_for.set_computed_styles( |
723 | 722 | page_type, |
724 | 723 | # @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 | |
726 | 725 | root=html.etree_element, parent=html.etree_element, |
727 | 726 | base_url=html.base_url) |
728 | 727 | |
834 | 833 | |
835 | 834 | """ |
836 | 835 | i = 0 |
836 | reported_footnotes = None | |
837 | 837 | while True: |
838 | 838 | remake_state = context.page_maker[i][-1] |
839 | 839 | if (len(pages) == 0 or |
846 | 846 | remake_state['anchors'] = [] |
847 | 847 | remake_state['content_lookups'] = [] |
848 | 848 | page, resume_at = remake_page(i, context, root_box, html) |
849 | reported_footnotes = context.reported_footnotes | |
849 | 850 | yield page |
850 | 851 | else: |
851 | 852 | PROGRESS_LOGGER.info( |
852 | 853 | 'Step 5 - Creating layout - Page %d (up-to-date)', i + 1) |
853 | 854 | resume_at = context.page_maker[i + 1][0] |
855 | reported_footnotes = None | |
854 | 856 | yield pages[i] |
855 | 857 | |
856 | 858 | 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 | |
859 | 861 | context.page_maker = context.page_maker[:i + 1] |
860 | 862 | context.broken_out_of_flow.clear() |
863 | context.reported_footnotes.clear() | |
861 | 864 | return |
92 | 92 | box, 'max_height', cb_height, main_flex_direction) |
93 | 93 | |
94 | 94 | # Used value == computed value |
95 | for side in ['top', 'right', 'bottom', 'left']: | |
95 | for side in ('top', 'right', 'bottom', 'left'): | |
96 | 96 | prop = f'border_{side}_width' |
97 | 97 | setattr(box, prop, box.style[prop]) |
98 | 98 |
3 | 3 | shrink-to-fit algorithm. |
4 | 4 | |
5 | 5 | 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/). | |
7 | 7 | |
8 | 8 | """ |
9 | 9 | |
21 | 21 | *Warning:* both available_content_width and the return value are |
22 | 22 | for width of the *content area*, not margin area. |
23 | 23 | |
24 | http://www.w3.org/TR/CSS21/visudet.html#float-width | |
24 | https://www.w3.org/TR/CSS21/visudet.html#float-width | |
25 | 25 | |
26 | 26 | """ |
27 | 27 | return min( |
91 | 91 | if width == 'auto' or width.unit == '%': |
92 | 92 | # "percentages on the following properties are treated instead as |
93 | 93 | # though they were the following: width: auto" |
94 | # http://dbaron.org/css/intrinsic/#outer-intrinsic | |
94 | # https://dbaron.org/css/intrinsic/#outer-intrinsic | |
95 | 95 | children_widths = [ |
96 | 96 | function(context, child, outer=True) for child in box.children |
97 | 97 | if not child.is_absolutely_positioned()] |
225 | 225 | def table_cell_min_content_width(context, box, outer): |
226 | 226 | """Return the min-content width for a ``TableCellBox``.""" |
227 | 227 | 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 | |
230 | 229 | if not child.is_absolutely_positioned()] |
231 | 230 | children_min_width = margin_width( |
232 | 231 | box, max(children_widths) if children_widths else 0) |
312 | 311 | current_line += lines[0] |
313 | 312 | break |
314 | 313 | else: |
315 | # http://www.w3.org/TR/css3-text/#line-break-details | |
314 | # https://www.w3.org/TR/css-text-3/#overflow-wrap | |
316 | 315 | # "The line breaking behavior of a replaced element |
317 | 316 | # or other atomic inline is equivalent to that |
318 | 317 | # of the Object Replacement Character (U+FFFC)." |
319 | # http://www.unicode.org/reports/tr14/#DescriptionOfProperties | |
318 | # https://www.unicode.org/reports/tr14/#DescriptionOfProperties | |
320 | 319 | # "By default, there is a break opportunity |
321 | 320 | # both before and after any inline object." |
322 | 321 | if minimum: |
341 | 340 | def _percentage_contribution(box): |
342 | 341 | """Return the percentage contribution of a cell, column or column group. |
343 | 342 | |
344 | http://dbaron.org/css/intrinsic/#pct-contrib | |
343 | https://dbaron.org/css/intrinsic/#pct-contrib | |
345 | 344 | |
346 | 345 | """ |
347 | 346 | min_width = ( |
365 | 364 | column_intrinsic_percentages, constrainedness, |
366 | 365 | total_horizontal_border_spacing, grid)`` |
367 | 366 | |
368 | http://dbaron.org/css/intrinsic/ | |
367 | https://dbaron.org/css/intrinsic/ | |
369 | 368 | |
370 | 369 | """ |
371 | 370 | from .table import distribute_excess_width |
407 | 406 | min_width = block_min_content_width(context, table, outer=False) |
408 | 407 | max_width = block_max_content_width(context, table, outer=False) |
409 | 408 | 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)) | |
412 | 410 | 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)) | |
415 | 412 | result = ([], [], [], [], total_horizontal_border_spacing, []) |
416 | 413 | context.tables[table] = result = { |
417 | 414 | False: (min_width, max_width) + result, |
585 | 582 | # infinitely large number if the numerator is nonzero [and] the |
586 | 583 | # denominator of that ratio is 0." |
587 | 584 | # |
588 | # http://dbaron.org/css/intrinsic/#autotableintrinsic | |
585 | # https://dbaron.org/css/intrinsic/#autotableintrinsic | |
589 | 586 | # |
590 | 587 | # Please note that "an infinitely large number" is not "infinite", |
591 | 588 | # and that's probably not a coincindence: putting 'inf' here breaks |
606 | 603 | if table.style['width'] != 'auto' and table.style['width'].unit == 'px': |
607 | 604 | # "percentages on the following properties are treated instead as |
608 | 605 | # though they were the following: width: auto" |
609 | # http://dbaron.org/css/intrinsic/#outer-intrinsic | |
606 | # https://dbaron.org/css/intrinsic/#outer-intrinsic | |
610 | 607 | table_min_width = table_max_width = table.style['width'].value |
611 | 608 | else: |
612 | 609 | table_min_width = table_min_content_width |
697 | 694 | # TODO: use real values, see |
698 | 695 | # https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes |
699 | 696 | min_contents = [ |
700 | min_content_width(context, child, outer=True) | |
697 | min_content_width(context, child) | |
701 | 698 | for child in box.children if child.is_flex_item] |
702 | 699 | if not min_contents: |
703 | 700 | return adjust(box, outer, 0) |
713 | 710 | # TODO: use real values, see |
714 | 711 | # https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes |
715 | 712 | max_contents = [ |
716 | max_content_width(context, child, outer=True) | |
713 | max_content_width(context, child) | |
717 | 714 | for child in box.children if child.is_flex_item] |
718 | 715 | if not max_contents: |
719 | 716 | return adjust(box, outer, 0) |
0 | 0 | """Layout for images and other replaced elements. |
1 | 1 | |
2 | See http://dev.w3.org/csswg/css-images-3/#sizing | |
2 | See https://drafts.csswg.org/css-images-3/#sizing | |
3 | 3 | |
4 | 4 | """ |
5 | 5 | |
14 | 14 | |
15 | 15 | Return a ``(concrete_width, concrete_height)`` tuple. |
16 | 16 | |
17 | See http://dev.w3.org/csswg/css-images-3/#default-sizing | |
17 | See https://drafts.csswg.org/css-images-3/#default-sizing | |
18 | 18 | |
19 | 19 | """ |
20 | 20 | if specified_width == 'auto': |
52 | 52 | |
53 | 53 | Return a ``(concrete_width, concrete_height)`` tuple. |
54 | 54 | |
55 | See http://dev.w3.org/csswg/css-images-3/#contain-constraint | |
55 | See https://drafts.csswg.org/css-images-3/#contain-constraint | |
56 | 56 | |
57 | 57 | """ |
58 | 58 | return _constraint_image_sizing( |
65 | 65 | |
66 | 66 | Return a ``(concrete_width, concrete_height)`` tuple. |
67 | 67 | |
68 | See http://dev.w3.org/csswg/css-images-3/#cover-constraint | |
68 | See https://drafts.csswg.org/css-images-3/#cover-constraint | |
69 | 69 | |
70 | 70 | """ |
71 | 71 | return _constraint_image_sizing( |
98 | 98 | if object_fit == 'fill': |
99 | 99 | draw_width, draw_height = box.width, box.height |
100 | 100 | else: |
101 | if object_fit == 'contain' or object_fit == 'scale-down': | |
101 | if object_fit in ('contain', 'scale-down'): | |
102 | 102 | draw_width, draw_height = contain_constraint_image_sizing( |
103 | 103 | box.width, box.height, intrinsic_ratio) |
104 | 104 | elif object_fit == 'cover': |
138 | 138 | box.style['image_resolution'], box.style['font_size']) |
139 | 139 | |
140 | 140 | # 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': | |
143 | 143 | if width is not None: |
144 | 144 | # Point #1 |
145 | 145 | box.width = width |
167 | 167 | @handle_min_max_height |
168 | 168 | def replaced_box_height(box): |
169 | 169 | """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 | |
171 | 171 | width, height, ratio = box.replacement.get_intrinsic_size( |
172 | 172 | box.style['image_resolution'], box.style['font_size']) |
173 | 173 | |
174 | 174 | # 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': | |
176 | 176 | box.height = height |
177 | 177 | elif box.height == 'auto' and ratio: |
178 | 178 | box.height = box.width / ratio |
179 | 179 | |
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: | |
181 | 181 | box.height = height |
182 | 182 | elif ratio is not None and box.height == 'auto': |
183 | 183 | box.height = box.width / ratio |
190 | 190 | |
191 | 191 | def inline_replaced_box_layout(box, containing_block): |
192 | 192 | """Lay out an inline :class:`boxes.ReplacedBox` ``box``.""" |
193 | for side in ['top', 'right', 'bottom', 'left']: | |
193 | for side in ('top', 'right', 'bottom', 'left'): | |
194 | 194 | if getattr(box, f'margin_{side}') == 'auto': |
195 | 195 | setattr(box, f'margin_{side}', 0) |
196 | 196 | inline_replaced_box_width_height(box, containing_block) |
197 | 197 | |
198 | 198 | |
199 | 199 | 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': | |
201 | 201 | replaced_box_width.without_min_max(box, containing_block) |
202 | 202 | replaced_box_height.without_min_max(box) |
203 | 203 | min_max_auto_replaced(box) |
268 | 268 | from .float import avoid_collisions |
269 | 269 | |
270 | 270 | box = box.copy() |
271 | if box.style['width'] == 'auto' and box.style['height'] == 'auto': | |
271 | if box.style['width'] == box.style['height'] == 'auto': | |
272 | 272 | computed_margins = box.margin_left, box.margin_right |
273 | 273 | block_replaced_width.without_min_max( |
274 | 274 | box, containing_block) |
281 | 281 | replaced_box_height(box) |
282 | 282 | |
283 | 283 | # 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 | |
285 | 285 | box.position_x, box.position_y, _ = avoid_collisions( |
286 | 286 | context, box, containing_block, outer=False) |
287 | 287 | resume_at = None |
295 | 295 | def block_replaced_width(box, containing_block): |
296 | 296 | from .block import block_level_width |
297 | 297 | |
298 | # http://www.w3.org/TR/CSS21/visudet.html#block-replaced-width | |
298 | # https://www.w3.org/TR/CSS21/visudet.html#block-replaced-width | |
299 | 299 | replaced_box_width.without_min_max(box, containing_block) |
300 | 300 | block_level_width.without_min_max(box, containing_block) |
211 | 211 | row = row.copy_with_children(new_row_children) |
212 | 212 | |
213 | 213 | # 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 | |
215 | 215 | |
216 | 216 | # cells with vertical-align: baseline |
217 | 217 | baseline_cells = [] |
611 | 611 | def fixed_table_layout(box): |
612 | 612 | """Run the fixed table layout and return a list of column widths. |
613 | 613 | |
614 | http://www.w3.org/TR/CSS21/tables.html#fixed-table-layout | |
614 | https://www.w3.org/TR/CSS21/tables.html#fixed-table-layout | |
615 | 615 | |
616 | 616 | """ |
617 | 617 | table = box.get_wrapped_table() |
700 | 700 | def auto_table_layout(context, box, containing_block): |
701 | 701 | """Run the auto table layout and return a list of column widths. |
702 | 702 | |
703 | http://www.w3.org/TR/CSS21/tables.html#auto-table-layout | |
703 | https://www.w3.org/TR/CSS21/tables.html#auto-table-layout | |
704 | 704 | |
705 | 705 | """ |
706 | 706 | table = box.get_wrapped_table() |
816 | 816 | def cell_baseline(cell): |
817 | 817 | """Return the y position of a cell baseline from the top of its border box. |
818 | 818 | |
819 | See http://www.w3.org/TR/CSS21/tables.html#height-layout | |
819 | See https://www.w3.org/TR/CSS21/tables.html#height-layout | |
820 | 820 | |
821 | 821 | """ |
822 | 822 | result = find_in_flow_baseline( |
856 | 856 | |
857 | 857 | Return excess width left when it's impossible without breaking rules. |
858 | 858 | |
859 | See http://dbaron.org/css/intrinsic/#distributetocols | |
859 | See https://dbaron.org/css/intrinsic/#distributetocols | |
860 | 860 | |
861 | 861 | """ |
862 | 862 | # First group |
33 | 33 | for page in pages: |
34 | 34 | page_links = [] |
35 | 35 | for link in page.links: |
36 | link_type, anchor_name, rectangle, _ = link | |
36 | link_type, anchor_name, _, _ = link | |
37 | 37 | if link_type == 'internal': |
38 | 38 | if anchor_name not in anchors: |
39 | 39 | LOGGER.error( |
40 | 40 | 'No anchor #%s for internal URI reference', |
41 | 41 | anchor_name) |
42 | 42 | else: |
43 | page_links.append( | |
44 | (link_type, anchor_name, rectangle, None)) | |
43 | page_links.append(link) | |
45 | 44 | else: |
46 | 45 | # External link |
47 | 46 | page_links.append(link) |
74 | 73 | # "Transforms apply to block-level and atomic inline-level elements, |
75 | 74 | # but do not apply to elements which may be split into |
76 | 75 | # multiple inline-level boxes." |
77 | # http://www.w3.org/TR/css3-2d-transforms/#introduction | |
76 | # https://www.w3.org/TR/css-transforms-1/#introduction | |
78 | 77 | if box.style['transform'] and not isinstance(box, boxes.InlineBox): |
79 | 78 | border_width = box.border_width() |
80 | 79 | border_height = box.border_height() |
135 | 134 | if link_type == 'external' and box.is_attachment: |
136 | 135 | link_type = 'attachment' |
137 | 136 | 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) | |
139 | 138 | links.append(link) |
140 | 139 | if matrix and (has_bookmark or has_anchor): |
141 | 140 | pos_x, pos_y = matrix.transform_point(pos_x, pos_y) |
13 | 13 | from ..logger import LOGGER, PROGRESS_LOGGER |
14 | 14 | from ..matrix import Matrix |
15 | 15 | from ..urls import URLFetchingError |
16 | from . import pdfa | |
16 | from . import pdfa, pdfua | |
17 | 17 | from .fonts import build_fonts_dictionary |
18 | 18 | from .stream import Stream |
19 | 19 | |
20 | 20 | 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()} | |
23 | 23 | |
24 | 24 | |
25 | 25 | def _w3c_date_to_pdf(string, attr_name): |
169 | 169 | alpha['SMask']['G'] = alpha['SMask']['G'].reference |
170 | 170 | |
171 | 171 | |
172 | def _add_links(links, anchors, matrix, pdf, page, names): | |
172 | def _add_links(links, anchors, matrix, pdf, page, names, mark): | |
173 | 173 | """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: | |
176 | 175 | x1, y1 = matrix.transform_point(*rectangle[:2]) |
177 | 176 | x2, y2 = matrix.transform_point(*rectangle[2:]) |
178 | 177 | if link_type in ('internal', 'external'): |
179 | annot = pydyf.Dictionary({ | |
178 | box.link_annotation = pydyf.Dictionary({ | |
180 | 179 | 'Type': '/Annot', |
181 | 180 | 'Subtype': '/Link', |
182 | 181 | 'Rect': pydyf.Array([x1, y1, x2, y2]), |
183 | 182 | 'BS': pydyf.Dictionary({'W': 0}), |
184 | 183 | }) |
184 | if mark: | |
185 | box.link_annotation['Contents'] = pydyf.String(link_target) | |
185 | 186 | if link_type == 'internal': |
186 | annot['Dest'] = pydyf.String(link_target) | |
187 | box.link_annotation['Dest'] = pydyf.String(link_target) | |
187 | 188 | else: |
188 | annot['A'] = pydyf.Dictionary({ | |
189 | box.link_annotation['A'] = pydyf.Dictionary({ | |
189 | 190 | 'Type': '/Action', |
190 | 191 | 'S': '/URI', |
191 | 192 | 'URI': pydyf.String(link_target), |
192 | 193 | }) |
193 | pdf.add_object(annot) | |
194 | pdf.add_object(box.link_annotation) | |
194 | 195 | if 'Annots' not in page: |
195 | 196 | page['Annots'] = pydyf.Array() |
196 | page['Annots'].append(annot.reference) | |
197 | page['Annots'].append(box.link_annotation.reference) | |
197 | 198 | |
198 | 199 | for anchor in anchors: |
199 | 200 | anchor_name, x, y = anchor |
206 | 207 | count = len(bookmarks) |
207 | 208 | outlines = [] |
208 | 209 | 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)) | |
211 | 211 | outline = pydyf.Dictionary({ |
212 | 212 | 'Title': pydyf.String(title), 'Dest': destination}) |
213 | 213 | pdf.add_object(outline) |
230 | 230 | return outlines, count |
231 | 231 | |
232 | 232 | |
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): | |
236 | 235 | # 0.75 = 72 PDF point per inch / 96 CSS pixel per inch |
237 | 236 | scale = zoom * 0.75 |
238 | 237 | |
239 | 238 | PROGRESS_LOGGER.info('Step 6 - Creating PDF') |
240 | 239 | |
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) | |
243 | 250 | states = pydyf.Dictionary() |
244 | 251 | x_objects = pydyf.Dictionary() |
245 | 252 | patterns = pydyf.Dictionary() |
254 | 261 | pdf.add_object(resources) |
255 | 262 | pdf_names = [] |
256 | 263 | |
257 | # Variants | |
258 | if variant: | |
259 | VARIANTS[variant](pdf, metadata) | |
260 | ||
261 | 264 | # Links and anchors |
262 | page_links_and_anchors = list(resolve_links(pages)) | |
265 | page_links_and_anchors = list(resolve_links(document.pages)) | |
263 | 266 | attachment_links = [ |
264 | 267 | [link for link in page_links if link[0] == 'attachment'] |
265 | 268 | for page_links, page_anchors in page_links_and_anchors] |
276 | 279 | # above about multiple regions won't always be correct, because |
277 | 280 | # two links might have the same href, but different titles. |
278 | 281 | annot_files[annot_target] = _write_pdf_attachment( |
279 | pdf, (annot_target, None), url_fetcher) | |
282 | pdf, (annot_target, None), document.url_fetcher) | |
280 | 283 | |
281 | 284 | # Bookmarks |
282 | 285 | root = [] |
287 | 290 | skipped_levels = [] |
288 | 291 | last_by_depth = [root] |
289 | 292 | previous_level = 0 |
293 | page_streams = [] | |
290 | 294 | |
291 | 295 | 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)): | |
293 | 297 | # Draw from the top-left corner |
294 | 298 | matrix = Matrix(scale, 0, 0, -scale, 0, page.height * scale) |
295 | 299 | |
309 | 313 | left / scale, top / scale, |
310 | 314 | (right - left) / scale, (bottom - top) / scale) |
311 | 315 | 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) | |
314 | 318 | stream.transform(d=-1, f=(page.height * scale)) |
315 | page.paint(stream, scale=scale) | |
316 | 319 | pdf.add_object(stream) |
320 | page_streams.append(stream) | |
317 | 321 | |
318 | 322 | pdf_page = pydyf.Dictionary({ |
319 | 323 | 'Type': '/Page', |
322 | 326 | 'Contents': stream.reference, |
323 | 327 | 'Resources': resources.reference, |
324 | 328 | }) |
329 | if mark: | |
330 | pdf_page['Tabs'] = '/S' | |
331 | pdf_page['StructParents'] = page_number | |
325 | 332 | pdf.add_page(pdf_page) |
326 | 333 | |
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) | |
328 | 336 | |
329 | 337 | # Bleed |
330 | 338 | bleed = {key: value * 0.75 for key, value in page.bleed.items()} |
400 | 408 | |
401 | 409 | # PDF information |
402 | 410 | pdf.info['Producer'] = pydyf.String(f'WeasyPrint {__version__}') |
411 | metadata = document.metadata | |
403 | 412 | if metadata.title: |
404 | 413 | pdf.info['Title'] = pydyf.String(metadata.title) |
405 | 414 | if metadata.authors: |
416 | 425 | if metadata.modified: |
417 | 426 | pdf.info['ModDate'] = pydyf.String( |
418 | 427 | _w3c_date_to_pdf(metadata.modified, 'modified')) |
428 | if metadata.lang: | |
429 | pdf.catalog['Lang'] = pydyf.String(metadata.lang) | |
419 | 430 | if custom_metadata: |
420 | 431 | for key, value in metadata.custom.items(): |
421 | 432 | key = ''.join(char for char in key if char.isalnum()) |
427 | 438 | attachments = metadata.attachments + (attachments or []) |
428 | 439 | pdf_attachments = [] |
429 | 440 | 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) | |
431 | 443 | if pdf_attachment is not None: |
432 | 444 | pdf_attachments.append(pdf_attachment) |
433 | 445 | if pdf_attachments: |
441 | 453 | pdf.catalog['Names']['EmbeddedFiles'] = content.reference |
442 | 454 | |
443 | 455 | # Embedded fonts |
444 | pdf_fonts = build_fonts_dictionary(pdf, fonts, optimize_size) | |
456 | pdf_fonts = build_fonts_dictionary(pdf, document.fonts, optimize_size) | |
445 | 457 | pdf.add_object(pdf_fonts) |
446 | 458 | resources['Font'] = pdf_fonts.reference |
447 | 459 | _use_references(pdf, resources, images) |
454 | 466 | name_array.append(pydyf.String(anchor[0])) |
455 | 467 | name_array.append(anchor[1]) |
456 | 468 | 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) | |
458 | 477 | |
459 | 478 | return pdf |
200 | 200 | } |
201 | 201 | |
202 | 202 | # 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): | |
204 | 206 | glyph_info['bitmap'] = data |
205 | 207 | elif glyph_format in (2, 5, 7): |
206 | 208 | 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 |
0 | 0 | """PDF/A generation.""" |
1 | 1 | |
2 | from functools import partial | |
2 | 3 | from importlib.resources import read_binary |
3 | from xml.etree.ElementTree import ( | |
4 | Element, SubElement, register_namespace, tostring) | |
5 | 4 | |
6 | 5 | import pydyf |
7 | 6 | |
8 | from .. import __version__ | |
9 | 7 | 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 | |
21 | 9 | |
22 | 10 | |
23 | def pdfa(pdf, metadata, version): | |
11 | def pdfa(pdf, metadata, document, page_streams, version): | |
24 | 12 | """Set metadata for PDF/A documents.""" |
25 | 13 | LOGGER.warning( |
26 | 14 | 'PDF/A support is experimental, ' |
27 | 15 | 'generated PDF files are not guaranteed to be valid. ' |
28 | 16 | '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' | |
37 | 17 | |
38 | 18 | # Add ICC profile |
39 | 19 | profile = pydyf.Stream( |
50 | 30 | }), |
51 | 31 | ]) |
52 | 32 | |
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') | |
116 | 35 | |
117 | 36 | |
118 | 37 | 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})} |
0 | 0 | """PDF stream.""" |
1 | 1 | |
2 | import hashlib | |
3 | 2 | import io |
4 | 3 | import struct |
4 | from functools import lru_cache | |
5 | 5 | |
6 | 6 | import pydyf |
7 | 7 | from fontTools import subset |
8 | 8 | from fontTools.ttLib import TTFont, TTLibError, ttFont |
9 | from fontTools.varLib.mutator import instantiateVariableFont | |
9 | 10 | |
10 | 11 | from ..logger import LOGGER |
11 | 12 | from ..matrix import Matrix |
12 | from ..text.ffi import ffi, harfbuzz, pango | |
13 | from ..text.ffi import ffi, harfbuzz, pango, units_to_double | |
13 | 14 | |
14 | 15 | |
15 | 16 | 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) | |
17 | 20 | hb_blob = ffi.gc( |
18 | 21 | harfbuzz.hb_face_reference_blob(hb_face), |
19 | 22 | harfbuzz.hb_blob_destroy) |
23 | 26 | self.index = harfbuzz.hb_face_get_index(hb_face) |
24 | 27 | |
25 | 28 | 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) | |
28 | 33 | self.style = pango.pango_font_description_get_style(description) |
29 | 34 | self.family = ffi.string( |
30 | 35 | 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')) | |
33 | 40 | |
34 | 41 | # Name |
35 | 42 | description_string = ffi.string( |
44 | 51 | self.name = b'/' + self.hash.encode() + b'+' + b'-'.join(fields) |
45 | 52 | |
46 | 53 | # Ascent & descent |
47 | if font_size: | |
54 | if self.font_size: | |
48 | 55 | self.ascent = int( |
49 | 56 | pango.pango_font_metrics_get_ascent(pango_metrics) / |
50 | font_size * 1000) | |
57 | self.font_size * 1000) | |
51 | 58 | self.descent = -int( |
52 | 59 | pango.pango_font_metrics_get_descent(pango_metrics) / |
53 | font_size * 1000) | |
60 | self.font_size * 1000) | |
54 | 61 | else: |
55 | 62 | self.ascent = self.descent = 0 |
56 | 63 | |
63 | 70 | self.ttfont = None |
64 | 71 | self.bitmap = False |
65 | 72 | 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) | |
67 | 77 | |
68 | 78 | # Various properties |
69 | 79 | self.italic_angle = 0 # TODO: this should be different |
80 | 90 | self.flags = 2 ** (3 - 1) # Symbolic, custom character set |
81 | 91 | if self.style: |
82 | 92 | self.flags += 2 ** (7 - 1) # Italic |
83 | if b'Serif' in self.family.split(): | |
93 | if b'Serif' in fields: | |
84 | 94 | self.flags += 2 ** (2 - 1) # Serif |
85 | 95 | widths = self.widths.values() |
86 | 96 | if len(widths) > 1 and len(set(widths)) == 1: |
90 | 100 | if self.ttfont is None: |
91 | 101 | return |
92 | 102 | |
103 | # Subset font | |
93 | 104 | if cmap: |
94 | 105 | optimized_font = io.BytesIO() |
95 | 106 | options = subset.Options( |
96 | 107 | retain_gids=True, passthrough_tables=True, |
97 | ignore_missing_glyphs=True, hinting=False) | |
108 | ignore_missing_glyphs=True, hinting=False, | |
109 | desubroutinize=True) | |
98 | 110 | options.drop_tables += ['GSUB', 'GPOS', 'SVG'] |
99 | 111 | subsetter = subset.Subsetter(options) |
100 | 112 | subsetter.populate(gids=cmap) |
105 | 117 | else: |
106 | 118 | self.ttfont.save(optimized_font) |
107 | 119 | 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() | |
108 | 161 | |
109 | 162 | if not (self.png or self.svg): |
110 | 163 | return |
139 | 192 | class Stream(pydyf.Stream): |
140 | 193 | """PDF stream object with extra features.""" |
141 | 194 | def __init__(self, fonts, page_rectangle, states, x_objects, patterns, |
142 | shadings, images, *args, **kwargs): | |
195 | shadings, images, mark, *args, **kwargs): | |
143 | 196 | super().__init__(*args, **kwargs) |
144 | 197 | self.compress = True |
145 | 198 | self.page_rectangle = page_rectangle |
199 | self.marked = [] | |
146 | 200 | self._fonts = fonts |
147 | 201 | self._states = states |
148 | 202 | self._x_objects = x_objects |
149 | 203 | self._patterns = patterns |
150 | 204 | self._shadings = shadings |
151 | 205 | self._images = images |
206 | self._mark = mark | |
152 | 207 | self._current_color = self._current_color_stroke = None |
153 | 208 | self._current_alpha = self._current_alpha_stroke = None |
154 | 209 | self._current_font = self._current_font_size = None |
169 | 224 | self._ctm_stack.append(self.ctm) |
170 | 225 | |
171 | 226 | 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() | |
173 | 231 | self._current_color = self._current_color_stroke = None |
174 | 232 | self._current_alpha = self._current_alpha_stroke = None |
175 | 233 | self._current_font = None |
257 | 315 | 'BM': f'/{mode}', |
258 | 316 | })) |
259 | 317 | |
318 | @lru_cache() | |
260 | 319 | 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] | |
266 | 330 | |
267 | 331 | def add_group(self, bounding_box): |
268 | 332 | states = pydyf.Dictionary() |
290 | 354 | }) |
291 | 355 | group = Stream( |
292 | 356 | self._fonts, self.page_rectangle, states, x_objects, patterns, |
293 | shadings, self._images, extra=extra) | |
357 | shadings, self._images, self._mark, extra=extra) | |
294 | 358 | group.id = f'x{len(self._x_objects)}' |
295 | 359 | self._x_objects[group.id] = group |
296 | 360 | return group |
425 | 489 | }) |
426 | 490 | pattern = Stream( |
427 | 491 | self._fonts, self.page_rectangle, states, x_objects, patterns, |
428 | shadings, self._images, extra=extra) | |
492 | shadings, self._images, self._mark, extra=extra) | |
429 | 493 | pattern.id = f'p{len(self._patterns)}' |
430 | 494 | self._patterns[pattern.id] = pattern |
431 | 495 | return pattern |
445 | 509 | self._shadings[shading.id] = shading |
446 | 510 | return shading |
447 | 511 | |
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 | ||
448 | 528 | @staticmethod |
449 | 529 | def create_interpolation_function(domain, c0, c1, n): |
450 | 530 | return pydyf.Dictionary({ |
464 | 544 | 'Bounds': pydyf.Array(bounds), |
465 | 545 | 'Functions': pydyf.Array(sub_functions), |
466 | 546 | }) |
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' |
0 | 0 | """Stacking contexts management.""" |
1 | ||
2 | import operator | |
3 | 1 | |
4 | 2 | from .formatting_structure import boxes |
5 | 3 | from .layout.absolute import AbsolutePlaceholder |
6 | ||
7 | _Z_INDEX_GETTER = operator.attrgetter('z_index') | |
8 | 4 | |
9 | 5 | |
10 | 6 | class StackingContext: |
11 | 7 | """Stacking contexts define the paint order of all pieces of a document. |
12 | 8 | |
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 | |
15 | 11 | |
16 | 12 | """ |
17 | 13 | def __init__(self, box, child_contexts, blocks, floats, blocks_and_cells, |
32 | 28 | self.zero_z_contexts.append(context) |
33 | 29 | else: # context.z_index > 0 |
34 | 30 | 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) | |
37 | 33 | # sort() is stable, so the lists are now storted |
38 | 34 | # by z-index, then tree order. |
39 | 35 | |
57 | 53 | child_contexts = children |
58 | 54 | # child_contexts: where to put sub-contexts that we find here. |
59 | 55 | # 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." | |
64 | 60 | blocks = [] |
65 | 61 | floats = [] |
66 | 62 | 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) | |
67 | 66 | |
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 | |
112 | 67 | |
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 | |
114 | 72 | |
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 | |
120 | 82 | |
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 | |
122 | 106 | |
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) | |
126 | 109 | |
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) | |
134 | 115 | |
135 | box = dispatch_children(box) | |
116 | return box | |
136 | 117 | |
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) |
0 | 0 | """Render SVG images.""" |
1 | 1 | |
2 | 2 | import re |
3 | from contextlib import suppress | |
3 | 4 | from math import cos, hypot, pi, radians, sin, sqrt |
4 | 5 | from xml.etree import ElementTree |
5 | 6 | |
286 | 287 | self.tree = Node(wrapper, style) |
287 | 288 | self.url = url |
288 | 289 | |
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 | ||
289 | 295 | self.filters = {} |
290 | 296 | self.gradients = {} |
291 | 297 | self.images = {} |
414 | 420 | |
415 | 421 | # Draw node |
416 | 422 | if visible and node.tag in TAGS: |
417 | try: | |
423 | with suppress(PointError): | |
418 | 424 | TAGS[node.tag](self, node, font_size) |
419 | except PointError: | |
420 | pass | |
421 | 425 | |
422 | 426 | # Draw node children |
423 | 427 | if display and node.tag not in DEF_TYPES: |
251 | 251 | cx = cxprime * cos(phi) - cyprime * sin(phi) + (x1 + x) / 2 |
252 | 252 | cy = cxprime * sin(phi) + cyprime * cos(phi) + (y1 + y) / 2 |
253 | 253 | |
254 | if phi == 0 or phi == pi: | |
254 | if phi in (0, pi): | |
255 | 255 | minx = cx - rx |
256 | 256 | tminx = atan2(0, -rx) |
257 | 257 | maxx = cx + rx |
260 | 260 | tminy = atan2(-ry, 0) |
261 | 261 | maxy = cy + ry |
262 | 262 | tmaxy = atan2(ry, 0) |
263 | elif phi == pi / 2 or phi == 3 * pi / 2: | |
263 | elif phi in (pi / 2, 3 * pi / 2): | |
264 | 264 | minx = cx - ry |
265 | 265 | tminx = atan2(0, -ry) |
266 | 266 | maxx = cx + ry |
57 | 57 | # TODO: support contentStyleType on <svg> |
58 | 58 | stylesheets = [] |
59 | 59 | 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 | |
61 | 61 | if (element.tag == '{http://www.w3.org/2000/svg}style' and |
62 | 62 | element.get('type', 'text/css') == 'text/css' and |
63 | 63 | element.text): |
47 | 47 | |
48 | 48 | if tree.tag in ('svg', 'symbol'): |
49 | 49 | # Explicitely specified |
50 | # http://www.w3.org/TR/SVG11/struct.html#UseElement | |
50 | # https://www.w3.org/TR/SVG11/struct.html#UseElement | |
51 | 51 | tree._etree_node.tag = 'svg' |
52 | 52 | if 'width' in node.attrib and 'height' in node.attrib: |
53 | 53 | tree.attrib['width'] = node.attrib['width'] |
244 | 244 | alpha_stream.stream = [f'/{alpha_shading.id} sh'] |
245 | 245 | |
246 | 246 | group.shading(shading.id) |
247 | pattern.set_alpha(1) | |
247 | 248 | pattern.draw_x_object(group.id) |
248 | 249 | svg.stream.color_space('Pattern', stroke=stroke) |
249 | 250 | svg.stream.set_color_special(pattern.id, stroke=stroke) |
67 | 67 | ry = height / 2 |
68 | 68 | |
69 | 69 | # Inspired by Cairo Cookbook |
70 | # http://cairographics.org/cookbook/roundedrectangles/ | |
70 | # https://cairographics.org/cookbook/roundedrectangles/ | |
71 | 71 | ARC_TO_BEZIER = 4 * (2 ** .5 - 1) / 3 |
72 | 72 | c1, c2 = ARC_TO_BEZIER * rx, ARC_TO_BEZIER * ry |
73 | 73 |
0 | 0 | """Util functions for SVG rendering.""" |
1 | 1 | |
2 | 2 | import re |
3 | from contextlib import suppress | |
3 | 4 | from math import cos, radians, sin, tan |
4 | 5 | from urllib.parse import urlparse |
5 | 6 | |
28 | 29 | if not string: |
29 | 30 | return 0 |
30 | 31 | |
31 | try: | |
32 | with suppress(ValueError): | |
32 | 33 | return float(string) |
33 | except ValueError: | |
34 | # Not a float, try something else | |
35 | pass | |
36 | 34 | |
35 | # Not a float, try something else | |
37 | 36 | string = normalize(string).split(' ', 1)[0] |
38 | 37 | if string.endswith('%'): |
39 | 38 | assert percentage_reference is not None |
123 | 122 | if url and url.startswith('url(') and url.endswith(')'): |
124 | 123 | url = url[4:-1] |
125 | 124 | 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: | |
128 | 127 | url = url[1:-1] |
129 | 128 | break |
130 | 129 | return urlparse(url or '') |
0 | 0 | """Imports of dynamic libraries used for text layout.""" |
1 | 1 | |
2 | 2 | import os |
3 | from contextlib import suppress | |
3 | 4 | |
4 | 5 | import cffi |
5 | 6 | |
43 | 44 | typedef ... PangoAttrList; |
44 | 45 | typedef ... PangoAttrClass; |
45 | 46 | typedef ... PangoFont; |
47 | typedef ... PangoFontFace; | |
46 | 48 | typedef guint PangoGlyph; |
47 | 49 | typedef gint PangoGlyphUnit; |
48 | 50 | |
104 | 106 | } GSList; |
105 | 107 | |
106 | 108 | 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 { | |
107 | 160 | const PangoAttrClass *klass; |
108 | 161 | guint start_index; |
109 | 162 | guint end_index; |
113 | 166 | PangoLayout *layout; |
114 | 167 | gint start_index; |
115 | 168 | gint length; |
116 | GSList *runs; | |
169 | GSListRuns *runs; | |
117 | 170 | guint is_paragraph_start : 1; |
118 | 171 | guint resolved_dir : 3; |
119 | 172 | } PangoLayoutLine; |
140 | 193 | guint is_expandable_space : 1; |
141 | 194 | guint is_word_boundary : 1; |
142 | 195 | } 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; | |
189 | 196 | |
190 | 197 | int pango_version (void); |
191 | 198 | |
219 | 226 | void pango_font_description_free (PangoFontDescription *desc); |
220 | 227 | PangoFontDescription * pango_font_description_copy ( |
221 | 228 | const PangoFontDescription *desc); |
229 | ||
222 | 230 | void pango_font_description_set_family ( |
223 | 231 | PangoFontDescription *desc, const char *family); |
224 | 232 | void pango_font_description_set_style ( |
225 | 233 | PangoFontDescription *desc, PangoStyle style); |
226 | PangoStyle pango_font_description_get_style ( | |
227 | const PangoFontDescription *desc); | |
228 | 234 | void pango_font_description_set_stretch ( |
229 | 235 | PangoFontDescription *desc, PangoStretch stretch); |
230 | 236 | void pango_font_description_set_weight ( |
231 | 237 | PangoFontDescription *desc, PangoWeight weight); |
232 | 238 | void pango_font_description_set_absolute_size ( |
233 | 239 | 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); | |
234 | 249 | int pango_font_description_get_size (PangoFontDescription *desc); |
235 | 250 | |
236 | 251 | int pango_glyph_string_get_width (PangoGlyphString *glyphs); |
240 | 255 | PangoFontDescription * pango_font_describe (PangoFont *font); |
241 | 256 | const char * pango_font_description_get_family ( |
242 | 257 | const PangoFontDescription *desc); |
243 | int pango_font_description_hash (const PangoFontDescription *desc); | |
258 | guint pango_font_description_hash (const PangoFontDescription *desc); | |
244 | 259 | |
245 | 260 | PangoContext * pango_context_new (); |
246 | 261 | PangoContext * pango_font_map_create_context (PangoFontMap *fontmap); |
248 | 263 | PangoFontMetrics * pango_context_get_metrics ( |
249 | 264 | PangoContext *context, const PangoFontDescription *desc, |
250 | 265 | PangoLanguage *language); |
266 | PangoFontMetrics * pango_font_get_metrics ( | |
267 | PangoFont *font, PangoLanguage *language); | |
251 | 268 | void pango_font_metrics_unref (PangoFontMetrics *metrics); |
252 | 269 | int pango_font_metrics_get_ascent (PangoFontMetrics *metrics); |
253 | 270 | int pango_font_metrics_get_descent (PangoFontMetrics *metrics); |
259 | 276 | PangoFontMetrics *metrics); |
260 | 277 | int pango_font_metrics_get_strikethrough_position ( |
261 | 278 | 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 | ||
269 | 279 | void pango_font_get_glyph_extents ( |
270 | 280 | PangoFont *font, PangoGlyph glyph, PangoRectangle *ink_rect, |
271 | 281 | 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); | |
272 | 287 | |
273 | 288 | PangoAttrList * pango_attr_list_new (void); |
274 | 289 | void pango_attr_list_unref (PangoAttrList *list); |
376 | 391 | def _dlopen(ffi, *names): |
377 | 392 | """Try various names for the same library, for different platforms.""" |
378 | 393 | for name in names: |
379 | try: | |
394 | with suppress(OSError): | |
380 | 395 | return ffi.dlopen(name) |
381 | except OSError: | |
382 | pass | |
383 | 396 | # 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 | |
384 | 406 | return ffi.dlopen(names[0]) # pragma: no cover |
385 | 407 | |
386 | 408 | |
389 | 411 | 'WEASYPRINT_DLL_DIRECTORIES', |
390 | 412 | 'C:\\Program Files\\GTK3-Runtime Win64\\bin').split(';') |
391 | 413 | for dll_directory in dll_directories: |
392 | try: | |
414 | with suppress((OSError, FileNotFoundError)): | |
393 | 415 | os.add_dll_directory(dll_directory) |
394 | except (OSError, FileNotFoundError): | |
395 | pass | |
396 | 416 | |
397 | 417 | gobject = _dlopen( |
398 | 418 | ffi, 'gobject-2.0-0', 'gobject-2.0', 'libgobject-2.0-0', |
406 | 426 | 'libharfbuzz-0.dll') |
407 | 427 | fontconfig = _dlopen( |
408 | 428 | ffi, 'fontconfig-1', 'fontconfig', 'libfontconfig', 'libfontconfig.so.1', |
409 | 'libfontconfig-1.dylib', 'libfontconfig-1.dll') | |
429 | 'libfontconfig.1.dylib', 'libfontconfig-1.dll') | |
410 | 430 | pangoft2 = _dlopen( |
411 | 431 | ffi, 'pangoft2-1.0-0', 'pangoft2-1.0', 'libpangoft2-1.0-0', |
412 | 432 | 'libpangoft2-1.0.so.0', 'libpangoft2-1.0.dylib', 'libpangoft2-1.0-0.dll') |
109 | 109 | self.font, style['font_weight']) |
110 | 110 | pango.pango_font_description_set_absolute_size( |
111 | 111 | 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) | |
112 | 117 | pango.pango_layout_set_font_description(self.layout, self.font) |
113 | 118 | |
114 | 119 | text_decoration = style['text_decoration_line'] |
140 | 145 | if features and context: |
141 | 146 | features = ','.join( |
142 | 147 | f'{key} {value}' for key, value in features.items()).encode() |
143 | # TODO: attributes should be freed. | |
144 | 148 | # In the meantime, keep a cache to avoid leaking too many of them. |
145 | 149 | attr = context.font_features.setdefault( |
146 | 150 | features, pango.pango_attr_font_features_new(features)) |
151 | 155 | def get_first_line(self): |
152 | 156 | first_line = pango.pango_layout_get_line_readonly(self.layout, 0) |
153 | 157 | 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 | |
158 | 159 | self.first_line_direction = first_line.resolved_dir |
159 | 160 | return first_line, index |
160 | 161 | |
161 | 162 | def set_text(self, text, justify=False): |
162 | try: | |
163 | index = text.find('\n') | |
164 | if index != -1: | |
163 | 165 | # 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 | |
168 | 168 | text, bytestring = unicode_to_char_p(text) |
169 | self.text = bytestring.decode() | |
170 | 169 | pango.pango_layout_set_text(self.layout, text, -1) |
171 | 170 | |
172 | 171 | word_spacing = self.style['word_spacing'] |
184 | 183 | |
185 | 184 | if self.text and (word_spacing or letter_spacing or word_breaking): |
186 | 185 | 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) | |
190 | 190 | |
191 | 191 | def add_attr(start, end, spacing): |
192 | # TODO: attributes should be freed | |
193 | 192 | attr = pango.pango_attr_letter_spacing_new(spacing) |
194 | 193 | attr.start_index, attr.end_index = start, end |
195 | 194 | pango.pango_attr_list_change(attr_list, attr) |
207 | 206 | position = bytestring.find(b' ', position + 1) |
208 | 207 | |
209 | 208 | if word_breaking: |
210 | # TODO: attributes should be freed | |
211 | 209 | attr = pango.pango_attr_insert_hyphens_new(False) |
212 | 210 | attr.start_index, attr.end_index = 0, len(bytestring) |
213 | 211 | pango.pango_attr_list_change(attr_list, attr) |
294 | 292 | max_width = None |
295 | 293 | |
296 | 294 | # Step #1: Get a draft layout with the first line |
297 | layout = None | |
298 | 295 | if max_width is not None and max_width != inf and style['font_size']: |
296 | short_text = text | |
299 | 297 | if max_width == 0: |
300 | 298 | # Trying to find minimum size, let's naively split on spaces and |
301 | 299 | # keep one word + one letter |
302 | 300 | 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 | |
307 | 303 | 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: | |
320 | 315 | layout = create_layout( |
321 | 316 | 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() | |
324 | 318 | |
325 | 319 | # Step #2: Don't split lines when it's not needed |
326 | 320 | if max_width is None: |
328 | 322 | return first_line_metrics( |
329 | 323 | first_line, text, layout, resume_index, space_collapse, style) |
330 | 324 | 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: | |
332 | 326 | # The first line fits in the available width |
333 | 327 | return first_line_metrics( |
334 | 328 | first_line, text, layout, resume_index, space_collapse, style) |
336 | 330 | # Step #3: Try to put the first word of the second line on the first line |
337 | 331 | # https://mail.gnome.org/archives/gtk-i18n-list/2013-September/msg00006 |
338 | 332 | # 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() | |
340 | 334 | first_line_fits = ( |
341 | 335 | first_line_width <= max_width or |
342 | 336 | ' ' in first_line_text.strip() or |
343 | 337 | can_break_text(first_line_text.strip(), style['lang'])) |
344 | 338 | if first_line_fits: |
345 | 339 | # 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() | |
347 | 341 | else: |
348 | 342 | # The line can't be split earlier, try to hyphenate the first word. |
349 | 343 | first_line_text = '' |
356 | 350 | # only try when space collapsing is allowed |
357 | 351 | new_first_line_text = first_line_text + next_word |
358 | 352 | 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 | |
375 | 366 | elif first_line_text: |
376 | 367 | # We found something on the first line but we did not find a word on |
377 | 368 | # the next line, no need to hyphenate, we can keep the current layout |
385 | 376 | hyphenated = False |
386 | 377 | soft_hyphen = '\xad' |
387 | 378 | |
388 | try_hyphenate = False | |
379 | auto_hyphenation = manual_hyphenation = False | |
389 | 380 | if hyphens != 'none': |
381 | manual_hyphenation = soft_hyphen in first_line_text + next_word | |
382 | if hyphens == 'auto' and lang: | |
390 | 383 | next_word_boundaries = get_next_word_boundaries(second_line_text, lang) |
391 | 384 | if next_word_boundaries: |
392 | 385 | # We have a word to hyphenate |
404 | 397 | if space > limit_zone or space < 0: |
405 | 398 | # Available space is worth the try, or the line is even too |
406 | 399 | # 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 | |
491 | 457 | 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) | |
493 | 462 | 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()) | |
494 | 475 | |
495 | 476 | if not hyphenated and first_line_text.endswith(soft_hyphen): |
496 | 477 | # Recreate the layout with no max_width to be sure that |
499 | 480 | hyphenated_first_line_text = ( |
500 | 481 | first_line_text + style['hyphenate_character']) |
501 | 482 | 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() | |
505 | 485 | resume_index = len(first_line_text.encode()) |
506 | 486 | |
507 | 487 | # Step 5: Try to break word if it's too long for the line |
14 | 14 | from . import __version__ |
15 | 15 | from .logger import LOGGER |
16 | 16 | |
17 | # See http://stackoverflow.com/a/11687993/1162888 | |
17 | # See https://stackoverflow.com/a/11687993/1162888 | |
18 | 18 | # 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 | |
20 | 20 | UNICODE_SCHEME_RE = re.compile('^([a-zA-Z][a-zA-Z0-9.+-]+):') |
21 | 21 | BYTES_SCHEME_RE = re.compile(b'^([a-zA-Z][a-zA-Z0-9.+-]+):') |
22 | 22 |