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
This commit is contained in:
Łukasz Pomietło 2024-07-31 05:51:22 +02:00 committed by Hermet Park
parent 8c4b9b2772
commit 045b30b94f
4 changed files with 122 additions and 17 deletions

View file

@ -289,7 +289,7 @@ bool TtfLoader::read()
auto rglyph = reader.glyph(code[idx], gmetrics); auto rglyph = reader.glyph(code[idx], gmetrics);
if (rglyph != INVALID_GLYPH) { if (rglyph != INVALID_GLYPH) {
if (lglyph != INVALID_GLYPH) reader.kerning(lglyph, rglyph, kerning); 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); offset.x += (gmetrics.advanceWidth + kerning.x);
lglyph = rglyph; lglyph = rglyph;

View file

@ -392,30 +392,35 @@ uint32_t TtfReader::glyph(uint32_t codepoint, TtfGlyphMetrics& gmetrics)
return INVALID_GLYPH; return INVALID_GLYPH;
} }
return glyphMetrics(glyph, gmetrics) ? glyph : INVALID_GLYPH;
}
bool TtfReader::glyphMetrics(uint32_t glyphIndex, TtfGlyphMetrics& gmetrics)
{
//horizontal metrics //horizontal metrics
if (!hmtx) hmtx = table("hmtx"); if (!hmtx) hmtx = table("hmtx");
//glyph is inside long metrics segment. //glyph is inside long metrics segment.
if (glyph < metrics.numHmtx) { if (glyphIndex < metrics.numHmtx) {
auto offset = hmtx + 4 * glyph; auto offset = hmtx + 4 * glyphIndex;
if (!validate(offset, 4)) return INVALID_GLYPH; if (!validate(offset, 4)) return false;
gmetrics.advanceWidth = _u16(data, offset); gmetrics.advanceWidth = _u16(data, offset);
gmetrics.leftSideBearing = _i16(data, offset + 2); gmetrics.leftSideBearing = _i16(data, offset + 2);
/* glyph is inside short metrics segment. */ /* glyph is inside short metrics segment. */
} else { } else {
auto boundary = hmtx + 4U * (uint32_t) metrics.numHmtx; auto boundary = hmtx + 4U * (uint32_t) metrics.numHmtx;
if (boundary < 4) return INVALID_GLYPH; if (boundary < 4) return false;
auto offset = boundary - 4; auto offset = boundary - 4;
if (!validate(offset, 4)) return INVALID_GLYPH; if (!validate(offset, 4)) return false;
gmetrics.advanceWidth = _u16(data, offset); gmetrics.advanceWidth = _u16(data, offset);
offset = boundary + 2 * (glyph - metrics.numHmtx); offset = boundary + 2 * (glyphIndex - metrics.numHmtx);
if (!validate(offset, 2)) return INVALID_GLYPH; if (!validate(offset, 2)) return false;
gmetrics.leftSideBearing = _i16(data, offset); gmetrics.leftSideBearing = _i16(data, offset);
} }
gmetrics.outline = outlineOffset(glyph); gmetrics.outline = outlineOffset(glyphIndex);
if (!gmetrics.outline || !validate(gmetrics.outline, 10)) return INVALID_GLYPH; if (!gmetrics.outline || !validate(gmetrics.outline, 10)) return false;
//read the bounding box from the font file verbatim. //read the bounding box from the font file verbatim.
float bbox[4]; float bbox[4];
@ -424,22 +429,31 @@ uint32_t TtfReader::glyph(uint32_t codepoint, TtfGlyphMetrics& gmetrics)
bbox[2] = static_cast<float>(_i16(data, gmetrics.outline + 6)); bbox[2] = static_cast<float>(_i16(data, gmetrics.outline + 6));
bbox[3] = static_cast<float>(_i16(data, gmetrics.outline + 8)); bbox[3] = static_cast<float>(_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.minw = bbox[2] - bbox[0] + 1;
gmetrics.minh = bbox[3] - bbox[1] + 1; gmetrics.minh = bbox[3] - bbox[1] + 1;
gmetrics.yOffset = bbox[3]; gmetrics.yOffset = bbox[3];
return glyph; return true;
} }
bool TtfReader::convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning, uint16_t componentDepth)
bool TtfReader::convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning)
{ {
#define ON_CURVE 0x01 #define ON_CURVE 0x01
auto cntrsCnt = (uint32_t) _i16(data, gmetrics.outline); auto outlineCnt = _i16(data, gmetrics.outline);
if (cntrsCnt == 0) return false; 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; auto outline = gmetrics.outline + 10;
if (!validate(outline, cntrsCnt * 2 + 2)) return false; if (!validate(outline, cntrsCnt * 2 + 2)) return false;
@ -512,6 +526,73 @@ bool TtfReader::convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& of
return true; 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<float>(_i16(data, pointer));
componentOffset.y = -static_cast<float>(_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<float>((int8_t)_u8(data, pointer));
componentOffset.y = -static_cast<float>((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) void TtfReader::kerning(uint32_t lglyph, uint32_t rglyph, Point& out)
{ {

View file

@ -64,7 +64,7 @@ public:
bool header(); bool header();
uint32_t glyph(uint32_t codepoint, TtfGlyphMetrics& gmetrics); uint32_t glyph(uint32_t codepoint, TtfGlyphMetrics& gmetrics);
void kerning(uint32_t lglyph, uint32_t rglyph, Point& out); 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: private:
//table offsets //table offsets
@ -73,6 +73,7 @@ private:
uint32_t loca = 0; uint32_t loca = 0;
uint32_t glyf = 0; uint32_t glyf = 0;
uint32_t kern = 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_12_13(uint32_t table, uint32_t codepoint, int which) const;
uint32_t cmap_4(uint32_t table, uint32_t codepoint) 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 table(const char* tag);
uint32_t outlineOffset(uint32_t glyph); uint32_t outlineOffset(uint32_t glyph);
uint32_t glyph(uint32_t codepoint); 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 genPath(uint8_t* flags, uint16_t basePoint, uint16_t count);
bool genSimpleOutline(Shape* shape, uint32_t outline, uint32_t cntrsCnt); 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); bool points(uint32_t outline, uint8_t* flags, Point* pts, uint32_t ptsCnt, const Point& offset);

View file

@ -141,4 +141,25 @@ TEST_CASE("Text Basic", "[tvgText]")
Initializer::term(CanvasEngine::Sw); Initializer::term(CanvasEngine::Sw);
} }
TEST_CASE("Text with composite glyphs", "[tvgText]")
{
Initializer::init(tvg::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(tvg::CanvasEngine::Sw);
}
#endif #endif