Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support of CSS Color Level 4 #2286

Merged
merged 6 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies = [
'pydyf >=0.11.0,<0.12',
'cffi >=0.6,<2',
'tinyhtml5 >=2.0.0b1,<3',
'tinycss2 >=1.3.0,<2',
'tinycss2 >=1.4.0,<2',
'cssselect2 >=0.1,<0.8',
'Pyphen >=0.9.1,<0.16',
'Pillow >=9.1.0,<11',
Expand Down
4 changes: 2 additions & 2 deletions tests/css/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def test_annotate_document():
assert p['margin_right'] == (0, 'px')
assert p['margin_bottom'] == (24, 'px')
assert p['margin_left'] == (0, 'px')
assert p['background_color'] == 'currentColor'
assert p['background_color'] == 'currentcolor'

# 2em * 1.25ex = 2 * 20 * 1.25 * 0.8 = 40
# 2.5ex * 1.25ex = 2.5 * 0.8 * 20 * 1.25 * 0.8 = 40
Expand Down Expand Up @@ -108,7 +108,7 @@ def test_annotate_document():
assert a['border_bottom_width'] == 42

assert a['color'] == (1, 0, 0, 1)
assert a['border_top_color'] == 'currentColor'
assert a['border_top_color'] == 'currentcolor'

assert div['font_size'] == 40 # 2 * 20px
assert span1['width'] == (160, 'px') # 10 * 16px (root default is 16px)
Expand Down
2 changes: 1 addition & 1 deletion tests/css/test_expanders.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest
import tinycss2
from tinycss2.color3 import parse_color
from tinycss2.color4 import parse_color

from weasyprint.css import preprocess_declarations
from weasyprint.css.properties import INITIAL_VALUES, ZERO_PIXELS
Expand Down
4 changes: 2 additions & 2 deletions tests/layout/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def layout(gradient_css, type_='linear', init=(),
assert result[2] == (None if init is None else pytest.approx(init))
assert result[3] == pytest.approx(positions)
for color1, color2 in zip(result[4], colors):
assert color1 == pytest.approx(color2)
assert tuple(color1) == pytest.approx(color2)

layout('linear-gradient(blue)', 'solid', None, [], [blue])
layout('repeating-linear-gradient(blue)', 'solid', None, [], [blue])
Expand Down Expand Up @@ -417,7 +417,7 @@ def layout(gradient_css, type_='radial', init=(),
assert result[2] == (None if init is None else pytest.approx(init))
assert result[3] == pytest.approx(positions)
for color1, color2 in zip(result[4], colors):
assert color1 == pytest.approx(color2)
assert tuple(color1) == pytest.approx(color2)

layout('radial-gradient(blue)', 'solid', None, [], [blue])
layout('repeating-radial-gradient(blue)', 'solid', None, [], [blue])
Expand Down
2 changes: 1 addition & 1 deletion weasyprint/css/computed_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from math import pi
from urllib.parse import unquote

from tinycss2.color3 import parse_color
from tinycss2.color4 import parse_color

from ..logger import LOGGER
from ..text.ffi import FROM_UNITS, ffi, pango
Expand Down
16 changes: 8 additions & 8 deletions weasyprint/css/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import collections
from math import inf

from tinycss2.color3 import parse_color
from tinycss2.color4 import parse_color

Dimension = collections.namedtuple('Dimension', ['value', 'unit'])

Expand Down Expand Up @@ -49,20 +49,20 @@
'top', Dimension(0, '%')),),
'background_repeat': (('repeat', 'repeat'),),
'background_size': (('auto', 'auto'),),
'border_bottom_color': 'currentColor',
'border_bottom_color': 'currentcolor',
'border_bottom_left_radius': (ZERO_PIXELS, ZERO_PIXELS),
'border_bottom_right_radius': (ZERO_PIXELS, ZERO_PIXELS),
'border_bottom_style': 'none',
'border_bottom_width': 3,
'border_collapse': 'separate',
'border_left_color': 'currentColor',
'border_left_color': 'currentcolor',
'border_left_style': 'none',
'border_left_width': 3,
'border_right_color': 'currentColor',
'border_right_color': 'currentcolor',
'border_right_style': 'none',
'border_right_width': 3,
'border_spacing': (0, 0),
'border_top_color': 'currentColor',
'border_top_color': 'currentcolor',
'border_top_left_radius': (ZERO_PIXELS, ZERO_PIXELS),
'border_top_right_radius': (ZERO_PIXELS, ZERO_PIXELS),
'border_top_style': 'none',
Expand Down Expand Up @@ -96,7 +96,7 @@
# Multi-column Layout (WD): https://www.w3.org/TR/css-multicol-1/
'column_width': 'auto',
'column_count': 'auto',
'column_rule_color': 'currentColor',
'column_rule_color': 'currentcolor',
'column_rule_style': 'none',
'column_rule_width': 'medium',
'column_fill': 'balance',
Expand Down Expand Up @@ -179,7 +179,7 @@

# User Interface 3/4 (REC/WD): https://www.w3.org/TR/css-ui-4/
'appearance': 'none',
'outline_color': 'currentColor', # invert is not supported
'outline_color': 'currentcolor', # invert is not supported
'outline_style': 'none',
'outline_width': 3, # computed value for 'medium'

Expand Down Expand Up @@ -224,7 +224,7 @@

# Text Decoration Module 3 (CR): https://www.w3.org/TR/css-text-decor-3/
'text_decoration_line': 'none',
'text_decoration_color': 'currentColor',
'text_decoration_color': 'currentcolor',
'text_decoration_style': 'solid',

# Overflow Module 3/4 (WD): https://www.w3.org/TR/css-overflow-4/
Expand Down
2 changes: 1 addition & 1 deletion weasyprint/css/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from abc import ABC, abstractmethod
from urllib.parse import unquote, urljoin

from tinycss2.color3 import parse_color
from tinycss2.color4 import parse_color

from .. import LOGGER
from ..urls import iri_to_uri, url_is_absolute
Expand Down
2 changes: 1 addition & 1 deletion weasyprint/css/validation/expanders.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import functools

from tinycss2.ast import DimensionToken, IdentToken, NumberToken
from tinycss2.color3 import parse_color
from tinycss2.color4 import parse_color

from ..properties import INITIAL_VALUES
from .descriptors import expand_font_variant
Expand Down
6 changes: 3 additions & 3 deletions weasyprint/css/validation/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from math import inf

from tinycss2 import parse_component_value_list
from tinycss2.color3 import parse_color
from tinycss2.color4 import parse_color

from .. import computed_values
from ..properties import KNOWN_PROPERTIES, ZERO_PIXELS, Dimension
Expand Down Expand Up @@ -135,7 +135,7 @@ def other_colors(token):
@single_token
def outline_color(token):
if get_keyword(token) == 'invert':
return 'currentColor'
return 'currentcolor'
else:
return parse_color(token)

Expand All @@ -158,7 +158,7 @@ def empty_cells(keyword):
def color(token):
"""``*-color`` and ``color`` properties validation."""
result = parse_color(token)
if result == 'currentColor':
if result == 'currentcolor':
return 'inherit'
else:
return result
Expand Down
3 changes: 1 addition & 2 deletions weasyprint/draw/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,7 @@ def draw_background(stream, bg, clip_box=True, bleed=None, marks=()):
# Draw background color.
if bg.color.alpha > 0:
with stacked(stream):
stream.set_color_rgb(*bg.color[:3])
stream.set_alpha(bg.color.alpha)
stream.set_color(bg.color)
painting_area = bg.layers[-1].painting_area
stream.rectangle(*painting_area)
stream.clip()
Expand Down
27 changes: 9 additions & 18 deletions weasyprint/draw/border.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,19 +500,16 @@ def draw_dots(dashes, line, way, x, y, px, py, chl):

def draw_rounded_border(stream, box, style, color):
if style in ('ridge', 'groove'):
stream.set_color_rgb(*color[0][:3])
stream.set_alpha(color[0][3])
stream.set_color(color[0])
rounded_box(stream, box.rounded_padding_box())
rounded_box(stream, box.rounded_box_ratio(1 / 2))
stream.fill(even_odd=True)
stream.set_color_rgb(*color[1][:3])
stream.set_alpha(color[1][3])
stream.set_color(color[1])
rounded_box(stream, box.rounded_box_ratio(1 / 2))
rounded_box(stream, box.rounded_border_box())
stream.fill(even_odd=True)
return
stream.set_color_rgb(*color[:3])
stream.set_alpha(color[3])
stream.set_color(color)
rounded_box(stream, box.rounded_padding_box())
if style == 'double':
rounded_box(stream, box.rounded_box_ratio(1 / 3))
Expand All @@ -525,8 +522,7 @@ def draw_rect_border(stream, box, widths, style, color):
bbx, bby, bbw, bbh = box
bt, br, bb, bl = widths
if style in ('ridge', 'groove'):
stream.set_color_rgb(*color[0][:3])
stream.set_alpha(color[0][3])
stream.set_color(color[0])
stream.rectangle(*box)
stream.rectangle(
bbx + bl / 2, bby + bt / 2,
Expand All @@ -536,12 +532,10 @@ def draw_rect_border(stream, box, widths, style, color):
bbx + bl / 2, bby + bt / 2,
bbw - (bl + br) / 2, bbh - (bt + bb) / 2)
stream.rectangle(bbx + bl, bby + bt, bbw - bl - br, bbh - bt - bb)
stream.set_color_rgb(*color[1][:3])
stream.set_alpha(color[1][3])
stream.set_color(color[1])
stream.fill(even_odd=True)
return
stream.set_color_rgb(*color[:3])
stream.set_alpha(color[3])
stream.set_color(color)
stream.rectangle(*box)
if style == 'double':
stream.rectangle(
Expand All @@ -559,8 +553,7 @@ def draw_line(stream, x1, y1, x2, y2, thickness, style, color, offset=0):

with stacked(stream):
if style not in ('ridge', 'groove'):
stream.set_color_rgb(*color[:3], stroke=True)
stream.set_alpha(color[3], stroke=True)
stream.set_color(color, stroke=True)

if style == 'dashed':
stream.set_dash([5 * thickness], offset)
Expand All @@ -581,17 +574,15 @@ def draw_line(stream, x1, y1, x2, y2, thickness, style, color, offset=0):
stream.line_to(x2, y2 + thickness / 3)
elif style in ('ridge', 'groove'):
stream.set_line_width(thickness / 2)
stream.set_color_rgb(*color[0][:3], stroke=True)
stream.set_alpha(color[0][3], stroke=True)
stream.set_color(color[0], stroke=True)
if x1 == x2:
stream.move_to(x1 + thickness / 4, y1)
stream.line_to(x2 + thickness / 4, y2)
elif y1 == y2:
stream.move_to(x1, y1 + thickness / 4)
stream.line_to(x2, y2 + thickness / 4)
stream.stroke()
stream.set_color_rgb(*color[1][:3], stroke=True)
stream.set_alpha(color[1][3], stroke=True)
stream.set_color(color[1], stroke=True)
if x1 == x2:
stream.move_to(x1 - thickness / 4, y1)
stream.line_to(x2 - thickness / 4, y2)
Expand Down
16 changes: 11 additions & 5 deletions weasyprint/draw/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,34 @@

from colorsys import hsv_to_rgb, rgb_to_hsv

from tinycss2.color4 import parse_color


def get_color(style, key):
"""Return color, taking care of possible currentColor value."""
value = style[key]
return value if value != 'currentColor' else style['color']
return value if value != 'currentcolor' else style['color']


def darken(color):
"""Return a darker color."""
hue, saturation, value = rgb_to_hsv(color.red, color.green, color.blue)
# TODO: handle color spaces.
hue, saturation, value = rgb_to_hsv(*color.to('srgb')[:3])
value /= 1.5
saturation /= 1.25
return (*hsv_to_rgb(hue, saturation, value), color.alpha)
return parse_color(
'rgb(%f%% %f%% %f%%/%f)' % (*hsv_to_rgb(hue, saturation, value), color.alpha))


def lighten(color):
"""Return a lighter color."""
hue, saturation, value = rgb_to_hsv(color.red, color.green, color.blue)
# TODO: handle color spaces.
hue, saturation, value = rgb_to_hsv(*color.to('srgb')[:3])
value = 1 - (1 - value) / 1.5
if saturation:
saturation = 1 - (1 - saturation) / 1.25
return (*hsv_to_rgb(hue, saturation, value), color.alpha)
return parse_color(
'rgb(%f%% %f%% %f%%/%f)' % (*hsv_to_rgb(hue, saturation, value), color.alpha))


def styled_color(style, color, side):
Expand Down
3 changes: 1 addition & 2 deletions weasyprint/draw/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ def draw_text(stream, textbox, offset_x, text_overflow, block_ellipsis):

# Draw text.
x, y = textbox.position_x, textbox.position_y + textbox.baseline
stream.set_color_rgb(*textbox.style['color'][:3])
stream.set_alpha(textbox.style['color'][3])
stream.set_color(textbox.style['color'])
textbox.pango_layout.reactivate(textbox.style)
stream.begin_text()
emojis = draw_first_line(
Expand Down
17 changes: 10 additions & 7 deletions weasyprint/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import pydyf
from PIL import Image, ImageFile, ImageOps
from tinycss2.color4 import parse_color

from . import DEFAULT_OPTIONS
from .layout.percent import percentage
Expand Down Expand Up @@ -441,6 +442,7 @@ def gradient_average_color(colors, positions):
"""
https://drafts.csswg.org/css-images-3/#gradient-average-color
"""
# TODO: handle color spaces.
nb_stops = len(positions)
assert nb_stops > 1
assert nb_stops == len(colors)
Expand All @@ -461,9 +463,13 @@ def gradient_average_color(colors, positions):
result_g += premul_g[j] * weight
result_b += premul_b[j] * weight
result_a += alpha[j] * weight
# Un-premultiply:
return (result_r / result_a, result_g / result_a,
result_b / result_a, result_a) if result_a != 0 else (0, 0, 0, 0)
# Un-premultiply.
if result_a == 0:
return parse_color('transparent')
else:
return parse_color(
f'rgb({result_r / result_a * 255} {result_g / result_a * 255} '
f'{result_b / result_a * 255}/{ result_a })')


class Gradient:
Expand All @@ -486,10 +492,7 @@ def draw(self, stream, concrete_width, concrete_height, _image_rendering):

if type_ == 'solid':
stream.rectangle(0, 0, concrete_width, concrete_height)
red, green, blue, alpha = colors[0]
stream.set_color_rgb(red, green, blue)
if alpha != 1:
stream.set_alpha(alpha, stroke=False)
stream.set_color(colors[0])
stream.fill()
return

Expand Down
2 changes: 1 addition & 1 deletion weasyprint/layout/background.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from collections import namedtuple
from itertools import cycle

from tinycss2.color3 import parse_color
from tinycss2.color4 import parse_color

from ..formatting_structure import boxes
from . import replaced
Expand Down
4 changes: 2 additions & 2 deletions weasyprint/layout/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from math import inf

import tinycss2.color3
import tinycss2.color4

from ..formatting_structure import boxes
from ..logger import LOGGER
Expand Down Expand Up @@ -928,7 +928,7 @@ def distribute_excess_width(context, grid, excess_width, column_widths, constrai
column_widths[i] += excess_width / len(columns)


TRANSPARENT = tinycss2.color3.parse_color('transparent')
TRANSPARENT = tinycss2.color4.parse_color('transparent')


def collapse_table_borders(table, grid_width, grid_height):
Expand Down
Loading
Loading