ttf/api: add metrics() api

Introduced the GlyphMetrics struct and the API:
  Result metrics(GlyphMetrics** metrics, uint32_t* size)
This allows retrieving glyph metrics for a given Text object.
The font and utf8 content must be set beforehand using
the font() and text() apis.

Memory for the metrics array is allocated internally by the API.
It is the user's responsibility to free it by calling the same API
with a valid metrics pointer and a nullptr passed as size.

@Issue: https://github.com/thorvg/thorvg/issues/3397
This commit is contained in:
Mira Grudzinska 2025-05-16 21:39:54 +02:00
parent e01ccb5db9
commit 708b24b589
8 changed files with 141 additions and 38 deletions

View file

@ -283,6 +283,23 @@ struct Matrix
};
/**
* @brief A data structure representing metrics for a single glyph.
*
* @since Experimental API
*/
struct GlyphMetrics
{
Point kerning = {0.0f, 0.0f};
float advanceWidth;
float leftSideBearing;
float yOffset;
float minw;
float minh;
};
/**
* @class Paint
*
@ -1668,6 +1685,23 @@ public:
*/
static Result unload(const char* filename) noexcept;
/**
* @brief Gets the detailed metrics for each glyph in the current text object.
*
* This function allocates memory internally to hold an array of glyph metrics corresponding to the currently set text and font.
*
* @param[out] metrics Pointer to an array of glyph metrics. Valid until the text is modified.
* @param[out] size The number of glyphs returned in the metrics array.
*
* @retval Result::InsufficientCondition If font or text was not set, a @c nullptr is passed as @p metrics, or font data loading/reading failed.
*
* @note The caller is responsible for freeing the allocated memory by calling this function again with the same @p metrics pointer and a @c nullptr @p size pointer.
* @note Must be called after setting both font() and text().
*
* @since Experimental API
*/
Result metrics(GlyphMetrics** metrics, uint32_t* size) noexcept;
/**
* @brief Creates a new Text object.
*

View file

@ -24,6 +24,8 @@
#include "tvgStr.h"
#include "tvgTtfLoader.h"
#define PX_PER_PT 1.333333f //1 pt = 1/72 in; 1 in = 96 px; -> 96/72
#if defined(_WIN32) && (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP)
#include <windows.h>
#elif defined(__linux__)
@ -196,6 +198,18 @@ static uint32_t* _codepoints(const char* text, size_t n)
}
static void _scale(GlyphMetrics& gmetric, float scale)
{
gmetric.kerning.x *= scale;
gmetric.kerning.y *= scale;
gmetric.advanceWidth *= scale;
gmetric.leftSideBearing *= scale;
gmetric.yOffset *= scale;
gmetric.minw *= scale;
gmetric.minh *= scale;
}
void TtfLoader::clear()
{
if (nomap) {
@ -224,8 +238,7 @@ void TtfLoader::clear()
float TtfLoader::transform(Paint* paint, FontMetrics& metrics, float fontSize, bool italic)
{
auto shift = 0.0f;
auto dpi = 96.0f / 72.0f; //dpi base?
auto scale = fontSize * dpi / reader.metrics.unitsPerEm;
auto scale = fontSize * PX_PER_PT / reader.metrics.unitsPerEm;
if (italic) shift = -scale * 0.18f; //experimental decision.
Matrix m = {scale, shift, -(shift * metrics.minw), 0, scale, 0, 0, 0, 1};
paint->transform(m);
@ -289,21 +302,20 @@ bool TtfLoader::read(Shape* shape, char* text, FontMetrics& out)
//TODO: optimize with the texture-atlas?
TtfGlyphMetrics gmetrics;
Point offset = {0.0f, reader.metrics.hhea.ascent};
Point kerning = {0.0f, 0.0f};
auto lglyph = INVALID_GLYPH;
auto loadMinw = true;
size_t idx = 0;
while (code[idx] && idx < n) {
auto rglyph = reader.glyph(code[idx], gmetrics);
auto rglyph = reader.glyph(code[idx], gmetrics.metrics, &gmetrics.outline);
if (rglyph != INVALID_GLYPH) {
if (lglyph != INVALID_GLYPH) reader.kerning(lglyph, rglyph, kerning);
if (!reader.convert(shape, gmetrics, offset, kerning, 1U)) break;
offset.x += (gmetrics.advanceWidth + kerning.x);
if (lglyph != INVALID_GLYPH) reader.kerning(lglyph, rglyph, gmetrics.metrics.kerning);
if (!reader.convert(shape, gmetrics, offset, 1U)) break;
offset.x += (gmetrics.metrics.advanceWidth + gmetrics.metrics.kerning.x);
lglyph = rglyph;
//store the first glyph with outline min size for italic transform.
if (loadMinw && gmetrics.outline) {
out.minw = gmetrics.minw;
out.minw = gmetrics.metrics.minw;
loadMinw = false;
}
}
@ -312,5 +324,38 @@ bool TtfLoader::read(Shape* shape, char* text, FontMetrics& out)
tvg::free(code);
return true;
}
bool TtfLoader::metrics(char* text, float fontSize, GlyphMetrics** metrics, uint32_t* size)
{
auto n = strlen(text);
auto code = _codepoints(text, n);
if (!code) return false;
GlyphMetrics gmetrics;
auto lglyph = INVALID_GLYPH, rglyph = INVALID_GLYPH;
Array<GlyphMetrics> out(n);
auto scale = fontSize * PX_PER_PT / reader.metrics.unitsPerEm;
size_t idx = 0;
while (code[idx] && idx < n) {
if ((rglyph = reader.glyph(code[idx], gmetrics, nullptr)) != INVALID_GLYPH) {
if (lglyph != INVALID_GLYPH) reader.kerning(lglyph, rglyph, gmetrics.kerning);
_scale(gmetrics, scale);
out.push(gmetrics);
lglyph = rglyph;
}
++idx;
}
tvg::free(code);
*metrics = out.data;
out.data = nullptr;
*size = out.count;
return true;
}

View file

@ -49,6 +49,7 @@ struct TtfLoader : public FontLoader
bool open(const char *data, uint32_t size, const char* rpath, bool copy) override;
float transform(Paint* paint, FontMetrics& metrices, float fontSize, bool italic) override;
bool read(Shape* shape, char* text, FontMetrics& out) override;
bool metrics(char* text, float fontSize, GlyphMetrics** metrics, uint32_t* size) override;
void clear();
};

View file

@ -377,17 +377,17 @@ uint32_t TtfReader::glyph(uint32_t codepoint)
}
uint32_t TtfReader::glyph(uint32_t codepoint, TtfGlyphMetrics& gmetrics)
uint32_t TtfReader::glyph(uint32_t codepoint, GlyphMetrics& gmetrics, uint32_t* goutline)
{
auto glyph = this->glyph(codepoint);
if (glyph == INVALID_GLYPH || !glyphMetrics(glyph, gmetrics)) {
if (glyph == INVALID_GLYPH || !glyphMetrics(glyph, gmetrics, goutline)) {
TVGERR("TTF", "invalid glyph id, codepoint(0x%x)", codepoint);
return INVALID_GLYPH;
}
return glyph;
}
bool TtfReader::glyphMetrics(uint32_t glyphIndex, TtfGlyphMetrics& gmetrics)
bool TtfReader::glyphMetrics(uint32_t glyphIndex, GlyphMetrics& gmetrics, uint32_t* goutline)
{
//horizontal metrics
auto hmtx = this->hmtx.load();
@ -412,20 +412,21 @@ bool TtfReader::glyphMetrics(uint32_t glyphIndex, TtfGlyphMetrics& gmetrics)
gmetrics.leftSideBearing = _i16(data, offset);
}
gmetrics.outline = outlineOffset(glyphIndex);
auto outline = outlineOffset(glyphIndex);
if (goutline) *goutline = outline;
// glyph without outline
if (gmetrics.outline == 0) {
if (outline == 0) {
gmetrics.minw = gmetrics.minh = gmetrics.yOffset = 0;
return true;
}
if (!validate(gmetrics.outline, 10)) return false;
if (!validate(outline, 10)) return false;
//read the bounding box from the font file verbatim.
float bbox[4];
bbox[0] = static_cast<float>(_i16(data, gmetrics.outline + 2));
bbox[1] = static_cast<float>(_i16(data, gmetrics.outline + 4));
bbox[2] = static_cast<float>(_i16(data, gmetrics.outline + 6));
bbox[3] = static_cast<float>(_i16(data, gmetrics.outline + 8));
bbox[0] = static_cast<float>(_i16(data, outline + 2));
bbox[1] = static_cast<float>(_i16(data, outline + 4));
bbox[2] = static_cast<float>(_i16(data, outline + 6));
bbox[3] = static_cast<float>(_i16(data, outline + 8));
if (bbox[2] <= bbox[0] || bbox[3] <= bbox[1]) return false;
@ -436,7 +437,7 @@ bool TtfReader::glyphMetrics(uint32_t glyphIndex, TtfGlyphMetrics& gmetrics)
return true;
}
bool TtfReader::convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning, uint16_t componentDepth)
bool TtfReader::convert(Shape* shape, const TtfGlyphMetrics& gmetrics, const Point& offset, uint16_t componentDepth)
{
#define ON_CURVE 0x01
@ -451,7 +452,7 @@ bool TtfReader::convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& of
maxComponentDepth = _u16(data, maxp + 30);
}
if (componentDepth > maxComponentDepth) return false;
return convertComposite(shape, gmetrics, offset, kerning, componentDepth + 1);
return convertComposite(shape, gmetrics, offset, componentDepth + 1);
}
auto cntrsCnt = (uint32_t) outlineCnt;
@ -471,7 +472,7 @@ bool TtfReader::convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& of
if (!this->flags(&outline, flags, ptsCnt)) return false;
auto pts = (Point*)alloca(ptsCnt * sizeof(Point));
if (!this->points(outline, flags, pts, ptsCnt, offset + kerning)) return false;
if (!this->points(outline, flags, pts, ptsCnt, offset + gmetrics.metrics.kerning)) return false;
//generate tvg paths.
auto& pathCmds = SHAPE(shape)->rs.path.cmds;
@ -526,7 +527,8 @@ bool TtfReader::convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& of
return true;
}
bool TtfReader::convertComposite(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning, uint16_t componentDepth)
bool TtfReader::convertComposite(Shape* shape, const TtfGlyphMetrics& gmetrics, const Point& offset, uint16_t componentDepth)
{
#define ARG_1_AND_2_ARE_WORDS 0x0001
#define ARGS_ARE_XY_VALUES 0x0002
@ -587,8 +589,8 @@ bool TtfReader::convertComposite(Shape* shape, TtfGlyphMetrics& gmetrics, const
// F2DOT14 yscale; /* Format 2.14 */
pointer += 8U;
}
if (!glyphMetrics(glyphIndex, componentGmetrics)) return false;
if (!convert(shape, componentGmetrics, offset + componentOffset, kerning, componentDepth)) return false;
if (!glyphMetrics(glyphIndex, componentGmetrics.metrics, &componentGmetrics.outline)) return false;
if (!convert(shape, componentGmetrics, offset + componentOffset, componentDepth)) return false;
} while (flags & MORE_COMPONENTS);
return true;
}

View file

@ -32,12 +32,7 @@
struct TtfGlyphMetrics
{
uint32_t outline; //glyph outline table offset
float advanceWidth;
float leftSideBearing;
float yOffset;
float minw;
float minh;
GlyphMetrics metrics;
};
@ -62,9 +57,9 @@ public:
} metrics;
bool header();
uint32_t glyph(uint32_t codepoint, TtfGlyphMetrics& gmetrics);
uint32_t glyph(uint32_t codepoint, GlyphMetrics& gmetric, uint32_t* goutline);
void kerning(uint32_t lglyph, uint32_t rglyph, Point& out);
bool convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning, uint16_t componentDepth);
bool convert(Shape* shape, const TtfGlyphMetrics& gmetric, const Point& offset, uint16_t componentDepth);
private:
//table offsets
@ -82,8 +77,8 @@ private:
uint32_t table(const char* tag);
uint32_t outlineOffset(uint32_t glyph);
uint32_t glyph(uint32_t codepoint);
bool glyphMetrics(uint32_t glyphIndex, TtfGlyphMetrics& gmetrics);
bool convertComposite(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning, uint16_t componentDepth);
bool glyphMetrics(uint32_t glyphIndex, GlyphMetrics& gmetric, uint32_t* goutline);
bool convertComposite(Shape* shape, const TtfGlyphMetrics& gmetric, const Point& offset, uint16_t componentDepth);
bool genPath(uint8_t* flags, uint16_t basePoint, uint16_t count);
bool genSimpleOutline(Shape* shape, uint32_t outline, uint32_t cntrsCnt);
bool points(uint32_t outline, uint8_t* flags, Point* pts, uint32_t ptsCnt, const Point& offset);

View file

@ -118,6 +118,7 @@ struct FontLoader : LoadModule
virtual bool read(Shape* shape, char* text, FontMetrics& out) = 0;
virtual float transform(Paint* paint, FontMetrics& mertrics, float fontSize, bool italic) = 0;
virtual bool metrics(char* text, float fontSize, GlyphMetrics** metrics, uint32_t* size) = 0;
};
#endif //_TVG_LOAD_MODULE_H_

View file

@ -107,3 +107,9 @@ Type Text::type() const noexcept
{
return Type::Text;
}
Result Text::metrics(GlyphMetrics** metrics, uint32_t* size) noexcept
{
return TEXT(this)->metrics(metrics, size);
}

View file

@ -37,9 +37,9 @@ struct TextImpl : Text
Paint::Impl impl;
Shape* shape; //text shape
FontLoader* loader = nullptr;
FontMetrics metrics;
FontMetrics fmetrics;
char* utf8 = nullptr;
float fontSize;
float fontSize = 0.0f;
bool italic = false;
bool changed = false;
@ -107,10 +107,10 @@ struct TextImpl : Text
//reload
if (changed) {
loader->read(shape, utf8, metrics);
loader->read(shape, utf8, fmetrics);
changed = false;
}
return loader->transform(shape, metrics, fontSize, italic);
return loader->transform(shape, fmetrics, fontSize, italic);
}
RenderData update(RenderMethod* renderer, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, TVG_UNUSED bool clipper)
@ -172,6 +172,25 @@ struct TextImpl : Text
{
return nullptr;
}
Result metrics(GlyphMetrics** metrics, uint32_t* size)
{
if (!metrics) return Result::InsufficientCondition;
//free allocated memory
if (*metrics && !size) {
tvg::free(*metrics);
*metrics = nullptr;
return Result::Success;
}
if (!utf8 || tvg::zero(fontSize)) return Result::InsufficientCondition;
if (load() == 0.0f) return Result::InsufficientCondition;
if (!loader->metrics(utf8, fontSize, metrics, size)) return Result::InsufficientCondition;
return Result::Success;
}
};
#endif //_TVG_TEXT_H