diff --git a/inc/thorvg.h b/inc/thorvg.h index e886055b..f903955e 100644 --- a/inc/thorvg.h +++ b/inc/thorvg.h @@ -284,6 +284,25 @@ 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; + //for glyph<->text mapping: + uint32_t length; +}; + + /** * @class Paint * @@ -1682,6 +1701,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. * diff --git a/src/loaders/ttf/tvgTtfLoader.cpp b/src/loaders/ttf/tvgTtfLoader.cpp index 099a8649..c63b6064 100644 --- a/src/loaders/ttf/tvgTtfLoader.cpp +++ b/src/loaders/ttf/tvgTtfLoader.cpp @@ -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 #elif defined(__linux__) @@ -196,6 +198,47 @@ static uint32_t* _codepoints(const char* text, size_t n) } +static uint32_t* _codepointsLength(const char* text, size_t n) { + auto length = tvg::malloc(sizeof(uint32_t) * (n + 1)); + + size_t i = 0; + auto p = text; + while(*p) { + if (!(*p & 0x80U)) { + length[i++] = 1; + ++p; + } else if ((*p & 0xe0U) == 0xc0U) { + length[i++] = 2; + p += 2; + } else if ((*p & 0xf0U) == 0xe0U) { + length[i++] = 3; + p += 3; + } else if ((*p & 0xf8U) == 0xf0U) { + length[i++] = 4; + p += 4; + } else { + tvg::free(length); + return nullptr; + } + } + length[i] = 0; + + return length; +} + + +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 +267,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 +331,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 +353,42 @@ 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; + auto length = _codepointsLength(text, n); + if (!length) return false; + + GlyphMetrics gmetrics; + auto lglyph = INVALID_GLYPH, rglyph = INVALID_GLYPH; + Array 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); + gmetrics.length = length[idx]; + out.push(gmetrics); + lglyph = rglyph; + } + ++idx; + } + + tvg::free(code); + tvg::free(length); + + *metrics = out.data; + out.data = nullptr; + *size = out.count; + return true; } \ No newline at end of file diff --git a/src/loaders/ttf/tvgTtfLoader.h b/src/loaders/ttf/tvgTtfLoader.h index a84219c8..12f9a2bc 100644 --- a/src/loaders/ttf/tvgTtfLoader.h +++ b/src/loaders/ttf/tvgTtfLoader.h @@ -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(); }; diff --git a/src/loaders/ttf/tvgTtfReader.cpp b/src/loaders/ttf/tvgTtfReader.cpp index 97c43150..d659659d 100644 --- a/src/loaders/ttf/tvgTtfReader.cpp +++ b/src/loaders/ttf/tvgTtfReader.cpp @@ -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(_i16(data, gmetrics.outline + 2)); - bbox[1] = static_cast(_i16(data, gmetrics.outline + 4)); - bbox[2] = static_cast(_i16(data, gmetrics.outline + 6)); - bbox[3] = static_cast(_i16(data, gmetrics.outline + 8)); + bbox[0] = static_cast(_i16(data, outline + 2)); + bbox[1] = static_cast(_i16(data, outline + 4)); + bbox[2] = static_cast(_i16(data, outline + 6)); + bbox[3] = static_cast(_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; } diff --git a/src/loaders/ttf/tvgTtfReader.h b/src/loaders/ttf/tvgTtfReader.h index 0ec7e8d4..9173fdf6 100644 --- a/src/loaders/ttf/tvgTtfReader.h +++ b/src/loaders/ttf/tvgTtfReader.h @@ -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); diff --git a/src/renderer/tvgLoadModule.h b/src/renderer/tvgLoadModule.h index fef8e2c7..dc5fc800 100644 --- a/src/renderer/tvgLoadModule.h +++ b/src/renderer/tvgLoadModule.h @@ -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_ diff --git a/src/renderer/tvgText.cpp b/src/renderer/tvgText.cpp index 9389e4d3..85a0b785 100644 --- a/src/renderer/tvgText.cpp +++ b/src/renderer/tvgText.cpp @@ -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); +} \ No newline at end of file diff --git a/src/renderer/tvgText.h b/src/renderer/tvgText.h index edc95e1d..bb64665c 100644 --- a/src/renderer/tvgText.h +++ b/src/renderer/tvgText.h @@ -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& 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