weasyprint 67.0__tar.gz → 68.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. {weasyprint-67.0 → weasyprint-68.0}/PKG-INFO +2 -1
  2. {weasyprint-67.0 → weasyprint-68.0}/docs/api_reference.rst +7 -1
  3. {weasyprint-67.0 → weasyprint-68.0}/docs/changelog.rst +121 -0
  4. {weasyprint-67.0 → weasyprint-68.0}/docs/common_use_cases.rst +63 -66
  5. {weasyprint-67.0 → weasyprint-68.0}/docs/conf.py +4 -2
  6. {weasyprint-67.0 → weasyprint-68.0}/docs/first_steps.rst +54 -31
  7. {weasyprint-67.0 → weasyprint-68.0}/pyproject.toml +1 -1
  8. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_common.py +4 -5
  9. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_target.py +21 -1
  10. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_validation.py +24 -0
  11. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/__init__.py +1 -1
  12. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_gradients.py +215 -1
  13. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_text.py +179 -0
  14. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_absolute.py +1 -1
  15. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_gradient.py +115 -0
  16. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_text.py +4 -2
  17. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_grid.py +137 -0
  18. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_page.py +66 -0
  19. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_table.py +17 -0
  20. {weasyprint-67.0 → weasyprint-68.0}/tests/test_acid2.py +2 -2
  21. {weasyprint-67.0 → weasyprint-68.0}/tests/test_api.py +266 -67
  22. {weasyprint-67.0 → weasyprint-68.0}/tests/test_boxes.py +5 -5
  23. {weasyprint-67.0 → weasyprint-68.0}/tests/test_fonts.py +1 -1
  24. {weasyprint-67.0 → weasyprint-68.0}/tests/test_text.py +58 -14
  25. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/__init__.py +35 -103
  26. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/__main__.py +107 -80
  27. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/__init__.py +4 -10
  28. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/functions.py +5 -0
  29. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/html5_ua.css +1 -1
  30. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/tokens.py +4 -1
  31. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/validation/properties.py +4 -4
  32. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/document.py +4 -64
  33. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/draw/text.py +4 -2
  34. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/formatting_structure/boxes.py +4 -1
  35. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/formatting_structure/build.py +111 -37
  36. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/images.py +27 -32
  37. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/__init__.py +2 -1
  38. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/grid.py +25 -14
  39. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/page.py +4 -4
  40. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/preferred.py +35 -2
  41. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/pdf/__init__.py +12 -1
  42. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/pdf/anchors.py +10 -16
  43. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/pdf/fonts.py +12 -3
  44. weasyprint-68.0/weasyprint/pdf/metadata.py +187 -0
  45. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/pdf/pdfa.py +1 -3
  46. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/pdf/pdfua.py +1 -3
  47. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/pdf/pdfx.py +1 -3
  48. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/pdf/stream.py +0 -2
  49. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/svg/__init__.py +51 -30
  50. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/svg/css.py +21 -4
  51. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/svg/defs.py +5 -3
  52. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/text/fonts.py +2 -3
  53. weasyprint-68.0/weasyprint/urls.py +480 -0
  54. weasyprint-67.0/weasyprint/pdf/metadata.py +0 -132
  55. weasyprint-67.0/weasyprint/urls.py +0 -304
  56. {weasyprint-67.0 → weasyprint-68.0}/LICENSE +0 -0
  57. {weasyprint-67.0 → weasyprint-68.0}/README.rst +0 -0
  58. {weasyprint-67.0 → weasyprint-68.0}/docs/contribute.rst +0 -0
  59. {weasyprint-67.0 → weasyprint-68.0}/docs/going_further.rst +0 -0
  60. {weasyprint-67.0 → weasyprint-68.0}/docs/index.rst +0 -0
  61. {weasyprint-67.0 → weasyprint-68.0}/docs/manpage.rst +0 -0
  62. {weasyprint-67.0 → weasyprint-68.0}/docs/support.rst +0 -0
  63. {weasyprint-67.0 → weasyprint-68.0}/tests/__init__.py +0 -0
  64. {weasyprint-67.0 → weasyprint-68.0}/tests/conftest.py +0 -0
  65. {weasyprint-67.0 → weasyprint-68.0}/tests/css/__init__.py +0 -0
  66. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_counters.py +0 -0
  67. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_descriptors.py +0 -0
  68. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_errors.py +0 -0
  69. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_expanders.py +0 -0
  70. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_fonts.py +0 -0
  71. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_layers.py +0 -0
  72. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_math.py +0 -0
  73. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_nesting.py +0 -0
  74. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_pages.py +0 -0
  75. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_ua.py +0 -0
  76. {weasyprint-67.0 → weasyprint-68.0}/tests/css/test_variables.py +0 -0
  77. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/__init__.py +0 -0
  78. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_bounding_box.py +0 -0
  79. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_clip.py +0 -0
  80. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_defs.py +0 -0
  81. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_images.py +0 -0
  82. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_markers.py +0 -0
  83. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_opacity.py +0 -0
  84. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_paths.py +0 -0
  85. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_patterns.py +0 -0
  86. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_shapes.py +0 -0
  87. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_transform.py +0 -0
  88. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_units.py +0 -0
  89. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/svg/test_visibility.py +0 -0
  90. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_background.py +0 -0
  91. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_before_after.py +0 -0
  92. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_box.py +0 -0
  93. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_cmyk_color_profiles.py +0 -0
  94. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_column.py +0 -0
  95. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_current_color.py +0 -0
  96. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_float.py +0 -0
  97. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_footnote.py +0 -0
  98. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_footnote_column.py +0 -0
  99. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_image.py +0 -0
  100. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_leader.py +0 -0
  101. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_list.py +0 -0
  102. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_opacity.py +0 -0
  103. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_overflow.py +0 -0
  104. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_page.py +0 -0
  105. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_table.py +0 -0
  106. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_transform.py +0 -0
  107. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_visibility.py +0 -0
  108. {weasyprint-67.0 → weasyprint-68.0}/tests/draw/test_whitespace.py +0 -0
  109. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/__init__.py +0 -0
  110. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_block.py +0 -0
  111. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_column.py +0 -0
  112. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_flex.py +0 -0
  113. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_float.py +0 -0
  114. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_footnotes.py +0 -0
  115. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_image.py +0 -0
  116. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_inline.py +0 -0
  117. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_inline_block.py +0 -0
  118. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_list.py +0 -0
  119. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_position.py +0 -0
  120. {weasyprint-67.0 → weasyprint-68.0}/tests/layout/test_preferred.py +0 -0
  121. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/acid2-reference.html +0 -0
  122. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/acid2-test.html +0 -0
  123. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/blue.jpg +0 -0
  124. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/border.svg +0 -0
  125. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/border2.svg +0 -0
  126. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/cmyk.icc +0 -0
  127. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/cmyk_with_icc.jpg +0 -0
  128. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/cmyk_without_icc.jpg +0 -0
  129. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/doc1.html +0 -0
  130. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/doc1_UTF-16BE.html +0 -0
  131. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/icon.png +0 -0
  132. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/latin1-test.css +0 -0
  133. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/logo_small.png +0 -0
  134. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/mask.svg +0 -0
  135. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/mini_ua.css +0 -0
  136. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/not-optimized-exif.jpg +0 -0
  137. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/not-optimized.jpg +0 -0
  138. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/pattern-transparent.svg +0 -0
  139. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/pattern.gif +0 -0
  140. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/pattern.palette.png +0 -0
  141. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/pattern.png +0 -0
  142. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/pattern.svg +0 -0
  143. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/really-a-png.svg +0 -0
  144. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/really-a-svg.png +0 -0
  145. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/sheet2.css +0 -0
  146. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/sub_directory/sheet1.css +0 -0
  147. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/tests_ua.css +0 -0
  148. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/user.css +0 -0
  149. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/utf8-test.css +0 -0
  150. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/weasyprint.otb +0 -0
  151. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/weasyprint.otf +0 -0
  152. {weasyprint-67.0 → weasyprint-68.0}/tests/resources/weasyprint.woff +0 -0
  153. {weasyprint-67.0 → weasyprint-68.0}/tests/test_pdf.py +0 -0
  154. {weasyprint-67.0 → weasyprint-68.0}/tests/test_presentational_hints.py +0 -0
  155. {weasyprint-67.0 → weasyprint-68.0}/tests/test_stacking.py +0 -0
  156. {weasyprint-67.0 → weasyprint-68.0}/tests/test_unicode.py +0 -0
  157. {weasyprint-67.0 → weasyprint-68.0}/tests/test_url.py +0 -0
  158. {weasyprint-67.0 → weasyprint-68.0}/tests/testing_utils.py +0 -0
  159. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/anchors.py +0 -0
  160. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/computed_values.py +0 -0
  161. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/counters.py +0 -0
  162. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/html5_ph.css +0 -0
  163. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/html5_ua_form.css +0 -0
  164. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/media_queries.py +0 -0
  165. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/properties.py +0 -0
  166. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/targets.py +0 -0
  167. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/units.py +0 -0
  168. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/validation/__init__.py +0 -0
  169. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/validation/descriptors.py +0 -0
  170. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/css/validation/expanders.py +0 -0
  171. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/draw/__init__.py +0 -0
  172. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/draw/border.py +0 -0
  173. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/draw/color.py +0 -0
  174. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/html.py +0 -0
  175. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/absolute.py +0 -0
  176. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/background.py +0 -0
  177. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/block.py +0 -0
  178. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/column.py +0 -0
  179. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/flex.py +0 -0
  180. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/float.py +0 -0
  181. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/inline.py +0 -0
  182. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/leader.py +0 -0
  183. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/min_max.py +0 -0
  184. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/percent.py +0 -0
  185. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/replaced.py +0 -0
  186. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/layout/table.py +0 -0
  187. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/logger.py +0 -0
  188. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/matrix.py +0 -0
  189. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/pdf/debug.py +0 -0
  190. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/pdf/sRGB2014.icc +0 -0
  191. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/pdf/tags.py +0 -0
  192. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/stacking.py +0 -0
  193. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/svg/bounding_box.py +0 -0
  194. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/svg/images.py +0 -0
  195. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/svg/path.py +0 -0
  196. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/svg/shapes.py +0 -0
  197. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/svg/text.py +0 -0
  198. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/svg/utils.py +0 -0
  199. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/text/constants.py +0 -0
  200. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/text/ffi.py +0 -0
  201. {weasyprint-67.0 → weasyprint-68.0}/weasyprint/text/line_break.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weasyprint
3
- Version: 67.0
3
+ Version: 68.0
4
4
  Summary: The Awesome Document Factory
5
5
  Keywords: html,css,pdf,converter
6
6
  Author-email: Simon Sapin <simon.sapin@exyr.org>
@@ -37,6 +37,7 @@ Requires-Dist: sphinx ; extra == "doc"
37
37
  Requires-Dist: furo ; extra == "doc"
38
38
  Requires-Dist: pytest ; extra == "test"
39
39
  Requires-Dist: ruff ; extra == "test"
40
+ Requires-Dist: Pillow >=12.1.0 ; extra == "test"
40
41
  Project-URL: Changelog, https://github.com/Kozea/WeasyPrint/releases
41
42
  Project-URL: Code, https://github.com/Kozea/WeasyPrint
42
43
  Project-URL: Documentation, https://doc.courtbouillon.org/weasyprint/
@@ -56,7 +56,6 @@ Python API
56
56
  :members:
57
57
  .. autoclass:: CSS(input, **kwargs)
58
58
  .. autoclass:: Attachment(input, **kwargs)
59
- .. autofunction:: default_url_fetcher
60
59
  .. autodata:: DEFAULT_OPTIONS
61
60
 
62
61
  .. module:: weasyprint.document
@@ -68,6 +67,13 @@ Python API
68
67
  :members:
69
68
  :exclude-members: paint
70
69
 
70
+ .. module:: weasyprint.urls
71
+ .. autoclass:: URLFetcher
72
+ :members:
73
+ :member-order: bysource
74
+ .. autoclass:: URLFetcherResponse
75
+ .. autoclass:: FatalURLFetchingError
76
+
71
77
  .. module:: weasyprint.text.fonts
72
78
  .. autoclass:: FontConfiguration()
73
79
 
@@ -2,6 +2,127 @@ Changelog
2
2
  =========
3
3
 
4
4
 
5
+ Version 68.0
6
+ ------------
7
+
8
+ Released on 2026-01-19.
9
+
10
+ **This is a security update (CVE-2025-68616).**
11
+
12
+ We strongly recommend to upgrade WeasyPrint to the latest version if you use the
13
+ ``default_url_fetcher`` function in your custom URL fetcher, or if you use the
14
+ ``allowed_protocols`` parameter of the ``default_url_fetcher`` function.
15
+
16
+ Security:
17
+
18
+ * Always use URL fetcher for HTTP redirects
19
+
20
+ Python API:
21
+
22
+ * ``default_url_fetcher()`` is deprecated, use the new ``URLFetcher`` class instead, see
23
+ :ref:`URL Fetchers` for more information about URL fetchers
24
+ * ``DocumentMetadata.generate_rdf_metadata`` is now a method that can be overridden
25
+ instead of a parameter, see :ref:`Factur-X / ZUGFeRD (Electronic Invoices)` for
26
+ examples to create e-invoices
27
+
28
+ Features:
29
+
30
+ * `#2609 <https://github.com/Kozea/WeasyPrint/pull/2609>`_,
31
+ `#2603 <https://github.com/Kozea/WeasyPrint/issues/2603>`_,
32
+ `#351 <https://github.com/Kozea/WeasyPrint/issues/351>`_:
33
+ Refactor URL fetcher API
34
+ * `#2632 <https://github.com/Kozea/WeasyPrint/pull/2632>`_:
35
+ Support legacy 0 value for angles
36
+ * `#2627 <https://github.com/Kozea/WeasyPrint/pull/2627>`_:
37
+ Add font-face support to SVG
38
+ * `#2646 <https://github.com/Kozea/WeasyPrint/pull/2646>`_,
39
+ `#2255 <https://github.com/Kozea/WeasyPrint/issues/2255>`_:
40
+ Add font shorthand support for SVG text elements
41
+ * `#2590 <https://github.com/Kozea/WeasyPrint/pull/2590>`_,
42
+ `#1749 <https://github.com/Kozea/WeasyPrint/issues/1749>`_:
43
+ Honor language-specific rules for text-transform
44
+ * `#2645 <https://github.com/Kozea/WeasyPrint/pull/2645>`_,
45
+ `#2613 <https://github.com/Kozea/WeasyPrint/issues/2613>`_:
46
+ Improve SVG and SVG emojis rendering
47
+ * `#2658 <https://github.com/Kozea/WeasyPrint/pull/2658>`_,
48
+ `#2583 <https://github.com/Kozea/WeasyPrint/issues/2583>`_:
49
+ Add CLI for Factur-X / ZUGFeRD e-invoices
50
+
51
+ Bug fixes:
52
+
53
+ * `#2649 <https://github.com/Kozea/WeasyPrint/issues/2649>`_:
54
+ Refactor URL fetcher API
55
+ * `#2643 <https://github.com/Kozea/WeasyPrint/pull/2643>`_,
56
+ `#2628 <https://github.com/Kozea/WeasyPrint/issues/2628>`_:
57
+ Handle box-sizing: border-box in grid layout
58
+ * `#2641 <https://github.com/Kozea/WeasyPrint/pull/2641>`_,
59
+ `#1875 <https://github.com/Kozea/WeasyPrint/issues/1875>`_:
60
+ Process whitespace after checking all pending targets
61
+ * `#2488 <https://github.com/Kozea/WeasyPrint/pull/2488>`_,
62
+ `#2485 <https://github.com/Kozea/WeasyPrint/issues/2485>`_:
63
+ Preserve page groups during layout repagination
64
+ * `#2642 <https://github.com/Kozea/WeasyPrint/pull/2642>`_,
65
+ `#2631 <https://github.com/Kozea/WeasyPrint/issues/2631>`_:
66
+ Don’t use isolated transparency groups
67
+ * `#2637 <https://github.com/Kozea/WeasyPrint/issues/2637>`_:
68
+ Fix repeating radial gradients rendering
69
+ * `#2622 <https://github.com/Kozea/WeasyPrint/issues/2622>`_:
70
+ Fix validation of colors
71
+ * `#2626 <https://github.com/Kozea/WeasyPrint/issues/2626>`_:
72
+ Share grid items rendering advancement between a box and its copies
73
+ * `#2621 <https://github.com/Kozea/WeasyPrint/issues/2621>`_:
74
+ Correctly handle fallback values of attr()
75
+ * `#2619 <https://github.com/Kozea/WeasyPrint/issues/2619>`_:
76
+ Fix SVG fonts
77
+ * `#2629 <https://github.com/Kozea/WeasyPrint/issues/2629>`_:
78
+ Always define extra skip height that may be used after
79
+ * `#2648 <https://github.com/Kozea/WeasyPrint/issues/2648>`_:
80
+ Fix numbers validation in font-feature-settings
81
+ * `#2648 <https://github.com/Kozea/WeasyPrint/issues/2660>`_:
82
+ Fix keyword values for text-decoration-thickness
83
+ * `#2661 <https://github.com/Kozea/WeasyPrint/issues/2661>`_:
84
+ Respect inline images when defining minimum table width
85
+
86
+ Documentation:
87
+
88
+ * `#2638 <https://github.com/Kozea/WeasyPrint/pull/2638>`_:
89
+ Update Python command for Windows installation steps
90
+
91
+ Contributors:
92
+
93
+ * Guillaume Ayoub
94
+ * Jurriaan Pruis
95
+ * Mohamed Hamed
96
+ * Alexandra Usatenko
97
+ * Andrea Corna
98
+ * Aoishik Khan
99
+ * Joe
100
+
101
+ Backers and sponsors:
102
+
103
+ * Spacinov
104
+ * Syslifters
105
+ * Kobalt
106
+ * Simon Sapin
107
+ * Grip Angebotssoftware
108
+ * Manuel Barkhau
109
+ * Simonsoft
110
+ * KontextWork
111
+ * Menutech
112
+ * TrainingSparkle
113
+ * Healthchecks.io
114
+ * Method B
115
+ * FieldHub
116
+ * Hammerbacher
117
+ * Yanal-Yves Fargialla
118
+ * Morntag
119
+ * Piloterr
120
+ * Xavid
121
+ * Charlie S.
122
+ * Prothesis Dental Solutions
123
+ * Kai DeLorenzo
124
+
125
+
5
126
  Version 67.0
6
127
  ------------
7
128
 
@@ -208,63 +208,58 @@ Here is an example of Factur-X document generation.
208
208
 
209
209
  .. code-block:: xml
210
210
 
211
- <x:xmpmeta
212
- xmlns:x="adobe:ns:meta/"
211
+ <rdf:RDF
213
212
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
214
- xmlns:pdf="http://ns.adobe.com/pdf/1.3/"
215
213
  xmlns:fx="urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"
216
214
  xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
217
215
  xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
218
216
  xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#">
219
- <!-- placeholder -->
220
- <rdf:RDF>
221
- <rdf:Description rdf:about="">
222
- <fx:ConformanceLevel>MINIMUM</fx:ConformanceLevel>
223
- <fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>
224
- <fx:DocumentType>INVOICE</fx:DocumentType>
225
- <fx:Version>1.0</fx:Version>
226
- </rdf:Description>
227
- <rdf:Description rdf:about="">
228
- <pdfaExtension:schemas>
229
- <rdf:Bag>
230
- <rdf:li rdf:parseType="Resource">
231
- <pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
232
- <pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
233
- <pdfaSchema:prefix>fx</pdfaSchema:prefix>
234
- <pdfaSchema:property>
235
- <rdf:Seq>
236
- <rdf:li rdf:parseType="Resource">
237
- <pdfaProperty:name>DocumentFileName</pdfaProperty:name>
238
- <pdfaProperty:valueType>Text</pdfaProperty:valueType>
239
- <pdfaProperty:category>external</pdfaProperty:category>
240
- <pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
241
- </rdf:li>
242
- <rdf:li rdf:parseType="Resource">
243
- <pdfaProperty:name>DocumentType</pdfaProperty:name>
244
- <pdfaProperty:valueType>Text</pdfaProperty:valueType>
245
- <pdfaProperty:category>external</pdfaProperty:category>
246
- <pdfaProperty:description>INVOICE</pdfaProperty:description>
247
- </rdf:li>
248
- <rdf:li rdf:parseType="Resource">
249
- <pdfaProperty:name>Version</pdfaProperty:name>
250
- <pdfaProperty:valueType>Text</pdfaProperty:valueType>
251
- <pdfaProperty:category>external</pdfaProperty:category>
252
- <pdfaProperty:description>The actual version of the Factur-X XML schema</pdfaProperty:description>
253
- </rdf:li>
254
- <rdf:li rdf:parseType="Resource">
255
- <pdfaProperty:name>ConformanceLevel</pdfaProperty:name>
256
- <pdfaProperty:valueType>Text</pdfaProperty:valueType>
257
- <pdfaProperty:category>external</pdfaProperty:category>
258
- <pdfaProperty:description>The conformance level of the embedded Factur-X data</pdfaProperty:description>
259
- </rdf:li>
260
- </rdf:Seq>
261
- </pdfaSchema:property>
262
- </rdf:li>
263
- </rdf:Bag>
264
- </pdfaExtension:schemas>
265
- </rdf:Description>
266
- </rdf:RDF>
267
- </x:xmpmeta>
217
+ <rdf:Description rdf:about="">
218
+ <fx:ConformanceLevel>MINIMUM</fx:ConformanceLevel>
219
+ <fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>
220
+ <fx:DocumentType>INVOICE</fx:DocumentType>
221
+ <fx:Version>1.0</fx:Version>
222
+ </rdf:Description>
223
+ <rdf:Description rdf:about="">
224
+ <pdfaExtension:schemas>
225
+ <rdf:Bag>
226
+ <rdf:li rdf:parseType="Resource">
227
+ <pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
228
+ <pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
229
+ <pdfaSchema:prefix>fx</pdfaSchema:prefix>
230
+ <pdfaSchema:property>
231
+ <rdf:Seq>
232
+ <rdf:li rdf:parseType="Resource">
233
+ <pdfaProperty:name>DocumentFileName</pdfaProperty:name>
234
+ <pdfaProperty:valueType>Text</pdfaProperty:valueType>
235
+ <pdfaProperty:category>external</pdfaProperty:category>
236
+ <pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
237
+ </rdf:li>
238
+ <rdf:li rdf:parseType="Resource">
239
+ <pdfaProperty:name>DocumentType</pdfaProperty:name>
240
+ <pdfaProperty:valueType>Text</pdfaProperty:valueType>
241
+ <pdfaProperty:category>external</pdfaProperty:category>
242
+ <pdfaProperty:description>INVOICE</pdfaProperty:description>
243
+ </rdf:li>
244
+ <rdf:li rdf:parseType="Resource">
245
+ <pdfaProperty:name>Version</pdfaProperty:name>
246
+ <pdfaProperty:valueType>Text</pdfaProperty:valueType>
247
+ <pdfaProperty:category>external</pdfaProperty:category>
248
+ <pdfaProperty:description>The actual version of the Factur-X XML schema</pdfaProperty:description>
249
+ </rdf:li>
250
+ <rdf:li rdf:parseType="Resource">
251
+ <pdfaProperty:name>ConformanceLevel</pdfaProperty:name>
252
+ <pdfaProperty:valueType>Text</pdfaProperty:valueType>
253
+ <pdfaProperty:category>external</pdfaProperty:category>
254
+ <pdfaProperty:description>The conformance level of the embedded Factur-X data</pdfaProperty:description>
255
+ </rdf:li>
256
+ </rdf:Seq>
257
+ </pdfaSchema:property>
258
+ </rdf:li>
259
+ </rdf:Bag>
260
+ </pdfaExtension:schemas>
261
+ </rdf:Description>
262
+ </rdf:RDF>
268
263
 
269
264
  ``factur-x.xml``:
270
265
 
@@ -329,33 +324,35 @@ Here is an example of Factur-X document generation.
329
324
  </rsm:SupplyChainTradeTransaction>
330
325
  </rsm:CrossIndustryInvoice>
331
326
 
332
- ``invoice.py``:
327
+ ``invoice.html``:
328
+
329
+ .. code-block:: html
330
+
331
+ <h1>Invoice</h1>
332
+
333
+ Command-line::
334
+
335
+ weasyprint invoice.html invoice.pdf --attachment=factur-x.xml --attachment-relationship=Data --xmp-metadata=rdf.xml --pdf-variant=pdf/a-3a
336
+
337
+ Or Python API:
333
338
 
334
339
  .. code-block:: python
335
340
 
336
- from pathlib import Path
337
341
  from weasyprint import Attachment, HTML
338
342
 
339
- def generate_rdf_metadata(metadata, variant, version, conformance):
340
- original_rdf = generate_original_rdf_metadata(metadata, variant, version, conformance)
341
- return Path("rdf.xml").read_bytes().replace(b"<!-- placeholder -->", original_rdf)
342
-
343
- document = HTML(string="<h1>Invoice</h1>").render()
344
- generate_original_rdf_metadata = document.metadata.generate_rdf_metadata
343
+ document = HTML("invoice.html").render()
345
344
 
346
345
  factur_x_xml = Path("factur-x.xml").read_text()
347
346
  attachment = Attachment(string=factur_x_xml, name="factur-x.xml", relationship="Data")
348
347
  document.metadata.attachments = [attachment]
349
348
 
350
- document.metadata.generate_rdf_metadata = generate_rdf_metadata
349
+ xmp_metadata = Path("rdf.xml").read_text().encode()
350
+ document.metadata.xmp_metadata = [xmp_metadata]
351
+
351
352
  document.write_pdf("invoice.pdf", pdf_variant="pdf/a-3b")
352
353
 
353
354
  Of course, the content of these files has to be adapted to the content of real
354
- invoices. Using XML generators instead of plain text manipulation is also
355
- highly recommended.
356
-
357
- A more detailed blog article is available on `Binary Butterfly’s website
358
- <https://binary-butterfly.de/artikel/factur-x-zugferd-e-invoices-with-python/>`_.
355
+ invoices.
359
356
 
360
357
 
361
358
  Include PDF Forms
@@ -72,9 +72,11 @@ man_pages = [
72
72
  ['Simon Sapin and contributors'], 1)
73
73
  ]
74
74
 
75
+ # Don’t autolabel man page titles.
76
+ autosectionlabel_maxdepth = 4
77
+
75
78
  # Grouping the document tree into Texinfo files. List of tuples
76
- # (source start file, target name, title, author,
77
- # dir menu entry, description, category)
79
+ # (source start file, target name, title, author, dir menu entry, description, category)
78
80
  texinfo_documents = [(
79
81
  'index', 'WeasyPrint', 'WeasyPrint Documentation',
80
82
  'Simon Sapin and contributors', 'WeasyPrint',
@@ -187,8 +187,8 @@ the latest release.
187
187
  If you want to use WeasyPrint as a Python library, you’ll have to follow a few
188
188
  extra steps. Please read this chapter carefully.
189
189
 
190
- The first step is to install the latest version of Python from the `Microsoft
191
- Store`_.
190
+ The first step is to install the `Python Install Manager`_ from the Microsoft
191
+ Store.
192
192
 
193
193
  When Python is installed, you have to install Pango and its dependencies. The
194
194
  easiest way to install these libraries is to use MSYS2. Here are the steps you
@@ -202,15 +202,15 @@ You can then launch a Windows command prompt by clicking on the Start menu,
202
202
  typing ``cmd`` and clicking the "Command Prompt" icon. Install WeasyPrint in a
203
203
  `virtual environment`_ using `pip`_::
204
204
 
205
- python3 -m venv venv
205
+ python -m venv venv
206
206
  venv\Scripts\activate.bat
207
- python3 -m pip install weasyprint
208
- python3 -m weasyprint --info
207
+ python -m pip install weasyprint
208
+ python -m weasyprint --info
209
209
 
210
210
  .. _executable: https://github.com/Kozea/WeasyPrint/releases
211
211
  .. _#2081: https://github.com/Kozea/WeasyPrint/issues/2081
212
212
  .. _#2092: https://github.com/Kozea/WeasyPrint/issues/2092
213
- .. _Microsoft Store: https://apps.microsoft.com/store/search/python
213
+ .. _Python Install Manager: https://apps.microsoft.com/detail/9nq7512cxl7t
214
214
  .. _MSYS2: https://www.msys2.org/#installation
215
215
 
216
216
 
@@ -482,43 +482,66 @@ WeasyPrint goes through a *URL fetcher* to fetch external resources such as
482
482
  images or CSS stylesheets. The default fetcher can natively open file and
483
483
  HTTP URLs, but the HTTP client does not support advanced features like cookies
484
484
  or authentication. This can be worked-around by passing a custom
485
- ``url_fetcher`` callable to the :class:`HTML` or :class:`CSS` classes.
486
- It must have the same signature as :func:`default_url_fetcher`.
485
+ :class:`URLFetcher <urls.URLFetcher>` to the :class:`HTML` or :class:`CSS`
486
+ classes.
487
487
 
488
- Custom fetchers can choose to handle some URLs and defer others
489
- to the default fetcher:
488
+ Some features, such as the timeout delay, can be configured as parameters:
490
489
 
491
490
  .. code-block:: python
492
491
 
493
- from weasyprint import default_url_fetcher, HTML
492
+ from weasyprint import HTML
493
+ from weasyprint.urls import URLFetcher
494
+
495
+ HTML(string='<html>', url_fetcher=URLFetcher(timeout=20)).write_pdf('out.pdf')
496
+
497
+ Custom fetchers can also choose to handle some URLs and defer others to the default
498
+ fetcher:
494
499
 
495
- def my_fetcher(url):
496
- if url.startswith('graph:'):
497
- graph_data = map(float, url[6:].split(','))
498
- string = generate_graph(graph_data)
499
- return {'string': string, 'mime_type': 'image/png'}
500
- return default_url_fetcher(url)
500
+ .. code-block:: python
501
+
502
+ from weasyprint import HTML
503
+ from weasyprint.urls import URLFetcher, URLFetcherResponse
504
+
505
+ class MyFetcher(URLFetcher):
506
+ def fetch(self, url, headers=None):
507
+ if url.startswith('graph:'):
508
+ graph_data = [float(value) for value in url[6:].split(',')]
509
+ string = generate_graph(graph_data)
510
+ return URLFetcherResponse(url, string, {'Content-Type': 'image/png'})
511
+ return super().fetch(url, headers)
501
512
 
502
513
  source = '<img src="graph:42,10.3,87">'
503
- HTML(string=source, url_fetcher=my_fetcher).write_pdf('out.pdf')
514
+ HTML(string=source, url_fetcher=MyFetcher()).write_pdf('out.pdf')
515
+
516
+ By default, all errors raised by the fetcher are caught by WeasyPrint and emit a
517
+ warning. If you want some errors to be fatal and stop the rendering, you can raise a
518
+ :class:`urls.FatalURLFetchingError`.
519
+
520
+ .. code-block:: python
521
+
522
+ from weasyprint import HTML
523
+ from weasyprint.urls import URLFetcher, FatalURLFetchingError
524
+
525
+ class MyFetcher(URLFetcher):
526
+ def fetch(self, url, headers=None):
527
+ try:
528
+ return super().fetch(url, headers)
529
+ except Exception as exception:
530
+ if url.endswith('.css'):
531
+ # Stop the rendering if a problem happens with a stylesheet.
532
+ message = f'Problem with stylesheet at {url}'
533
+ raise FatalURLFetchingError(message) from exception
534
+ else:
535
+ # Raise original error that will be caught by WeasyPrint.
536
+ raise exception
537
+
538
+ HTML(string='<html>', url_fetcher=MyFetcher()).write_pdf('out.pdf')
504
539
 
505
540
  Flask-WeasyPrint_ for Flask_ and Django-Weasyprint_ for Django_ both make
506
541
  use of a custom URL fetcher to integrate WeasyPrint and use the filesystem
507
542
  instead of a network call for static and media files.
508
543
 
509
- A custom fetcher should be returning a :obj:`dict` with
510
-
511
- * One of ``string`` (a :obj:`bytestring <bytes>`) or ``file_obj`` (a
512
- :term:`file object`).
513
- * Optionally: ``mime_type``, a MIME type extracted e.g. from a *Content-Type*
514
- header. If not provided, the type is guessed from the file extension in the
515
- URL.
516
- * Optionally: ``encoding``, a character encoding extracted e.g. from a
517
- *charset* parameter in a *Content-Type* header
518
- * Optionally: ``redirected_url``, the actual URL of the resource if there were
519
- e.g. HTTP redirects.
520
- * Optionally: ``filename``, the filename of the resource. Usually derived from
521
- the *filename* parameter in a *Content-Disposition* header
544
+ A custom fetcher should be returning a :class:`urls.URLFetcherResponse`.
522
545
 
523
546
  If a ``file_obj`` is given, the resource will be closed automatically by
524
547
  the function internally used by WeasyPrint to retrieve data.
@@ -52,7 +52,7 @@ Donation = 'https://opencollective.com/courtbouillon'
52
52
 
53
53
  [project.optional-dependencies]
54
54
  doc = ['sphinx', 'furo']
55
- test = ['pytest', 'ruff']
55
+ test = ['pytest', 'ruff', 'Pillow >=12.1.0']
56
56
 
57
57
  [project.scripts]
58
58
  weasyprint = 'weasyprint.__main__:main'
@@ -4,9 +4,9 @@ from math import isclose
4
4
 
5
5
  import pytest
6
6
 
7
- from weasyprint import CSS, default_url_fetcher
7
+ from weasyprint import CSS
8
8
  from weasyprint.css import find_stylesheets, get_all_computed_styles
9
- from weasyprint.urls import path2url
9
+ from weasyprint.urls import URLFetcher, path2url
10
10
 
11
11
  from ..testing_utils import ( # isort:skip
12
12
  BASE_URL, FakeHTML, assert_no_logs, capture_logs, resource_path)
@@ -17,9 +17,8 @@ def test_find_stylesheets():
17
17
  html = FakeHTML(resource_path('doc1.html'))
18
18
 
19
19
  sheets = list(find_stylesheets(
20
- html.wrapper_element, 'print', default_url_fetcher, html.base_url,
21
- font_config=None, counter_style=None, color_profiles=None, page_rules=None,
22
- layers=None))
20
+ html.wrapper_element, 'print', URLFetcher(), html.base_url, font_config=None,
21
+ counter_style=None, color_profiles=None, page_rules=None, layers=None))
23
22
  assert len(sheets) == 2
24
23
  # Also test that stylesheets are in tree order.
25
24
  sheet_names = [
@@ -40,7 +40,7 @@ def test_target_counter_attr():
40
40
  div:first-child { counter-reset: div }
41
41
  div { counter-increment: div }
42
42
  div::before { content: target-counter(attr(data-count), div) }
43
- #id2::before { content: target-counter(attr(data-count, url), div) }
43
+ #id2::before { content: target-counter(attr(data-count url), div) }
44
44
  #id4::before {
45
45
  content: target-counter(attr(data-count), div, lower-alpha) }
46
46
  </style>
@@ -132,6 +132,26 @@ def test_target_text():
132
132
  assert before.text == '1'
133
133
 
134
134
 
135
+ @assert_no_logs
136
+ def test_target_text_whitespace_around_target():
137
+ # Regression test for #1875.
138
+ page, = render_pages('''
139
+ <style>
140
+ a::before { content: target-text(attr(href)) }
141
+ </style>
142
+ <p>before <a href="#ref"></a> after</p>
143
+ <h2 id="ref">text</h2>
144
+ ''')
145
+ html, = page.children
146
+ body, = html.children
147
+ p, h2 = body.children
148
+ line, = p.children
149
+ before, a, after = line.children
150
+ assert before.text == 'before '
151
+ assert after.text == ' after'
152
+ assert a.children[0].children[0].text == 'text'
153
+
154
+
135
155
  @assert_no_logs
136
156
  def test_target_float():
137
157
  page, = render_pages('''
@@ -196,6 +196,7 @@ def test_size_invalid(rule):
196
196
  ('none', ()),
197
197
  ('translate(6px) rotate(90deg)', (
198
198
  ('translate', ((6, 'px'), (0, 'px'))), ('rotate', pi / 2))),
199
+ ('rotate(0)', (('rotate', 0),)),
199
200
  ('translate(-4px, 0)', (('translate', ((-4, 'px'), (0, None))),)),
200
201
  ('translate(6px, 20%)', (('translate', ((6, 'px'), (20, '%'))),)),
201
202
  ('scale(2)', (('scale', (2, 2)),)),
@@ -612,6 +613,11 @@ def test_mask_border_mode_invalid(rule):
612
613
  ('test1', (('string', 'string'),)),
613
614
  ('test2', (('string', 'string'),)))),
614
615
  ('test attr(class)', (('test', (('attr()', ('class', 'string', '')),)),)),
616
+ ('test attr(class url)', (('test', (('attr()', ('class', 'url', '')),)),)),
617
+ ('test attr(class, "test")', (
618
+ ('test', (('attr()', ('class', 'string', 'test')),)),)),
619
+ ('test attr(class string, "test")', (
620
+ ('test', (('attr()', ('class', 'string', 'test')),)),)),
615
621
  ('test counter(count)', (
616
622
  ('test', (('counter()', ('count', 'decimal')),)),)),
617
623
  ('test counter(count, upper-roman)', (
@@ -1267,6 +1273,24 @@ def test_justify_self_invalid(rule):
1267
1273
  assert_invalid(f'justify-self: {rule}')
1268
1274
 
1269
1275
 
1276
+ @assert_no_logs
1277
+ @pytest.mark.parametrize('rule', [
1278
+ 'color',
1279
+ 'background',
1280
+ 'background-color',
1281
+ 'border-color',
1282
+ 'border-left-color',
1283
+ 'outline-color',
1284
+ ])
1285
+ @pytest.mark.parametrize('value', [
1286
+ 'rgb()',
1287
+ 'device-cmyk(0%, 0%, 0%, 30%)',
1288
+ '#abcde',
1289
+ ])
1290
+ def test_colors_invalid(rule, value):
1291
+ assert_invalid(f'{rule}: {value}')
1292
+
1293
+
1270
1294
  @assert_no_logs
1271
1295
  @pytest.mark.parametrize(('rule', 'value'), [
1272
1296
  ('normal', 'normal'),
@@ -134,4 +134,4 @@ def html_to_pixels(html):
134
134
  def document_to_pixels(document):
135
135
  """Render an HTML document to PNG, check its size and return pixel data."""
136
136
  image = Image.open(io.BytesIO(document.write_png()))
137
- return image.width, image.height, image.getdata()
137
+ return image.width, image.height, image.get_flattened_data()