LibWeb: Make TransformationSV::to_matrix infallible

Parameter values are absolutized at style computation time (or are
confirmed to be resolvable in the case of reification or DOMMatrix) so
there is no reason this function should fail.
This commit is contained in:
Callum Law 2026-05-21 01:06:08 +12:00 committed by Sam Atkins
parent e07ba236d1
commit 883199cb39
7 changed files with 68 additions and 148 deletions

View file

@ -921,7 +921,7 @@ RefPtr<StyleValue const> CSSStyleProperties::style_value_for_computed_property(L
VERIFY(first_paintable);
auto const& paintable_box = as<Painting::PaintableBox const>(*first_paintable);
for (auto const& transformation : transformations) {
transform = transform * transformation->to_matrix(paintable_box).release_value();
transform = transform * transformation->to_matrix(paintable_box);
}
// https://drafts.csswg.org/css-transforms-1/#2d-matrix

View file

@ -1244,7 +1244,7 @@ RefPtr<StyleValue const> interpolate_transform(DOM::Element& element, Calculatio
return *box;
return {};
}();
parameters = matrix_to_style_value_vector(MUST(transform->to_matrix(paintable_box)));
parameters = matrix_to_style_value_vector(transform->to_matrix(paintable_box));
}
return TransformationStyleValue::create(PropertyID::Transform, generic_function, move(parameters));
};
@ -1349,12 +1349,9 @@ RefPtr<StyleValue const> interpolate_transform(DOM::Element& element, Calculatio
auto post_multiply_remaining_transformations = [&paintable_box](size_t start_index, Vector<NonnullRefPtr<TransformationStyleValue const>> const& transformations) -> Optional<FloatMatrix4x4> {
FloatMatrix4x4 result = FloatMatrix4x4::identity();
for (auto index = start_index; index < transformations.size(); ++index) {
auto transformation_matrix = transformations[index]->to_matrix(paintable_box);
if (transformation_matrix.is_error())
return {};
result = result * transformation_matrix.value();
}
for (auto index = start_index; index < transformations.size(); ++index)
result = result * transformations[index]->to_matrix(paintable_box);
return result;
};
auto from_matrix = post_multiply_remaining_transformations(index, from_transformations);

View file

@ -133,93 +133,27 @@ bool TransformationStyleValue::can_be_converted_to_matrix_without_reference_box(
return true;
}
ErrorOr<FloatMatrix4x4> TransformationStyleValue::to_matrix(Optional<Painting::PaintableBox const&> paintable_box) const
FloatMatrix4x4 TransformationStyleValue::to_matrix(Optional<Painting::PaintableBox const&> paintable_box) const
{
auto count = m_properties.values.size();
auto function_metadata = transform_function_metadata(m_properties.transform_function);
auto length_to_px = [&](Length const& length) -> ErrorOr<float> {
if (paintable_box.has_value())
return length.to_px(paintable_box->layout_node()).to_float();
if (length.is_absolute())
return length.absolute_length_to_px().to_float();
return Error::from_string_literal("Transform contains non absolute units");
};
auto get_value = [&](size_t argument_index, Optional<CSSPixels> reference_length = {}) -> float {
auto const& transformation_value = *m_properties.values[argument_index];
auto get_value = [&](size_t argument_index, Optional<CSSPixels> reference_length = {}) -> ErrorOr<float> {
auto& transformation_value = *m_properties.values[argument_index];
CalculationResolutionContext context;
if (reference_length.has_value())
context.percentage_basis = Length::make_px(reference_length.value());
if (transformation_value.is_calculated()) {
auto& calculated = transformation_value.as_calculated();
switch (function_metadata.parameters[argument_index].type) {
case TransformFunctionParameterType::Angle: {
if (!calculated.resolves_to_angle())
return Error::from_string_literal("Calculated angle parameter to transform function doesn't resolve to an angle.");
if (auto resolved = calculated.resolve_angle(context); resolved.has_value())
return resolved->to_radians();
return Error::from_string_literal("Couldn't resolve calculated angle.");
}
case TransformFunctionParameterType::Length:
case TransformFunctionParameterType::LengthNone: {
if (!calculated.resolves_to_length())
return Error::from_string_literal("Calculated length parameter to transform function doesn't resolve to a length.");
if (auto resolved = calculated.resolve_length(context); resolved.has_value())
return length_to_px(resolved.value());
return Error::from_string_literal("Couldn't resolve calculated length.");
}
case TransformFunctionParameterType::LengthPercentage: {
if (!calculated.resolves_to_length_percentage())
return Error::from_string_literal("Calculated length-percentage parameter to transform function doesn't resolve to a length-percentage.");
if (auto resolved = calculated.resolve_length(context); resolved.has_value())
return length_to_px(resolved.value());
return Error::from_string_literal("Couldn't resolve calculated length-percentage.");
}
case TransformFunctionParameterType::Number: {
if (!calculated.resolves_to_number())
return Error::from_string_literal("Calculated number parameter to transform function doesn't resolve to a number.");
if (auto resolved = calculated.resolve_number(context); resolved.has_value())
return resolved.release_value();
return Error::from_string_literal("Couldn't resolve calculated number.");
}
case TransformFunctionParameterType::NumberPercentage: {
if (calculated.resolves_to_number()) {
if (auto resolved = calculated.resolve_number(context); resolved.has_value())
return calculated.resolve_number(context).value();
return Error::from_string_literal("Couldn't resolve calculated number.");
}
if (calculated.resolves_to_percentage()) {
if (auto resolved = calculated.resolve_percentage(context); resolved.has_value())
return calculated.resolve_percentage(context).value().as_fraction();
return Error::from_string_literal("Couldn't resolve calculated percentage.");
}
return Error::from_string_literal("Calculated number/percentage parameter to transform function doesn't resolve to a number or percentage.");
}
}
switch (function_metadata.parameters[argument_index].type) {
case TransformFunctionParameterType::Angle:
return Angle::from_style_value(transformation_value, {}).to_radians();
case TransformFunctionParameterType::Length:
case TransformFunctionParameterType::LengthNone:
case TransformFunctionParameterType::LengthPercentage:
return Length::from_style_value(transformation_value, reference_length.map([](CSSPixels px) { return Length::make_px(px); })).absolute_length_to_px().to_float();
case TransformFunctionParameterType::Number:
case TransformFunctionParameterType::NumberPercentage:
return number_from_style_value(transformation_value, 1);
}
if (transformation_value.is_length())
return length_to_px(transformation_value.as_length().length());
if (transformation_value.is_percentage()) {
if (function_metadata.parameters[argument_index].type == TransformFunctionParameterType::NumberPercentage) {
return transformation_value.as_percentage().percentage().as_fraction();
}
if (!reference_length.has_value())
return Error::from_string_literal("Can't resolve percentage to length without a reference value.");
return length_to_px(Length::make_px(reference_length.value()).percentage_of(transformation_value.as_percentage().percentage()));
}
if (transformation_value.is_number())
return transformation_value.as_number().number();
if (transformation_value.is_angle())
return transformation_value.as_angle().angle().to_radians();
dbgln("FIXME: Unsupported value in transform! {}", transformation_value.to_string(SerializationMode::Normal));
return Error::from_string_literal("Unsupported value in transform function");
VERIFY_NOT_REACHED();
};
Optional<CSSPixels> width;
@ -238,7 +172,7 @@ ErrorOr<FloatMatrix4x4> TransformationStyleValue::to_matrix(Optional<Painting::P
return FloatMatrix4x4::identity();
// FIXME: Add support for the 'perspective-origin' CSS property.
auto distance = TRY(get_value(0));
auto distance = get_value(0);
// If the depth value is less than '1px', it must be treated as '1px' for the purpose of rendering, for
// computing the resolved value of 'transform', and when used as the endpoint of interpolation.
// Note: The intent of the above rules on values less than '1px' is that they cover the cases where
@ -253,39 +187,39 @@ ErrorOr<FloatMatrix4x4> TransformationStyleValue::to_matrix(Optional<Painting::P
break;
case TransformFunction::Matrix:
if (count == 6)
return FloatMatrix4x4(TRY(get_value(0)), TRY(get_value(2)), 0, TRY(get_value(4)),
TRY(get_value(1)), TRY(get_value(3)), 0, TRY(get_value(5)),
return FloatMatrix4x4(get_value(0), get_value(2), 0, get_value(4),
get_value(1), get_value(3), 0, get_value(5),
0, 0, 1, 0,
0, 0, 0, 1);
break;
case TransformFunction::Matrix3d:
if (count == 16)
return FloatMatrix4x4(TRY(get_value(0)), TRY(get_value(4)), TRY(get_value(8)), TRY(get_value(12)),
TRY(get_value(1)), TRY(get_value(5)), TRY(get_value(9)), TRY(get_value(13)),
TRY(get_value(2)), TRY(get_value(6)), TRY(get_value(10)), TRY(get_value(14)),
TRY(get_value(3)), TRY(get_value(7)), TRY(get_value(11)), TRY(get_value(15)));
return FloatMatrix4x4(get_value(0), get_value(4), get_value(8), get_value(12),
get_value(1), get_value(5), get_value(9), get_value(13),
get_value(2), get_value(6), get_value(10), get_value(14),
get_value(3), get_value(7), get_value(11), get_value(15));
break;
case TransformFunction::Translate:
if (count == 1)
return FloatMatrix4x4(1, 0, 0, TRY(get_value(0, width)),
return FloatMatrix4x4(1, 0, 0, get_value(0, width),
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
if (count == 2)
return FloatMatrix4x4(1, 0, 0, TRY(get_value(0, width)),
0, 1, 0, TRY(get_value(1, height)),
return FloatMatrix4x4(1, 0, 0, get_value(0, width),
0, 1, 0, get_value(1, height),
0, 0, 1, 0,
0, 0, 0, 1);
break;
case TransformFunction::Translate3d:
return FloatMatrix4x4(1, 0, 0, TRY(get_value(0, width)),
0, 1, 0, TRY(get_value(1, height)),
0, 0, 1, TRY(get_value(2)),
return FloatMatrix4x4(1, 0, 0, get_value(0, width),
0, 1, 0, get_value(1, height),
0, 0, 1, get_value(2),
0, 0, 0, 1);
break;
case TransformFunction::TranslateX:
if (count == 1)
return FloatMatrix4x4(1, 0, 0, TRY(get_value(0, width)),
return FloatMatrix4x4(1, 0, 0, get_value(0, width),
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
@ -293,7 +227,7 @@ ErrorOr<FloatMatrix4x4> TransformationStyleValue::to_matrix(Optional<Painting::P
case TransformFunction::TranslateY:
if (count == 1)
return FloatMatrix4x4(1, 0, 0, 0,
0, 1, 0, TRY(get_value(0, height)),
0, 1, 0, get_value(0, height),
0, 0, 1, 0,
0, 0, 0, 1);
break;
@ -301,31 +235,31 @@ ErrorOr<FloatMatrix4x4> TransformationStyleValue::to_matrix(Optional<Painting::P
if (count == 1)
return FloatMatrix4x4(1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, TRY(get_value(0)),
0, 0, 1, get_value(0),
0, 0, 0, 1);
break;
case TransformFunction::Scale:
if (count == 1)
return FloatMatrix4x4(TRY(get_value(0)), 0, 0, 0,
0, TRY(get_value(0)), 0, 0,
return FloatMatrix4x4(get_value(0), 0, 0, 0,
0, get_value(0), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
if (count == 2)
return FloatMatrix4x4(TRY(get_value(0)), 0, 0, 0,
0, TRY(get_value(1)), 0, 0,
return FloatMatrix4x4(get_value(0), 0, 0, 0,
0, get_value(1), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
case TransformFunction::Scale3d:
if (count == 3)
return FloatMatrix4x4(TRY(get_value(0)), 0, 0, 0,
0, TRY(get_value(1)), 0, 0,
0, 0, TRY(get_value(2)), 0,
return FloatMatrix4x4(get_value(0), 0, 0, 0,
0, get_value(1), 0, 0,
0, 0, get_value(2), 0,
0, 0, 0, 1);
break;
case TransformFunction::ScaleX:
if (count == 1)
return FloatMatrix4x4(TRY(get_value(0)), 0, 0, 0,
return FloatMatrix4x4(get_value(0), 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
@ -333,7 +267,7 @@ ErrorOr<FloatMatrix4x4> TransformationStyleValue::to_matrix(Optional<Painting::P
case TransformFunction::ScaleY:
if (count == 1)
return FloatMatrix4x4(1, 0, 0, 0,
0, TRY(get_value(0)), 0, 0,
0, get_value(0), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
@ -341,46 +275,46 @@ ErrorOr<FloatMatrix4x4> TransformationStyleValue::to_matrix(Optional<Painting::P
if (count == 1)
return FloatMatrix4x4(1, 0, 0, 0,
0, 1, 0, 0,
0, 0, TRY(get_value(0)), 0,
0, 0, get_value(0), 0,
0, 0, 0, 1);
break;
case TransformFunction::Rotate3d:
if (count == 4) {
auto axis = FloatVector3 { TRY(get_value(0)), TRY(get_value(1)), TRY(get_value(2)) };
auto axis = FloatVector3 { get_value(0), get_value(1), get_value(2) };
auto epsilon = 1e-5f;
if (axis.length() < epsilon)
return FloatMatrix4x4::identity();
return Gfx::rotation_matrix(axis.normalized(), TRY(get_value(3)));
return Gfx::rotation_matrix(axis.normalized(), get_value(3));
}
break;
case TransformFunction::RotateX:
if (count == 1)
return Gfx::rotation_matrix({ 1.0f, 0.0f, 0.0f }, TRY(get_value(0)));
return Gfx::rotation_matrix({ 1.0f, 0.0f, 0.0f }, get_value(0));
break;
case TransformFunction::RotateY:
if (count == 1)
return Gfx::rotation_matrix({ 0.0f, 1.0f, 0.0f }, TRY(get_value(0)));
return Gfx::rotation_matrix({ 0.0f, 1.0f, 0.0f }, get_value(0));
break;
case TransformFunction::Rotate:
case TransformFunction::RotateZ:
if (count == 1)
return Gfx::rotation_matrix({ 0.0f, 0.0f, 1.0f }, TRY(get_value(0)));
return Gfx::rotation_matrix({ 0.0f, 0.0f, 1.0f }, get_value(0));
break;
case TransformFunction::Skew:
if (count == 1)
return FloatMatrix4x4(1, tanf(TRY(get_value(0))), 0, 0,
return FloatMatrix4x4(1, tanf(get_value(0)), 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
if (count == 2)
return FloatMatrix4x4(1, tanf(TRY(get_value(0))), 0, 0,
tanf(TRY(get_value(1))), 1, 0, 0,
return FloatMatrix4x4(1, tanf(get_value(0)), 0, 0,
tanf(get_value(1)), 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
case TransformFunction::SkewX:
if (count == 1)
return FloatMatrix4x4(1, tanf(TRY(get_value(0))), 0, 0,
return FloatMatrix4x4(1, tanf(get_value(0)), 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
@ -388,7 +322,7 @@ ErrorOr<FloatMatrix4x4> TransformationStyleValue::to_matrix(Optional<Painting::P
case TransformFunction::SkewY:
if (count == 1)
return FloatMatrix4x4(1, 0, 0, 0,
tanf(TRY(get_value(0))), 1, 0, 0,
tanf(get_value(0)), 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
@ -590,7 +524,7 @@ GC::Ptr<CSSTransformComponent> TransformationStyleValue::reify_a_transform_funct
if (!can_be_converted_to_matrix_without_reference_box())
return nullptr;
auto transform_as_matrix = MUST(to_matrix({}));
auto transform_as_matrix = to_matrix({});
auto matrix = Geometry::DOMMatrix::create(realm);
matrix->set_m11(transform_as_matrix[0, 0]);
matrix->set_m12(transform_as_matrix[1, 0]);

View file

@ -29,7 +29,7 @@ public:
StyleValueVector const& values() const { return m_properties.values; }
bool can_be_converted_to_matrix_without_reference_box() const;
ErrorOr<FloatMatrix4x4> to_matrix(Optional<Painting::PaintableBox const&>) const;
FloatMatrix4x4 to_matrix(Optional<Painting::PaintableBox const&>) const;
virtual void serialize(StringBuilder&, SerializationMode) const override;
GC::Ptr<CSSTransformComponent> reify_a_transform_function(JS::Realm&) const;

View file

@ -975,12 +975,8 @@ WebIDL::ExceptionOr<ParsedMatrix> parse_dom_matrix_init_string(JS::Realm& realm,
// 5. Transform all <transform-function>s to 4x4 abstract matrices by following the “Mathematical Description of Transform Functions”. [CSS3-TRANSFORMS]
// 6. Let matrix be a 4x4 abstract matrix as shown in the initial figure of this section. Post-multiply all matrices from left to right and set matrix to this product.
for (auto const& transform : parsed_value) {
auto const& transform_matrix = transform->as_transformation().to_matrix({});
if (transform_matrix.is_error())
return WebIDL::SyntaxError::create(realm, Utf16String::formatted("Failed to parse CSS transform string: {}", transform_matrix.error()));
matrix = matrix * transform_matrix.value();
}
for (auto const& transform : parsed_value)
matrix = matrix * transform->as_transformation().to_matrix({});
// 7. Return matrix and 2dTransform.
Gfx::DoubleMatrix4x4 double_matrix {

View file

@ -194,13 +194,13 @@ static Optional<TransformData> compute_transform(PaintableBox const& paintable_b
auto matrix = Gfx::FloatMatrix4x4::identity();
if (auto const& translate = computed_values.translate())
matrix = matrix * translate->to_matrix(paintable_box).release_value();
matrix = matrix * translate->to_matrix(paintable_box);
if (auto const& rotate = computed_values.rotate())
matrix = matrix * rotate->to_matrix(paintable_box).release_value();
matrix = matrix * rotate->to_matrix(paintable_box);
if (auto const& scale = computed_values.scale())
matrix = matrix * scale->to_matrix(paintable_box).release_value();
matrix = matrix * scale->to_matrix(paintable_box);
for (auto const& transform : computed_values.transformations())
matrix = matrix * transform->to_matrix(paintable_box).release_value();
matrix = matrix * transform->to_matrix(paintable_box);
auto const& css_transform_origin = computed_values.transform_origin();
auto reference_box = paintable_box.transform_reference_box();
CSSPixelPoint origin {
@ -235,7 +235,7 @@ static Optional<Gfx::FloatMatrix4x4> compute_perspective_matrix(PaintableBox con
// length is provided by the value of the perspective property
// NB: Length values less than 1px being clamped to 1px is handled by the perspective() function already.
// FIXME: Create the matrix directly.
perspective_matrix = perspective_matrix * CSS::TransformationStyleValue::create(CSS::PropertyID::Transform, CSS::TransformFunction::Perspective, CSS::StyleValueVector { CSS::LengthStyleValue::create(CSS::Length::make_px(perspective.value())) })->to_matrix({}).release_value();
perspective_matrix = perspective_matrix * CSS::TransformationStyleValue::create(CSS::PropertyID::Transform, CSS::TransformFunction::Perspective, CSS::StyleValueVector { CSS::LengthStyleValue::create(CSS::Length::make_px(perspective.value())) })->to_matrix({});
// 4. Translate by the negated computed X and Y values of 'perspective-origin'
perspective_matrix = perspective_matrix * Gfx::translation_matrix(Vector3<float>(-computed_x, -computed_y, 0));

View file

@ -307,17 +307,10 @@ Optional<Painting::PaintStyle> SVGPatternElement::to_gfx_paint_style(SVGPaintCon
auto css_transformations = computed_properties()->transformations();
if (!css_transformations.is_empty()) {
auto matrix = Gfx::FloatMatrix4x4::identity();
bool transform_valid = true;
for (auto const& css_transform : css_transformations) {
auto result = css_transform->to_matrix(*pattern_paintable);
if (result.is_error()) {
transform_valid = false;
break;
}
matrix = matrix * result.release_value();
}
if (transform_valid)
user_space_pattern_transform = extract_2d_affine_transform(matrix);
for (auto const& css_transform : css_transformations)
matrix = matrix * css_transform->to_matrix(*pattern_paintable);
user_space_pattern_transform = extract_2d_affine_transform(matrix);
} else {
user_space_pattern_transform = pattern_transform();
}