From 2b3930b46deffe6ed0d7b63772fb0dd49133afb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Pomiet=C5=82o?= <46203179+lpogic@users.noreply.github.com> Date: Wed, 31 Jul 2024 05:51:22 +0200 Subject: [PATCH] ttf_loader: Basic support for composite glyphs loading (#2600) Adds the ability to load some composite glyphs and prevents an error when a composite glyph is used. Implementation based on ttf glyf table documentation: https://learn.microsoft.com/en-us/typography/opentype/spec/glyf There are still some missing features like scaling, parent glyph point based positioning etc. I think this is a topic for future work. Howerever, it looks like implemented features are enough for utf-8 latin subset in major fonts. issue: #2599 --- src/loaders/ttf/tvgTtfLoader.cpp | 2 +- src/loaders/ttf/tvgTtfReader.cpp | 111 ++++++++++++++++++++++++++----- src/loaders/ttf/tvgTtfReader.h | 5 +- test/testText.cpp | 21 ++++++ 4 files changed, 122 insertions(+), 17 deletions(-) diff --git a/src/loaders/ttf/tvgTtfLoader.cpp b/src/loaders/ttf/tvgTtfLoader.cpp index a9aade2c..b84c3cca 100644 --- a/src/loaders/ttf/tvgTtfLoader.cpp +++ b/src/loaders/ttf/tvgTtfLoader.cpp @@ -289,7 +289,7 @@ bool TtfLoader::read() auto rglyph = reader.glyph(code[idx], gmetrics); if (rglyph != INVALID_GLYPH) { if (lglyph != INVALID_GLYPH) reader.kerning(lglyph, rglyph, kerning); - if (!reader.convert(shape, gmetrics, offset, kerning)) break; + if (!reader.convert(shape, gmetrics, offset, kerning, 1U)) break; } offset.x += (gmetrics.advanceWidth + kerning.x); lglyph = rglyph; diff --git a/src/loaders/ttf/tvgTtfReader.cpp b/src/loaders/ttf/tvgTtfReader.cpp index d86ad2a2..174c65e9 100644 --- a/src/loaders/ttf/tvgTtfReader.cpp +++ b/src/loaders/ttf/tvgTtfReader.cpp @@ -391,31 +391,36 @@ uint32_t TtfReader::glyph(uint32_t codepoint, TtfGlyphMetrics& gmetrics) TVGERR("TTF", "invalid glyph id, codepoint(0x%x)", codepoint); return INVALID_GLYPH; } + + return glyphMetrics(glyph, gmetrics) ? glyph : INVALID_GLYPH; +} +bool TtfReader::glyphMetrics(uint32_t glyphIndex, TtfGlyphMetrics& gmetrics) +{ //horizontal metrics if (!hmtx) hmtx = table("hmtx"); //glyph is inside long metrics segment. - if (glyph < metrics.numHmtx) { - auto offset = hmtx + 4 * glyph; - if (!validate(offset, 4)) return INVALID_GLYPH; + if (glyphIndex < metrics.numHmtx) { + auto offset = hmtx + 4 * glyphIndex; + if (!validate(offset, 4)) return false; gmetrics.advanceWidth = _u16(data, offset); gmetrics.leftSideBearing = _i16(data, offset + 2); /* glyph is inside short metrics segment. */ } else { auto boundary = hmtx + 4U * (uint32_t) metrics.numHmtx; - if (boundary < 4) return INVALID_GLYPH; + if (boundary < 4) return false; auto offset = boundary - 4; - if (!validate(offset, 4)) return INVALID_GLYPH; + if (!validate(offset, 4)) return false; gmetrics.advanceWidth = _u16(data, offset); - offset = boundary + 2 * (glyph - metrics.numHmtx); - if (!validate(offset, 2)) return INVALID_GLYPH; + offset = boundary + 2 * (glyphIndex - metrics.numHmtx); + if (!validate(offset, 2)) return false; gmetrics.leftSideBearing = _i16(data, offset); } - gmetrics.outline = outlineOffset(glyph); - if (!gmetrics.outline || !validate(gmetrics.outline, 10)) return INVALID_GLYPH; + gmetrics.outline = outlineOffset(glyphIndex); + if (!gmetrics.outline || !validate(gmetrics.outline, 10)) return false; //read the bounding box from the font file verbatim. float bbox[4]; @@ -424,22 +429,31 @@ uint32_t TtfReader::glyph(uint32_t codepoint, TtfGlyphMetrics& gmetrics) bbox[2] = static_cast(_i16(data, gmetrics.outline + 6)); bbox[3] = static_cast(_i16(data, gmetrics.outline + 8)); - if (bbox[2] <= bbox[0] || bbox[3] <= bbox[1]) return INVALID_GLYPH; + if (bbox[2] <= bbox[0] || bbox[3] <= bbox[1]) return false; gmetrics.minw = bbox[2] - bbox[0] + 1; gmetrics.minh = bbox[3] - bbox[1] + 1; gmetrics.yOffset = bbox[3]; - return glyph; + return true; } - -bool TtfReader::convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning) +bool TtfReader::convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning, uint16_t componentDepth) { #define ON_CURVE 0x01 - auto cntrsCnt = (uint32_t) _i16(data, gmetrics.outline); - if (cntrsCnt == 0) return false; + auto outlineCnt = _i16(data, gmetrics.outline); + if (outlineCnt == 0) return false; + if (outlineCnt < 0) { + uint16_t maxComponentDepth = 1U; + if (!maxp) maxp = table("maxp"); + if (validate(maxp, 32) && _u32(data, maxp) >= 0x00010000U) { // >= version 1.0 + maxComponentDepth = _u16(data, maxp + 30); + } + if (componentDepth > maxComponentDepth) return false; + return convertComposite(shape, gmetrics, offset, kerning, componentDepth + 1); + } + auto cntrsCnt = (uint32_t) outlineCnt; auto outline = gmetrics.outline + 10; if (!validate(outline, cntrsCnt * 2 + 2)) return false; @@ -512,6 +526,73 @@ 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) +{ + #define ARG_1_AND_2_ARE_WORDS 0x0001 + #define ARGS_ARE_XY_VALUES 0x0002 + #define WE_HAVE_A_SCALE 0x0008 + #define MORE_COMPONENTS 0x0020 + #define WE_HAVE_AN_X_AND_Y_SCALE 0x0040 + #define WE_HAVE_A_TWO_BY_TWO 0x0080 + + TtfGlyphMetrics componentGmetrics; + Point componentOffset; + uint16_t flags, glyphIndex; + uint32_t pointer = gmetrics.outline + 10; + do { + if (!validate(pointer, 4)) return false; + flags = _u16(data, pointer); + glyphIndex = _u16(data, pointer + 2U); + pointer += 4U; + if (flags & ARG_1_AND_2_ARE_WORDS) { + if (!validate(pointer, 4)) return false; + if(flags & ARGS_ARE_XY_VALUES) { + componentOffset.x = static_cast(_i16(data, pointer)); + componentOffset.y = -static_cast(_i16(data, pointer + 2U)); + } else { + // TODO align to parent point + componentOffset.x = 0; + componentOffset.y = 0; + } + pointer += 4U; + } else { + if (!validate(pointer, 2)) return false; + if(flags & ARGS_ARE_XY_VALUES) { + componentOffset.x = static_cast((int8_t)_u8(data, pointer)); + componentOffset.y = -static_cast((int8_t)_u8(data, pointer + 1U)); + } else { + // TODO align to parent point + componentOffset.x = 0; + componentOffset.y = 0; + } + pointer += 2U; + } + if (flags & WE_HAVE_A_SCALE) { + if (!validate(pointer, 2)) return false; + // TODO transformation + // F2DOT14 scale; /* Format 2.14 */ + pointer += 2U; + } else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) { + if (!validate(pointer, 4)) return false; + // TODO transformation + // F2DOT14 xscale; /* Format 2.14 */ + // F2DOT14 yscale; /* Format 2.14 */ + pointer += 4U; + } else if (flags & WE_HAVE_A_TWO_BY_TWO) { + if (!validate(pointer, 8)) return false; + // TODO transformation + // F2DOT14 xscale; /* Format 2.14 */ + // F2DOT14 scale01; /* Format 2.14 */ + // F2DOT14 scale10; /* Format 2.14 */ + // F2DOT14 yscale; /* Format 2.14 */ + pointer += 8U; + } + if (!glyphMetrics(glyphIndex, componentGmetrics)) return false; + if (!convert(shape, componentGmetrics, offset + componentOffset, kerning, componentDepth)) return false; + } while (flags & MORE_COMPONENTS); + return true; +} + void TtfReader::kerning(uint32_t lglyph, uint32_t rglyph, Point& out) { diff --git a/src/loaders/ttf/tvgTtfReader.h b/src/loaders/ttf/tvgTtfReader.h index ffe7050a..7ee1aab3 100644 --- a/src/loaders/ttf/tvgTtfReader.h +++ b/src/loaders/ttf/tvgTtfReader.h @@ -64,7 +64,7 @@ public: bool header(); uint32_t glyph(uint32_t codepoint, TtfGlyphMetrics& gmetrics); void kerning(uint32_t lglyph, uint32_t rglyph, Point& out); - bool convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning); + bool convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning, uint16_t componentDepth); private: //table offsets @@ -73,6 +73,7 @@ private: uint32_t loca = 0; uint32_t glyf = 0; uint32_t kern = 0; + uint32_t maxp = 0; uint32_t cmap_12_13(uint32_t table, uint32_t codepoint, int which) const; uint32_t cmap_4(uint32_t table, uint32_t codepoint) const; @@ -81,6 +82,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 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/test/testText.cpp b/test/testText.cpp index e99147db..3328cb0a 100644 --- a/test/testText.cpp +++ b/test/testText.cpp @@ -138,4 +138,25 @@ TEST_CASE("Text Basic", "[tvgText]") Initializer::term(CanvasEngine::Sw); } +TEST_CASE("Text with composite glyphs", "[tvgText]") +{ + Initializer::init(CanvasEngine::Sw, 0); + + auto canvas = SwCanvas::gen(); + + auto text = Text::gen(); + REQUIRE(text); + + REQUIRE(Text::load(TEST_DIR"/Arial.ttf") == tvg::Result::Success); + REQUIRE(text->font("Arial", 80) == tvg::Result::Success); + + REQUIRE(text->text("\xc5\xbb\x6f\xc5\x82\xc4\x85\x64\xc5\xba \xc8\xab") == tvg::Result::Success); + + REQUIRE(text->fill(255, 255, 255) == tvg::Result::Success); + + REQUIRE(canvas->push(std::move(text)) == Result::Success); + + Initializer::term(CanvasEngine::Sw); +} + #endif