loader/ttf: introduce a new sfnt(scalable font) loader.

ttf is an industry standard format that is the most widely used
in the products. Now thorvg supports the basic features of
the font to supplement the text drawing.

The implementation is followed the ttf spec,
the covered features are:

- horizontal layouting with kerning.
- utf8 -> utf32 converted glyph drawing.

To use the feature, please enable ttf loader:
$meson -Dloaders="ttf_beta, ..."

@Issue: https://github.com/thorvg/thorvg/issues/969
This commit is contained in:
Hermet Park 2023-12-20 14:22:42 +09:00 committed by Hermet Park
parent bd37e8ba37
commit 3c4e434b39
10 changed files with 1006 additions and 16 deletions

View file

@ -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'),

View file

@ -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".)')

View file

@ -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 <typename T>
static inline T mathLerp(const T &start, const T &end, float t)
{

View file

@ -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

View file

@ -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
)]

View file

@ -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 <windows.h>
#else
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#endif
#include <memory.h>
#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;
}

View file

@ -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_

View file

@ -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 <memory.h>
#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<uint16_t>(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<uint32_t>(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<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));
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;
}
}

View file

@ -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

View file

@ -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_