diff --git a/meson.build b/meson.build index 4b4c473b..3837bada 100644 --- a/meson.build +++ b/meson.build @@ -57,6 +57,9 @@ if get_option('loaders').contains('webp_beta') == true config_h.set10('THORVG_WEBP_LOADER_SUPPORT', true) endif +if get_option('loaders').contains('ttf_beta') == true + config_h.set10('THORVG_TTF_LOADER_SUPPORT', true) +endif #Savers @@ -135,20 +138,21 @@ Summary: Raster Engine (WG): @6@ Loader (TVG): @7@ Loader (SVG): @8@ - Loader (PNG): @9@ - Loader (JPG): @10@ - Loader (WEBP_BETA): @11@ - Loader (LOTTIE): @12@ - Saver (TVG): @13@ - Saver (GIF): @14@ - Binding (CAPI): @15@ - Binding (WASM_BETA): @16@ - Log Message: @17@ - Tests: @18@ - Examples: @19@ - Tool (Svg2Tvg): @20@ - Tool (Svg2Png): @21@ - Tool (Lottie2Gif): @22@ + Loader (TTF_BETA): @9@ + Loader (LOTTIE): @10@ + Loader (PNG): @11@ + Loader (JPG): @12@ + Loader (WEBP_BETA): @13@ + Saver (TVG): @14@ + Saver (GIF): @15@ + Binding (CAPI): @16@ + Binding (WASM_BETA): @17@ + Log Message: @18@ + Tests: @19@ + Examples: @20@ + Tool (Svg2Tvg): @21@ + Tool (Svg2Png): @22@ + Tool (Lottie2Gif): @23@ '''.format( meson.project_version(), @@ -160,10 +164,11 @@ Summary: get_option('engines').contains('wg_beta'), all_loaders or get_option('loaders').contains('tvg'), all_loaders or get_option('loaders').contains('svg'), + get_option('loaders').contains('ttf_beta'), + all_loaders or get_option('loaders').contains('lottie'), all_loaders or get_option('loaders').contains('png'), all_loaders or get_option('loaders').contains('jpg'), get_option('loaders').contains('webp_beta'), - all_loaders or get_option('loaders').contains('lottie'), all_savers or get_option('savers').contains('tvg'), all_savers or get_option('savers').contains('gif'), get_option('bindings').contains('capi'), diff --git a/meson_options.txt b/meson_options.txt index 828af39e..b4ee30df 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -6,7 +6,7 @@ option('engines', option('loaders', type: 'array', - choices: ['', 'tvg', 'svg', 'png', 'jpg', 'webp_beta', 'lottie', 'all'], + choices: ['', 'tvg', 'svg', 'png', 'jpg', 'lottie', 'ttf_beta', 'webp_beta', 'all'], value: ['svg', 'tvg', 'lottie'], description: 'Enable File Loaders in thorvg ("all" does not include "*_beta".)') diff --git a/src/common/tvgMath.h b/src/common/tvgMath.h index 004fff1e..8d5a7dd0 100644 --- a/src/common/tvgMath.h +++ b/src/common/tvgMath.h @@ -174,6 +174,18 @@ static inline Point operator*(const Point& lhs, float rhs) } +static inline Point operator*(const float& lhs, const Point& rhs) +{ + return {lhs * rhs.x, lhs * rhs.y}; +} + + +static inline Point operator/(const Point& lhs, const float rhs) +{ + return {lhs.x / rhs, lhs.y / rhs}; +} + + template static inline T mathLerp(const T &start, const T &end, float t) { diff --git a/src/loaders/meson.build b/src/loaders/meson.build index 58b5008a..bcc71db9 100644 --- a/src/loaders/meson.build +++ b/src/loaders/meson.build @@ -8,6 +8,10 @@ if all_loaders or get_option('loaders').contains('svg') == true subdir('svg') endif +if get_option('loaders').contains('ttf_beta') == true + subdir('ttf') +endif + if all_loaders or get_option('loaders').contains('lottie') == true subdir('lottie') endif diff --git a/src/loaders/ttf/meson.build b/src/loaders/ttf/meson.build new file mode 100644 index 00000000..b75d397f --- /dev/null +++ b/src/loaders/ttf/meson.build @@ -0,0 +1,11 @@ +source_file = [ + 'tvgTtfLoader.h', + 'tvgTtfReader.h', + 'tvgTtfLoader.cpp', + 'tvgTtfReader.cpp' +] + +subloader_dep += [declare_dependency( + include_directories : include_directories('.'), + sources : source_file +)] diff --git a/src/loaders/ttf/tvgTtfLoader.cpp b/src/loaders/ttf/tvgTtfLoader.cpp new file mode 100644 index 00000000..ea1d73bd --- /dev/null +++ b/src/loaders/ttf/tvgTtfLoader.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2023 the ThorVG project. All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifdef _WIN32 + #include +#else + #include + #include + #include + #include +#endif + +#include +#include "tvgTtfLoader.h" + + +/************************************************************************/ +/* Internal Class Implementation */ +/************************************************************************/ + +#ifdef _WIN32 + +static bool _map(TtfLoader* loader, const string& path) +{ + auto& reader = loader->reader; + + auto file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (file == INVALID_HANDLE_VALUE) return false; + + DWORD high; + auto low = GetFileSize(file, &high); + if (low == INVALID_FILE_SIZE) { + CloseHandle(file); + return false; + } + + reader.size = (uint32_t)((size_t)high << (8 * sizeof(DWORD)) | low); + + loader.mapping = (uint8_t*)CreateFileMapping(file, NULL, PAGE_READONLY, high, low, NULL); + + CloseHandle(file); + + if (!mapping) return false; + + font->data = (uint8_t*) MapViewOfFile(loader.mapping, FILE_MAP_READ, 0, 0, 0); + if (!font->data) { + CloseHandle(loader.mapping); + font->reader = NULL; + return false; + } + return true; +} + +static void _unmap(TtfLoader* loader) +{ + auto& reader = loader->reader; + + if (reader->data) { + UnmapViewOfFile(reader->data); + reader->data = nullptr; + } + if (loader->mapping) { + CloseHandle(loader->mapping); + loader->mapping = nullptr; + } +} + +#else + +static bool _map(TtfLoader* loader, const string& path) +{ + auto& reader = loader->reader; + + auto fd = open(path.c_str(), O_RDONLY); + if (fd < 0) return false; + + struct stat info; + if (fstat(fd, &info) < 0) { + close(fd); + return false; + } + + reader.data = (uint8_t*)mmap(NULL, (size_t) info.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (reader.data == (uint8_t*)-1) return false; + reader.size = (uint32_t) info.st_size; + close(fd); + + return true; +} + + +static void _unmap(TtfLoader* loader) +{ + auto& reader = loader->reader; + if (reader.data == (uint8_t*) -1) return; + munmap((void *) reader.data, reader.size); + reader.data = (uint8_t*)-1; + reader.size = 0; +} +#endif + + +static uint32_t* _codepoints(const char* text, size_t n) +{ + uint32_t c; + size_t i = 0; + + auto utf8 = text; + //preserve approximate enough space. + auto utf32 = (uint32_t*) malloc(sizeof(uint32_t) * n + 1); + + while(*utf8) { + if (!(*utf8 & 0x80U)) { + utf32[i++] = *utf8++; + } else if ((*utf8 & 0xe0U) == 0xc0U) { + c = (*utf8++ & 0x1fU) << 6; + utf32[i++] = c + (*utf8++ & 0x3fU); + } else if ((*utf8 & 0xf0U) == 0xe0U) { + c = (*utf8++ & 0x0fU) << 12; + c += (*utf8++ & 0x3fU) << 6; + utf32[i++] = c + (*utf8++ & 0x3fU); + } else if ((*utf8 & 0xf8U) == 0xf0U) { + c = (*utf8++ & 0x07U) << 18; + c += (*utf8++ & 0x3fU) << 12; + c += (*utf8++ & 0x3fU) << 6; + c += (*utf8++ & 0x3fU); + utf32[i++] = c; + } else { + free(utf32); + return nullptr; + } + } + utf32[i] = 0; //end of the unicdoe + return utf32; +} + + +void TtfLoader::clear() +{ + _unmap(this); + shape = nullptr; +} + + +/************************************************************************/ +/* External Class Implementation */ +/************************************************************************/ + + +bool TtfLoader::resize(Paint* paint, float sx, TVG_UNUSED float sy) +{ + if (!paint) return false; + auto shift = 0.0f; + auto dpi = 96.0f / 72.0f; //dpi base? + auto scale = sx * dpi / reader.metrics.unitsPerEm; + if (italic) shift = -scale * 0.18f; //experimental decision. + Matrix m = {scale, shift, -(shift * reader.metrics.minw), 0, scale, 0, 0, 0, 1}; + paint->transform(m); + + return true; +} + + +TtfLoader::TtfLoader() : FontLoader(FileType::Ttf) +{ +} + + +TtfLoader::~TtfLoader() +{ + clear(); +} + + +bool TtfLoader::open(const string& path) +{ + clear(); + if (!_map(this, path)) return false; + return reader.header(); +} + + +bool TtfLoader::request(Shape* shape, char* text, bool italic) +{ + this->shape = shape; + this->text = text; + this->italic = italic; + + return true; +} + + +bool TtfLoader::read() +{ + if (!text) return false; + + shape->reset(); + shape->fill(FillRule::EvenOdd); + + auto n = strlen(text); + auto code = _codepoints(text, n); + + //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; + + size_t idx = 0; + while (code[idx] && idx < n) { + 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; + } + offset.x += (gmetrics.advanceWidth + kerning.x); + lglyph = rglyph; + //store the first glyph min size for italic transform. + if (idx == 0) reader.metrics.minw = gmetrics.minw; + ++idx; + } + + free(code); + + return true; +} diff --git a/src/loaders/ttf/tvgTtfLoader.h b/src/loaders/ttf/tvgTtfLoader.h new file mode 100644 index 00000000..2c196030 --- /dev/null +++ b/src/loaders/ttf/tvgTtfLoader.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 the ThorVG project. All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef _TVG_TTF_LOADER_H_ +#define _TVG_TTF_LOADER_H_ + +#include "tvgLoader.h" +#include "tvgTaskScheduler.h" +#include "tvgTtfReader.h" + + +struct TtfLoader : public FontLoader +{ +#if defined(_WIN32) + HANDLE mapping = nullptr; +#endif + TtfReader reader; + char* text = nullptr; + Shape* shape = nullptr; + bool italic = false; + + TtfLoader(); + ~TtfLoader(); + + using FontLoader::open; + bool open(const string& path) override; + bool resize(Paint* paint, float w, float h) override; + bool request(Shape* shape, char* text, bool italic = false) override; + bool read() override; + void clear(); +}; + +#endif //_TVG_PNG_LOADER_H_ diff --git a/src/loaders/ttf/tvgTtfReader.cpp b/src/loaders/ttf/tvgTtfReader.cpp new file mode 100644 index 00000000..fcfd4784 --- /dev/null +++ b/src/loaders/ttf/tvgTtfReader.cpp @@ -0,0 +1,563 @@ +/* + * Copyright (c) 2023 the ThorVG project. All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include + +#include "tvgMath.h" +#include "tvgShape.h" +#include "tvgTtfReader.h" + +/************************************************************************/ +/* Internal Class Implementation */ +/************************************************************************/ + + +static inline uint32_t _u32(uint8_t* data, uint32_t offset) +{ + auto base = data + offset; + return (base[0] << 24 | base[1] << 16 | base[2] << 8 | base[3]); +} + + +static inline uint16_t _u16(uint8_t* data, uint32_t offset) +{ + auto base = data + offset; + return (base[0] << 8 | base[1]); +} + + +static inline int16_t _i16(uint8_t* data, uint32_t offset) +{ + return (int16_t) _u16(data, offset); +} + + +static inline uint8_t _u8(uint8_t* data, uint32_t offset) +{ + return *(data + offset); +} + + +static inline int8_t _i8(uint8_t* data, uint32_t offset) +{ + return (int8_t) _u8(data, offset); +} + + +static int _cmpu32(const void *a, const void *b) +{ + return memcmp(a, b, 4); +} + + +bool TtfReader::validate(uint32_t offset, uint32_t margin) const +{ +#if 1 + if ((offset > size) || (size - offset < margin)) { + TVGERR("TTF", "Invalidate data"); + return false; + } +#endif + return true; +} + + +uint32_t TtfReader::table(const char* tag) +{ + auto tableCnt = _u16(data, 4); + if (!validate(12, (uint32_t) tableCnt * 16)) return 0; + + auto match = bsearch(tag, data + 12, tableCnt, 16, _cmpu32); + if (!match) { + TVGLOG("TTF", "No searching table = %s", tag); + return 0; + } + + return _u32(data, (uint32_t) ((uint8_t*) match - data + 8)); +} + + +uint32_t TtfReader::cmap_12_13(uint32_t table, uint32_t codepoint, int fmt) const +{ + //A minimal header is 16 bytes + auto len = _u32(data, table + 4); + if (len < 16) return -1; + + if (!validate(table, len)) return -1; + + auto entryCnt = _u32(data, table + 12); + + for (uint32_t i = 0; i < entryCnt; ++i) { + auto firstCode = _u32(data, table + (i * 12) + 16); + auto lastCode = _u32(data, table + (i * 12) + 16 + 4); + if (codepoint < firstCode || codepoint > lastCode) continue; + auto glyphOffset = _u32(data, table + (i * 12) + 16 + 8); + if (fmt == 12) return (codepoint - firstCode) + glyphOffset; + else return glyphOffset; + } + return -1; +} + + +uint32_t TtfReader::cmap_4(uint32_t table, uint32_t codepoint) const +{ + //cmap format 4 only supports the Unicode BMP. + if (codepoint > 0xffff) return -1; + + auto shortCode = static_cast(codepoint); + + if (!validate(table, 8)) return -1; + + auto segmentCnt = _u16(data, table); + if ((segmentCnt & 1) || segmentCnt == 0) return -1; + + //find starting positions of the relevant arrays. + auto endCodes = table + 8; + auto startCodes = endCodes + segmentCnt + 2; + auto idDeltas = startCodes + segmentCnt; + auto idRangeOffsets = idDeltas + segmentCnt; + + if (!validate(idRangeOffsets, segmentCnt)) return -1; + + //find the segment that contains shortCode by binary searching over the highest codes in the segments. + //binary search, but returns the next highest element if key could not be found. + uint8_t* segment = nullptr; + auto n = segmentCnt / 2; + if (n > 0) { + uint8_t key[2] = {(uint8_t)(codepoint >> 8), (uint8_t)codepoint}; + auto bytes = data + endCodes; + auto low = 0; + auto high = n - 1; + while (low != high) { + auto mid = low + (high - low) / 2; + auto sample = bytes + mid * 2; + if (memcmp(key, sample, 2) > 0) { + low = mid + 1; + } else { + high = mid; + } + } + segment = (bytes + low * 2); + } + + auto segmentIdx = static_cast(segment - (data + endCodes)); + + //look up segment info from the arrays & short circuit if the spec requires. + auto startCode = _u16(data, startCodes + segmentIdx); + if (startCode > shortCode) return 0; + + auto delta = _u16(data, idDeltas + segmentIdx); + auto idRangeOffset = _u16(data, idRangeOffsets + segmentIdx); + //intentional integer under- and overflow. + if (idRangeOffset == 0) return (shortCode + delta) & 0xffff; + + //calculate offset into glyph array and determine ultimate value. + auto offset = idRangeOffsets + segmentIdx + idRangeOffset + 2U * (unsigned int)(shortCode - startCode); + if (!validate(offset, 2)) return -1; + + auto id = _u16(data, offset); + //intentional integer under- and overflow. + if (id > 0) return (id + delta) & 0xffff; + + return 0; +} + + +uint32_t TtfReader::cmap_6(uint32_t table, uint32_t codepoint) const +{ + //cmap format 6 only supports the Unicode BMP. + if (codepoint > 0xFFFF) return 0; + + auto firstCode = _u16(data, table); + auto entryCnt = _u16(data, table + 2); + if (!validate(table, 4 + 2 * entryCnt)) return -1; + + if (codepoint < firstCode) return -1; + codepoint -= firstCode; + if (codepoint >= entryCnt) return -1; + return _u16(data, table + 4 + 2 * codepoint); +} + + +//Returns the offset into the font that the glyph's outline is stored at +uint32_t TtfReader::outlineOffset(uint32_t glyph) +{ + uint32_t cur, next; + + if (!loca) loca = table("loca"); + if (!glyf) glyf = table("glyf"); + + if (metrics.locaFormat == 0) { + auto base = loca + 2 * glyph; + if (!validate(base, 4)) { + TVGERR("TTF", "invalid outline offset"); + return 0; + } + cur = 2U * (uint32_t) _u16(data, base); + next = 2U * (uint32_t) _u16(data, base + 2); + } else { + auto base = loca + 4 * glyph; + if (!validate(base, 8)) return 0; + cur = _u32(data, base); + next = _u32(data, base + 4); + } + if (cur == next) return 0; + return glyf + cur; +} + + +bool TtfReader::points(uint32_t outline, uint8_t* flags, Point* pts, uint32_t ptsCnt, const Point& offset) +{ + #define X_CHANGE_IS_SMALL 0x02 + #define X_CHANGE_IS_POSITIVE 0x10 + #define X_CHANGE_IS_ZERO 0x10 + #define Y_CHANGE_IS_SMALL 0x04 + #define Y_CHANGE_IS_ZERO 0x20 + #define Y_CHANGE_IS_POSITIVE 0x20 + + long accum = 0L; + + for (uint32_t i = 0; i < ptsCnt; ++i) { + if (flags[i] & X_CHANGE_IS_SMALL) { + if (!validate(outline, 1)) { + return false; + } + auto value = (long) _u8(data, outline++); + auto bit = !!(flags[i] & X_CHANGE_IS_POSITIVE); + accum -= (value ^ -bit) + bit; + } else if (!(flags[i] & X_CHANGE_IS_ZERO)) { + if (!validate(outline, 2)) return false; + accum += _i16(data, outline); + outline += 2; + } + pts[i].x = offset.x + (float) accum; + } + + accum = 0L; + + for (uint32_t i = 0; i < ptsCnt; ++i) { + if (flags[i] & Y_CHANGE_IS_SMALL) { + if (!validate(outline, 1)) return false; + auto value = (long) _u8(data, outline++); + auto bit = !!(flags[i] & Y_CHANGE_IS_POSITIVE); + accum -= (value ^ -bit) + bit; + } else if (!(flags[i] & Y_CHANGE_IS_ZERO)) { + if (!validate(outline, 2)) return false; + accum += _i16(data, outline); + outline += 2; + } + pts[i].y = offset.y - (float) accum; + } + return true; +} + + +bool TtfReader::flags(uint32_t *outline, uint8_t* flags, uint32_t flagsCnt) +{ + #define REPEAT_FLAG 0x08 + + auto offset = *outline; + uint8_t value = 0; + uint8_t repeat = 0; + + for (uint32_t i = 0; i < flagsCnt; ++i) { + if (repeat) { + --repeat; + } else { + if (!validate(offset, 1)) return false; + value = _u8(data, offset++); + if (value & REPEAT_FLAG) { + if (!validate(offset, 1)) return false; + repeat = _u8(data, offset++); + } + } + flags[i] = value; + } + *outline = offset; + return true; +} + + +/************************************************************************/ +/* External Class Implementation */ +/************************************************************************/ + +bool TtfReader::header() +{ + if (!validate(0, 12)) return false; + + //verify ttf(scalable font) + auto type = _u32(data, 0); + if (type != 0x00010000 && type != 0x74727565) return false; + + //header + auto head = table("head"); + if (!validate(head, 54)) return false; + + metrics.unitsPerEm = _u16(data, head + 18); + metrics.locaFormat = _u16(data, head + 50); + + //horizontal metrics + auto hhea = table("hhea"); + if (!validate(hhea, 36)) return false; + + metrics.hhea.ascent = _i16(data, hhea + 4); + metrics.hhea.descent = _i16(data, hhea + 6); + metrics.hhea.lineGap = _i16(data, hhea + 8); + metrics.numHmtx = _u16(data, hhea + 34); + + //kerning + this->kern = table("kern"); + if (this->kern) { + if (!validate(kern, 4)) return false; + if (_u16(data, kern) != 0) return false; + } + + return true; +} + + +uint32_t TtfReader::glyph(uint32_t codepoint) +{ + if (cmap == 0) { + cmap = table("cmap"); + if (!validate(cmap, 4)) return -1; + } + + auto entryCnt = _u16(data, cmap + 2); + if (!validate(cmap, 4 + entryCnt * 8)) return -1; + + //full repertory (non-BMP map). + for (auto idx = 0; idx < entryCnt; ++idx) { + auto entry = cmap + 4 + idx * 8; + auto type = _u16(data, entry) * 0100 + _u16(data, entry + 2); + //unicode map + if (type == 0004 || type == 0312) { + auto table = cmap + _u32(data, entry + 4); + if (!validate(table, 8)) return -1; + //dispatch based on cmap format. + auto format = _u16(data, table); + switch (format) { + case 12: return cmap_12_13(table, codepoint, 12); + default: return -1; + } + } + } + + //Try looking for a BMP map. + for (auto idx = 0; idx < entryCnt; ++idx) { + auto entry = cmap + 4 + idx * 8; + auto type = _u16(data, entry) * 0100 + _u16(data, entry + 2); + //Unicode BMP + if (type == 0003 || type == 0301) { + auto table = cmap + _u32(data, entry + 4); + if (!validate(table, 6)) return -1; + //Dispatch based on cmap format. + switch (_u16(data, table)) { + case 4: return cmap_4(table + 6, codepoint); + case 6: return cmap_6(table + 6, codepoint); + default: return -1; + } + } + } + return -1; +} + + +uint32_t TtfReader::glyph(uint32_t codepoint, TtfGlyphMetrics& gmetrics) +{ + auto glyph = this->glyph(codepoint); + if (glyph == INVALID_GLYPH) { + TVGERR("TTF", "invalid glyph id, codepoint(0x%x)", codepoint); + return INVALID_GLYPH; + } + + //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; + 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; + + auto offset = boundary - 4; + if (!validate(offset, 4)) return INVALID_GLYPH; + gmetrics.advanceWidth = _u16(data, offset); + offset = boundary + 2 * (glyph - metrics.numHmtx); + if (!validate(offset, 2)) return INVALID_GLYPH; + gmetrics.leftSideBearing = _i16(data, offset); + } + + gmetrics.outline = outlineOffset(glyph); + if (!gmetrics.outline || !validate(gmetrics.outline, 10)) return INVALID_GLYPH; + + //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)); + + if (bbox[2] <= bbox[0] || bbox[3] <= bbox[1]) return INVALID_GLYPH; + + gmetrics.minw = bbox[2] - bbox[0] + 1; + gmetrics.minh = bbox[3] - bbox[1] + 1; + gmetrics.yOffset = bbox[3]; + + return glyph; +} + + +bool TtfReader::convert(Shape* shape, TtfGlyphMetrics& gmetrics, const Point& offset, const Point& kerning) +{ + #define ON_CURVE 0x01 + + auto cntrsCnt = (uint32_t) _i16(data, gmetrics.outline); + if (cntrsCnt == 0) return false; + + auto outline = gmetrics.outline + 10; + if (!validate(outline, cntrsCnt * 2 + 2)) return false; + + auto ptsCnt = _u16(data, outline + (cntrsCnt - 1) * 2) + 1; + size_t endPts[cntrsCnt]; //the index of the contour points. + + for (uint32_t i = 0; i < cntrsCnt; ++i) { + endPts[i] = (uint32_t) _u16(data, outline); + outline += 2; + } + outline += 2U + _u16(data, outline); + + uint8_t flags[ptsCnt]; + if (!this->flags(&outline, flags, ptsCnt)) return false; + + Point pts[ptsCnt]; + if (!this->points(outline, flags, pts, ptsCnt, offset + kerning)) return false; + + //generate tvg pathes. + auto& pathCmds = P(shape)->rs.path.cmds; + auto& pathPts = P(shape)->rs.path.pts; + pathCmds.reserve(ptsCnt); + pathPts.reserve(ptsCnt); + + uint32_t begin = 0; + + for (uint32_t i = 0; i < cntrsCnt; ++i) { + //contour must start with move to + pathCmds.push(PathCommand::MoveTo); + pathPts.push(pts[begin]); + + bool offCurve = false; + auto last = (endPts[i] - begin) + 1; + + for (uint32_t x = 1; x < last; ++x) { + if (flags[begin + x] & ON_CURVE) { + if (offCurve) { + pathCmds.push(PathCommand::CubicTo); + pathPts.push(pathPts.last() + (2.0f/3.0f) * (pts[begin + x - 1] - pathPts.last())); + pathPts.push(pts[begin + x] + (2.0f/3.0f) * (pts[begin + x - 1] - pts[begin + x])); + pathPts.push(pts[begin + x]); + offCurve = false; + } else { + pathCmds.push(PathCommand::LineTo); + pathPts.push(pts[begin + x]); + } + } else { + if (offCurve) { + pathCmds.push(PathCommand::CubicTo); + auto end = (pts[begin + x] + pts[begin + x - 1]) * 0.5f; + pathPts.push(pathPts.last() + (2.0f/3.0f) * (pts[begin + x - 1] - pathPts.last())); + pathPts.push(end + (2.0f/3.0f) * (pts[begin + x - 1] - end)); + pathPts.push(end); + } else { + offCurve = true; + } + } + } + if (offCurve) { + pathCmds.push(PathCommand::CubicTo); + pathPts.push(pathPts.last() + (2.0f/3.0f) * (pts[begin + last - 1] - pathPts.last())); + pathPts.push(pts[begin] + (2.0f/3.0f) * (pts[begin + last - 1] - pts[begin])); + pathPts.push(pts[begin]); + } + //contour must end with close + pathCmds.push(PathCommand::Close); + begin = endPts[i] + 1; + } + return true; +} + + +void TtfReader::kerning(uint32_t lglyph, uint32_t rglyph, Point& out) +{ + #define HORIZONTAL_KERNING 0x01 + #define MINIMUM_KERNING 0x02 + #define CROSS_STREAM_KERNING 0x04 + + if (!kern) return; + + auto kern = this->kern; + + out.x = out.y = 0.0f; + + //kern tables + auto tableCnt = _u16(data, kern + 2); + kern += 4; + + while (tableCnt > 0) { + //read subtable header. + if (!validate(kern, 6)) return; + auto length = _u16(data, kern + 2); + auto format = _u8(data, kern + 4); + auto flags = _u8(data, kern + 5); + kern += 6; + + if (format == 0 && (flags & HORIZONTAL_KERNING) && !(flags & MINIMUM_KERNING)) { + //read format 0 header. + if (!validate(kern, 8)) return; + auto pairCnt = _u16(data, kern); + kern += 8; + + //look up character code pair via binary search. + uint8_t key[4]; + key[0] = (lglyph >> 8) & 0xff; + key[1] = lglyph & 0xff; + key[2] = (rglyph >> 8) & 0xff; + key[3] = rglyph & 0xff; + + auto match = bsearch(key, data + kern, pairCnt, 6, _cmpu32); + + if (match) { + auto value = _i16(data, (uint32_t)((uint8_t *) match - data + 4)); + if (flags & CROSS_STREAM_KERNING) out.y += value; + else out.x += value; + } + } + kern += length; + --tableCnt; + } +} diff --git a/src/loaders/ttf/tvgTtfReader.h b/src/loaders/ttf/tvgTtfReader.h new file mode 100644 index 00000000..e688db85 --- /dev/null +++ b/src/loaders/ttf/tvgTtfReader.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 the ThorVG project. All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef _TVG_TTF_READER_H +#define _TVG_TTF_READER_H + +#include "tvgCommon.h" +#include "tvgArray.h" + +#define INVALID_GLYPH ((uint32_t)-1) + +struct TtfGlyphMetrics +{ + uint32_t outline; //glyph outline table offset + + float advanceWidth; + float leftSideBearing; + float yOffset; + float minw; + float minh; +}; + + +struct TtfReader +{ +public: + uint8_t* data = nullptr; + uint32_t size = 0; + + struct + { + //horizontal header info + struct { + float ascent; + float descent; + float lineGap; + } hhea; + + float minw; //first glyph width (used for italic) + uint16_t unitsPerEm; + uint16_t numHmtx; //the number of Horizontal metrics table + uint8_t locaFormat; //0 for short offsets, 1 for long + } metrics; + + 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); + +private: + //table offsets + uint32_t cmap = 0; + uint32_t hmtx = 0; + uint32_t loca = 0; + uint32_t glyf = 0; + uint32_t kern = 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; + uint32_t cmap_6(uint32_t table, uint32_t codepoint) const; + bool validate(uint32_t offset, uint32_t margin) const; + uint32_t table(const char* tag); + uint32_t outlineOffset(uint32_t glyph); + uint32_t glyph(uint32_t codepoint); + 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); + bool flags(uint32_t *outline, uint8_t* flags, uint32_t flagsCnt); +}; + +#endif //_TVG_TTF_READER_H \ No newline at end of file diff --git a/src/renderer/tvgLoadModule.h b/src/renderer/tvgLoadModule.h index 00461dbc..557ba3eb 100644 --- a/src/renderer/tvgLoadModule.h +++ b/src/renderer/tvgLoadModule.h @@ -81,4 +81,12 @@ struct ImageLoader : LoadModule virtual Paint* paint() { return nullptr; } }; + +struct FontLoader : LoadModule +{ + FontLoader(FileType type) : LoadModule(type) {} + + virtual bool request(Shape* shape, char* text, bool italic = false) = 0; +}; + #endif //_TVG_LOAD_MODULE_H_