diff --git a/Libraries/LibGfx/Path.h b/Libraries/LibGfx/Path.h index 1526e01c8f..d899170810 100644 --- a/Libraries/LibGfx/Path.h +++ b/Libraries/LibGfx/Path.h @@ -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 clone() const = 0; virtual NonnullOwnPtr copy_transformed(Gfx::AffineTransform const&) const = 0; - virtual NonnullOwnPtr place_text_along(Utf8View const& text, Font const&) const = 0; - virtual NonnullOwnPtr place_text_along(Utf16View const& text, Font const&) const = 0; + virtual NonnullOwnPtr place_text_along(Utf8View const& text, Font const&, float offset = 0) const = 0; + virtual NonnullOwnPtr 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(); } diff --git a/Libraries/LibGfx/PathSkia.cpp b/Libraries/LibGfx/PathSkia.cpp index 48989c2e51..b5c1151ccf 100644 --- a/Libraries/LibGfx/PathSkia.cpp +++ b/Libraries/LibGfx/PathSkia.cpp @@ -131,7 +131,7 @@ void PathImplSkia::offset(Gfx::FloatPoint const& offset) } template -static NonnullOwnPtr place_text_along_impl(SkPath const& path, Font const& font, size_t length_in_code_points, TextToGlyphs&& text_to_glyphs) +static NonnullOwnPtr 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 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 place_text_along_impl(SkPath const& path, Font co return output_path; } -NonnullOwnPtr PathImplSkia::place_text_along(Utf8View const& text, Font const& font) const +NonnullOwnPtr 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 PathImplSkia::place_text_along(Utf16View const& text, Font const& font) const +NonnullOwnPtr 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; diff --git a/Libraries/LibGfx/PathSkia.h b/Libraries/LibGfx/PathSkia.h index 8e5710b1e2..a83fe7fda5 100644 --- a/Libraries/LibGfx/PathSkia.h +++ b/Libraries/LibGfx/PathSkia.h @@ -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 clone() const override; virtual NonnullOwnPtr copy_transformed(Gfx::AffineTransform const&) const override; - virtual NonnullOwnPtr place_text_along(Utf8View const& text, Font const&) const override; - virtual NonnullOwnPtr place_text_along(Utf16View const& text, Font const&) const override; + virtual NonnullOwnPtr place_text_along(Utf8View const& text, Font const&, float offset = 0) const override; + virtual NonnullOwnPtr place_text_along(Utf16View const& text, Font const&, float offset = 0) const override; virtual String to_svg_string() const override; diff --git a/Libraries/LibWeb/Layout/SVGFormattingContext.cpp b/Libraries/LibWeb/Layout/SVGFormattingContext.cpp index 85a7d138cd..05a8cae0be 100644 --- a/Libraries/LibWeb/Layout/SVGFormattingContext.cpp +++ b/Libraries/LibWeb/Layout/SVGFormattingContext.cpp @@ -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(*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) diff --git a/Libraries/LibWeb/SVG/SVGTextPathElement.cpp b/Libraries/LibWeb/SVG/SVGTextPathElement.cpp index 6d357ed59f..baddc80e71 100644 --- a/Libraries/LibWeb/SVG/SVGTextPathElement.cpp +++ b/Libraries/LibWeb/SVG/SVGTextPathElement.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace Web::SVG { @@ -19,6 +20,14 @@ SVGTextPathElement::SVGTextPathElement(DOM::Document& document, DOM::QualifiedNa { } +void SVGTextPathElement::attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional 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 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 SVGTextPathElement::path_or_shape() const return try_resolve_url_to(*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 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); diff --git a/Libraries/LibWeb/SVG/SVGTextPathElement.h b/Libraries/LibWeb/SVG/SVGTextPathElement.h index 4f7d83e32f..3b4ea8a7b0 100644 --- a/Libraries/LibWeb/SVG/SVGTextPathElement.h +++ b/Libraries/LibWeb/SVG/SVGTextPathElement.h @@ -6,6 +6,8 @@ #pragma once +#include +#include #include #include #include @@ -24,11 +26,19 @@ public: GC::Ptr path_or_shape() const; + float start_offset_for_path_length(float path_length) const; + + GC::Ref 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 const& old_value, Optional const& value, Optional const& namespace_) override; + +private: + Optional m_start_offset; }; } diff --git a/Libraries/LibWeb/SVG/SVGTextPathElement.idl b/Libraries/LibWeb/SVG/SVGTextPathElement.idl index 4ab872e575..ebb39c16c9 100644 --- a/Libraries/LibWeb/SVG/SVGTextPathElement.idl +++ b/Libraries/LibWeb/SVG/SVGTextPathElement.idl @@ -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; }; diff --git a/Tests/LibWeb/Ref/expected/svg-textPath-startOffset-ref.html b/Tests/LibWeb/Ref/expected/svg-textPath-startOffset-ref.html new file mode 100644 index 0000000000..7f2f2d00a0 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/svg-textPath-startOffset-ref.html @@ -0,0 +1,9 @@ + + + + + + + Test + + diff --git a/Tests/LibWeb/Ref/input/svg-textPath-startOffset.html b/Tests/LibWeb/Ref/input/svg-textPath-startOffset.html new file mode 100644 index 0000000000..df06a8a7e8 --- /dev/null +++ b/Tests/LibWeb/Ref/input/svg-textPath-startOffset.html @@ -0,0 +1,10 @@ + + + + + + + + Test + + diff --git a/Tests/LibWeb/Text/expected/wpt-import/svg/idlharness.window.txt b/Tests/LibWeb/Text/expected/wpt-import/svg/idlharness.window.txt index 7dc0edd5fd..3f25b9daa9 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/svg/idlharness.window.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/svg/idlharness.window.txt @@ -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