LibWeb: Implement CanvasRenderingContext2D.measureText

This requires an implementation of the "text preparation algorithm" as
specified here:

html.spec.whatwg.org/multipage/canvas.html#text-preparation-algorithm

However, we're missing a lot of things such as the
CanvasTextDrawingStyles interface, so most of the algorithm was not
implemented. Additionally, we also are not able to use a LineBox like
the algorithm suggests, because our layouting infra is not up to the
task yet. The prepare_text function does nothing other than figuring out
the width of the given text and return glyphs with offsets at the
moment.
This commit is contained in:
sin-ack 2021-12-30 22:15:38 +00:00 committed by Linus Groh
parent 732e41714a
commit 9121cc7cae
Notes: sideshowbarker 2024-07-17 21:39:27 +09:00
10 changed files with 253 additions and 0 deletions

View file

@ -937,6 +937,8 @@ static bool is_wrappable_type(IDL::Type const& type)
return true;
if (type.name == "NamedNodeMap")
return true;
if (type.name == "TextMetrics")
return true;
return false;
}
@ -1637,6 +1639,7 @@ void generate_implementation(IDL::Interface const& interface)
#include <LibWeb/Bindings/MessagePortWrapper.h>
#include <LibWeb/Bindings/NamedNodeMapWrapper.h>
#include <LibWeb/Bindings/NodeWrapperFactory.h>
#include <LibWeb/Bindings/TextMetricsWrapper.h>
#include <LibWeb/Bindings/TextWrapper.h>
#include <LibWeb/Bindings/WindowObject.h>
#include <LibWeb/DOM/Element.h>
@ -2808,6 +2811,7 @@ void generate_prototype_implementation(IDL::Interface const& interface)
#include <LibWeb/Bindings/RangeWrapper.h>
#include <LibWeb/Bindings/StyleSheetListWrapper.h>
#include <LibWeb/Bindings/SubtleCryptoWrapper.h>
#include <LibWeb/Bindings/TextMetricsWrapper.h>
#include <LibWeb/Bindings/TextWrapper.h>
#include <LibWeb/Bindings/URLSearchParamsWrapper.h>
#include <LibWeb/Bindings/WindowObject.h>

View file

@ -266,6 +266,8 @@
#include <LibWeb/Bindings/TextConstructor.h>
#include <LibWeb/Bindings/TextEncoderConstructor.h>
#include <LibWeb/Bindings/TextEncoderPrototype.h>
#include <LibWeb/Bindings/TextMetricsConstructor.h>
#include <LibWeb/Bindings/TextMetricsPrototype.h>
#include <LibWeb/Bindings/TextPrototype.h>
#include <LibWeb/Bindings/UIEventConstructor.h>
#include <LibWeb/Bindings/UIEventPrototype.h>
@ -422,6 +424,7 @@
ADD_WINDOW_OBJECT_INTERFACE(SVGSVGElement) \
ADD_WINDOW_OBJECT_INTERFACE(Text) \
ADD_WINDOW_OBJECT_INTERFACE(TextEncoder) \
ADD_WINDOW_OBJECT_INTERFACE(TextMetrics) \
ADD_WINDOW_OBJECT_INTERFACE(UIEvent) \
ADD_WINDOW_OBJECT_INTERFACE(URLSearchParams) \
ADD_WINDOW_OBJECT_INTERFACE(URL) \

View file

@ -195,6 +195,7 @@ set(SOURCES
HTML/Scripting/Script.cpp
HTML/SyntaxHighlighter/SyntaxHighlighter.cpp
HTML/TagNames.cpp
HTML/TextMetrics.cpp
HTML/WebSocket.cpp
HighResolutionTime/Performance.cpp
ImageDecoding.cpp
@ -485,6 +486,7 @@ libweb_js_wrapper(HTML/MessagePort)
libweb_js_wrapper(HTML/PageTransitionEvent)
libweb_js_wrapper(HTML/PromiseRejectionEvent)
libweb_js_wrapper(HTML/SubmitEvent)
libweb_js_wrapper(HTML/TextMetrics)
libweb_js_wrapper(HTML/WebSocket)
libweb_js_wrapper(HighResolutionTime/Performance)
libweb_js_wrapper(IntersectionObserver/IntersectionObserver)

View file

@ -208,6 +208,7 @@ class MessagePort;
class PageTransitionEvent;
class PromiseRejectionEvent;
class SubmitEvent;
class TextMetrics;
class WebSocket;
}
@ -439,6 +440,7 @@ class SVGGraphicsElementWrapper;
class SVGPathElementWrapper;
class SVGSVGElementWrapper;
class TextEncoderWrapper;
class TextMetricsWrapper;
class TextWrapper;
class UIEventWrapper;
class URLConstructor;

View file

@ -8,10 +8,14 @@
#include <AK/OwnPtr.h>
#include <LibGfx/Painter.h>
#include <LibWeb/Bindings/CanvasRenderingContext2DWrapper.h>
#include <LibWeb/Bindings/WindowObject.h>
#include <LibWeb/DOM/Window.h>
#include <LibWeb/HTML/CanvasRenderingContext2D.h>
#include <LibWeb/HTML/HTMLCanvasElement.h>
#include <LibWeb/HTML/HTMLImageElement.h>
#include <LibWeb/HTML/ImageData.h>
#include <LibWeb/HTML/TextMetrics.h>
#include <LibWeb/Layout/TextNode.h>
namespace Web::HTML {
@ -365,4 +369,126 @@ void CanvasRenderingContext2D::reset_to_default_state()
did_draw(painter->target()->rect().to_type<float>());
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-measuretext
RefPtr<TextMetrics> CanvasRenderingContext2D::measure_text(String const& text)
{
// The measureText(text) method steps are to run the text preparation
// algorithm, passing it text and the object implementing the CanvasText
// interface, and then using the returned inline box must return a new
// TextMetrics object with members behaving as described in the following
// list:
auto prepared_text = prepare_text(text);
auto metrics = TextMetrics::create();
// FIXME: Use the font that was used to create the glyphs in prepared_text.
auto& font = Gfx::FontDatabase::default_font();
// width attribute: The width of that inline box, in CSS pixels. (The text's advance width.)
metrics->set_width(prepared_text.bounding_box.width());
// actualBoundingBoxLeft attribute: The distance parallel to the baseline from the alignment point given by the textAlign attribute to the left side of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going left from the given alignment point.
metrics->set_actual_bounding_box_left(-prepared_text.bounding_box.left());
// actualBoundingBoxRight attribute: The distance parallel to the baseline from the alignment point given by the textAlign attribute to the right side of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going right from the given alignment point.
metrics->set_actual_bounding_box_right(prepared_text.bounding_box.right());
// fontBoundingBoxAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the ascent metric of the first available font, in CSS pixels; positive numbers indicating a distance going up from the given baseline.
metrics->set_font_bounding_box_ascent(font.baseline());
// fontBoundingBoxDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the descent metric of the first available font, in CSS pixels; positive numbers indicating a distance going down from the given baseline.
metrics->set_font_bounding_box_descent(prepared_text.bounding_box.height() - font.baseline());
// actualBoundingBoxAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the top of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going up from the given baseline.
metrics->set_actual_bounding_box_ascent(font.baseline());
// actualBoundingBoxDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the bottom of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going down from the given baseline.
metrics->set_actual_bounding_box_descent(prepared_text.bounding_box.height() - font.baseline());
// emHeightAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the highest top of the em squares in the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the top of that em square (so this value will usually be positive). Zero if the given baseline is the top of that em square; half the font size if the given baseline is the middle of that em square.
metrics->set_em_height_ascent(font.baseline());
// emHeightDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the lowest bottom of the em squares in the inline box, in CSS pixels; positive numbers indicating that the given baseline is above the bottom of that em square. (Zero if the given baseline is the bottom of that em square.)
metrics->set_em_height_descent(prepared_text.bounding_box.height() - font.baseline());
// hangingBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the hanging baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the hanging baseline. (Zero if the given baseline is the hanging baseline.)
metrics->set_hanging_baseline(font.baseline());
// alphabeticBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the alphabetic baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the alphabetic baseline. (Zero if the given baseline is the alphabetic baseline.)
metrics->set_font_bounding_box_ascent(0);
// ideographicBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the ideographic-under baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the ideographic-under baseline. (Zero if the given baseline is the ideographic-under baseline.)
metrics->set_font_bounding_box_ascent(0);
return metrics;
}
// https://html.spec.whatwg.org/multipage/canvas.html#text-preparation-algorithm
CanvasRenderingContext2D::PreparedText CanvasRenderingContext2D::prepare_text(String const& text, float max_width)
{
// 1. If maxWidth was provided but is less than or equal to zero or equal to NaN, then return an empty array.
if (max_width <= 0 || max_width != max_width) {
return {};
}
// 2. Replace all ASCII whitespace in text with U+0020 SPACE characters.
// NOTE: This also replaces vertical tabs with space even though WHATWG
// doesn't consider it as whitespace.
StringBuilder builder { text.length() };
for (auto c : text) {
builder.append(is_ascii_space(c) ? ' ' : c);
}
String replaced_text = builder.build();
// 3. Let font be the current font of target, as given by that object's font attribute.
// FIXME: Once we have CanvasTextDrawingStyles, implement font selection.
// 4. Apply the appropriate step from the following list to determine the value of direction:
// 4.1. If the target object's direction attribute has the value "ltr": Let direction be 'ltr'.
// 4.2. If the target object's direction attribute has the value "rtl": Let direction be 'rtl'.
// 4.3. If the target object's font style source object is an element: Let direction be the directionality of the target object's font style source object.
// 4.4. If the target object's font style source object is a Document with a non-null document element: Let direction be the directionality of the target object's font style source object's document element.
// 4.5. Otherwise: Let direction be 'ltr'.
// FIXME: Once we have CanvasTextDrawingStyles, implement directionality.
// 5. Form a hypothetical infinitely-wide CSS line box containing a single inline box containing the text text, with its CSS properties set as follows:
// 'direction' -> direction
// 'font' -> font
// 'font-kerning' -> target's fontKerning
// 'font-stretch' -> target's fontStretch
// 'font-variant-caps' -> target's fontVariantCaps
// 'letter-spacing' -> target's letterSpacing
// SVG text-rendering -> target's textRendering
// 'white-space' -> 'pre'
// 'word-spacing' -> target's wordSpacing
// ...and with all other properties set to their initial values.
// FIXME: Actually use a LineBox here instead of, you know, using the default font and measuring its size (which is not the spec at all).
// FIXME: Once we have CanvasTextDrawingStyles, add the CSS attributes.
auto& font = Gfx::FontDatabase::default_font();
size_t width = 0;
size_t height = font.glyph_height();
for (auto c : Utf8View { replaced_text }) {
width += font.glyph_or_emoji_width(c);
}
// 6. If maxWidth was provided and the hypothetical width of the inline box in the hypothetical line box is greater than maxWidth CSS pixels, then change font to have a more condensed font (if one is available or if a reasonably readable one can be synthesized by applying a horizontal scale factor to the font) or a smaller font, and return to the previous step.
// FIXME: Record the font size used for this piece of text, and actually retry with a smaller size if needed.
// 7. The anchor point is a point on the inline box, and the physical alignment is one of the values left, right, and center. These variables are determined by the textAlign and textBaseline values as follows:
// Horizontal position:
// 7.1. If textAlign is left, if textAlign is start and direction is 'ltr' or if textAlign is end and direction is 'rtl': Let the anchor point's horizontal position be the left edge of the inline box, and let physical alignment be left.
// 7.2. If textAlign is right, if textAlign is end and direction is 'ltr' or if textAlign is start and direction is 'rtl': Let the anchor point's horizontal position be the right edge of the inline box, and let physical alignment be right.
// 7.3. If textAlign is center: Let the anchor point's horizontal position be half way between the left and right edges of the inline box, and let physical alignment be center.
// Vertical position:
// 7.4. If textBaseline is top: Let the anchor point's vertical position be the top of the em box of the first available font of the inline box.
// 7.5. If textBaseline is hanging: Let the anchor point's vertical position be the hanging baseline of the first available font of the inline box.
// 7.6. If textBaseline is middle: Let the anchor point's vertical position be half way between the bottom and the top of the em box of the first available font of the inline box.
// 7.7. If textBaseline is alphabetic: Let the anchor point's vertical position be the alphabetic baseline of the first available font of the inline box.
// 7.8. If textBaseline is ideographic: Let the anchor point's vertical position be the ideographic-under baseline of the first available font of the inline box.
// 7.9. If textBaseline is bottom: Let the anchor point's vertical position be the bottom of the em box of the first available font of the inline box.
// FIXME: Once we have CanvasTextDrawingStyles, handle the alignment and baseline.
Gfx::IntPoint anchor { 0, 0 };
auto physical_alignment = Gfx::TextAlignment::CenterLeft;
// 8. Let result be an array constructed by iterating over each glyph in the inline box from left to right (if any), adding to the array, for each glyph, the shape of the glyph as it is in the inline box, positioned on a coordinate space using CSS pixels with its origin is at the anchor point.
PreparedText prepared_text { {}, physical_alignment, { 0, 0, static_cast<int>(width), static_cast<int>(height) } };
prepared_text.glyphs.ensure_capacity(replaced_text.length());
size_t offset = 0;
for (auto c : Utf8View { replaced_text }) {
prepared_text.glyphs.append({ c, { static_cast<int>(offset), 0 } });
offset += font.glyph_or_emoji_width(c);
}
// 9. Return result, physical alignment, and the inline box.
return prepared_text;
}
}

View file

@ -14,6 +14,8 @@
#include <LibGfx/Path.h>
#include <LibWeb/Bindings/Wrappable.h>
#include <LibWeb/DOM/ExceptionOr.h>
#include <LibWeb/Layout/InlineNode.h>
#include <LibWeb/Layout/LineBox.h>
namespace Web::HTML {
@ -78,10 +80,24 @@ public:
HTMLCanvasElement* canvas() { return m_element; }
RefPtr<TextMetrics> measure_text(String const& text);
private:
explicit CanvasRenderingContext2D(HTMLCanvasElement&);
struct PreparedTextGlyph {
unsigned int c;
Gfx::IntPoint position;
};
struct PreparedText {
Vector<PreparedTextGlyph> glyphs;
Gfx::TextAlignment physical_alignment;
Gfx::IntRect bounding_box;
};
void did_draw(const Gfx::FloatRect&);
PreparedText prepare_text(String const& text, float max_width = INFINITY);
OwnPtr<Gfx::Painter> painter();

View file

@ -38,4 +38,5 @@ interface CanvasRenderingContext2D {
readonly attribute HTMLCanvasElement canvas;
TextMetrics measureText(DOMString text);
};

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2021, sin-ack <sin-ack@protonmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "TextMetrics.h"
namespace Web::HTML {
RefPtr<TextMetrics> TextMetrics::create()
{
return adopt_ref(*new TextMetrics());
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2021, sin-ack <sin-ack@protonmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Heap/Handle.h>
#include <LibWeb/Bindings/Wrappable.h>
namespace Web::HTML {
class TextMetrics
: public RefCounted<TextMetrics>
, public Bindings::Wrappable {
public:
using WrapperType = Bindings::TextMetricsWrapper;
static RefPtr<TextMetrics> create();
double width() const { return m_width; }
double actual_bounding_box_left() const { return m_actual_bounding_box_left; }
double actual_bounding_box_right() const { return m_actual_bounding_box_right; }
double font_bounding_box_ascent() const { return m_font_bounding_box_ascent; }
double font_bounding_box_descent() const { return m_font_bounding_box_descent; }
double actual_bounding_box_ascent() const { return m_actual_bounding_box_ascent; }
double actual_bounding_box_descent() const { return m_actual_bounding_box_descent; }
double em_height_ascent() const { return m_em_height_ascent; }
double em_height_descent() const { return m_em_height_descent; }
double hanging_baseline() const { return m_hanging_baseline; }
double alphabetic_baseline() const { return m_alphabetic_baseline; }
double ideographic_baseline() const { return m_ideographic_baseline; }
void set_width(double width) { m_width = width; }
void set_actual_bounding_box_left(double left) { m_actual_bounding_box_left = left; }
void set_actual_bounding_box_right(double right) { m_actual_bounding_box_right = right; }
void set_font_bounding_box_ascent(double ascent) { m_font_bounding_box_ascent = ascent; }
void set_font_bounding_box_descent(double descent) { m_font_bounding_box_descent = descent; }
void set_actual_bounding_box_ascent(double ascent) { m_actual_bounding_box_ascent = ascent; }
void set_actual_bounding_box_descent(double descent) { m_actual_bounding_box_descent = descent; }
void set_em_height_ascent(double ascent) { m_em_height_ascent = ascent; }
void set_em_height_descent(double descent) { m_em_height_descent = descent; }
void set_hanging_baseline(double baseline) { m_hanging_baseline = baseline; }
void set_alphabetic_baseline(double baseline) { m_alphabetic_baseline = baseline; }
void set_ideographic_baseline(double baseline) { m_ideographic_baseline = baseline; }
private:
explicit TextMetrics() { }
double m_width { 0 };
double m_actual_bounding_box_left { 0 };
double m_actual_bounding_box_right { 0 };
double m_font_bounding_box_ascent { 0 };
double m_font_bounding_box_descent { 0 };
double m_actual_bounding_box_ascent { 0 };
double m_actual_bounding_box_descent { 0 };
double m_em_height_ascent { 0 };
double m_em_height_descent { 0 };
double m_hanging_baseline { 0 };
double m_alphabetic_baseline { 0 };
double m_ideographic_baseline { 0 };
};
}

View file

@ -0,0 +1,17 @@
interface TextMetrics {
// x-direction
readonly attribute double width; // advance width
readonly attribute double actualBoundingBoxLeft;
readonly attribute double actualBoundingBoxRight;
// y-direction
readonly attribute double fontBoundingBoxAscent;
readonly attribute double fontBoundingBoxDescent;
readonly attribute double actualBoundingBoxAscent;
readonly attribute double actualBoundingBoxDescent;
readonly attribute double emHeightAscent;
readonly attribute double emHeightDescent;
readonly attribute double hangingBaseline;
readonly attribute double alphabeticBaseline;
readonly attribute double ideographicBaseline;
};