LibGfx+LibWeb: Implement SVGTextPathElement.startOffset

This commit is contained in:
Tim Ledbetter 2026-05-27 11:35:55 +01:00 committed by Andreas Kling
parent d92f93f34e
commit ad7105b570
10 changed files with 83 additions and 19 deletions

View file

@ -47,13 +47,14 @@ public:
[[nodiscard]] virtual bool is_empty() const = 0;
virtual Gfx::FloatPoint last_point() const = 0;
virtual Gfx::FloatRect bounding_box() const = 0;
virtual float length() const = 0;
virtual void set_fill_type(Gfx::WindingRule winding_rule) = 0;
virtual bool contains(FloatPoint point, Gfx::WindingRule) const = 0;
virtual NonnullOwnPtr<PathImpl> clone() const = 0;
virtual NonnullOwnPtr<PathImpl> copy_transformed(Gfx::AffineTransform const&) const = 0;
virtual NonnullOwnPtr<PathImpl> place_text_along(Utf8View const& text, Font const&) const = 0;
virtual NonnullOwnPtr<PathImpl> place_text_along(Utf16View const& text, Font const&) const = 0;
virtual NonnullOwnPtr<PathImpl> place_text_along(Utf8View const& text, Font const&, float offset = 0) const = 0;
virtual NonnullOwnPtr<PathImpl> place_text_along(Utf16View const& text, Font const&, float offset = 0) const = 0;
virtual String to_svg_string() const = 0;
};
@ -116,13 +117,14 @@ public:
[[nodiscard]] bool is_empty() const { return impl().is_empty(); }
Gfx::FloatPoint last_point() const { return impl().last_point(); }
Gfx::FloatRect bounding_box() const { return impl().bounding_box(); }
float length() const { return impl().length(); }
bool contains(FloatPoint point, Gfx::WindingRule winding_rule) const { return impl().contains(point, winding_rule); }
void set_fill_type(Gfx::WindingRule winding_rule) { impl().set_fill_type(winding_rule); }
Gfx::Path clone() const { return Gfx::Path { impl().clone() }; }
Gfx::Path copy_transformed(Gfx::AffineTransform const& transform) const { return Gfx::Path { impl().copy_transformed(transform) }; }
Gfx::Path place_text_along(Utf8View const& text, Font const& font) const { return Gfx::Path { impl().place_text_along(text, font) }; }
Gfx::Path place_text_along(Utf16View const& text, Font const& font) const { return Gfx::Path { impl().place_text_along(text, font) }; }
Gfx::Path place_text_along(Utf8View const& text, Font const& font, float offset = 0) const { return Gfx::Path { impl().place_text_along(text, font, offset) }; }
Gfx::Path place_text_along(Utf16View const& text, Font const& font, float offset = 0) const { return Gfx::Path { impl().place_text_along(text, font, offset) }; }
String to_svg_string() const { return impl().to_svg_string(); }

View file

@ -131,7 +131,7 @@ void PathImplSkia::offset(Gfx::FloatPoint const& offset)
}
template<typename TextToGlyphs>
static NonnullOwnPtr<PathImpl> place_text_along_impl(SkPath const& path, Font const& font, size_t length_in_code_points, TextToGlyphs&& text_to_glyphs)
static NonnullOwnPtr<PathImpl> place_text_along_impl(SkPath const& path, Font const& font, size_t length_in_code_points, float offset, TextToGlyphs&& text_to_glyphs)
{
auto sk_font = font.skia_font(1);
SkScalar x = 0;
@ -142,7 +142,7 @@ static NonnullOwnPtr<PathImpl> place_text_along_impl(SkPath const& path, Font co
text_to_glyphs(sk_font, run_buffer);
SkPathMeasure path_measure(path, false);
SkScalar accumulated_distance = 0;
SkScalar accumulated_distance = offset;
auto output_path = PathImplSkia::create();
SkScalar path_length = path_measure.getLength();
@ -178,23 +178,23 @@ static NonnullOwnPtr<PathImpl> place_text_along_impl(SkPath const& path, Font co
return output_path;
}
NonnullOwnPtr<PathImpl> PathImplSkia::place_text_along(Utf8View const& text, Font const& font) const
NonnullOwnPtr<PathImpl> PathImplSkia::place_text_along(Utf8View const& text, Font const& font, float offset) const
{
auto length_in_code_points = text.length();
return place_text_along_impl(*m_path, font, length_in_code_points, [&](auto const& sk_font, auto const& run_buffer) {
return place_text_along_impl(*m_path, font, length_in_code_points, offset, [&](auto const& sk_font, auto const& run_buffer) {
sk_font.textToGlyphs(text.as_string().characters_without_null_termination(), text.as_string().length(), SkTextEncoding::kUTF8, run_buffer.glyphs, length_in_code_points);
});
}
NonnullOwnPtr<PathImpl> PathImplSkia::place_text_along(Utf16View const& text, Font const& font) const
NonnullOwnPtr<PathImpl> PathImplSkia::place_text_along(Utf16View const& text, Font const& font, float offset) const
{
if (text.has_ascii_storage())
return place_text_along(Utf8View { text.bytes() }, font);
return place_text_along(Utf8View { text.bytes() }, font, offset);
auto length_in_code_points = text.length_in_code_points();
return place_text_along_impl(*m_path, font, length_in_code_points, [&](auto const& sk_font, auto const& run_buffer) {
return place_text_along_impl(*m_path, font, length_in_code_points, offset, [&](auto const& sk_font, auto const& run_buffer) {
sk_font.textToGlyphs(text.utf16_span().data(), text.length_in_code_units() * sizeof(char16_t), SkTextEncoding::kUTF16, run_buffer.glyphs, length_in_code_points);
});
}
@ -243,6 +243,12 @@ Gfx::FloatRect PathImplSkia::bounding_box() const
return { bounds.fLeft, bounds.fTop, bounds.fRight - bounds.fLeft, bounds.fBottom - bounds.fTop };
}
float PathImplSkia::length() const
{
SkPathMeasure path_measure(*m_path, false);
return path_measure.getLength();
}
bool PathImplSkia::contains(FloatPoint point, Gfx::WindingRule winding_rule) const
{
SkPath temp_path = *m_path;

View file

@ -40,13 +40,14 @@ public:
[[nodiscard]] virtual bool is_empty() const override;
virtual Gfx::FloatPoint last_point() const override;
virtual Gfx::FloatRect bounding_box() const override;
virtual float length() const override;
virtual bool contains(FloatPoint point, Gfx::WindingRule) const override;
virtual void set_fill_type(Gfx::WindingRule winding_rule) override;
virtual NonnullOwnPtr<PathImpl> clone() const override;
virtual NonnullOwnPtr<PathImpl> copy_transformed(Gfx::AffineTransform const&) const override;
virtual NonnullOwnPtr<PathImpl> place_text_along(Utf8View const& text, Font const&) const override;
virtual NonnullOwnPtr<PathImpl> place_text_along(Utf16View const& text, Font const&) const override;
virtual NonnullOwnPtr<PathImpl> place_text_along(Utf8View const& text, Font const&, float offset = 0) const override;
virtual NonnullOwnPtr<PathImpl> place_text_along(Utf16View const& text, Font const&, float offset = 0) const override;
virtual String to_svg_string() const override;

View file

@ -460,7 +460,8 @@ Gfx::Path SVGFormattingContext::compute_path_for_text_path(SVGTextPathBox const&
auto text_contents = rendered_text_contents(text_path_element);
auto shape_path = const_cast<SVG::SVGGeometryElement&>(*path_or_shape).get_path(m_viewport_size);
return shape_path.place_text_along(text_contents, font);
auto start_offset = text_path_element.start_offset_for_path_length(shape_path.length());
return shape_path.place_text_along(text_contents, font, start_offset);
}
void SVGFormattingContext::layout_path_like_element(SVGGraphicsBox const& graphics_box)

View file

@ -8,6 +8,7 @@
#include <LibWeb/Bindings/SVGTextPathElement.h>
#include <LibWeb/Layout/SVGTextPathBox.h>
#include <LibWeb/SVG/AttributeNames.h>
#include <LibWeb/SVG/SVGLength.h>
#include <LibWeb/SVG/SVGTextPathElement.h>
namespace Web::SVG {
@ -19,6 +20,14 @@ SVGTextPathElement::SVGTextPathElement(DOM::Document& document, DOM::QualifiedNa
{
}
void SVGTextPathElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_)
{
Base::attribute_changed(name, old_value, value, namespace_);
if (name == SVG::AttributeNames::startOffset)
m_start_offset = AttributeParser::parse_number_percentage(value.value_or(String {}));
}
GC::Ptr<SVGGeometryElement const> SVGTextPathElement::path_or_shape() const
{
auto href = has_attribute(AttributeNames::href) ? get_attribute(AttributeNames::href) : get_attribute(AttributeNames::xlink_href);
@ -27,6 +36,22 @@ GC::Ptr<SVGGeometryElement const> SVGTextPathElement::path_or_shape() const
return try_resolve_url_to<SVGGeometryElement const>(*href);
}
// https://svgwg.org/svg2-draft/text.html#TextPathElementStartOffsetAttribute
float SVGTextPathElement::start_offset_for_path_length(float path_length) const
{
if (!m_start_offset.has_value())
return 0;
return m_start_offset->resolve_relative_to(path_length);
}
// https://svgwg.org/svg2-draft/text.html#__svg__SVGTextPathElement__startOffset
GC::Ref<SVGAnimatedLength> SVGTextPathElement::start_offset() const
{
auto base_length = SVGLength::create(realm(), 0, m_start_offset.value_or(NumberPercentage::create_number(0)).value(), SVGLength::ReadOnly::No);
auto anim_length = SVGLength::create(realm(), 0, m_start_offset.value_or(NumberPercentage::create_number(0)).value(), SVGLength::ReadOnly::Yes);
return SVGAnimatedLength::create(realm(), base_length, anim_length);
}
void SVGTextPathElement::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGTextPathElement);

View file

@ -6,6 +6,8 @@
#pragma once
#include <LibWeb/SVG/AttributeParser.h>
#include <LibWeb/SVG/SVGAnimatedLength.h>
#include <LibWeb/SVG/SVGGeometryElement.h>
#include <LibWeb/SVG/SVGTextContentElement.h>
#include <LibWeb/SVG/SVGURIReference.h>
@ -24,11 +26,19 @@ public:
GC::Ptr<SVGGeometryElement const> path_or_shape() const;
float start_offset_for_path_length(float path_length) const;
GC::Ref<SVGAnimatedLength> start_offset() const;
protected:
SVGTextPathElement(DOM::Document&, DOM::QualifiedName);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
private:
Optional<NumberPercentage> m_start_offset;
};
}

View file

@ -11,7 +11,7 @@ interface SVGTextPathElement : SVGTextContentElement {
const unsigned short TEXTPATH_SPACINGTYPE_AUTO = 1;
const unsigned short TEXTPATH_SPACINGTYPE_EXACT = 2;
[FIXME, SameObject] readonly attribute SVGAnimatedLength startOffset;
[SameObject] readonly attribute SVGAnimatedLength startOffset;
[FIXME, SameObject] readonly attribute SVGAnimatedEnumeration method;
[FIXME, SameObject] readonly attribute SVGAnimatedEnumeration spacing;
};

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<svg width="300" height="100" viewBox="0 0 300 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="line" d="M 150 50 h 100"/>
</defs>
<text font-family="sans-serif" font-size="20">
<textPath href="#line">Test</textPath>
</text>
</svg>

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-textPath-startOffset-ref.html">
<svg width="300" height="100" viewBox="0 0 300 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="line" d="M 50 50 h 200"/>
</defs>
<text font-family="sans-serif" font-size="20">
<textPath href="#line" startOffset="50%">Test</textPath>
</text>
</svg>

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 1780 tests
1101 Pass
679 Fail
1103 Pass
677 Fail
Pass idl_test setup
Pass idl_test validation
Pass Partial interface Document: original interface defined
@ -1236,7 +1236,7 @@ Pass SVGTextPathElement interface: constant TEXTPATH_SPACINGTYPE_AUTO on interfa
Pass SVGTextPathElement interface: constant TEXTPATH_SPACINGTYPE_AUTO on interface prototype object
Pass SVGTextPathElement interface: constant TEXTPATH_SPACINGTYPE_EXACT on interface object
Pass SVGTextPathElement interface: constant TEXTPATH_SPACINGTYPE_EXACT on interface prototype object
Fail SVGTextPathElement interface: attribute startOffset
Pass SVGTextPathElement interface: attribute startOffset
Fail SVGTextPathElement interface: attribute method
Fail SVGTextPathElement interface: attribute spacing
Pass SVGTextPathElement interface: attribute href
@ -1248,7 +1248,7 @@ Pass SVGTextPathElement interface: objects.textPath must inherit property "TEXTP
Pass SVGTextPathElement interface: objects.textPath must inherit property "TEXTPATH_SPACINGTYPE_UNKNOWN" with the proper type
Pass SVGTextPathElement interface: objects.textPath must inherit property "TEXTPATH_SPACINGTYPE_AUTO" with the proper type
Pass SVGTextPathElement interface: objects.textPath must inherit property "TEXTPATH_SPACINGTYPE_EXACT" with the proper type
Fail SVGTextPathElement interface: objects.textPath must inherit property "startOffset" with the proper type
Pass SVGTextPathElement interface: objects.textPath must inherit property "startOffset" with the proper type
Fail SVGTextPathElement interface: objects.textPath must inherit property "method" with the proper type
Fail SVGTextPathElement interface: objects.textPath must inherit property "spacing" with the proper type
Pass SVGTextPathElement interface: objects.textPath must inherit property "href" with the proper type