blob: efcb424f3bedd3baaeb0916e94537308dcb90090 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert' show ByteConversionSink, jsonDecode, utf8;
import 'dart:io' as io;
import 'dart:typed_data';
import 'package:args/command_runner.dart';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart' as crypto;
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
// ignore: avoid_relative_lib_imports
import '../lib/src/engine/noto_font_encoding.dart';
import 'cipd.dart';
import 'environment.dart';
import 'exceptions.dart';
import 'utils.dart';
const String expectedUrlPrefix = 'https://fonts.gstatic.com/s/';
class RollFallbackFontsCommand extends Command<bool>
with ArgUtils<bool> {
RollFallbackFontsCommand() {
argParser.addOption(
'key',
defaultsTo: '',
help: 'The Google Fonts API key. Used to get data about fonts hosted on '
'Google Fonts.',
);
argParser.addFlag(
'dry-run',
help: 'Whether or not to push changes to CIPD. When --dry-run is set, the '
'script will download everything and attempt to prepare the bundle '
'but will stop before publishing. When not set, the bundle will be '
'published.',
negatable: false,
);
}
@override
final String name = 'roll-fallback-fonts';
@override
final String description = 'Generate fallback font data from GoogleFonts and '
'upload fonts to cipd.';
String get apiKey => stringArg('key');
bool get isDryRun => boolArg('dry-run');
@override
Future<bool> run() async {
await _generateFallbackFontData();
return true;
}
Future<void> _generateFallbackFontData() async {
if (apiKey.isEmpty) {
throw UsageException('No Google Fonts API key provided', argParser.usage);
}
final http.Client client = http.Client();
final http.Response response = await client.get(Uri.parse(
'https://www.googleapis.com/webfonts/v1/webfonts?key=$apiKey'));
if (response.statusCode != 200) {
throw ToolExit('Failed to download Google Fonts list.');
}
final Map<String, dynamic> googleFontsResult =
jsonDecode(response.body) as Map<String, dynamic>;
final List<Map<String, dynamic>> fontDatas =
(googleFontsResult['items'] as List<dynamic>)
.cast<Map<String, dynamic>>();
final Map<String, Uri> urlForFamily = <String, Uri>{};
for (final Map<String, dynamic> fontData in fontDatas) {
if (fallbackFonts.contains(fontData['family'])) {
final Uri uri = Uri.parse(fontData['files']['regular'] as String)
.replace(scheme: 'https');
urlForFamily[fontData['family'] as String] = uri;
}
}
final Map<String, String> charsetForFamily = <String, String>{};
final io.Directory fontDir = await io.Directory.systemTemp.createTemp('flutter_fallback_fonts');
print('Downloading fonts into temp directory: ${fontDir.path}');
final AccumulatorSink<crypto.Digest> hashSink = AccumulatorSink<crypto.Digest>();
final ByteConversionSink hasher = crypto.sha256.startChunkedConversion(hashSink);
for (final String family in fallbackFonts) {
print('Downloading $family...');
final Uri? uri = urlForFamily[family];
if (uri == null) {
throw ToolExit('Unable to determine URL to download $family. '
'Check if it is still hosted on Google Fonts.');
}
final http.Response fontResponse = await client.get(uri);
if (fontResponse.statusCode != 200) {
throw ToolExit('Failed to download font for $family');
}
final String urlString = uri.toString();
if (!urlString.startsWith(expectedUrlPrefix)) {
throw ToolExit('Unexpected url format received from Google Fonts API: $urlString.');
}
final String urlSuffix = urlString.substring(expectedUrlPrefix.length);
final io.File fontFile =
io.File(path.join(fontDir.path, urlSuffix));
final Uint8List bodyBytes = fontResponse.bodyBytes;
if (!_checkForLicenseAttribution(bodyBytes)) {
throw ToolExit(
'Expected license attribution not found in file: $urlString');
}
hasher.add(utf8.encode(urlSuffix));
hasher.add(bodyBytes);
await fontFile.create(recursive: true);
await fontFile.writeAsBytes(bodyBytes, flush: true);
final io.ProcessResult fcQueryResult =
await io.Process.run('fc-query', <String>[
'--format=%{charset}',
'--',
fontFile.path,
]);
final String encodedCharset = fcQueryResult.stdout as String;
charsetForFamily[family] = encodedCharset;
}
final StringBuffer sb = StringBuffer();
final List<_Font> fonts = <_Font>[];
for (final String family in fallbackFonts) {
final List<int> starts = <int>[];
final List<int> ends = <int>[];
final String charset = charsetForFamily[family]!;
for (final String range in charset.split(' ')) {
// Range is one hexadecimal number or two, separated by `-`.
final List<String> parts = range.split('-');
if (parts.length != 1 && parts.length != 2) {
throw ToolExit('Malformed charset range "$range"');
}
final int first = int.parse(parts.first, radix: 16);
final int last = int.parse(parts.last, radix: 16);
starts.add(first);
ends.add(last);
}
fonts.add(_Font(family, fonts.length, starts, ends));
}
final String fontSetsCode = _computeEncodedFontSets(fonts);
sb.writeln('// Copyright 2013 The Flutter Authors. All rights reserved.');
sb.writeln('// Use of this source code is governed by a BSD-style license '
'that can be');
sb.writeln('// found in the LICENSE file.');
sb.writeln();
sb.writeln('// DO NOT EDIT! This file is generated. See:');
sb.writeln('// dev/roll_fallback_fonts.dart');
sb.writeln("import 'noto_font.dart';");
sb.writeln();
sb.writeln('List<NotoFont> getFallbackFontList(bool useColorEmoji) => <NotoFont>[');
for (final _Font font in fonts) {
final String family = font.family;
String enabledArgument = '';
if (family == 'Noto Emoji') {
enabledArgument = 'enabled: !useColorEmoji, ';
}
if (family == 'Noto Color Emoji') {
enabledArgument = 'enabled: useColorEmoji, ';
}
final String urlString = urlForFamily[family]!.toString();
if (!urlString.startsWith(expectedUrlPrefix)) {
throw ToolExit(
'Unexpected url format received from Google Fonts API: $urlString.');
}
final String urlSuffix = urlString.substring(expectedUrlPrefix.length);
sb.writeln(" NotoFont('$family', $enabledArgument'$urlSuffix'),");
}
sb.writeln('];');
sb.writeln();
sb.write(fontSetsCode);
final io.File fontDataFile = io.File(path.join(
environment.webUiRootDir.path,
'lib',
'src',
'engine',
'font_fallback_data.dart',
));
await fontDataFile.writeAsString(sb.toString());
final io.File licenseFile = io.File(path.join(
fontDir.path,
'LICENSE.txt',
));
const String licenseString = r'''
© Copyright 2015-2021 Google LLC. All Rights Reserved.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
''';
final List<int> licenseData = utf8.encode(licenseString);
await licenseFile.create(recursive: true);
await licenseFile.writeAsBytes(licenseData);
hasher.add(licenseData);
hasher.close();
final crypto.Digest digest = hashSink.events.single;
final String versionString = digest.toString();
const String packageName = 'flutter/flutter_font_fallbacks';
if (await cipdKnowsPackageVersion(
package: packageName,
versionTag: versionString)) {
print('Package already exists with hash $versionString. Skipping upload');
} else {
print('Uploading fallback fonts to CIPD with hash $versionString');
await uploadDirectoryToCipd(
directory: fontDir,
packageName: packageName,
configFileName: 'cipd.flutter_font_fallbacks.yaml',
description: 'A set of Noto fonts to fall back to for use in testing.',
root: fontDir.path,
version: versionString,
buildId: versionString,
isDryRun: isDryRun,
);
}
print('Setting new fallback fonts deps version to $versionString');
final String depFilePath = path.join(
environment.engineSrcDir.path,
'flutter',
'DEPS',
);
await runProcess('gclient', <String>[
'setdep',
'--revision=src/third_party/google_fonts_for_unit_tests:$packageName@$versionString',
'--deps-file=$depFilePath'
]);
}
}
const List<String> fallbackFonts = <String>[
'Noto Sans',
'Noto Color Emoji',
'Noto Emoji',
'Noto Sans Symbols',
'Noto Sans Symbols 2',
'Noto Sans Adlam',
'Noto Sans Anatolian Hieroglyphs',
'Noto Sans Arabic',
'Noto Sans Armenian',
'Noto Sans Avestan',
'Noto Sans Balinese',
'Noto Sans Bamum',
'Noto Sans Bassa Vah',
'Noto Sans Batak',
'Noto Sans Bengali',
'Noto Sans Bhaiksuki',
'Noto Sans Brahmi',
'Noto Sans Buginese',
'Noto Sans Buhid',
'Noto Sans Canadian Aboriginal',
'Noto Sans Carian',
'Noto Sans Caucasian Albanian',
'Noto Sans Chakma',
'Noto Sans Cham',
'Noto Sans Cherokee',
'Noto Sans Coptic',
'Noto Sans Cuneiform',
'Noto Sans Cypriot',
'Noto Sans Deseret',
'Noto Sans Devanagari',
'Noto Sans Duployan',
'Noto Sans Egyptian Hieroglyphs',
'Noto Sans Elbasan',
'Noto Sans Elymaic',
'Noto Sans Georgian',
'Noto Sans Glagolitic',
'Noto Sans Gothic',
'Noto Sans Grantha',
'Noto Sans Gujarati',
'Noto Sans Gunjala Gondi',
'Noto Sans Gurmukhi',
'Noto Sans HK',
'Noto Sans Hanunoo',
'Noto Sans Hatran',
'Noto Sans Hebrew',
'Noto Sans Imperial Aramaic',
'Noto Sans Indic Siyaq Numbers',
'Noto Sans Inscriptional Pahlavi',
'Noto Sans Inscriptional Parthian',
'Noto Sans JP',
'Noto Sans Javanese',
'Noto Sans KR',
'Noto Sans Kaithi',
'Noto Sans Kannada',
'Noto Sans Kayah Li',
'Noto Sans Kharoshthi',
'Noto Sans Khmer',
'Noto Sans Khojki',
'Noto Sans Khudawadi',
'Noto Sans Lao',
'Noto Sans Lepcha',
'Noto Sans Limbu',
'Noto Sans Linear A',
'Noto Sans Linear B',
'Noto Sans Lisu',
'Noto Sans Lycian',
'Noto Sans Lydian',
'Noto Sans Mahajani',
'Noto Sans Malayalam',
'Noto Sans Mandaic',
'Noto Sans Manichaean',
'Noto Sans Marchen',
'Noto Sans Masaram Gondi',
'Noto Sans Math',
'Noto Sans Mayan Numerals',
'Noto Sans Medefaidrin',
'Noto Sans Meetei Mayek',
'Noto Sans Meroitic',
'Noto Sans Miao',
'Noto Sans Modi',
'Noto Sans Mongolian',
'Noto Sans Mro',
'Noto Sans Multani',
'Noto Sans Myanmar',
'Noto Sans NKo',
'Noto Sans Nabataean',
'Noto Sans New Tai Lue',
'Noto Sans Newa',
'Noto Sans Nushu',
'Noto Sans Ogham',
'Noto Sans Ol Chiki',
'Noto Sans Old Hungarian',
'Noto Sans Old Italic',
'Noto Sans Old North Arabian',
'Noto Sans Old Permic',
'Noto Sans Old Persian',
'Noto Sans Old Sogdian',
'Noto Sans Old South Arabian',
'Noto Sans Old Turkic',
'Noto Sans Oriya',
'Noto Sans Osage',
'Noto Sans Osmanya',
'Noto Sans Pahawh Hmong',
'Noto Sans Palmyrene',
'Noto Sans Pau Cin Hau',
'Noto Sans Phags Pa',
'Noto Sans Phoenician',
'Noto Sans Psalter Pahlavi',
'Noto Sans Rejang',
'Noto Sans Runic',
'Noto Sans SC',
'Noto Sans Saurashtra',
'Noto Sans Sharada',
'Noto Sans Shavian',
'Noto Sans Siddham',
'Noto Sans Sinhala',
'Noto Sans Sogdian',
'Noto Sans Sora Sompeng',
'Noto Sans Soyombo',
'Noto Sans Sundanese',
'Noto Sans Syloti Nagri',
'Noto Sans Syriac',
'Noto Sans TC',
'Noto Sans Tagalog',
'Noto Sans Tagbanwa',
'Noto Sans Tai Le',
'Noto Sans Tai Tham',
'Noto Sans Tai Viet',
'Noto Sans Takri',
'Noto Sans Tamil',
'Noto Sans Tamil Supplement',
'Noto Sans Telugu',
'Noto Sans Thaana',
'Noto Sans Thai',
'Noto Sans Tifinagh',
'Noto Sans Tirhuta',
'Noto Sans Ugaritic',
'Noto Sans Vai',
'Noto Sans Wancho',
'Noto Sans Warang Citi',
'Noto Sans Yi',
'Noto Sans Zanabazar Square',
];
bool _checkForLicenseAttribution(Uint8List fontBytes) {
final ByteData fontData = fontBytes.buffer.asByteData();
final int codePointCount = fontData.lengthInBytes ~/ 2;
const String attributionString =
'This Font Software is licensed under the SIL Open Font License, Version 1.1.';
for (int i = 0; i < codePointCount - attributionString.length; i++) {
bool match = true;
for (int j = 0; j < attributionString.length; j++) {
if (fontData.getUint16((i + j) * 2) != attributionString.codeUnitAt(j)) {
match = false;
break;
}
}
if (match) {
return true;
}
}
return false;
}
class _Font {
_Font(this.family, this.index, this.starts, this.ends);
final String family;
final int index;
final List<int> starts;
final List<int> ends; // inclusive ends
static int compare(_Font a, _Font b) => a.index.compareTo(b.index);
String get shortName =>
_shortName +
String.fromCharCodes(
'$index'.codeUnits.map((int ch) => ch - 48 + 0x2080));
String get _shortName => family.startsWith('Noto Sans ')
? family.substring('Noto Sans '.length)
: family;
}
/// The boundary of a range of a font.
class _Boundary {
_Boundary(this.value, this.isStart, this.font);
final int value; // inclusive start or exclusive end.
final bool isStart;
final _Font font;
static int compare(_Boundary a, _Boundary b) => a.value.compareTo(b.value);
}
class _Range {
_Range(this.start, this.end, this.fontSet);
final int start;
final int end;
final _FontSet fontSet;
@override
String toString() {
return '[${start.toRadixString(16)}, ${end.toRadixString(16)}]'
' (${end - start + 1})'
' ${fontSet.description()}';
}
}
/// A canonical representative for a set of _Fonts. The fonts are stored in
/// order of increasing `_Font.index`.
class _FontSet {
_FontSet(this.fonts);
/// The number of [_Font]s in this set.
int get length => fonts.length;
/// The members of this set.
final List<_Font> fonts;
/// Number of unicode ranges that are supported by this set of fonts.
int rangeCount = 0;
/// The serialization order of this set. This index is assigned after building
/// all the sets.
late final int index;
static int orderByDecreasingRangeCount(_FontSet a, _FontSet b) {
final int r = b.rangeCount.compareTo(a.rangeCount);
if (r != 0) {
return r;
}
return orderByLexicographicFontIndexes(a, b);
}
static int orderByLexicographicFontIndexes(_FontSet a, _FontSet b) {
for (int i = 0; i < a.length && i < b.length; i++) {
final int r = _Font.compare(a.fonts[i], b.fonts[i]);
if (r != 0) {
return r;
}
}
assert(a.length != b.length); // _FontSets are canonical.
return a.length - b.length;
}
@override
String toString() {
return description();
}
String description() {
return fonts.map((_Font font) => font.shortName).join(', ');
}
}
/// A trie node [1] used to find the canonical _FontSet.
///
/// [1]: https://en.wikipedia.org/wiki/Trie
class _TrieNode {
final Map<_Font, _TrieNode> _children = <_Font, _TrieNode>{};
_FontSet? fontSet;
/// Inserts a string of fonts into the trie and returns the trie node
/// representing the string. [this] must be the root node of the trie.
///
/// Inserting the same sequence again will traverse the same path through the
/// trie and return the same node, canonicalizing the sequence to its
/// representative node.
_TrieNode insertSequenceAtRoot(Iterable<_Font> fonts) {
_TrieNode node = this;
for (final _Font font in fonts) {
node = node._children[font] ??= _TrieNode();
}
return node;
}
}
/// Computes the Dart source code for the encoded data structures used by the
/// fallback font selection algorithm.
///
/// The data structures allow the fallback font selection algorithm to quickly
/// determine which fonts support a given code point. The structures are
/// essentially a map from a code point to a set of fonts that support that code
/// point.
///
/// The universe of code points is partitioned into a set of subsets, or
/// components, where each component contains all the code points that are in
/// exactly the same set of fonts. A font can be considered to be a union of
/// some subset of the components and may share components with other fonts. A
/// `_FontSet` is used to represent a component and the set of fonts that use
/// the component. One way to visualize this is as a Venn diagram. The fonts are
/// the overlapping circles and the components are the spaces between the lines.
///
/// The emitted data structures are
///
/// (1) A list of sets of fonts.
/// (2) A list of code point ranges mapping to an index of list (1).
///
/// Each set of fonts is represented as a list of font indexes. The indexes are
/// always increasing so the delta is stored. The stored value is biased by -1
/// (i.e. `delta - 1`) since a delta is never less than 1. The deltas are STMR
/// encoded.
///
/// A code point with no fonts is mapped to an empty set of fonts. This allows
/// the list of code point ranges to be complete, covering every code
/// point. There are no gaps between ranges; instead there are some ranges that
/// map to the empty set. Each range is encoded as the size (number of code
/// points) in the range followed by the value which is the index of the
/// corresponding set in the list of sets.
///
///
/// STMR (Self terminating multiple radix) encoding
/// ---
///
/// This encoding is a minor adaptation of [VLQ encoding][1], using different
/// ranges of characters to represent continuing or terminating digits instead
/// of using a 'continuation' bit.
///
/// The separators between the numbers can be a significant proportion of the
/// number of characters needed to encode a sequence of numbers as a string.
/// Instead values are encoded with two kinds of digits: prefix digits and
/// terminating digits. Each kind of digit uses a different set of characters,
/// and the radix (number of digit characters) can differ between the different
/// kinds of digit. Lets say we use decimal digits `0`..`9` for prefix digits
/// and `A`..`Z` as terminating digits.
///
/// M = ('M' - 'A') = 12
/// 38M = (3 * 10 + 8) * 26 + 12 = 38 * 26 + 12 = 1000
///
/// Choosing a large terminating radix is especially effective when most of the
/// encoded values are small, as is the case with delta-encoding.
///
/// There can be multiple terminating digit kinds to represent different sorts
/// of values. For the range table, the size uses a different terminating digit,
/// 'a'..'z'. This allows the very common size of 1 (accounting over a third of
/// the range sizes) to be omitted. A range is encoded as either
/// `<size><value>`, or `<value>` with an implicit size of 1. Since the size 1
/// can be implicit, it is always implicit, and the stored sizes are biased by
/// -2.
///
/// | encoding | value | size |
/// | :--- | ---: | ---: |
/// | A | 0 | 1 |
/// | B | 1 | 1 |
/// | 38M | 1000 | 1 |
/// | aA | 0 | 2 |
/// | bB | 1 | 3 |
/// | zZ | 25 | 27 |
/// | 1a1A | 26 | 28 |
/// | 38a38M | 1000 | 1002 |
///
/// STMR-encoded strings are decoded efficiently by a simple loop that updates
/// the current value and performs some additional operation for a terminating
/// digit, e.g. recording the optional size, or creating a range.
///
/// [1]: https://en.wikipedia.org/wiki/Variable-length_quantity
String _computeEncodedFontSets(List<_Font> fonts) {
final List<_Range> ranges = <_Range>[];
final List<_FontSet> allSets = <_FontSet>[];
{
// The fonts have their supported code points provided as list of inclusive
// [start, end] ranges. We want to intersect all of these ranges and find
// the fonts that overlap each intersected range.
//
// It is easier to work with the boundaries of the ranges rather than the
// ranges themselves. The boundaries of the intersected ranges is the union
// of the boundaries of the individual font ranges. We scan the boundaries
// in increasing order, keeping track of the current set of fonts that are
// in the current intersected range. Each time the boundary value changes,
// the current set of fonts is canonicalized and recorded.
//
// There has to be a wiki article for this algorithm but I didn't find one.
final List<_Boundary> boundaries = <_Boundary>[];
for (final _Font font in fonts) {
for (final int start in font.starts) {
boundaries.add(_Boundary(start, true, font));
}
for (final int end in font.ends) {
boundaries.add(_Boundary(end + 1, false, font));
}
}
boundaries.sort(_Boundary.compare);
// The trie root represents the empty set of fonts.
final _TrieNode trieRoot = _TrieNode();
final Set<_Font> currentElements = <_Font>{};
void newRange(int start, int end) {
// Ensure we are using the canonical font order.
final List<_Font> fonts = List<_Font>.of(currentElements)
..sort(_Font.compare);
final _TrieNode node = trieRoot.insertSequenceAtRoot(fonts);
final _FontSet fontSet = node.fontSet ??= _FontSet(fonts);
if (fontSet.rangeCount == 0) {
allSets.add(fontSet);
}
fontSet.rangeCount++;
final _Range range = _Range(start, end, fontSet);
ranges.add(range);
}
int start = 0;
for (final _Boundary boundary in boundaries) {
final int value = boundary.value;
if (value > start) {
// Boundary has changed, record the pending range `[start, value - 1]`,
// and start a new range at `value`. `value` must be > 0 to get here.
newRange(start, value - 1);
start = value;
}
if (boundary.isStart) {
currentElements.add(boundary.font);
} else {
currentElements.remove(boundary.font);
}
}
assert(currentElements.isEmpty);
// Ensure the ranges cover the whole unicode code point space.
if (start <= kMaxCodePoint) {
newRange(start, kMaxCodePoint);
}
}
print('${allSets.length} sets covering ${ranges.length} ranges');
// Sort _FontSets by the number of ranges that map to that _FontSet, so that
// _FontSets that are referenced from many ranges have smaller indexes. This
// makes the range table encoding smaller, by about half.
allSets.sort(_FontSet.orderByDecreasingRangeCount);
for (int i = 0; i < allSets.length; i++) {
allSets[i].index = i;
}
final StringBuffer code = StringBuffer();
final StringBuffer sb = StringBuffer();
int totalEncodedLength = 0;
void encode(int value, int radix, int firstDigitCode) {
final int prefix = value ~/ radix;
assert(kPrefixDigit0 == '0'.codeUnitAt(0) && kPrefixRadix == 10);
if (prefix != 0) {
sb.write(prefix);
}
sb.writeCharCode(firstDigitCode + value.remainder(radix));
}
for (final _FontSet fontSet in allSets) {
int previousFontIndex = -1;
for (final _Font font in fontSet.fonts) {
final int fontIndexDelta = font.index - previousFontIndex;
previousFontIndex = font.index;
encode(fontIndexDelta - 1, kFontIndexRadix, kFontIndexDigit0);
}
if (fontSet != allSets.last) {
sb.write(',');
}
final String fragment = sb.toString();
sb.clear();
totalEncodedLength += fragment.length;
final int length = fontSet.fonts.length;
code.write(' // #${fontSet.index}: $length font');
if (length != 1) {
code.write('s');
}
if (length > 0) {
code.write(': ${fontSet.description()}');
}
code.writeln('.');
code.writeln(" '$fragment'");
}
final StringBuffer declarations = StringBuffer();
final int references =
allSets.fold(0, (int sum, _FontSet set) => sum + set.length);
declarations
..writeln('// ${allSets.length} unique sets of fonts'
' containing $references font references'
' encoded in $totalEncodedLength characters')
..writeln('const String encodedFontSets =')
..write(code)
..writeln(' ;');
// Encode ranges.
code.clear();
totalEncodedLength = 0;
for (final _Range range in ranges) {
final int start = range.start;
final int end = range.end;
final int index = range.fontSet.index;
final int size = end - start + 1;
// Encode <size><index> or <index> for unit ranges.
if (size >= 2) {
encode(size - 2, kRangeSizeRadix, kRangeSizeDigit0);
}
encode(index, kRangeValueRadix, kRangeValueDigit0);
final String encoding = sb.toString();
sb.clear();
totalEncodedLength += encoding.length;
String description = start.toRadixString(16);
if (end != start) {
description = '$description-${end.toRadixString(16)}';
}
if (range.fontSet.fonts.isNotEmpty) {
description = '${description.padRight(12)} #$index';
}
final String encodingText = "'$encoding'".padRight(10);
code.writeln(' $encodingText // $description');
}
declarations
..writeln()
..writeln(
'// ${ranges.length} ranges encoded in $totalEncodedLength characters')
..writeln('const String encodedFontSetRanges =')
..write(code)
..writeln(' ;');
return declarations.toString();
}