[web] More efficient fallback font selection (flutter/engine#44526)
The PR improves the code size and runtime performance of fallback font selection. ### Performance improvements Initialization of the data structures to support fallback font selection has been moved from creating the FallbackFontManager (first frame) to the first use, i.e. the first frame actually needing a fallback font. The numbers reported below are for a lightly edited version of the counter demo that appends to the counter about ~300 missing code points that need ~25 fallback fonts to cover the missing code points. Timings taken from a few profiles on my performance workstation. | | Before | After | | --- | ---: | ---: | | FallbackFontManager() |~100ms | <2ms | | First need | 0ms | 12ms | | Subsequent need | 20-30ms | <1ms | ### Size improvements | | Before | After | Î | | --- | ---: | ---: | ---: | | main.dart.js | 1586405 | 1477319 | -109086 (-6.87%) | | brotli -9 | 427304 | 401611 | -25693 (-6.01%) | ### Algorithm notes #### Startup The old algorithm built an interval tree from the code point ranges of the ~140 fallback fonts and uses the interval tree to build a list of fonts that support each missing code point. The new algorithm uses a binary search map that directly produces the list of fonts. There are fewer binary search ranges (~22k) than the aggregate ranges for all the fonts (~26k). Most of the startup time gain comes from using a data unpacks directly into a useful form rather than needing processing to build an interval tree (~12ms vs ~100ms). #### Running The runtime for font selection is greatly improved for several reasons - The code point space is partitioned into components so that code point counting can be batched. - When a font is selected, the counts are updated incrementally rather than being recomputed. - The counts are held in fields of the NotoFont and component objects rather than in Maps or Sets. Batching, incremental update and avoiding hash tables are roughly multiplicative in effect. ## Issues - https://github.com/flutter/flutter/issues/131440 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
@@ -2032,7 +2032,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/transform.dart + ../../.
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html_image_codec.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/image_decoder.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/initialization.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/interval_tree.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_loader.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_promise.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart + ../../../flutter/LICENSE
|
||||
@@ -2042,6 +2041,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart + ../../../flutte
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/noto_font_encoding.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart + ../../../flutter/LICENSE
|
||||
@@ -4783,7 +4783,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/transform.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html_image_codec.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/image_decoder.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/initialization.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/interval_tree.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_loader.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_promise.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart
|
||||
@@ -4793,6 +4792,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/noto_font_encoding.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
|
||||
|
||||
@@ -12,6 +12,9 @@ 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';
|
||||
@@ -103,7 +106,8 @@ class RollFallbackFontsCommand extends Command<bool>
|
||||
|
||||
final Uint8List bodyBytes = fontResponse.bodyBytes;
|
||||
if (!_checkForLicenseAttribution(bodyBytes)) {
|
||||
throw ToolExit('Expected license attribution not found in file: $urlString');
|
||||
throw ToolExit(
|
||||
'Expected license attribution not found in file: $urlString');
|
||||
}
|
||||
hasher.add(utf8.encode(urlSuffix));
|
||||
hasher.add(bodyBytes);
|
||||
@@ -122,6 +126,29 @@ class RollFallbackFontsCommand extends Command<bool>
|
||||
|
||||
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');
|
||||
@@ -131,51 +158,28 @@ class RollFallbackFontsCommand extends Command<bool>
|
||||
sb.writeln('// dev/roll_fallback_fonts.dart');
|
||||
sb.writeln("import 'noto_font.dart';");
|
||||
sb.writeln();
|
||||
sb.writeln('List<NotoFont> getFallbackFontData(bool useColorEmoji) => <NotoFont>[');
|
||||
sb.writeln('List<NotoFont> getFallbackFontList(bool useColorEmoji) => <NotoFont>[');
|
||||
|
||||
for (final String family in fallbackFonts) {
|
||||
for (final _Font font in fonts) {
|
||||
final String family = font.family;
|
||||
String enabledArgument = '';
|
||||
if (family == 'Noto Emoji') {
|
||||
sb.write(' if (!useColorEmoji)');
|
||||
enabledArgument = 'enabled: !useColorEmoji, ';
|
||||
}
|
||||
if (family == 'Noto Color Emoji') {
|
||||
sb.write(' if (useColorEmoji)');
|
||||
enabledArgument = 'enabled: useColorEmoji, ';
|
||||
}
|
||||
final String urlString = urlForFamily[family]!.toString();
|
||||
if (!urlString.startsWith(expectedUrlPrefix)) {
|
||||
throw ToolExit('Unexpected url format received from Google Fonts API: $urlString.');
|
||||
throw ToolExit(
|
||||
'Unexpected url format received from Google Fonts API: $urlString.');
|
||||
}
|
||||
final String urlSuffix = urlString.substring(expectedUrlPrefix.length);
|
||||
sb.writeln(" NotoFont('$family', '$urlSuffix',");
|
||||
final List<String> starts = <String>[];
|
||||
final List<String> ends = <String>[];
|
||||
for (final String range in charsetForFamily[family]!.split(' ')) {
|
||||
final List<String> parts = range.split('-');
|
||||
if (parts.length == 1) {
|
||||
starts.add(parts[0]);
|
||||
ends.add(parts[0]);
|
||||
} else {
|
||||
starts.add(parts[0]);
|
||||
ends.add(parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Print the unicode ranges in a readable format for easier review. This
|
||||
// shouldn't affect code size because comments are removed in release mode.
|
||||
sb.write(' // <int>[');
|
||||
for (final String start in starts) {
|
||||
sb.write('0x$start,');
|
||||
}
|
||||
sb.writeln('],');
|
||||
sb.write(' // <int>[');
|
||||
for (final String end in ends) {
|
||||
sb.write('0x$end,');
|
||||
}
|
||||
sb.writeln(']');
|
||||
|
||||
sb.writeln(" '${_packFontRanges(starts, ends)}',");
|
||||
sb.writeln(' ),');
|
||||
sb.writeln(" NotoFont('$family', $enabledArgument'$urlSuffix'),");
|
||||
}
|
||||
sb.writeln('];');
|
||||
sb.writeln();
|
||||
sb.write(fontSetsCode);
|
||||
|
||||
final io.File fontDataFile = io.File(path.join(
|
||||
environment.webUiRootDir.path,
|
||||
@@ -471,30 +475,11 @@ const List<String> fallbackFonts = <String>[
|
||||
'Noto Sans Zanabazar Square',
|
||||
];
|
||||
|
||||
String _packFontRanges(List<String> starts, List<String> ends) {
|
||||
assert(starts.length == ends.length);
|
||||
|
||||
final StringBuffer sb = StringBuffer();
|
||||
|
||||
for (int i = 0; i < starts.length; i++) {
|
||||
final int start = int.parse(starts[i], radix: 16);
|
||||
final int end = int.parse(ends[i], radix: 16);
|
||||
|
||||
sb.write(start.toRadixString(36));
|
||||
sb.write('|');
|
||||
if (start != end) {
|
||||
sb.write((end - start).toRadixString(36));
|
||||
}
|
||||
sb.write(';');
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
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.';
|
||||
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++) {
|
||||
@@ -509,3 +494,370 @@ bool _checkForLicenseAttribution(Uint8List fontBytes) {
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -105,7 +105,6 @@ export 'engine/html/transform.dart';
|
||||
export 'engine/html_image_codec.dart';
|
||||
export 'engine/image_decoder.dart';
|
||||
export 'engine/initialization.dart';
|
||||
export 'engine/interval_tree.dart';
|
||||
export 'engine/js_interop/js_loader.dart';
|
||||
export 'engine/js_interop/js_promise.dart';
|
||||
export 'engine/js_interop/js_typed_data.dart';
|
||||
@@ -115,6 +114,7 @@ export 'engine/layers.dart';
|
||||
export 'engine/mouse_cursor.dart';
|
||||
export 'engine/navigation/history.dart';
|
||||
export 'engine/noto_font.dart';
|
||||
export 'engine/noto_font_encoding.dart';
|
||||
export 'engine/onscreen_logging.dart';
|
||||
export 'engine/picture.dart';
|
||||
export 'engine/platform_dispatcher.dart';
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17,7 +17,7 @@ class FontFallbackManager {
|
||||
factory FontFallbackManager(FallbackFontRegistry registry) =>
|
||||
FontFallbackManager._(
|
||||
registry,
|
||||
getFallbackFontData(configuration.useColorEmoji)
|
||||
getFallbackFontList(configuration.useColorEmoji)
|
||||
);
|
||||
|
||||
FontFallbackManager._(this.registry, this.fallbackFonts) :
|
||||
@@ -26,10 +26,9 @@ class FontFallbackManager {
|
||||
_notoSansHK = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans HK'),
|
||||
_notoSansJP = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans JP'),
|
||||
_notoSansKR = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans KR'),
|
||||
_notoSymbols = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans Symbols'),
|
||||
notoTree = createNotoFontTree(fallbackFonts) {
|
||||
_notoSymbols = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans Symbols') {
|
||||
downloadQueue = FallbackFontDownloadQueue(this);
|
||||
}
|
||||
}
|
||||
|
||||
final FallbackFontRegistry registry;
|
||||
|
||||
@@ -43,9 +42,6 @@ class FontFallbackManager {
|
||||
|
||||
final List<NotoFont> fallbackFonts;
|
||||
|
||||
/// Index of all font families by code point range.
|
||||
final IntervalTree<NotoFont> notoTree;
|
||||
|
||||
final NotoFont _notoSansSC;
|
||||
final NotoFont _notoSansTC;
|
||||
final NotoFont _notoSansHK;
|
||||
@@ -56,19 +52,6 @@ class FontFallbackManager {
|
||||
|
||||
Future<void> _idleFuture = Future<void>.value();
|
||||
|
||||
static IntervalTree<NotoFont> createNotoFontTree(List<NotoFont> fallbackFonts) {
|
||||
final Map<NotoFont, List<CodePointRange>> ranges =
|
||||
<NotoFont, List<CodePointRange>>{};
|
||||
|
||||
for (final NotoFont font in fallbackFonts) {
|
||||
final List<CodePointRange> fontRanges =
|
||||
ranges.putIfAbsent(font, () => <CodePointRange>[]);
|
||||
fontRanges.addAll(font.computeUnicodeRanges());
|
||||
}
|
||||
|
||||
return IntervalTree<NotoFont>.createFromRanges(ranges);
|
||||
}
|
||||
|
||||
final List<String> globalFontFallbacks = <String>['Roboto'];
|
||||
|
||||
/// A list of code points to check against the global fallback fonts.
|
||||
@@ -171,32 +154,83 @@ class FontFallbackManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the minimum set of fonts which covers all of the [codePoints].
|
||||
///
|
||||
/// Since set cover is NP-complete, we approximate using a greedy algorithm
|
||||
/// which finds the font which covers the most code points. If multiple CJK
|
||||
/// fonts match the same number of code points, we choose one based on the
|
||||
/// user's locale.
|
||||
///
|
||||
/// If a code point is not covered by any font, it is added to
|
||||
/// [codePointsWithNoKnownFont] so it can be omitted next time to avoid
|
||||
/// searching for fonts unnecessarily.
|
||||
void findFontsForMissingCodePoints(List<int> codePoints) {
|
||||
Set<NotoFont> fonts = <NotoFont>{};
|
||||
final Set<int> coveredCodePoints = <int>{};
|
||||
final Set<int> missingCodePoints = <int>{};
|
||||
final List<int> missingCodePoints = <int>[];
|
||||
|
||||
final List<FallbackFontComponent> requiredComponents =
|
||||
<FallbackFontComponent>[];
|
||||
final List<NotoFont> candidateFonts = <NotoFont>[];
|
||||
|
||||
// Collect the components that cover the code points.
|
||||
for (final int codePoint in codePoints) {
|
||||
final List<NotoFont> fontsForPoint = notoTree.intersections(codePoint);
|
||||
fonts.addAll(fontsForPoint);
|
||||
if (fontsForPoint.isNotEmpty) {
|
||||
coveredCodePoints.add(codePoint);
|
||||
} else {
|
||||
final FallbackFontComponent component =
|
||||
codePointToComponents.lookup(codePoint);
|
||||
if (component.fonts.isEmpty) {
|
||||
missingCodePoints.add(codePoint);
|
||||
} else {
|
||||
// A zero cover count means we have not yet seen this component.
|
||||
if (component.coverCount == 0) {
|
||||
requiredComponents.add(component);
|
||||
}
|
||||
component.coverCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// The call to `findMinimumFontsForCodePoints` will remove all code points that
|
||||
// were matched by `fonts` from `unmatchedCodePoints`.
|
||||
final Set<int> unmatchedCodePoints = Set<int>.from(coveredCodePoints);
|
||||
fonts = findMinimumFontsForCodePoints(unmatchedCodePoints, fonts);
|
||||
// Aggregate the component cover counts to the fonts that use the component.
|
||||
for (final FallbackFontComponent component in requiredComponents) {
|
||||
for (final NotoFont font in component.fonts) {
|
||||
// A zero cover cover count means we have not yet seen this font.
|
||||
if (font.coverCount == 0) {
|
||||
candidateFonts.add(font);
|
||||
}
|
||||
font.coverCount += component.coverCount;
|
||||
font.coverComponents.add(component);
|
||||
}
|
||||
}
|
||||
|
||||
fonts.forEach(downloadQueue.add);
|
||||
final List<NotoFont> selectedFonts = <NotoFont>[];
|
||||
|
||||
// We looked through the Noto font tree and didn't find any font families
|
||||
// covering some code points.
|
||||
if (missingCodePoints.isNotEmpty || unmatchedCodePoints.isNotEmpty) {
|
||||
while (candidateFonts.isNotEmpty) {
|
||||
final NotoFont selectedFont = _selectFont(candidateFonts);
|
||||
selectedFonts.add(selectedFont);
|
||||
|
||||
// All the code points in the selected font are now covered. Zero out each
|
||||
// component that is used by the font and adjust the counts of other fonts
|
||||
// that use the same components.
|
||||
for (final FallbackFontComponent component in <FallbackFontComponent>[
|
||||
...selectedFont.coverComponents
|
||||
]) {
|
||||
for (final NotoFont font in component.fonts) {
|
||||
font.coverCount -= component.coverCount;
|
||||
font.coverComponents.remove(component);
|
||||
}
|
||||
component.coverCount = 0;
|
||||
}
|
||||
assert(selectedFont.coverCount == 0);
|
||||
assert(selectedFont.coverComponents.isEmpty);
|
||||
// The selected font will have a zero cover count, but other fonts may
|
||||
// too. Remove these from further consideration.
|
||||
candidateFonts.removeWhere((NotoFont font) => font.coverCount == 0);
|
||||
}
|
||||
|
||||
selectedFonts.forEach(downloadQueue.add);
|
||||
|
||||
// Report code points not covered by any fallback font and ensure we don't
|
||||
// process those code points again.
|
||||
if (missingCodePoints.isNotEmpty) {
|
||||
if (!downloadQueue.isPending) {
|
||||
printWarning('Could not find a set of Noto fonts to display all missing '
|
||||
printWarning(
|
||||
'Could not find a set of Noto fonts to display all missing '
|
||||
'characters. Please add a font asset for the missing characters.'
|
||||
' See: https://flutter.dev/docs/cookbook/design/fonts');
|
||||
codePointsWithNoKnownFont.addAll(missingCodePoints);
|
||||
@@ -204,102 +238,209 @@ class FontFallbackManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the minimum set of fonts which covers all of the [codePoints].
|
||||
///
|
||||
/// Removes all code points covered by [fonts] from [codePoints]. The code
|
||||
/// points remaining in the [codePoints] set after calling this function do not
|
||||
/// have a font that covers them and can be omitted next time to avoid
|
||||
/// searching for fonts unnecessarily.
|
||||
///
|
||||
/// Since set cover is NP-complete, we approximate using a greedy algorithm
|
||||
/// which finds the font which covers the most code points. If multiple CJK
|
||||
/// fonts match the same number of code points, we choose one based on the user's
|
||||
/// locale.
|
||||
Set<NotoFont> findMinimumFontsForCodePoints(
|
||||
Set<int> codePoints, Set<NotoFont> fonts) {
|
||||
assert(fonts.isNotEmpty || codePoints.isEmpty);
|
||||
final Set<NotoFont> minimumFonts = <NotoFont>{};
|
||||
NotoFont _selectFont(List<NotoFont> fonts) {
|
||||
int maxCodePointsCovered = -1;
|
||||
final List<NotoFont> bestFonts = <NotoFont>[];
|
||||
NotoFont? bestFont;
|
||||
|
||||
final String language = domWindow.navigator.language;
|
||||
|
||||
while (codePoints.isNotEmpty) {
|
||||
int maxCodePointsCovered = 0;
|
||||
bestFonts.clear();
|
||||
for (final NotoFont font in fonts) {
|
||||
int codePointsCovered = 0;
|
||||
for (final int codePoint in codePoints) {
|
||||
if (font.contains(codePoint)) {
|
||||
codePointsCovered++;
|
||||
}
|
||||
}
|
||||
if (codePointsCovered > maxCodePointsCovered) {
|
||||
bestFonts.clear();
|
||||
bestFonts.add(font);
|
||||
maxCodePointsCovered = codePointsCovered;
|
||||
} else if (codePointsCovered == maxCodePointsCovered) {
|
||||
bestFonts.add(font);
|
||||
for (final NotoFont font in fonts) {
|
||||
if (font.coverCount > maxCodePointsCovered) {
|
||||
bestFonts.clear();
|
||||
bestFonts.add(font);
|
||||
bestFont = font;
|
||||
maxCodePointsCovered = font.coverCount;
|
||||
} else if (font.coverCount == maxCodePointsCovered) {
|
||||
bestFonts.add(font);
|
||||
// Tie-break with the lowest index which corresponds to a font name
|
||||
// being earlier in the list of fonts in the font fallback data
|
||||
// generator.
|
||||
if (font.index < bestFont!.index) {
|
||||
bestFont = font;
|
||||
}
|
||||
}
|
||||
if (maxCodePointsCovered == 0) {
|
||||
// Fonts cannot cover remaining unmatched characters.
|
||||
break;
|
||||
}
|
||||
// If the list of best fonts are all CJK fonts, choose the best one based
|
||||
// on locale. Otherwise just choose the first font.
|
||||
NotoFont bestFont = bestFonts.first;
|
||||
if (bestFonts.length > 1) {
|
||||
if (bestFonts.every((NotoFont font) =>
|
||||
}
|
||||
|
||||
// If the list of best fonts are all CJK fonts, choose the best one based
|
||||
// on locale. Otherwise just choose the first font.
|
||||
if (bestFonts.length > 1) {
|
||||
if (bestFonts.every((NotoFont font) =>
|
||||
font == _notoSansSC ||
|
||||
font == _notoSansTC ||
|
||||
font == _notoSansHK ||
|
||||
font == _notoSansJP ||
|
||||
font == _notoSansKR
|
||||
)) {
|
||||
if (language == 'zh-Hans' ||
|
||||
language == 'zh-CN' ||
|
||||
language == 'zh-SG' ||
|
||||
language == 'zh-MY') {
|
||||
if (bestFonts.contains(_notoSansSC)) {
|
||||
bestFont = _notoSansSC;
|
||||
}
|
||||
} else if (language == 'zh-Hant' ||
|
||||
language == 'zh-TW' ||
|
||||
language == 'zh-MO') {
|
||||
if (bestFonts.contains(_notoSansTC)) {
|
||||
bestFont = _notoSansTC;
|
||||
}
|
||||
} else if (language == 'zh-HK') {
|
||||
if (bestFonts.contains(_notoSansHK)) {
|
||||
bestFont = _notoSansHK;
|
||||
}
|
||||
} else if (language == 'ja') {
|
||||
if (bestFonts.contains(_notoSansJP)) {
|
||||
bestFont = _notoSansJP;
|
||||
}
|
||||
} else if (language == 'ko') {
|
||||
if (bestFonts.contains(_notoSansKR)) {
|
||||
bestFont = _notoSansKR;
|
||||
}
|
||||
} else if (bestFonts.contains(_notoSansSC)) {
|
||||
font == _notoSansKR)) {
|
||||
final String language = domWindow.navigator.language;
|
||||
|
||||
if (language == 'zh-Hans' ||
|
||||
language == 'zh-CN' ||
|
||||
language == 'zh-SG' ||
|
||||
language == 'zh-MY') {
|
||||
if (bestFonts.contains(_notoSansSC)) {
|
||||
bestFont = _notoSansSC;
|
||||
}
|
||||
} else {
|
||||
// To be predictable, if there is a tie for best font, choose a font
|
||||
// from this list first, then just choose the first font.
|
||||
if (bestFonts.contains(_notoSymbols)) {
|
||||
bestFont = _notoSymbols;
|
||||
} else if (bestFonts.contains(_notoSansSC)) {
|
||||
bestFont = _notoSansSC;
|
||||
} else if (language == 'zh-Hant' ||
|
||||
language == 'zh-TW' ||
|
||||
language == 'zh-MO') {
|
||||
if (bestFonts.contains(_notoSansTC)) {
|
||||
bestFont = _notoSansTC;
|
||||
}
|
||||
} else if (language == 'zh-HK') {
|
||||
if (bestFonts.contains(_notoSansHK)) {
|
||||
bestFont = _notoSansHK;
|
||||
}
|
||||
} else if (language == 'ja') {
|
||||
if (bestFonts.contains(_notoSansJP)) {
|
||||
bestFont = _notoSansJP;
|
||||
}
|
||||
} else if (language == 'ko') {
|
||||
if (bestFonts.contains(_notoSansKR)) {
|
||||
bestFont = _notoSansKR;
|
||||
}
|
||||
} else if (bestFonts.contains(_notoSansSC)) {
|
||||
bestFont = _notoSansSC;
|
||||
}
|
||||
} else {
|
||||
// To be predictable, if there is a tie for best font, choose a font
|
||||
// from this list first, then just choose the first font.
|
||||
if (bestFonts.contains(_notoSymbols)) {
|
||||
bestFont = _notoSymbols;
|
||||
} else if (bestFonts.contains(_notoSansSC)) {
|
||||
bestFont = _notoSansSC;
|
||||
}
|
||||
}
|
||||
codePoints.removeWhere((int codePoint) {
|
||||
return bestFont.contains(codePoint);
|
||||
});
|
||||
minimumFonts.add(bestFont);
|
||||
}
|
||||
return minimumFonts;
|
||||
return bestFont!;
|
||||
}
|
||||
|
||||
late final List<FallbackFontComponent> fontComponents =
|
||||
_decodeFontComponents(encodedFontSets);
|
||||
|
||||
late final _UnicodePropertyLookup<FallbackFontComponent> codePointToComponents =
|
||||
_UnicodePropertyLookup<FallbackFontComponent>.fromPackedData(
|
||||
encodedFontSetRanges, fontComponents);
|
||||
|
||||
List<FallbackFontComponent> _decodeFontComponents(String data) {
|
||||
return <FallbackFontComponent>[
|
||||
for (final String componentData in data.split(','))
|
||||
FallbackFontComponent(_decodeFontSet(componentData))
|
||||
];
|
||||
}
|
||||
|
||||
List<NotoFont> _decodeFontSet(String data) {
|
||||
final List<NotoFont> result = <NotoFont>[];
|
||||
int previousIndex = -1;
|
||||
int prefix = 0;
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
final int code = data.codeUnitAt(i);
|
||||
|
||||
if (kFontIndexDigit0 <= code &&
|
||||
code < kFontIndexDigit0 + kFontIndexRadix) {
|
||||
final int delta = prefix * kFontIndexRadix + (code - kFontIndexDigit0);
|
||||
final int index = previousIndex + delta + 1;
|
||||
result.add(fallbackFonts[index]);
|
||||
previousIndex = index;
|
||||
prefix = 0;
|
||||
} else if (kPrefixDigit0 <= code && code < kPrefixDigit0 + kPrefixRadix) {
|
||||
prefix = prefix * kPrefixRadix + (code - kPrefixDigit0);
|
||||
} else {
|
||||
throw StateError('Unreachable');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// A lookup structure from code point to a property type [P].
|
||||
class _UnicodePropertyLookup<P> {
|
||||
_UnicodePropertyLookup._(this._boundaries, this._values);
|
||||
|
||||
factory _UnicodePropertyLookup.fromPackedData(
|
||||
String packedData,
|
||||
List<P> propertyEnumValues,
|
||||
) {
|
||||
final List<int> boundaries = <int>[];
|
||||
final List<P> values = <P>[];
|
||||
|
||||
int start = 0;
|
||||
int prefix = 0;
|
||||
int size = 1;
|
||||
|
||||
for (int i = 0; i < packedData.length; i++) {
|
||||
final int code = packedData.codeUnitAt(i);
|
||||
if (kRangeValueDigit0 <= code &&
|
||||
code < kRangeValueDigit0 + kRangeValueRadix) {
|
||||
final int index =
|
||||
prefix * kRangeValueRadix + (code - kRangeValueDigit0);
|
||||
final P value = propertyEnumValues[index];
|
||||
start += size;
|
||||
boundaries.add(start);
|
||||
values.add(value);
|
||||
prefix = 0;
|
||||
size = 1;
|
||||
} else if (kRangeSizeDigit0 <= code &&
|
||||
code < kRangeSizeDigit0 + kRangeSizeRadix) {
|
||||
size = prefix * kRangeSizeRadix + (code - kRangeSizeDigit0) + 2;
|
||||
prefix = 0;
|
||||
} else if (kPrefixDigit0 <= code && code < kPrefixDigit0 + kPrefixRadix) {
|
||||
prefix = prefix * kPrefixRadix + (code - kPrefixDigit0);
|
||||
} else {
|
||||
throw StateError('Unreachable');
|
||||
}
|
||||
}
|
||||
if (start != kMaxCodePoint + 1) {
|
||||
throw StateError('Bad map size: $start');
|
||||
}
|
||||
|
||||
return _UnicodePropertyLookup<P>._(boundaries, values);
|
||||
}
|
||||
|
||||
/// There are two parallel lists - one of boundaries between adjacent unicode
|
||||
/// ranges and second of the values for the ranges.
|
||||
///
|
||||
/// `_boundaries[i]` is the open-interval end of the `i`th range and the start
|
||||
/// of the `i+1`th range. The implicit start of the 0th range is zero.
|
||||
///
|
||||
/// `_values[i]` is the value for the range [`_boundaries[i-1]`, `_boundaries[i]`).
|
||||
/// Default values are stored as explicit ranges.
|
||||
///
|
||||
/// Example: the unicode range properies `[10-50]=>A`, `[100]=>B`, with
|
||||
/// default value `X` would be represented as:
|
||||
///
|
||||
/// boundaries: [10, 51, 100, 101, 1114112]
|
||||
/// values: [ X, A, X, B, X]
|
||||
///
|
||||
final List<int> _boundaries;
|
||||
final List<P> _values;
|
||||
|
||||
int get length => _boundaries.length;
|
||||
|
||||
P lookup(int value) {
|
||||
assert(0 <= value && value <= kMaxCodePoint);
|
||||
assert(_boundaries.last == kMaxCodePoint + 1);
|
||||
int start = 0, end = _boundaries.length;
|
||||
while (true) {
|
||||
if (start == end) {
|
||||
return _values[start];
|
||||
}
|
||||
final int mid = start + (end - start) ~/ 2;
|
||||
if (value >= _boundaries[mid]) {
|
||||
start = mid + 1;
|
||||
} else {
|
||||
end = mid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the ranges, calling [action] with the start and end
|
||||
/// (inclusive) code points and value.
|
||||
void forEachRange(void Function(int start, int end, P value) action) {
|
||||
int start = 0;
|
||||
for (int i = 0; i < _boundaries.length; i++) {
|
||||
final int end = _boundaries[i];
|
||||
final P value = _values[i];
|
||||
action(start, end - 1, value);
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,9 +449,11 @@ class FallbackFontDownloadQueue {
|
||||
|
||||
final FontFallbackManager fallbackManager;
|
||||
|
||||
static const String _defaultFallbackFontsUrlPrefix = 'https://fonts.gstatic.com/s/';
|
||||
static const String _defaultFallbackFontsUrlPrefix =
|
||||
'https://fonts.gstatic.com/s/';
|
||||
String? fallbackFontUrlPrefixOverride;
|
||||
String get fallbackFontUrlPrefix => fallbackFontUrlPrefixOverride ?? _defaultFallbackFontsUrlPrefix;
|
||||
String get fallbackFontUrlPrefix =>
|
||||
fallbackFontUrlPrefixOverride ?? _defaultFallbackFontsUrlPrefix;
|
||||
|
||||
final Set<NotoFont> downloadedFonts = <NotoFont>{};
|
||||
final Map<String, NotoFont> pendingFonts = <String, NotoFont>{};
|
||||
@@ -331,8 +474,7 @@ class FallbackFontDownloadQueue {
|
||||
}
|
||||
|
||||
void add(NotoFont font) {
|
||||
if (downloadedFonts.contains(font) ||
|
||||
pendingFonts.containsKey(font.url)) {
|
||||
if (downloadedFonts.contains(font) || pendingFonts.containsKey(font.url)) {
|
||||
return;
|
||||
}
|
||||
final bool firstInBatch = pendingFonts.isEmpty;
|
||||
@@ -375,9 +517,8 @@ class FallbackFontDownloadQueue {
|
||||
}
|
||||
|
||||
if (pendingFonts.isEmpty) {
|
||||
fallbackManager.registry.updateFallbackFontFamilies(
|
||||
fallbackManager.globalFontFallbacks
|
||||
);
|
||||
fallbackManager.registry
|
||||
.updateFallbackFontFamilies(fallbackManager.globalFontFallbacks);
|
||||
sendFontChangeMessage();
|
||||
final Completer<void> idleCompleter = _idleCompleter!;
|
||||
_idleCompleter = null;
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
// 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:math' as math;
|
||||
|
||||
import 'noto_font.dart' show CodePointRange;
|
||||
|
||||
/// A tree which stores a set of intervals that can be queried for intersection.
|
||||
class IntervalTree<T> {
|
||||
IntervalTree._(this.root);
|
||||
|
||||
/// Creates an interval tree from a mapping of [T] values to a list of ranges.
|
||||
///
|
||||
/// When the interval tree is queried, it will return a list of [T]s which
|
||||
/// have a range which contains the point.
|
||||
factory IntervalTree.createFromRanges(Map<T, List<CodePointRange>> rangesMap) {
|
||||
assert(rangesMap.isNotEmpty);
|
||||
// Get a list of all the ranges ordered by start index.
|
||||
final List<IntervalTreeNode<T>> intervals = <IntervalTreeNode<T>>[];
|
||||
rangesMap.forEach((T key, List<CodePointRange> rangeList) {
|
||||
for (final CodePointRange range in rangeList) {
|
||||
intervals.add(IntervalTreeNode<T>(key, range.start, range.end));
|
||||
}
|
||||
});
|
||||
assert(intervals.isNotEmpty);
|
||||
|
||||
intervals
|
||||
.sort((IntervalTreeNode<T> a, IntervalTreeNode<T> b) => a.low - b.low);
|
||||
|
||||
// Make a balanced binary search tree from the nodes sorted by low value.
|
||||
IntervalTreeNode<T>? makeBalancedTree(List<IntervalTreeNode<T>> nodes) {
|
||||
if (nodes.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (nodes.length == 1) {
|
||||
return nodes.single;
|
||||
}
|
||||
final int mid = nodes.length ~/ 2;
|
||||
final IntervalTreeNode<T> root = nodes[mid];
|
||||
root.left = makeBalancedTree(nodes.sublist(0, mid));
|
||||
root.right = makeBalancedTree(nodes.sublist(mid + 1));
|
||||
return root;
|
||||
}
|
||||
|
||||
// Given a node, computes the highest `high` point of all of the subnodes.
|
||||
//
|
||||
// As a side effect, this also computes the high point of all subnodes.
|
||||
void computeHigh(IntervalTreeNode<T> root) {
|
||||
if (root.left == null && root.right == null) {
|
||||
root.computedHigh = root.high;
|
||||
} else if (root.left == null) {
|
||||
computeHigh(root.right!);
|
||||
root.computedHigh = math.max(root.high, root.right!.computedHigh);
|
||||
} else if (root.right == null) {
|
||||
computeHigh(root.left!);
|
||||
root.computedHigh = math.max(root.high, root.left!.computedHigh);
|
||||
} else {
|
||||
computeHigh(root.right!);
|
||||
computeHigh(root.left!);
|
||||
root.computedHigh = math.max(
|
||||
root.high,
|
||||
math.max(
|
||||
root.left!.computedHigh,
|
||||
root.right!.computedHigh,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
final IntervalTreeNode<T> root = makeBalancedTree(intervals)!;
|
||||
computeHigh(root);
|
||||
|
||||
return IntervalTree<T>._(root);
|
||||
}
|
||||
|
||||
/// The root node of the interval tree.
|
||||
final IntervalTreeNode<T> root;
|
||||
|
||||
/// Returns the list of objects which have been associated with intervals that
|
||||
/// intersect with [x].
|
||||
List<T> intersections(int x) {
|
||||
final List<T> results = <T>[];
|
||||
root.searchForPoint(x, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Whether this tree contains at least one interval that includes [x].
|
||||
bool containsDeep(int x) {
|
||||
return root.containsDeep(x);
|
||||
}
|
||||
}
|
||||
|
||||
class IntervalTreeNode<T> {
|
||||
IntervalTreeNode(this.value, this.low, this.high) : computedHigh = high;
|
||||
|
||||
final T value;
|
||||
final int low;
|
||||
final int high;
|
||||
int computedHigh;
|
||||
|
||||
IntervalTreeNode<T>? left;
|
||||
IntervalTreeNode<T>? right;
|
||||
|
||||
Iterable<T> enumerateAllElements() {
|
||||
final Iterable<T> leftElements = left?.enumerateAllElements() ?? Iterable<T>.empty();
|
||||
final Iterable<T> rightElements = right?.enumerateAllElements() ?? Iterable<T>.empty();
|
||||
return leftElements.followedBy(<T>[value]).followedBy(rightElements);
|
||||
}
|
||||
|
||||
/// Whether this node contains [x].
|
||||
///
|
||||
/// Does not recursively check whether child nodes contain [x].
|
||||
bool containsShallow(int x) {
|
||||
return low <= x && x <= high;
|
||||
}
|
||||
|
||||
/// Whether this sub-tree contains [x].
|
||||
///
|
||||
/// Recursively checks whether child nodes contain [x].
|
||||
bool containsDeep(int x) {
|
||||
if (x > computedHigh) {
|
||||
// x is above the highest possible value stored in this subtree.
|
||||
// Don't bother checking intervals.
|
||||
return false;
|
||||
}
|
||||
if (containsShallow(x)) {
|
||||
return true;
|
||||
}
|
||||
if (left?.containsDeep(x) ?? false) {
|
||||
return true;
|
||||
}
|
||||
if (x < low) {
|
||||
// The right tree can't possible contain x. Don't bother checking.
|
||||
return false;
|
||||
}
|
||||
return right?.containsDeep(x) ?? false;
|
||||
}
|
||||
|
||||
// Searches the tree rooted at this node for all T containing [x].
|
||||
void searchForPoint(int x, List<T> result) {
|
||||
if (x > computedHigh) {
|
||||
return;
|
||||
}
|
||||
left?.searchForPoint(x, result);
|
||||
if (containsShallow(x)) {
|
||||
result.add(value);
|
||||
}
|
||||
if (x < low) {
|
||||
return;
|
||||
}
|
||||
right?.searchForPoint(x, result);
|
||||
}
|
||||
}
|
||||
@@ -2,107 +2,47 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'text/unicode_range.dart';
|
||||
|
||||
class NotoFont {
|
||||
NotoFont(this.name, this.url, this._packedRanges);
|
||||
NotoFont(this.name, this.url, {this.enabled = true});
|
||||
|
||||
final String name;
|
||||
final String url;
|
||||
final String _packedRanges;
|
||||
// A sorted list of Unicode ranges.
|
||||
late final List<CodePointRange> _ranges = _unpackFontRange(_packedRanges);
|
||||
|
||||
List<CodePointRange> computeUnicodeRanges() => _ranges;
|
||||
/// `true` if this font is to be considered as a fallback font. Almost all
|
||||
/// fonts are enabled, but [enabled] may be `false` to exclude a font. This is
|
||||
/// used to choose between color and monochrome emoji fonts - only one of them
|
||||
/// is enabled.
|
||||
final bool enabled;
|
||||
|
||||
// Returns `true` if this font has a glyph for the given [codeunit].
|
||||
bool contains(int codeUnit) {
|
||||
// Binary search through the unicode ranges to see if there
|
||||
// is a range that contains the codeunit.
|
||||
int min = 0;
|
||||
int max = _ranges.length - 1;
|
||||
while (min <= max) {
|
||||
final int mid = (min + max) ~/ 2;
|
||||
final CodePointRange range = _ranges[mid];
|
||||
if (range.start > codeUnit) {
|
||||
max = mid - 1;
|
||||
} else {
|
||||
// range.start <= codeUnit
|
||||
if (range.end >= codeUnit) {
|
||||
return true;
|
||||
}
|
||||
min = mid + 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
final int index = _index++;
|
||||
static int _index = 0;
|
||||
|
||||
/// During fallback font selection this is the number of missing code points
|
||||
/// that are covered by (i.e. in) this font.
|
||||
int coverCount = 0;
|
||||
|
||||
/// During fallback font selection this is a list of [FallbackFontComponent]s
|
||||
/// from this font that are required to cover some of the missing code
|
||||
/// points. The cover count for the font is the sum of the cover counts for
|
||||
/// the components that make up the font.
|
||||
final List<FallbackFontComponent> coverComponents = <FallbackFontComponent>[];
|
||||
}
|
||||
|
||||
class CodePointRange {
|
||||
const CodePointRange(this.start, this.end);
|
||||
/// A component is a set of code points common to some fonts. Each code point is
|
||||
/// in a single component. Each font can be represented as a disjoint union of
|
||||
/// components. We store the inverse of this relationship, the fonts that use
|
||||
/// this component. The font fallback selection algorithm does not need the code
|
||||
/// points in a component or a font, so this is not stored, but can be recovered
|
||||
/// via the map from code-point to component.
|
||||
class FallbackFontComponent {
|
||||
FallbackFontComponent(this._allFonts);
|
||||
final List<NotoFont> _allFonts;
|
||||
late final List<NotoFont> _activeFonts = List<NotoFont>.unmodifiable(
|
||||
_allFonts.where((NotoFont font) => font.enabled));
|
||||
|
||||
final int start;
|
||||
final int end;
|
||||
List<NotoFont> get fonts => _activeFonts;
|
||||
|
||||
bool contains(int codeUnit) {
|
||||
return start <= codeUnit && codeUnit <= end;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! CodePointRange) {
|
||||
return false;
|
||||
}
|
||||
final CodePointRange range = other;
|
||||
return range.start == start && range.end == end;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(start, end);
|
||||
|
||||
@override
|
||||
String toString() => '[$start, $end]';
|
||||
}
|
||||
|
||||
final int _kCharPipe = '|'.codeUnitAt(0);
|
||||
final int _kCharSemicolon = ';'.codeUnitAt(0);
|
||||
|
||||
class MutableInt {
|
||||
MutableInt(this.value);
|
||||
|
||||
int value;
|
||||
}
|
||||
|
||||
List<CodePointRange> _unpackFontRange(String packedRange) {
|
||||
final MutableInt i = MutableInt(0);
|
||||
final List<CodePointRange> ranges = <CodePointRange>[];
|
||||
|
||||
while (i.value < packedRange.length) {
|
||||
final int rangeStart = _consumeInt36(packedRange, i, until: _kCharPipe);
|
||||
final int rangeLength = _consumeInt36(packedRange, i, until: _kCharSemicolon);
|
||||
final int rangeEnd = rangeStart + rangeLength;
|
||||
ranges.add(CodePointRange(rangeStart, rangeEnd));
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
int _consumeInt36(String packedData, MutableInt index, {required int until}) {
|
||||
// The implementation is similar to:
|
||||
//
|
||||
// ```dart
|
||||
// return int.tryParse(packedData.substring(index, indexOfUntil), radix: 36);
|
||||
// ```
|
||||
//
|
||||
// But using substring is slow when called too many times. This custom
|
||||
// implementation parses the integer without extra memory.
|
||||
|
||||
int result = 0;
|
||||
while (true) {
|
||||
final int charCode = packedData.codeUnitAt(index.value);
|
||||
index.value++;
|
||||
if (charCode == until) {
|
||||
return result;
|
||||
}
|
||||
result = result * 36 + getIntFromCharCode(charCode);
|
||||
}
|
||||
/// During fallback font selection this is the number of missing code points
|
||||
/// that are covered by this component.
|
||||
int coverCount = 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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.
|
||||
|
||||
const int kMaxCodePoint = 0x10ffff;
|
||||
|
||||
const int kPrefixDigit0 = 48;
|
||||
const int kPrefixRadix = 10;
|
||||
|
||||
const int kFontIndexDigit0 = 65 + 32; // 'a'..'z'
|
||||
const int kFontIndexRadix = 26;
|
||||
|
||||
const int kRangeSizeDigit0 = 65 + 32; // 'a'..'z'
|
||||
const int kRangeSizeRadix = 26;
|
||||
|
||||
const int kRangeValueDigit0 = 65; // 'A'..'Z'
|
||||
const int kRangeValueRadix = 26;
|
||||
@@ -1,82 +0,0 @@
|
||||
// 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 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('$IntervalTree', () {
|
||||
test('is balanced', () {
|
||||
final Map<String, List<CodePointRange>> ranges = <String, List<CodePointRange>>{
|
||||
'A': const <CodePointRange>[CodePointRange(0, 5), CodePointRange(6, 10)],
|
||||
'B': const <CodePointRange>[CodePointRange(4, 6)],
|
||||
};
|
||||
|
||||
// Should create a balanced 3-node tree with a root with a left and right
|
||||
// child.
|
||||
final IntervalTree<String> tree = IntervalTree<String>.createFromRanges(ranges);
|
||||
final IntervalTreeNode<String> root = tree.root;
|
||||
expect(root.left, isNotNull);
|
||||
expect(root.right, isNotNull);
|
||||
expect(root.left!.left, isNull);
|
||||
expect(root.left!.right, isNull);
|
||||
expect(root.right!.left, isNull);
|
||||
expect(root.right!.right, isNull);
|
||||
|
||||
// Should create a balanced 15-node tree (4 layers deep).
|
||||
final Map<String, List<CodePointRange>> ranges2 = <String, List<CodePointRange>>{
|
||||
'A': const <CodePointRange>[
|
||||
CodePointRange(1, 1),
|
||||
CodePointRange(2, 2),
|
||||
CodePointRange(3, 3),
|
||||
CodePointRange(4, 4),
|
||||
CodePointRange(5, 5),
|
||||
CodePointRange(6, 6),
|
||||
CodePointRange(7, 7),
|
||||
CodePointRange(8, 8),
|
||||
CodePointRange(9, 9),
|
||||
CodePointRange(10, 10),
|
||||
CodePointRange(11, 11),
|
||||
CodePointRange(12, 12),
|
||||
CodePointRange(13, 13),
|
||||
CodePointRange(14, 14),
|
||||
CodePointRange(15, 15),
|
||||
],
|
||||
};
|
||||
|
||||
// Should create a balanced 3-node tree with a root with a left and right
|
||||
// child.
|
||||
final IntervalTree<String> tree2 = IntervalTree<String>.createFromRanges(ranges2);
|
||||
final IntervalTreeNode<String> root2 = tree2.root;
|
||||
|
||||
expect(root2.left!.left!.left, isNotNull);
|
||||
expect(root2.left!.left!.right, isNotNull);
|
||||
expect(root2.left!.right!.left, isNotNull);
|
||||
expect(root2.left!.right!.right, isNotNull);
|
||||
expect(root2.right!.left!.left, isNotNull);
|
||||
expect(root2.right!.left!.right, isNotNull);
|
||||
expect(root2.right!.right!.left, isNotNull);
|
||||
expect(root2.right!.right!.right, isNotNull);
|
||||
});
|
||||
|
||||
test('finds values whose intervals overlap with a given point', () {
|
||||
final Map<String, List<CodePointRange>> ranges = <String, List<CodePointRange>>{
|
||||
'A': const <CodePointRange>[CodePointRange(0, 5), CodePointRange(7, 10)],
|
||||
'B': const <CodePointRange>[CodePointRange(4, 6)],
|
||||
};
|
||||
final IntervalTree<String> tree = IntervalTree<String>.createFromRanges(ranges);
|
||||
|
||||
expect(tree.intersections(1), <String>['A']);
|
||||
expect(tree.intersections(4), <String>['A', 'B']);
|
||||
expect(tree.intersections(6), <String>['B']);
|
||||
expect(tree.intersections(7), <String>['A']);
|
||||
expect(tree.intersections(11), <String>[]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -225,16 +225,23 @@ void testMain() {
|
||||
// font tree.
|
||||
final Set<String> testedFonts = <String>{};
|
||||
final Set<int> supportedUniqueCodePoints = <int>{};
|
||||
final IntervalTree<NotoFont> notoTree =
|
||||
renderer.fontCollection.fontFallbackManager!.notoTree;
|
||||
for (final NotoFont font in renderer.fontCollection.fontFallbackManager!.fallbackFonts) {
|
||||
testedFonts.add(font.name);
|
||||
for (final CodePointRange range in font.computeUnicodeRanges()) {
|
||||
for (int codePoint = range.start; codePoint < range.end; codePoint++) {
|
||||
supportedUniqueCodePoints.add(codePoint);
|
||||
renderer.fontCollection.fontFallbackManager!.codePointToComponents
|
||||
.forEachRange((int start, int end, FallbackFontComponent component) {
|
||||
if (component.fonts.isNotEmpty) {
|
||||
bool componentHasEnabledFont = false;
|
||||
for (final NotoFont font in component.fonts) {
|
||||
if (font.enabled) {
|
||||
testedFonts.add(font.name);
|
||||
componentHasEnabledFont = true;
|
||||
}
|
||||
}
|
||||
if (componentHasEnabledFont) {
|
||||
for (int codePoint = start; codePoint <= end; codePoint++) {
|
||||
supportedUniqueCodePoints.add(codePoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(
|
||||
supportedUniqueCodePoints.length, greaterThan(10000)); // sanity check
|
||||
@@ -402,7 +409,10 @@ void testMain() {
|
||||
}
|
||||
final Set<NotoFont> fonts = <NotoFont>{};
|
||||
for (final int codePoint in codePoints) {
|
||||
final List<NotoFont> fontsForPoint = notoTree.intersections(codePoint);
|
||||
final List<NotoFont> fontsForPoint = renderer
|
||||
.fontCollection.fontFallbackManager!.codePointToComponents
|
||||
.lookup(codePoint)
|
||||
.fonts;
|
||||
|
||||
// All code points are extracted from the same tree, so there must
|
||||
// be at least one font supporting each code point
|
||||
@@ -411,10 +421,11 @@ void testMain() {
|
||||
}
|
||||
|
||||
try {
|
||||
renderer.fontCollection.fontFallbackManager!.findMinimumFontsForCodePoints(codePoints, fonts);
|
||||
renderer.fontCollection.fontFallbackManager!
|
||||
.findFontsForMissingCodePoints(codePoints.toList());
|
||||
} catch (e) {
|
||||
print(
|
||||
'findMinimumFontsForCodePoints failed:\n'
|
||||
'findFontsForMissingCodePoints failed:\n'
|
||||
' Code points: ${codePoints.join(', ')}\n'
|
||||
' Fonts: ${fonts.map((NotoFont f) => f.name).join(', ')}',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user