svg_loader: handle embedded fonts

It is possible to embed fonts directly into an SVG file.
Support for parsing and loading embedded fonts has been added.

@Issue: https://github.com/thorvg/thorvg/issues/1897
This commit is contained in:
Mira Grudzinska 2025-02-19 11:59:39 +01:00 committed by Hermet Park
parent f1e9ce0460
commit 468a1db1fa
4 changed files with 122 additions and 0 deletions

View file

@ -375,6 +375,27 @@ static char* _idFromUrl(const char* url)
}
static size_t _srcFromUrl(const char* url, char*& src)
{
src = (char*)strchr(url, '(');
auto close = strchr(url, ')');
if (!src || !close || src >= close) return 0;
src = strchr(src, '\'');
if (!src || src >= close) return 0;
++src;
close = strchr(src, '\'');
if (!close || close == src) return 0;
--close;
while (src < close && *src == ' ') ++src;
while (src < close && *close == ' ') --close;
return close - src + 1;
}
static unsigned char _parseColor(const char* value, char** end)
{
float r;
@ -2082,6 +2103,42 @@ static SvgNode* _createImageNode(SvgLoaderData* loader, SvgNode* parent, const c
}
static char* _unquote(const char* str)
{
auto len = str ? strlen(str) : 0;
if (len >= 2 && str[0] == '\'' && str[len - 1] == '\'') return duplicate(str + 1, len - 2);
return strdup(str);
}
static bool _attrParseFontFace(void* data, const char* key, const char* value)
{
if (!key || !value) return false;
key = _skipSpace(key, nullptr);
value = _skipSpace(value, nullptr);
auto loader = (SvgLoaderData*)data;
auto& font = loader->fonts.last();
if (STR_AS(key, "font-family")) {
if (font.name) tvg::free(font.name);
font.name = _unquote(value);
} else if (STR_AS(key, "src")) {
font.srcLen = _srcFromUrl(value, font.src);
}
return true;
}
static void _createFontFace(SvgLoaderData* loader, const char* buf, unsigned bufLength, parseAttributes func)
{
loader->fonts.push(FontFace());
func(buf, bufLength, _attrParseFontFace, loader);
}
static SvgNode* _getDefsNode(SvgNode* node)
{
if (!node) return nullptr;
@ -3518,6 +3575,8 @@ static void _svgLoaderParserXmlCssStyle(SvgLoaderData* loader, const char* conte
TVGLOG("SVG", "Unsupported elements used in the internal CSS style sheets [Elements: %s]", tag);
} else if (STR_AS(tag, "all")) {
if ((node = _createCssStyleNode(loader, loader->cssStyle, attrs, attrsLength, simpleXmlParseW3CAttribute))) node->id = _copyId(name);
} else if (STR_AS(tag, "@font-face")) { //css at-rule specifying font
_createFontFace(loader, attrs, attrsLength, simpleXmlParseW3CAttribute);
} else if (!isIgnoreUnsupportedLogElements(tag)) {
TVGLOG("SVG", "Unsupported elements used in the internal CSS style sheets [Elements: %s]", tag);
}
@ -3871,6 +3930,13 @@ void SvgLoader::clear(bool all)
ARRAY_FOREACH(p, loaderData.images) tvg::free(*p);
loaderData.images.reset();
ARRAY_FOREACH(p, loaderData.fonts) {
Text::unload(p->name);
tvg::free(p->decoded);
tvg::free(p->name);
}
loaderData.fonts.reset();
if (copy) tvg::free((char*)content);
delete(root);

View file

@ -568,6 +568,14 @@ struct SvgNodeIdPair
char *id;
};
struct FontFace
{
char* name = nullptr;
char* src = nullptr;
size_t srcLen = 0;
char* decoded = nullptr;
};
enum class OpenedTagType : uint8_t
{
Other = 0,
@ -587,6 +595,7 @@ struct SvgLoaderData
Array<SvgNodeIdPair> cloneNodes;
Array<SvgNodeIdPair> nodesToStyle;
Array<char*> images; //embedded images
Array<FontFace> fonts;
int level = 0;
bool result = false;
OpenedTagType openedTag = OpenedTagType::Other;

View file

@ -947,6 +947,43 @@ static void _updateInvalidViewSize(Scene* scene, Box& vBox, float& w, float& h,
if (!validHeight) h *= vBox.h;
}
static void _loadFonts(Array<FontFace>& fonts)
{
if (fonts.empty()) return;
static constexpr struct {
const char* prefix;
size_t len;
} prefixes[] = {
{"data:font/ttf;base64,", sizeof("data:font/ttf;base64,") - 1},
{"data:application/font-ttf;base64,", sizeof("data:application/font-ttf;base64,") - 1}
};
ARRAY_FOREACH(p, fonts) {
if (!p->name) continue;
size_t shift = 0;
for (const auto& prefix : prefixes) {
if (p->srcLen > prefix.len && !memcmp(p->src, prefix.prefix, prefix.len)) {
shift = prefix.len;
break;
}
}
if (shift == 0) {
TVGLOG("SVG", "The embedded font \"%s\" data not loaded properly.", p->name);
continue;
}
auto size = b64Decode(p->src + shift, p->srcLen - shift, &p->decoded);
TaskScheduler::async(false);
if (Text::load(p->name, p->decoded, size) != Result::Success) TVGERR("SVG", "Error while loading the ttf font named \"%s\".", p->name);
TaskScheduler::async(true);
}
}
/************************************************************************/
/* External Class Implementation */
/************************************************************************/
@ -957,6 +994,8 @@ Scene* svgSceneBuild(SvgLoaderData& loaderData, Box vBox, float w, float h, Aspe
if (!loaderData.doc || (loaderData.doc->type != SvgNodeType::Doc)) return nullptr;
_loadFonts(loaderData.fonts);
auto docNode = _sceneBuildHelper(loaderData, loaderData.doc, vBox, svgPath, false, 0);
if (!(viewFlag & SvgViewFlag::Viewbox)) _updateInvalidViewSize(docNode, vBox, w, h, viewFlag);

View file

@ -473,6 +473,14 @@ bool simpleXmlParseW3CAttribute(const char* buf, unsigned bufLength, simpleXMLAt
do {
char* sep = (char*)strchr(buf, ':');
next = (char*)strchr(buf, ';');
if (auto src = strstr(buf, "src")) {//src tag from css font-face contains extra semicolon
if (src < sep) {
if (next + 1 < end) next = (char*)strchr(next + 1, ';');
else return true;
}
}
if (sep >= end) {
next = nullptr;
sep = nullptr;