1 /*
2 * Copyright 2011 The Android Open Source Project
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 * 2021.9.9 SkFontMgr_config_parser on previewer of ohos.
7 * Copyright (c) 2021 Huawei Device Co., Ltd. All rights reserved.
8 */
9
10 // Despite the name and location, this is portable code.
11
12 #include "src/ports/SkFontMgr_config_parser.h"
13 #include "src/ports/skia_ohos/HmSymbolConfig_ohos.h"
14
15 #include <expat.h>
16
17 #include "include/core/SkStream.h"
18 #include "include/private/base/SkFixed.h"
19 #include "src/base/SkTSearch.h"
20 #include "include/private/base/SkTDArray.h"
21
22 #define SK_FONT_CONFIG_FILE_NAME "fonts.xml"
23 std::string SkFontMgr::containerFontPath = "";
24
25 namespace {
26
27 std::string g_lmpSystemFontsFile = "INVALID_FILE_PATH";
28
29 }
30
31 /**
32 * This parser file for android contains TWO 'familyset' handlers:
33 * One for JB and earlier and the other for LMP and later.
34 * For previewer, we only use the fonts.xml in LMP, so the LMP 'familyset' handler is only needed.
35 */
36
37 struct FamilyData;
38
39 struct TagHandler {
40 /** Called at the start tag.
41 * Called immediately after the parent tag retuns this handler from a call to 'tag'.
42 * Allows setting up for handling the tag content and processing attributes.
43 * If nullptr, will not be called.
44 */
45 void (*start)(FamilyData* data, const char* tag, const char** attributes);
46
47 /** Called at the end tag.
48 * Allows post-processing of any accumulated information.
49 * This will be the last call made in relation to the current tag.
50 * If nullptr, will not be called.
51 */
52 void (*end)(FamilyData* data, const char* tag);
53
54 /** Called when a nested tag is encountered.
55 * This is responsible for determining how to handle the tag.
56 * If the tag is not recognized, return nullptr to skip the tag.
57 * If nullptr, all nested tags will be skipped.
58 */
59 const TagHandler* (*tag)(FamilyData* data, const char* tag, const char** attributes);
60
61 /** The character handler for this tag.
62 * This is only active for character data contained directly in this tag (not sub-tags).
63 * The first parameter will be castable to a FamilyData*.
64 * If nullptr, any character data in this tag will be ignored.
65 */
66 XML_CharacterDataHandler chars;
67 };
68
69 /** Represents the current parsing state. */
70 struct FamilyData {
FamilyDataFamilyData71 FamilyData(XML_Parser parser, SkTDArray<FontFamily*>& families,
72 const SkString& basePath, bool isFallback, const char* filename,
73 const TagHandler* topLevelHandler)
74 : fParser(parser)
75 , fFamilies(families)
76 , fCurrentFamily(nullptr)
77 , fCurrentFontInfo(nullptr)
78 , fVersion(0)
79 , fBasePath(basePath)
80 , fIsFallback(isFallback)
81 , fFilename(filename)
82 , fDepth(1)
83 , fSkip(0)
84 , fHandler(&topLevelHandler, 1)
85 { }
86
87 XML_Parser fParser; // The expat parser doing the work, owned by caller
88 SkTDArray<FontFamily*>& fFamilies; // The array to append families, owned by caller
89 std::unique_ptr<FontFamily> fCurrentFamily; // The family being created, owned by this
90 FontFileInfo* fCurrentFontInfo; // The info being created, owned by fCurrentFamily
91 int fVersion; // The version of the file parsed.
92 const SkString& fBasePath; // The current base path.
93 const bool fIsFallback; // The file being parsed is a fallback file
94 const char* fFilename; // The name of the file currently being parsed.
95
96 int fDepth; // The current element depth of the parse.
97 int fSkip; // The depth to stop skipping, 0 if not skipping.
98 SkTDArray<const TagHandler*> fHandler; // The stack of current tag handlers.
99 };
100
memeq(const char * s1,const char * s2,size_t n1,size_t n2)101 static bool memeq(const char *s1, const char *s2, size_t n1, size_t n2)
102 {
103 return n1 == n2 && 0 == memcmp(s1, s2, n1);
104 }
105 #define MEMEQ(c, s, n) memeq(c, s, sizeof(c) - 1, n)
106
107 #define ATTS_NON_NULL(a, i) (a[i] != nullptr && a[i + 1] != nullptr)
108
109 #define SK_FONTMGR_CONFIG_PARSER_PREFIX "[SkFontMgr Config Parser] "
110
111 #define SK_FONTCONFIGPARSER_WARNING(message, ...) \
112 SkDebugf(SK_FONTMGR_CONFIG_PARSER_PREFIX "%s:%d:%d: warning: " message "\n", self->fFilename, \
113 XML_GetCurrentLineNumber(self->fParser), XML_GetCurrentColumnNumber(self->fParser), ##__VA_ARGS__)
114
is_whitespace(char c)115 static bool is_whitespace(char c)
116 {
117 return c == ' ' || c == '\n' || c == '\r' || c == '\t';
118 }
119
trim_string(SkString * s)120 static void trim_string(SkString* s)
121 {
122 char* str = s->data();
123 const char* start = str; // start is inclusive
124 const char* end = start + s->size(); // end is exclusive
125 while (is_whitespace(*start)) { ++start; }
126 if (start != end) {
127 --end; // make end inclusive
128 while (is_whitespace(*end)) {
129 --end;
130 }
131 ++end; // make end exclusive
132 }
133 size_t len = end - start;
134 memmove(str, start, len);
135 s->resize(len);
136 }
137
138 namespace lmpParser {
139
140 static const TagHandler axisHandler = {
__anond7a08b590202() 141 /*start*/[](FamilyData* self, const char* tag, const char** attributes) {
142 FontFileInfo& file = *self->fCurrentFontInfo;
143 SkFourByteTag axisTag = SkSetFourByteTag('\0', '\0', '\0', '\0');
144 SkFixed axisStyleValue = 0;
145 bool axisTagIsValid = false;
146 bool axisStyleValueIsValid = false;
147 for (size_t i = 0; ATTS_NON_NULL(attributes, i); i += 2) {
148 const char* name = attributes[i];
149 const char* value = attributes[i + 1];
150 size_t nameLen = strlen(name);
151 if (MEMEQ("tag", name, nameLen)) {
152 size_t valueLen = strlen(value);
153 if (valueLen == 4) {
154 axisTag = SkSetFourByteTag(value[0], value[1], value[2], value[3]);
155 axisTagIsValid = true;
156 for (int j = 0; j < file.fVariationDesignPosition.size() - 1; ++j) {
157 if (file.fVariationDesignPosition[j].axis == axisTag) {
158 axisTagIsValid = false;
159 SK_FONTCONFIGPARSER_WARNING("'%c%c%c%c' axis specified more than once",
160 (axisTag >> 24) & 0xFF,
161 (axisTag >> 16) & 0xFF,
162 (axisTag >> 8) & 0xFF,
163 (axisTag ) & 0xFF);
164 }
165 }
166 } else {
167 SK_FONTCONFIGPARSER_WARNING("'%s' is an invalid axis tag", value);
168 }
169 } else if (MEMEQ("stylevalue", name, nameLen)) {
170 if (parse_fixed<16>(value, &axisStyleValue)) {
171 axisStyleValueIsValid = true;
172 } else {
173 SK_FONTCONFIGPARSER_WARNING("'%s' is an invalid axis stylevalue", value);
174 }
175 }
176 }
177 if (axisTagIsValid && axisStyleValueIsValid) {
178 auto& coordinate = file.fVariationDesignPosition.push_back();
179 coordinate.axis = axisTag;
180 coordinate.value = SkFixedToScalar(axisStyleValue);
181 }
182 },
183 /*end*/nullptr,
184 /*tag*/nullptr,
185 /*chars*/nullptr,
186 };
187
188 static const TagHandler fontHandler = {
__anond7a08b590302() 189 /*start*/[](FamilyData* self, const char* tag, const char** attributes) {
190 // 'weight' (non-negative integer) [default 0]
191 // 'style' ("normal", "italic") [default "auto"]
192 // 'index' (non-negative integer) [default 0]
193 // The character data should be a filename.
194 FontFileInfo& file = self->fCurrentFamily->fFonts.push_back();
195 self->fCurrentFontInfo = &file;
196 SkString fallbackFor;
197 for (size_t i = 0; ATTS_NON_NULL(attributes, i); i += 2) {
198 const char* name = attributes[i];
199 const char* value = attributes[i + 1];
200 size_t nameLen = strlen(name);
201 if (MEMEQ("weight", name, nameLen)) {
202 if (!parse_non_negative_integer(value, &file.fWeight)) {
203 SK_FONTCONFIGPARSER_WARNING("'%s' is an invalid weight", value);
204 }
205 } else if (MEMEQ("style", name, nameLen)) {
206 size_t valueLen = strlen(value);
207 if (MEMEQ("normal", value, valueLen)) {
208 file.fStyle = FontFileInfo::Style::kNormal;
209 } else if (MEMEQ("italic", value, valueLen)) {
210 file.fStyle = FontFileInfo::Style::kItalic;
211 }
212 } else if (MEMEQ("index", name, nameLen)) {
213 if (!parse_non_negative_integer(value, &file.fIndex)) {
214 SK_FONTCONFIGPARSER_WARNING("'%s' is an invalid index", value);
215 }
216 } else if (MEMEQ("fallbackFor", name, nameLen)) {
217 /** fallbackFor specifies a family fallback and should have been on family. */
218 fallbackFor = value;
219 }
220 }
221 if (!fallbackFor.isEmpty()) {
222 std::unique_ptr<FontFamily>* fallbackFamily =
223 self->fCurrentFamily->fallbackFamilies.find(fallbackFor);
224 if (!fallbackFamily) {
225 std::unique_ptr<FontFamily> newFallbackFamily(
226 new FontFamily(self->fCurrentFamily->fBasePath, true));
227 fallbackFamily = self->fCurrentFamily->fallbackFamilies.set(
228 fallbackFor, std::move(newFallbackFamily));
229 (*fallbackFamily)->fLanguages = self->fCurrentFamily->fLanguages;
230 (*fallbackFamily)->fVariant = self->fCurrentFamily->fVariant;
231 (*fallbackFamily)->fOrder = self->fCurrentFamily->fOrder;
232 (*fallbackFamily)->fFallbackFor = fallbackFor;
233 }
234 self->fCurrentFontInfo = &(*fallbackFamily)->fFonts.emplace_back(file);
235 self->fCurrentFamily->fFonts.pop_back();
236 }
237 },
__anond7a08b590402() 238 /*end*/[](FamilyData* self, const char* tag) {
239 trim_string(&self->fCurrentFontInfo->fFileName);
240 },
__anond7a08b590502() 241 /*tag*/[](FamilyData* self, const char* tag, const char** attributes) -> const TagHandler* {
242 size_t len = strlen(tag);
243 if (MEMEQ("axis", tag, len)) {
244 return &axisHandler;
245 }
246 return nullptr;
247 },
__anond7a08b590602() 248 /*chars*/[](void* data, const char* s, int len) {
249 FamilyData* self = static_cast<FamilyData*>(data);
250 self->fCurrentFontInfo->fFileName.append(s, len);
251 }
252 };
253
254 static const TagHandler familyHandler = {
__anond7a08b590702() 255 /*start*/[](FamilyData* self, const char* tag, const char** attributes) {
256 // 'name' (string) [optional]
257 // 'lang' (space separated string) [default ""]
258 // 'variant' ("elegant", "compact") [default "default"]
259 // If there is no name, this is a fallback only font.
260 FontFamily* family = new FontFamily(self->fBasePath, true);
261 self->fCurrentFamily.reset(family);
262 for (size_t i = 0; ATTS_NON_NULL(attributes, i); i += 2) {
263 const char* name = attributes[i];
264 const char* value = attributes[i + 1];
265 size_t nameLen = strlen(name);
266 size_t valueLen = strlen(value);
267 if (MEMEQ("name", name, nameLen)) {
268 SkAutoAsciiToLC tolc(value);
269 family->fNames.push_back().set(tolc.lc());
270 family->fIsFallbackFont = false;
271 } else if (MEMEQ("lang", name, nameLen)) {
272 size_t i = 0;
273 while (true) {
274 for (; i < valueLen && is_whitespace(value[i]); ++i) { }
275 if (i == valueLen) {
276 break;
277 }
278 size_t j;
279 for (j = i + 1; j < valueLen && !is_whitespace(value[j]); ++j) { }
280 family->fLanguages.emplace_back(value + i, j - i);
281 i = j;
282 if (i == valueLen) {
283 break;
284 }
285 }
286 } else if (MEMEQ("variant", name, nameLen)) {
287 if (MEMEQ("elegant", value, valueLen)) {
288 family->fVariant = kElegant_FontVariant;
289 } else if (MEMEQ("compact", value, valueLen)) {
290 family->fVariant = kCompact_FontVariant;
291 }
292 }
293 }
294 },
__anond7a08b590802() 295 /*end*/[](FamilyData* self, const char* tag) {
296 *self->fFamilies.append() = self->fCurrentFamily.release();
297 },
__anond7a08b590902() 298 /*tag*/[](FamilyData* self, const char* tag, const char** attributes) -> const TagHandler* {
299 size_t len = strlen(tag);
300 if (MEMEQ("font", tag, len)) {
301 return &fontHandler;
302 }
303 return nullptr;
304 },
305 /*chars*/nullptr,
306 };
307
find_family(FamilyData * self,const SkString & familyName)308 static FontFamily* find_family(FamilyData* self, const SkString& familyName)
309 {
310 for (int i = 0; i < self->fFamilies.size(); i++) {
311 FontFamily* candidate = self->fFamilies[i];
312 for (int j = 0; j < candidate->fNames.size(); j++) {
313 if (candidate->fNames[j] == familyName) {
314 return candidate;
315 }
316 }
317 }
318 return nullptr;
319 }
320
321 static const TagHandler aliasHandler = {
__anond7a08b590a02() 322 /*start*/[](FamilyData* self, const char* tag, const char** attributes) {
323 // 'name' (string) introduces a new family name.
324 // 'to' (string) specifies which (previous) family to alias
325 // 'weight' (non-negative integer) [optional]
326 // If it *does not* have a weight, 'name' is an alias for the entire 'to' family.
327 // If it *does* have a weight, 'name' is a new family consisting of
328 // the font(s) with 'weight' from the 'to' family.
329
330 SkString aliasName;
331 SkString to;
332 int weight = 0;
333 for (size_t i = 0; ATTS_NON_NULL(attributes, i); i += 2) {
334 const char* name = attributes[i];
335 const char* value = attributes[i + 1];
336 size_t nameLen = strlen(name);
337 if (MEMEQ("name", name, nameLen)) {
338 SkAutoAsciiToLC tolc(value);
339 aliasName.set(tolc.lc());
340 } else if (MEMEQ("to", name, nameLen)) {
341 to.set(value);
342 } else if (MEMEQ("weight", name, nameLen)) {
343 if (!parse_non_negative_integer(value, &weight)) {
344 SK_FONTCONFIGPARSER_WARNING("'%s' is an invalid weight", value);
345 }
346 }
347 }
348
349 // Assumes that the named family is already declared
350 FontFamily* targetFamily = find_family(self, to);
351 if (!targetFamily) {
352 SK_FONTCONFIGPARSER_WARNING("'%s' alias target not found", to.c_str());
353 return;
354 }
355
356 if (weight) {
357 FontFamily* family = new FontFamily(targetFamily->fBasePath, self->fIsFallback);
358 family->fNames.push_back().set(aliasName);
359
360 for (int i = 0; i < targetFamily->fFonts.size(); i++) {
361 if (targetFamily->fFonts[i].fWeight == weight) {
362 family->fFonts.push_back(targetFamily->fFonts[i]);
363 }
364 }
365 *self->fFamilies.append() = family;
366 } else {
367 targetFamily->fNames.push_back().set(aliasName);
368 }
369 },
370 /*end*/nullptr,
371 /*tag*/nullptr,
372 /*chars*/nullptr,
373 };
374
375 static const TagHandler familySetHandler = {
__anond7a08b590b02() 376 /*start*/[](FamilyData* self, const char* tag, const char** attributes) { },
377 /*end*/nullptr,
__anond7a08b590c02() 378 /*tag*/[](FamilyData* self, const char* tag, const char** attributes) -> const TagHandler* {
379 size_t len = strlen(tag);
380 if (MEMEQ("family", tag, len)) {
381 return &familyHandler;
382 } else if (MEMEQ("alias", tag, len)) {
383 return &aliasHandler;
384 }
385 return nullptr;
386 },
387 /*chars*/nullptr,
388 };
389
390 } // namespace lmpParser
391
392 static const TagHandler topLevelHandler = {
393 /*start*/nullptr,
394 /*end*/nullptr,
__anond7a08b590d02() 395 /*tag*/[](FamilyData* self, const char* tag, const char** attributes) -> const TagHandler* {
396 size_t len = strlen(tag);
397 if (MEMEQ("familyset", tag, len)) {
398 // 'version' (non-negative integer) [default 0]
399 for (size_t i = 0; ATTS_NON_NULL(attributes, i); i += 2) {
400 const char* name = attributes[i];
401 size_t nameLen = strlen(name);
402 if (MEMEQ("version", name, nameLen)) {
403 const char* value = attributes[i + 1];
404 if (parse_non_negative_integer(value, &self->fVersion)) {
405 if (self->fVersion >= 21) {
406 return &lmpParser::familySetHandler;
407 }
408 }
409 }
410 }
411 }
412 return nullptr;
413 },
414 /*chars*/ nullptr,
415 };
416
start_element_handler(void * data,const char * tag,const char ** attributes)417 static void XMLCALL start_element_handler(void *data, const char *tag, const char **attributes)
418 {
419 FamilyData* self = static_cast<FamilyData*>(data);
420
421 if (!self->fSkip) {
422 const TagHandler* parent = self->fHandler.back();
423 const TagHandler* child = parent->tag ? parent->tag(self, tag, attributes) : nullptr;
424 if (child) {
425 if (child->start) {
426 child->start(self, tag, attributes);
427 }
428 self->fHandler.push_back(child);
429 XML_SetCharacterDataHandler(self->fParser, child->chars);
430 } else {
431 SK_FONTCONFIGPARSER_WARNING("'%s' tag not recognized, skipping", tag);
432 XML_SetCharacterDataHandler(self->fParser, nullptr);
433 self->fSkip = self->fDepth;
434 }
435 }
436
437 ++self->fDepth;
438 }
439
end_element_handler(void * data,const char * tag)440 static void XMLCALL end_element_handler(void* data, const char* tag)
441 {
442 FamilyData* self = static_cast<FamilyData*>(data);
443 --self->fDepth;
444
445 if (!self->fSkip) {
446 const TagHandler* child = self->fHandler.back();
447 if (child->end) {
448 child->end(self, tag);
449 }
450 self->fHandler.pop_back();
451 const TagHandler* parent = self->fHandler.back();
452 XML_SetCharacterDataHandler(self->fParser, parent->chars);
453 }
454
455 if (self->fSkip == self->fDepth) {
456 self->fSkip = 0;
457 const TagHandler* parent = self->fHandler.back();
458 XML_SetCharacterDataHandler(self->fParser, parent->chars);
459 }
460 }
461
xml_entity_decl_handler(void * data,const XML_Char * entityName,int is_parameter_entity,const XML_Char * value,int value_length,const XML_Char * base,const XML_Char * systemId,const XML_Char * publicId,const XML_Char * notationName)462 static void XMLCALL xml_entity_decl_handler(void *data,
463 const XML_Char *entityName,
464 int is_parameter_entity,
465 const XML_Char *value,
466 int value_length,
467 const XML_Char *base,
468 const XML_Char *systemId,
469 const XML_Char *publicId,
470 const XML_Char *notationName)
471 {
472 FamilyData* self = static_cast<FamilyData*>(data);
473 SK_FONTCONFIGPARSER_WARNING("'%s' entity declaration found, stopping processing", entityName);
474 XML_StopParser(self->fParser, XML_FALSE);
475 }
476
477 static const XML_Memory_Handling_Suite sk_XML_alloc = {
478 sk_malloc_throw,
479 sk_realloc_throw,
480 sk_free
481 };
482
483 /**
484 * This function parses the given filename and stores the results in the given
485 * families array. Returns the version of the file, negative if the file does not exist.
486 */
parse_config_file(const char * filename,SkTDArray<FontFamily * > & families,const SkString & basePath,bool isFallback)487 static int parse_config_file(const char* filename, SkTDArray<FontFamily*>& families,
488 const SkString& basePath, bool isFallback)
489 {
490 SkFILEStream file(filename);
491
492 // Some of the files we attempt to parse (in particular, /vendor/etc/fallback_fonts.xml)
493 // are optional - failure here is okay because one of these optional files may not exist.
494 if (!file.isValid()) {
495 SkDebugf(SK_FONTMGR_CONFIG_PARSER_PREFIX "'%s' could not be opened\n", filename);
496 return -1;
497 }
498
499 SkAutoTCallVProc<std::remove_pointer_t<XML_Parser>, XML_ParserFree> parser(
500 XML_ParserCreate_MM(nullptr, &sk_XML_alloc, nullptr));
501 if (!parser) {
502 SkDebugf(SK_FONTMGR_CONFIG_PARSER_PREFIX "could not create XML parser\n");
503 return -1;
504 }
505
506 FamilyData self(parser, families, basePath, isFallback, filename, &topLevelHandler);
507 XML_SetUserData(parser, &self);
508
509 // Disable entity processing, to inhibit internal entity expansion. See expat CVE-2013-0340
510 XML_SetEntityDeclHandler(parser, xml_entity_decl_handler);
511
512 // Start parsing oldschool; switch these in flight if we detect a newer version of the file.
513 XML_SetElementHandler(parser, start_element_handler, end_element_handler);
514
515 // One would assume it would be faster to have a buffer on the stack and call XML_Parse.
516 // But XML_Parse will call XML_GetBuffer anyway and memmove the passed buffer into it.
517 // (Unless XML_CONTEXT_BYTES is undefined, but all users define it.)
518 // In debug, buffer a small odd number of bytes to detect slicing in XML_CharacterDataHandler.
519 static const int bufferSize = 512 SkDEBUGCODE(-507);
520 bool done = false;
521 while (!done) {
522 void* buffer = XML_GetBuffer(parser, bufferSize);
523 if (!buffer) {
524 SkDebugf(SK_FONTMGR_CONFIG_PARSER_PREFIX "could not buffer enough to continue\n");
525 return -1;
526 }
527 size_t len = file.read(buffer, bufferSize);
528 done = file.isAtEnd();
529 XML_Status status = XML_ParseBuffer(parser, len, done);
530 if (XML_STATUS_ERROR == status) {
531 XML_Error error = XML_GetErrorCode(parser);
532 int line = XML_GetCurrentLineNumber(parser);
533 int column = XML_GetCurrentColumnNumber(parser);
534 const XML_LChar* errorString = XML_ErrorString(error);
535 SkDebugf(SK_FONTMGR_CONFIG_PARSER_PREFIX "%s:%d:%d error %d: %s.\n",
536 filename, line, column, error, errorString);
537 return -1;
538 }
539 }
540 return self.fVersion;
541 }
542
543 /** Returns the version of the system font file actually found, negative if none. */
append_system_font_families(SkTDArray<FontFamily * > & fontFamilies,const SkString & basePath)544 static int append_system_font_families(SkTDArray<FontFamily*>& fontFamilies,
545 const SkString& basePath)
546 {
547 int version = parse_config_file(g_lmpSystemFontsFile.c_str(), fontFamilies, basePath, false);
548 return version;
549 }
550
GetSystemFontFamilies(SkTDArray<FontFamily * > & fontFamilies)551 void SkFontMgr_Config_Parser::GetSystemFontFamilies(SkTDArray<FontFamily*>& fontFamilies)
552 {
553 std::string containerFontBasePath = SkFontMgr::containerFontPath;
554 if (containerFontBasePath.empty()) {
555 printf("error getting font base path '%s'\n", containerFontBasePath.c_str());
556 }
557 SkString basePath(containerFontBasePath.c_str());
558 g_lmpSystemFontsFile = containerFontBasePath.append(SK_FONT_CONFIG_FILE_NAME);
559 skia::text::HmSymbolConfig_OHOS::LoadSymbolConfig("hm_symbol_config_next.json", basePath);
560 append_system_font_families(fontFamilies, basePath);
561 }
562
getParent() const563 SkLanguage SkLanguage::getParent() const
564 {
565 SkASSERT(!fTag.isEmpty());
566 const char* tag = fTag.c_str();
567
568 // strip off the rightmost "-.*"
569 const char* parentTagEnd = strrchr(tag, '-');
570 if (parentTagEnd == nullptr) {
571 return SkLanguage();
572 }
573 size_t parentTagLen = parentTagEnd - tag;
574 return SkLanguage(tag, parentTagLen);
575 }