未验证 提交 5b9cd44b 编写于 作者: H Harry Terkelsen 提交者: GitHub

Automatically download Noto fonts as backup fonts in CanvasKit mode (#23728)

* Revert "Revert "[CanvasKit] Automatically fall back to Noto fonts (#23096)" (#23357)"

This reverts commit f9f4d016.

* WIP

* Use an Interval Tree to store the unicode ranges for the Noto Fonts

* Update licenses

* Remove debug print statements

* Respond to comments

* Fix analysis error

* Add tests

* Respond to comments

* Fix test

* Update goldens lock

* Skip screenshot test on Safari

* Skip CanvasKit tests on iOS Safari

* Move CanvasKit initialization so it doesn't run on iOS Safari
上级 5c2003f0
......@@ -434,10 +434,12 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/initialization.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart
......
repository: https://github.com/flutter/goldens.git
revision: 999507db8c924635a605325252702bad661e2ad2
revision: bdb442c42588b25c657779c78523822e349742d5
......@@ -234,7 +234,7 @@ class TestCommand extends Command<bool> with ArgUtils {
Future<bool> runIntegrationTests() async {
// Parse additional arguments specific for integration testing.
IntegrationTestsArgumentParser.instance.parseOptions(argResults);
if(!_testPreparationReady) {
if (!_testPreparationReady) {
await _prepare();
}
return IntegrationTestsManager(
......@@ -522,9 +522,12 @@ class TestCommand extends Command<bool> with ArgUtils {
}
// All files under test/golden_tests are considered golden tests.
final bool isUnderGoldenTestsDirectory = path.split(testFilePath.relativeToWebUi).contains('golden_tests');
final bool isUnderGoldenTestsDirectory =
path.split(testFilePath.relativeToWebUi).contains('golden_tests');
// Any file whose name ends with "_golden_test.dart" is run as a golden test.
final bool isGoldenTestFile = path.basename(testFilePath.relativeToWebUi).endsWith('_golden_test.dart');
final bool isGoldenTestFile = path
.basename(testFilePath.relativeToWebUi)
.endsWith('_golden_test.dart');
if (isUnderGoldenTestsDirectory || isGoldenTestFile) {
screenshotTestFiles.add(testFilePath);
} else {
......@@ -767,7 +770,11 @@ class TestCommand extends Command<bool> with ArgUtils {
}
}
const List<String> _kTestFonts = <String>['ahem.ttf', 'Roboto-Regular.ttf'];
const List<String> _kTestFonts = <String>[
'ahem.ttf',
'Roboto-Regular.ttf',
'NotoNaskhArabic-Regular.ttf',
];
void _copyTestFontsIntoWebUi() {
final String fontsPath = path.join(
......
......@@ -32,9 +32,11 @@ part 'engine/canvaskit/canvaskit_api.dart';
part 'engine/canvaskit/color_filter.dart';
part 'engine/canvaskit/embedded_views.dart';
part 'engine/canvaskit/fonts.dart';
part 'engine/canvaskit/font_fallbacks.dart';
part 'engine/canvaskit/image.dart';
part 'engine/canvaskit/image_filter.dart';
part 'engine/canvaskit/initialization.dart';
part 'engine/canvaskit/interval_tree.dart';
part 'engine/canvaskit/layer.dart';
part 'engine/canvaskit/layer_scene_builder.dart';
part 'engine/canvaskit/layer_tree.dart';
......
......@@ -1177,7 +1177,8 @@ class SkPath {
/// Serializes the path into a list of commands.
///
/// The list can be used to create a new [SkPath] using [CanvasKit.Path.MakeFromCmds].
/// The list can be used to create a new [SkPath] using
/// [CanvasKit.Path.MakeFromCmds].
external List<dynamic> toCmds();
external void delete();
......
// 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.
// @dart = 2.12
part of engine;
/// Whether or not "Noto Sans Symbols" and "Noto Color Emoji" fonts have been
/// downloaded. We download these as fallbacks when no other font covers the
/// given code units.
bool _registeredSymbolsAndEmoji = false;
final Set<int> codeUnitsWithNoKnownFont = <int>{};
Future<void> _findFontsForMissingCodeunits(List<int> codeunits) async {
_ensureNotoFontTreeCreated();
// If all of the code units are known to have no Noto Font which covers them,
// then just give up. We have already logged a warning.
if (codeunits.every((u) => codeUnitsWithNoKnownFont.contains(u))) {
return;
}
Set<_NotoFont> fonts = <_NotoFont>{};
Set<int> coveredCodeUnits = <int>{};
Set<int> missingCodeUnits = <int>{};
for (int codeunit in codeunits) {
List<_NotoFont> fontsForUnit = _notoTree!.intersections(codeunit);
fonts.addAll(fontsForUnit);
if (fontsForUnit.isNotEmpty) {
coveredCodeUnits.add(codeunit);
} else {
missingCodeUnits.add(codeunit);
}
}
fonts = _findMinimumFontsForCodeunits(coveredCodeUnits, fonts);
for (_NotoFont font in fonts) {
await font.ensureResolved();
}
Set<_ResolvedNotoSubset> resolvedFonts = <_ResolvedNotoSubset>{};
for (int codeunit in coveredCodeUnits) {
for (_NotoFont font in fonts) {
if (font.resolvedFont == null) {
// We failed to resolve the font earlier.
continue;
}
resolvedFonts.addAll(font.resolvedFont!.tree.intersections(codeunit));
}
}
for (_ResolvedNotoSubset resolvedFont in resolvedFonts) {
notoDownloadQueue.add(resolvedFont);
}
if (missingCodeUnits.isNotEmpty && !notoDownloadQueue.isPending) {
if (!_registeredSymbolsAndEmoji) {
_registerSymbolsAndEmoji();
} else {
if (!notoDownloadQueue.isPending) {
html.window.console.log(
'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');
codeUnitsWithNoKnownFont.addAll(missingCodeUnits);
}
}
}
}
/// Parse the CSS file for a font and make a list of resolved subsets.
///
/// A CSS file from Google Fonts looks like this:
///
/// /* [0] */
/// @font-face {
/// font-family: 'Noto Sans KR';
/// font-style: normal;
/// font-weight: 400;
/// src: url(https://fonts.gstatic.com/s/notosanskr/v13/PbykFmXiEBPT4ITbgNA5Cgm20xz64px_1hVWr0wuPNGmlQNMEfD4.0.woff2) format('woff2');
/// unicode-range: U+f9ca-fa0b, U+ff03-ff05, U+ff07, U+ff0a-ff0b, U+ff0d-ff19, U+ff1b, U+ff1d, U+ff20-ff5b, U+ff5d, U+ffe0-ffe3, U+ffe5-ffe6;
/// }
/// /* [1] */
/// @font-face {
/// font-family: 'Noto Sans KR';
/// font-style: normal;
/// font-weight: 400;
/// src: url(https://fonts.gstatic.com/s/notosanskr/v13/PbykFmXiEBPT4ITbgNA5Cgm20xz64px_1hVWr0wuPNGmlQNMEfD4.1.woff2) format('woff2');
/// unicode-range: U+f92f-f980, U+f982-f9c9;
/// }
/// /* [2] */
/// @font-face {
/// font-family: 'Noto Sans KR';
/// font-style: normal;
/// font-weight: 400;
/// src: url(https://fonts.gstatic.com/s/notosanskr/v13/PbykFmXiEBPT4ITbgNA5Cgm20xz64px_1hVWr0wuPNGmlQNMEfD4.2.woff2) format('woff2');
/// unicode-range: U+d723-d728, U+d72a-d733, U+d735-d748, U+d74a-d74f, U+d752-d753, U+d755-d757, U+d75a-d75f, U+d762-d764, U+d766-d768, U+d76a-d76b, U+d76d-d76f, U+d771-d787, U+d789-d78b, U+d78d-d78f, U+d791-d797, U+d79a, U+d79c, U+d79e-d7a3, U+f900-f909, U+f90b-f92e;
/// }
_ResolvedNotoFont? _makeResolvedNotoFontFromCss(String css, String name) {
List<_ResolvedNotoSubset> subsets = <_ResolvedNotoSubset>[];
bool resolvingFontFace = false;
String? fontFaceUrl;
List<CodeunitRange>? fontFaceUnicodeRanges;
for (final String line in LineSplitter.split(css)) {
// Search for the beginning of a @font-face.
if (!resolvingFontFace) {
if (line == '@font-face {') {
resolvingFontFace = true;
} else {
continue;
}
} else {
// We are resolving a @font-face, read out the url and ranges.
if (line.startsWith(' src:')) {
int urlStart = line.indexOf('url(');
if (urlStart == -1) {
html.window.console.warn('Unable to resolve Noto font URL: $line');
return null;
}
int urlEnd = line.indexOf(')');
fontFaceUrl = line.substring(urlStart + 4, urlEnd);
} else if (line.startsWith(' unicode-range:')) {
fontFaceUnicodeRanges = <CodeunitRange>[];
String rangeString = line.substring(17, line.length - 1);
List<String> rawRanges = rangeString.split(', ');
for (final String rawRange in rawRanges) {
List<String> startEnd = rawRange.split('-');
if (startEnd.length == 1) {
String singleRange = startEnd.single;
assert(singleRange.startsWith('U+'));
int rangeValue = int.parse(singleRange.substring(2), radix: 16);
fontFaceUnicodeRanges.add(CodeunitRange(rangeValue, rangeValue));
} else {
assert(startEnd.length == 2);
String startRange = startEnd[0];
String endRange = startEnd[1];
assert(startRange.startsWith('U+'));
int startValue = int.parse(startRange.substring(2), radix: 16);
int endValue = int.parse(endRange, radix: 16);
fontFaceUnicodeRanges.add(CodeunitRange(startValue, endValue));
}
}
} else if (line == '}') {
if (fontFaceUrl == null || fontFaceUnicodeRanges == null) {
html.window.console.warn('Unable to parse Google Fonts CSS: $css');
return null;
}
subsets
.add(_ResolvedNotoSubset(fontFaceUrl, name, fontFaceUnicodeRanges));
resolvingFontFace = false;
} else {
continue;
}
}
}
if (resolvingFontFace) {
html.window.console.warn('Unable to parse Google Fonts CSS: $css');
return null;
}
Map<_ResolvedNotoSubset, List<CodeunitRange>> rangesMap =
<_ResolvedNotoSubset, List<CodeunitRange>>{};
for (_ResolvedNotoSubset subset in subsets) {
for (CodeunitRange range in subset.ranges) {
rangesMap.putIfAbsent(subset, () => <CodeunitRange>[]).add(range);
}
}
IntervalTree<_ResolvedNotoSubset> tree =
IntervalTree<_ResolvedNotoSubset>.createFromRanges(rangesMap);
return _ResolvedNotoFont(name, subsets, tree);
}
/// In the case where none of the known Noto Fonts cover a set of code units,
/// try the Symbols and Emoji fonts. We don't know the exact range of code units
/// that are covered by these fonts, so we download them and hope for the best.
Future<void> _registerSymbolsAndEmoji() async {
if (_registeredSymbolsAndEmoji) {
return;
}
_registeredSymbolsAndEmoji = true;
const String symbolsUrl =
'https://fonts.googleapis.com/css2?family=Noto+Sans+Symbols';
const String emojiUrl =
'https://fonts.googleapis.com/css2?family=Noto+Color+Emoji+Compat';
String symbolsCss =
await notoDownloadQueue.downloader.downloadAsString(symbolsUrl);
String emojiCss =
await notoDownloadQueue.downloader.downloadAsString(emojiUrl);
String? extractUrlFromCss(String css) {
for (final String line in LineSplitter.split(css)) {
if (line.startsWith(' src:')) {
int urlStart = line.indexOf('url(');
if (urlStart == -1) {
html.window.console.warn('Unable to resolve Noto font URL: $line');
return null;
}
int urlEnd = line.indexOf(')');
return line.substring(urlStart + 4, urlEnd);
}
}
html.window.console.warn('Unable to determine URL for Noto font');
return null;
}
String? symbolsFontUrl = extractUrlFromCss(symbolsCss);
String? emojiFontUrl = extractUrlFromCss(emojiCss);
if (symbolsFontUrl == null || emojiFontUrl == null) {
html.window.console
.warn('Error parsing CSS for Noto Emoji and Symbols font.');
}
notoDownloadQueue.add(_ResolvedNotoSubset(
symbolsFontUrl!, 'Noto Sans Symbols', const <CodeunitRange>[]));
notoDownloadQueue.add(_ResolvedNotoSubset(
emojiFontUrl!, 'Noto Color Emoji Compat', const <CodeunitRange>[]));
}
/// Finds the minimum set of fonts which covers all of the [codeunits].
///
/// Since set cover is NP-complete, we approximate using a greedy algorithm
/// which finds the font which covers the most codeunits. If multiple CJK
/// fonts match the same number of codeunits, we choose one based on the user's
/// locale.
Set<_NotoFont> _findMinimumFontsForCodeunits(
Iterable<int> codeunits, Set<_NotoFont> fonts) {
List<int> unmatchedCodeunits = List<int>.from(codeunits);
Set<_NotoFont> minimumFonts = <_NotoFont>{};
List<_NotoFont> bestFonts = <_NotoFont>[];
int maxCodeunitsCovered = 0;
String language = html.window.navigator.language;
// This is guaranteed to terminate because [codeunits] is a list of fonts
// which we've already determined are covered by [fonts].
while (unmatchedCodeunits.isNotEmpty) {
for (var font in fonts) {
int codeunitsCovered = 0;
for (int codeunit in unmatchedCodeunits) {
if (font.matchesCodeunit(codeunit)) {
codeunitsCovered++;
}
}
if (codeunitsCovered > maxCodeunitsCovered) {
bestFonts.clear();
bestFonts.add(font);
maxCodeunitsCovered = codeunitsCovered;
} else if (codeunitsCovered == maxCodeunitsCovered) {
bestFonts.add(font);
}
}
assert(bestFonts.isNotEmpty);
// 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((font) => _cjkFonts.contains(font))) {
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;
}
}
}
}
unmatchedCodeunits
.removeWhere((codeunit) => bestFont.matchesCodeunit(codeunit));
minimumFonts.add(bestFont);
}
return minimumFonts;
}
void _ensureNotoFontTreeCreated() {
if (_notoTree != null) {
return;
}
Map<_NotoFont, List<CodeunitRange>> ranges =
<_NotoFont, List<CodeunitRange>>{};
for (_NotoFont font in _notoFonts) {
for (CodeunitRange range in font.unicodeRanges) {
ranges.putIfAbsent(font, () => <CodeunitRange>[]).add(range);
}
}
_notoTree = IntervalTree<_NotoFont>.createFromRanges(ranges);
}
class _NotoFont {
final String name;
final List<CodeunitRange> unicodeRanges;
Completer<void>? _decodingCompleter;
_ResolvedNotoFont? resolvedFont;
_NotoFont(this.name, this.unicodeRanges);
bool matchesCodeunit(int codeunit) {
for (CodeunitRange range in unicodeRanges) {
if (range.contains(codeunit)) {
return true;
}
}
return false;
}
String get googleFontsCssUrl =>
'https://fonts.googleapis.com/css2?family=${name.replaceAll(' ', '+')}';
Future<void> ensureResolved() async {
if (resolvedFont == null) {
if (_decodingCompleter == null) {
_decodingCompleter = Completer<void>();
String googleFontCss = await notoDownloadQueue.downloader
.downloadAsString(googleFontsCssUrl);
final _ResolvedNotoFont? googleFont =
_makeResolvedNotoFontFromCss(googleFontCss, name);
resolvedFont = googleFont;
_decodingCompleter!.complete();
} else {
await _decodingCompleter!.future;
}
}
}
}
class CodeunitRange {
final int start;
final int end;
const CodeunitRange(this.start, this.end);
bool contains(int codeUnit) {
return start <= codeUnit && codeUnit <= end;
}
@override
bool operator ==(dynamic other) {
if (other is! CodeunitRange) {
return false;
}
CodeunitRange range = other;
return range.start == start && range.end == end;
}
@override
int get hashCode => ui.hashValues(start, end);
@override
String toString() => '[$start, $end]';
}
class _ResolvedNotoFont {
final String name;
final List<_ResolvedNotoSubset> subsets;
final IntervalTree<_ResolvedNotoSubset> tree;
const _ResolvedNotoFont(this.name, this.subsets, this.tree);
}
class _ResolvedNotoSubset {
final String url;
final String family;
final List<CodeunitRange> ranges;
_ResolvedNotoSubset(this.url, this.family, this.ranges);
}
_NotoFont _notoSansSC = _NotoFont('Noto Sans SC', <CodeunitRange>[
CodeunitRange(12288, 12591),
CodeunitRange(12800, 13311),
CodeunitRange(19968, 40959),
CodeunitRange(65072, 65135),
CodeunitRange(65280, 65519),
]);
_NotoFont _notoSansTC = _NotoFont('Noto Sans TC', <CodeunitRange>[
CodeunitRange(12288, 12351),
CodeunitRange(12549, 12585),
CodeunitRange(19968, 40959),
]);
_NotoFont _notoSansHK = _NotoFont('Noto Sans HK', <CodeunitRange>[
CodeunitRange(12288, 12351),
CodeunitRange(12549, 12585),
CodeunitRange(19968, 40959),
]);
_NotoFont _notoSansJP = _NotoFont('Noto Sans JP', <CodeunitRange>[
CodeunitRange(12288, 12543),
CodeunitRange(19968, 40959),
CodeunitRange(65280, 65519),
]);
List<_NotoFont> _cjkFonts = <_NotoFont>[
_notoSansSC,
_notoSansTC,
_notoSansHK,
_notoSansJP,
];
List<_NotoFont> _notoFonts = <_NotoFont>[
_notoSansSC,
_notoSansTC,
_notoSansHK,
_notoSansJP,
_NotoFont('Noto Naskh Arabic UI', <CodeunitRange>[
CodeunitRange(1536, 1791),
CodeunitRange(8204, 8206),
CodeunitRange(8208, 8209),
CodeunitRange(8271, 8271),
CodeunitRange(11841, 11841),
CodeunitRange(64336, 65023),
CodeunitRange(65132, 65276),
]),
_NotoFont('Noto Sans Armenian', <CodeunitRange>[
CodeunitRange(1328, 1424),
CodeunitRange(64275, 64279),
]),
_NotoFont('Noto Sans Bengali UI', <CodeunitRange>[
CodeunitRange(2404, 2405),
CodeunitRange(2433, 2555),
CodeunitRange(8204, 8205),
CodeunitRange(8377, 8377),
CodeunitRange(9676, 9676),
]),
_NotoFont('Noto Sans Myanmar UI', <CodeunitRange>[
CodeunitRange(4096, 4255),
CodeunitRange(8204, 8205),
CodeunitRange(9676, 9676),
]),
_NotoFont('Noto Sans Egyptian Hieroglyphs', <CodeunitRange>[
CodeunitRange(77824, 78894),
]),
_NotoFont('Noto Sans Ethiopic', <CodeunitRange>[
CodeunitRange(4608, 5017),
CodeunitRange(11648, 11742),
CodeunitRange(43777, 43822),
]),
_NotoFont('Noto Sans Georgian', <CodeunitRange>[
CodeunitRange(1417, 1417),
CodeunitRange(4256, 4351),
CodeunitRange(11520, 11567),
]),
_NotoFont('Noto Sans Gujarati UI', <CodeunitRange>[
CodeunitRange(2404, 2405),
CodeunitRange(2688, 2815),
CodeunitRange(8204, 8205),
CodeunitRange(8377, 8377),
CodeunitRange(9676, 9676),
CodeunitRange(43056, 43065),
]),
_NotoFont('Noto Sans Gurmukhi UI', <CodeunitRange>[
CodeunitRange(2404, 2405),
CodeunitRange(2561, 2677),
CodeunitRange(8204, 8205),
CodeunitRange(8377, 8377),
CodeunitRange(9676, 9676),
CodeunitRange(9772, 9772),
CodeunitRange(43056, 43065),
]),
_NotoFont('Noto Sans Hebrew', <CodeunitRange>[
CodeunitRange(1424, 1535),
CodeunitRange(8362, 8362),
CodeunitRange(9676, 9676),
CodeunitRange(64285, 64335),
]),
_NotoFont('Noto Sans Devanagari UI', <CodeunitRange>[
CodeunitRange(2304, 2431),
CodeunitRange(7376, 7414),
CodeunitRange(7416, 7417),
CodeunitRange(8204, 9205),
CodeunitRange(8360, 8360),
CodeunitRange(8377, 8377),
CodeunitRange(9676, 9676),
CodeunitRange(43056, 43065),
CodeunitRange(43232, 43259),
]),
_NotoFont('Noto Sans Kannada UI', <CodeunitRange>[
CodeunitRange(2404, 2405),
CodeunitRange(3202, 3314),
CodeunitRange(8204, 8205),
CodeunitRange(8377, 8377),
CodeunitRange(9676, 9676),
]),
_NotoFont('Noto Sans Khmer UI', <CodeunitRange>[
CodeunitRange(6016, 6143),
CodeunitRange(8204, 8204),
CodeunitRange(9676, 9676),
]),
_NotoFont('Noto Sans KR', <CodeunitRange>[
CodeunitRange(12593, 12686),
CodeunitRange(12800, 12828),
CodeunitRange(12896, 12923),
CodeunitRange(44032, 55215),
]),
_NotoFont('Noto Sans Lao UI', <CodeunitRange>[
CodeunitRange(3713, 3807),
CodeunitRange(9676, 9676),
]),
_NotoFont('Noto Sans Malayalam UI', <CodeunitRange>[
CodeunitRange(775, 775),
CodeunitRange(803, 803),
CodeunitRange(2404, 2405),
CodeunitRange(3330, 3455),
CodeunitRange(8204, 8205),
CodeunitRange(8377, 8377),
CodeunitRange(9676, 9676),
]),
_NotoFont('Noto Sans Sinhala', <CodeunitRange>[
CodeunitRange(2404, 2405),
CodeunitRange(3458, 3572),
CodeunitRange(8204, 8205),
CodeunitRange(9676, 9676),
]),
_NotoFont('Noto Sans Tamil UI', <CodeunitRange>[
CodeunitRange(2404, 2405),
CodeunitRange(2946, 3066),
CodeunitRange(8204, 8205),
CodeunitRange(8377, 8377),
CodeunitRange(9676, 9676),
]),
_NotoFont('Noto Sans Telugu UI', <CodeunitRange>[
CodeunitRange(2385, 2386),
CodeunitRange(2404, 2405),
CodeunitRange(3072, 3199),
CodeunitRange(7386, 7386),
CodeunitRange(8204, 8205),
CodeunitRange(9676, 9676),
]),
_NotoFont('Noto Sans Thai UI', <CodeunitRange>[
CodeunitRange(3585, 3675),
CodeunitRange(8204, 8205),
CodeunitRange(9676, 9676),
]),
_NotoFont('Noto Sans', <CodeunitRange>[
CodeunitRange(0, 255),
CodeunitRange(305, 305),
CodeunitRange(338, 339),
CodeunitRange(699, 700),
CodeunitRange(710, 710),
CodeunitRange(730, 730),
CodeunitRange(732, 732),
CodeunitRange(8192, 8303),
CodeunitRange(8308, 8308),
CodeunitRange(8364, 8364),
CodeunitRange(8482, 8482),
CodeunitRange(8593, 8593),
CodeunitRange(8595, 8595),
CodeunitRange(8722, 8722),
CodeunitRange(8725, 8725),
CodeunitRange(65279, 65279),
CodeunitRange(65533, 65533),
CodeunitRange(1024, 1119),
CodeunitRange(1168, 1169),
CodeunitRange(1200, 1201),
CodeunitRange(8470, 8470),
CodeunitRange(1120, 1327),
CodeunitRange(7296, 7304),
CodeunitRange(8372, 8372),
CodeunitRange(11744, 11775),
CodeunitRange(42560, 42655),
CodeunitRange(65070, 65071),
CodeunitRange(880, 1023),
CodeunitRange(7936, 8191),
CodeunitRange(256, 591),
CodeunitRange(601, 601),
CodeunitRange(7680, 7935),
CodeunitRange(8224, 8224),
CodeunitRange(8352, 8363),
CodeunitRange(8365, 8399),
CodeunitRange(8467, 8467),
CodeunitRange(11360, 11391),
CodeunitRange(42784, 43007),
CodeunitRange(258, 259),
CodeunitRange(272, 273),
CodeunitRange(296, 297),
CodeunitRange(360, 361),
CodeunitRange(416, 417),
CodeunitRange(431, 432),
CodeunitRange(7840, 7929),
CodeunitRange(8363, 8363),
]),
];
class FallbackFontDownloadQueue {
NotoDownloader downloader = NotoDownloader();
final Set<_ResolvedNotoSubset> downloadedSubsets = <_ResolvedNotoSubset>{};
final Set<_ResolvedNotoSubset> pendingSubsets = <_ResolvedNotoSubset>{};
bool get isPending => pendingSubsets.isNotEmpty;
void add(_ResolvedNotoSubset subset) {
if (downloadedSubsets.contains(subset) || pendingSubsets.contains(subset)) {
return;
}
bool firstInBatch = pendingSubsets.isEmpty;
pendingSubsets.add(subset);
if (firstInBatch) {
Timer.run(startDownloads);
}
}
Future<void> startDownloads() async {
List<Future<void>> downloads = <Future>[];
for (_ResolvedNotoSubset subset in pendingSubsets) {
downloads.add(Future<void>(() async {
ByteBuffer buffer;
try {
buffer = await downloader.downloadAsBytes(subset.url);
} catch (e) {
html.window.console
.warn('Failed to load font ${subset.family} at ${subset.url}');
html.window.console.warn(e);
return;
}
final Uint8List bytes = buffer.asUint8List();
skiaFontCollection.registerFallbackFont(subset.family, bytes);
pendingSubsets.remove(subset);
downloadedSubsets.add(subset);
if (pendingSubsets.isEmpty) {
await skiaFontCollection.ensureFontsLoaded();
sendFontChangeMessage();
}
}));
}
await Future.wait<void>(downloads);
if (pendingSubsets.isNotEmpty) {
await startDownloads();
}
}
}
class NotoDownloader {
/// Downloads the [url] and returns it as a [ByteBuffer].
///
/// Override this for testing.
Future<ByteBuffer> downloadAsBytes(String url) {
return html.window.fetch(url).then((dynamic fetchResult) => fetchResult
.arrayBuffer()
.then<ByteBuffer>((dynamic x) => x as ByteBuffer));
}
/// Downloads the [url] and returns is as a [String].
///
/// Override this for testing.
Future<String> downloadAsString(String url) {
return html.window.fetch(url).then((dynamic response) =>
response.text().then<String>((dynamic x) => x as String));
}
}
/// The Noto font interval tree.
IntervalTree<_NotoFont>? _notoTree;
FallbackFontDownloadQueue notoDownloadQueue = FallbackFontDownloadQueue();
......@@ -22,15 +22,38 @@ class SkiaFontCollection {
/// Fonts which have been registered and loaded.
final List<_RegisteredFont> _registeredFonts = <_RegisteredFont>[];
final Set<String?> registeredFamilies = <String?>{};
/// Fallback fonts which have been registered and loaded.
final List<_RegisteredFont> _registeredFallbackFonts = <_RegisteredFont>[];
final Map<String, List<SkTypeface>> familyToTypefaceMap =
<String, List<SkTypeface>>{};
final List<String> globalFontFallbacks = <String>[];
final Map<String, int> _fontFallbackCounts = <String, int>{};
Future<void> ensureFontsLoaded() async {
await _loadFonts();
if (fontProvider != null) {
fontProvider!.delete();
fontProvider = null;
}
fontProvider = canvasKit.TypefaceFontProvider.Make();
familyToTypefaceMap.clear();
for (var font in _registeredFonts) {
fontProvider.registerFont(font.bytes, font.flutterFamily);
fontProvider!.registerFont(font.bytes, font.family);
familyToTypefaceMap
.putIfAbsent(font.family, () => <SkTypeface>[])
.add(font.typeface);
}
for (var font in _registeredFallbackFonts) {
fontProvider!.registerFont(font.bytes, font.family);
familyToTypefaceMap
.putIfAbsent(font.family, () => <SkTypeface>[])
.add(font.typeface);
}
}
......@@ -51,24 +74,16 @@ class SkiaFontCollection {
}
Future<void> loadFontFromList(Uint8List list, {String? fontFamily}) async {
String? actualFamily = _readActualFamilyName(list);
if (actualFamily == null) {
if (fontFamily == null) {
fontFamily = _readActualFamilyName(list);
if (fontFamily == null) {
html.window.console
.warn('Failed to read font family name. Aborting font load.');
return;
}
actualFamily = fontFamily;
}
if (fontFamily == null) {
fontFamily = actualFamily;
}
registeredFamilies.add(fontFamily);
_registeredFonts.add(_RegisteredFont(list, fontFamily, actualFamily));
_registeredFonts.add(_RegisteredFont(list, fontFamily));
await ensureFontsLoaded();
}
......@@ -94,12 +109,16 @@ class SkiaFontCollection {
'There was a problem trying to load FontManifest.json');
}
bool registeredRoboto = false;
for (Map<String, dynamic> fontFamily
in fontManifest.cast<Map<String, dynamic>>()) {
final String family = fontFamily['family']!;
final List<dynamic> fontAssets = fontFamily['fonts'];
registeredFamilies.add(family);
if (family == 'Roboto') {
registeredRoboto = true;
}
for (dynamic fontAssetItem in fontAssets) {
final Map<String, dynamic> fontAsset = fontAssetItem;
......@@ -112,7 +131,7 @@ class SkiaFontCollection {
/// We need a default fallback font for CanvasKit, in order to
/// avoid crashing while laying out text with an unregistered font. We chose
/// Roboto to match Android.
if (!registeredFamilies.contains('Roboto')) {
if (!registeredRoboto) {
// Download Roboto and add it to the font buffers.
_unloadedFonts.add(_registerFont(_robotoUrl, 'Roboto'));
}
......@@ -129,15 +148,16 @@ class SkiaFontCollection {
}
final Uint8List bytes = buffer.asUint8List();
String? actualFamily = _readActualFamilyName(bytes);
if (actualFamily == null) {
html.window.console.warn('Failed to determine the actual name of the '
'font $family at $url. Defaulting to $family.');
actualFamily = family;
}
return _RegisteredFont(bytes, family);
}
return _RegisteredFont(bytes, family, actualFamily);
void registerFallbackFont(String family, Uint8List bytes) {
_fontFallbackCounts.putIfAbsent(family, () => 0);
int fontFallbackTag = _fontFallbackCounts[family]!;
_fontFallbackCounts[family] = _fontFallbackCounts[family]! + 1;
String countedFamily = '$family $fontFallbackTag';
_registeredFallbackFonts.add(_RegisteredFont(bytes, countedFamily));
globalFontFallbacks.add(countedFamily);
}
String? _readActualFamilyName(Uint8List bytes) {
......@@ -154,20 +174,31 @@ class SkiaFontCollection {
.then<ByteBuffer>((dynamic x) => x as ByteBuffer);
}
/// Resets the fallback fonts. Used for tests.
void debugResetFallbackFonts() {
_registeredFallbackFonts.clear();
globalFontFallbacks.clear();
_fontFallbackCounts.clear();
}
SkFontMgr? skFontMgr;
late TypefaceFontProvider fontProvider;
TypefaceFontProvider? fontProvider;
}
/// Represents a font that has been registered.
class _RegisteredFont {
/// The font family that the font was declared to have by Flutter.
final String flutterFamily;
/// The font family name for this font.
final String family;
/// The byte data for this font.
final Uint8List bytes;
/// The font family that was parsed from the font's bytes.
final String actualFamily;
/// The [SkTypeface] created from this font's [bytes].
///
/// This is used to determine which code points are supported by this font.
final SkTypeface typeface;
_RegisteredFont(this.bytes, this.flutterFamily, this.actualFamily);
_RegisteredFont(this.bytes, this.family)
: this.typeface =
canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(bytes);
}
......@@ -40,7 +40,10 @@ const bool _autoDetect =
const bool _useSkia =
bool.fromEnvironment('FLUTTER_WEB_USE_SKIA', defaultValue: false);
// If set to true, forces CPU-only rendering (i.e. no WebGL).
/// If set to true, forces CPU-only rendering (i.e. no WebGL).
///
/// This is mainly used for testing or for apps that want to ensure they
/// run on devices which don't support WebGL.
const bool canvasKitForceCpuOnly = bool.fromEnvironment(
'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY',
defaultValue: false);
......
// 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.
// @dart = 2.12
part of engine;
/// A tree which stores a set of intervals that can be queried for intersection.
class IntervalTree<T> {
/// The root node of the interval tree.
final IntervalTreeNode<T> root;
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<CodeunitRange>> rangesMap) {
// Get a list of all the ranges ordered by start index.
List<IntervalTreeNode<T>> intervals = <IntervalTreeNode<T>>[];
rangesMap.forEach((T key, List<CodeunitRange> rangeList) {
for (CodeunitRange range in rangeList) {
intervals.add(IntervalTreeNode<T>(key, range.start, range.end));
}
});
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.length == 0) {
return null;
}
if (nodes.length == 1) {
return nodes.single;
}
int mid = nodes.length ~/ 2;
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,
));
}
}
IntervalTreeNode<T> root = _makeBalancedTree(intervals)!;
_computeHigh(root);
return IntervalTree._(root);
}
/// Returns the list of objects which have been associated with intervals that
/// intersect with [x].
List<T> intersections(int x) {
List<T> results = <T>[];
root.searchForPoint(x, results);
return results;
}
}
class IntervalTreeNode<T> {
final T value;
final int low;
final int high;
int computedHigh;
IntervalTreeNode<T>? left;
IntervalTreeNode<T>? right;
IntervalTreeNode(this.value, this.low, this.high) : computedHigh = high;
bool contains(int x) {
return low <= x && x <= high;
}
// 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 (this.contains(x)) {
result.add(value);
}
if (x < low) {
return;
}
right?.searchForPoint(x, result);
}
}
......@@ -62,11 +62,7 @@ class CkParagraphStyle implements ui.ParagraphStyle {
skTextStyle.fontSize = fontSize;
}
if (fontFamily == null ||
!skiaFontCollection.registeredFamilies.contains(fontFamily)) {
fontFamily = 'Roboto';
}
skTextStyle.fontFamilies = [fontFamily];
skTextStyle.fontFamilies = _getEffectiveFontFamilies(fontFamily);
return skTextStyle;
}
......@@ -74,20 +70,8 @@ class CkParagraphStyle implements ui.ParagraphStyle {
static SkStrutStyleProperties toSkStrutStyleProperties(ui.StrutStyle value) {
EngineStrutStyle style = value as EngineStrutStyle;
final SkStrutStyleProperties skStrutStyle = SkStrutStyleProperties();
if (style._fontFamily != null) {
String fontFamily = style._fontFamily!;
if (!skiaFontCollection.registeredFamilies.contains(fontFamily)) {
fontFamily = 'Roboto';
}
final List<String> fontFamilies = <String>[fontFamily];
if (style._fontFamilyFallback != null) {
fontFamilies.addAll(style._fontFamilyFallback!);
}
skStrutStyle.fontFamilies = fontFamilies;
} else {
// If no strut font family is given, default to Roboto.
skStrutStyle.fontFamilies = ['Roboto'];
}
skStrutStyle.fontFamilies =
_getEffectiveFontFamilies(style._fontFamily, style._fontFamilyFallback);
if (style._fontSize != null) {
skStrutStyle.fontSize = style._fontSize;
......@@ -279,18 +263,8 @@ class CkTextStyle implements ui.TextStyle {
properties.locale = locale.toLanguageTag();
}
if (fontFamily == null ||
!skiaFontCollection.registeredFamilies.contains(fontFamily)) {
fontFamily = 'Roboto';
}
List<String> fontFamilies = <String>[fontFamily];
if (fontFamilyFallback != null &&
!fontFamilyFallback.every((font) => fontFamily == font)) {
fontFamilies.addAll(fontFamilyFallback);
}
properties.fontFamilies = fontFamilies;
properties.fontFamilies =
_getEffectiveFontFamilies(fontFamily, fontFamilyFallback);
if (fontWeight != null || fontStyle != null) {
properties.fontStyle = toSkFontStyle(fontWeight, fontStyle);
......@@ -564,7 +538,6 @@ class CkParagraph extends ManagedSkiaObject<SkParagraph>
@override
void layout(ui.ParagraphConstraints constraints) {
assert(constraints.width != null); // ignore: unnecessary_null_comparison
_lastLayoutConstraints = constraints;
// TODO(het): CanvasKit throws an exception when laid out with
......@@ -667,8 +640,64 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
return properties;
}
/// Determines if the given [text] contains any code points which are not
/// supported by the current set of fonts.
void _ensureFontsSupportText(String text) {
// TODO(hterkelsen): Make this faster for the common case where the text
// is supported by the given fonts.
// If the text is ASCII, then skip this check.
bool isAscii = true;
for (int i = 0; i < text.length; i++) {
if (text.codeUnitAt(i) >= 160) {
isAscii = false;
break;
}
}
if (isAscii) {
return;
}
CkTextStyle style = _peekStyle();
List<String> fontFamilies =
_getEffectiveFontFamilies(style.fontFamily, style.fontFamilyFallback);
List<SkTypeface> typefaces = <SkTypeface>[];
for (var font in fontFamilies) {
List<SkTypeface>? typefacesForFamily =
skiaFontCollection.familyToTypefaceMap[font];
if (typefacesForFamily != null) {
typefaces.addAll(typefacesForFamily);
}
}
List<bool> codeUnitsSupported = List<bool>.filled(text.length, false);
for (SkTypeface typeface in typefaces) {
SkFont font = SkFont(typeface);
Uint8List glyphs = font.getGlyphIDs(text);
assert(glyphs.length == codeUnitsSupported.length);
for (int i = 0; i < glyphs.length; i++) {
codeUnitsSupported[i] |=
glyphs[i] != 0 || _isControlCode(text.codeUnitAt(i));
}
}
if (codeUnitsSupported.any((x) => !x)) {
List<int> missingCodeUnits = <int>[];
for (int i = 0; i < codeUnitsSupported.length; i++) {
if (!codeUnitsSupported[i]) {
missingCodeUnits.add(text.codeUnitAt(i));
}
}
_findFontsForMissingCodeunits(missingCodeUnits);
}
}
/// Returns [true] if [codepoint] is a Unicode control code.
bool _isControlCode(int codepoint) {
return codepoint < 32 || (codepoint > 127 && codepoint < 160);
}
@override
void addText(String text) {
_ensureFontsSupportText(text);
_commands.add(_ParagraphCommand.addText(text));
_paragraphBuilder.addText(text);
}
......@@ -719,8 +748,10 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
_styleStack.add(skStyle);
_commands.add(_ParagraphCommand.pushStyle(ckStyle));
if (skStyle.foreground != null || skStyle.background != null) {
final SkPaint foreground = skStyle.foreground?.skiaObject ?? _defaultTextStylePaint;
final SkPaint background = skStyle.background?.skiaObject ?? _defaultTextStylePaint;
final SkPaint foreground =
skStyle.foreground?.skiaObject ?? _defaultTextStylePaint;
final SkPaint background =
skStyle.background?.skiaObject ?? _defaultTextStylePaint;
_paragraphBuilder.pushPaintStyle(
skStyle.skTextStyle, foreground, background);
} else {
......@@ -779,3 +810,19 @@ enum _ParagraphCommandType {
pushStyle,
addPlaceholder,
}
List<String> _getEffectiveFontFamilies(String? fontFamily,
[List<String>? fontFamilyFallback]) {
List<String> fontFamilies = <String>[];
if (fontFamily == null) {
fontFamilies.add('Roboto');
} else {
fontFamilies.add(fontFamily);
}
if (fontFamilyFallback != null &&
!fontFamilyFallback.every((font) => fontFamily == font)) {
fontFamilies.addAll(fontFamilyFallback);
}
fontFamilies.addAll(skiaFontCollection.globalFontFallbacks);
return fontFamilies;
}
......@@ -597,45 +597,45 @@ int clampInt(int value, int min, int max) {
}
ui.Rect computeBoundingRectangleFromMatrix(Matrix4 transform, ui.Rect rect) {
final Float32List m = transform.storage;
// Apply perspective transform to all 4 corners. Can't use left,top, bottom,
// right since for example rotating 45 degrees would yield inaccurate size.
double x = rect.left;
double y = rect.top;
double wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
double xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
double yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
double minX = xp, maxX = xp;
double minY =yp, maxY = yp;
x = rect.right;
y = rect.bottom;
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
minX = math.min(minX, xp);
maxX = math.max(maxX, xp);
minY = math.min(minY, yp);
maxY = math.max(maxY, yp);
x = rect.left;
y = rect.bottom;
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
minX = math.min(minX, xp);
maxX = math.max(maxX, xp);
minY = math.min(minY, yp);
maxY = math.max(maxY, yp);
x = rect.right;
y = rect.top;
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
minX = math.min(minX, xp);
maxX = math.max(maxX, xp);
minY = math.min(minY, yp);
maxY = math.max(maxY, yp);
return ui.Rect.fromLTWH(minX, minY, maxX-minX, maxY-minY);
}
final Float32List m = transform.storage;
// Apply perspective transform to all 4 corners. Can't use left,top, bottom,
// right since for example rotating 45 degrees would yield inaccurate size.
double x = rect.left;
double y = rect.top;
double wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
double xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
double yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
double minX = xp, maxX = xp;
double minY = yp, maxY = yp;
x = rect.right;
y = rect.bottom;
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
minX = math.min(minX, xp);
maxX = math.max(maxX, xp);
minY = math.min(minY, yp);
maxY = math.max(maxY, yp);
x = rect.left;
y = rect.bottom;
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
minX = math.min(minX, xp);
maxX = math.max(maxX, xp);
minY = math.min(minY, yp);
maxY = math.max(maxY, yp);
x = rect.right;
y = rect.top;
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
minX = math.min(minX, xp);
maxX = math.max(maxX, xp);
minY = math.min(minY, yp);
maxY = math.max(maxY, yp);
return ui.Rect.fromLTWH(minX, minY, maxX - minX, maxY - minY);
}
// 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.
// @dart = 2.12
import 'dart:async';
import 'dart:typed_data';
import 'package:ui/ui.dart' as ui;
import 'package:ui/src/engine.dart';
import 'package:test/test.dart';
import 'package:test/bootstrap/browser.dart';
import 'package:web_engine_tester/golden_tester.dart';
import 'common.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
const ui.Rect kDefaultRegion = const ui.Rect.fromLTRB(0, 0, 500, 250);
Future<void> matchPictureGolden(String goldenFile, CkPicture picture,
{ui.Rect region = kDefaultRegion, bool write = false}) async {
final EnginePlatformDispatcher dispatcher =
ui.window.platformDispatcher as EnginePlatformDispatcher;
final LayerSceneBuilder sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPicture(ui.Offset.zero, picture);
dispatcher.rasterizer!.draw(sb.build().layerTree);
await matchGoldenFile(goldenFile,
region: region, maxDiffRatePercent: 0.0, write: write);
}
void testMain() {
group('Font fallbacks', () {
setUpCanvasKitTest();
/// Used to save and restore [ui.window.onPlatformMessage] after each test.
ui.PlatformMessageCallback? savedCallback;
setUp(() {
notoDownloadQueue.downloader = TestDownloader();
TestDownloader.mockDownloads.clear();
savedCallback = ui.window.onPlatformMessage;
skiaFontCollection.debugResetFallbackFonts();
});
tearDown(() {
ui.window.onPlatformMessage = savedCallback;
});
test('will download Noto Naskh Arabic if Arabic text is added', () async {
final Completer<void> fontChangeCompleter = Completer<void>();
// Intercept the system font change message.
ui.window.onPlatformMessage = (String name, ByteData? data,
ui.PlatformMessageResponseCallback? callback) {
if (name == 'flutter/system') {
const JSONMessageCodec codec = JSONMessageCodec();
final dynamic message = codec.decodeMessage(data);
if (message is Map) {
if (message['type'] == 'fontsChange') {
fontChangeCompleter.complete();
}
}
}
if (savedCallback != null) {
savedCallback!(name, data, callback);
}
};
TestDownloader.mockDownloads[
'https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic+UI'] =
'''
/* arabic */
@font-face {
font-family: 'Noto Naskh Arabic UI';
font-style: normal;
font-weight: 400;
src: url(packages/ui/assets/NotoNaskhArabic-Regular.ttf) format('ttf');
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC;
}
''';
expect(skiaFontCollection.globalFontFallbacks, isEmpty);
// Creating this paragraph should cause us to start to download the
// fallback font.
CkParagraphBuilder pb = CkParagraphBuilder(
CkParagraphStyle(),
);
pb.addText('مرحبا');
await fontChangeCompleter.future;
expect(skiaFontCollection.globalFontFallbacks,
contains('Noto Naskh Arabic UI 0'));
final CkPictureRecorder recorder = CkPictureRecorder();
final CkCanvas canvas = recorder.beginRecording(kDefaultRegion);
pb = CkParagraphBuilder(
CkParagraphStyle(
fontSize: 32,
),
);
pb.addText('مرحبا');
final CkParagraph paragraph = pb.build();
paragraph.layout(ui.ParagraphConstraints(width: 1000));
canvas.drawParagraph(paragraph, ui.Offset(200, 120));
await matchPictureGolden(
'canvaskit_font_fallback_arabic.png', recorder.endRecording());
// TODO: https://github.com/flutter/flutter/issues/60040
// TODO: https://github.com/flutter/flutter/issues/71520
}, skip: isIosSafari || isFirefox);
test('will gracefully fail if we cannot parse the Google Fonts CSS',
() async {
TestDownloader.mockDownloads[
'https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic+UI'] =
'invalid CSS... this should cause our parser to fail';
expect(skiaFontCollection.globalFontFallbacks, isEmpty);
// Creating this paragraph should cause us to start to download the
// fallback font.
CkParagraphBuilder pb = CkParagraphBuilder(
CkParagraphStyle(),
);
pb.addText('مرحبا');
// Flush microtasks and test that we didn't start any downloads.
await Future<void>.delayed(Duration.zero);
expect(notoDownloadQueue.isPending, isFalse);
expect(skiaFontCollection.globalFontFallbacks, isEmpty);
});
// TODO: https://github.com/flutter/flutter/issues/60040
}, skip: isIosSafari);
}
class TestDownloader extends NotoDownloader {
static final Map<String, String> mockDownloads = <String, String>{};
@override
Future<String> downloadAsString(String url) async {
if (mockDownloads.containsKey(url)) {
return mockDownloads[url]!;
} else {
return '';
}
}
}
// 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.
// @dart = 2.12
import 'package:ui/src/engine.dart';
import 'package:test/test.dart';
import 'package:test/bootstrap/browser.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('$IntervalTree', () {
test('is balanced', () {
var ranges = <String, List<CodeunitRange>>{
'A': [CodeunitRange(0, 5), CodeunitRange(6, 10)],
'B': [CodeunitRange(4, 6)],
};
// Should create a balanced 3-node tree with a root with a left and right
// child.
var tree = IntervalTree<String>.createFromRanges(ranges);
var 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).
var ranges2 = <String, List<CodeunitRange>>{
'A': [
CodeunitRange(1, 1),
CodeunitRange(2, 2),
CodeunitRange(3, 3),
CodeunitRange(4, 4),
CodeunitRange(5, 5),
CodeunitRange(6, 6),
CodeunitRange(7, 7),
CodeunitRange(8, 8),
CodeunitRange(9, 9),
CodeunitRange(10, 10),
CodeunitRange(11, 11),
CodeunitRange(12, 12),
CodeunitRange(13, 13),
CodeunitRange(14, 14),
CodeunitRange(15, 15),
],
};
// Should create a balanced 3-node tree with a root with a left and right
// child.
var tree2 = IntervalTree<String>.createFromRanges(ranges2);
var 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', () {
var ranges = <String, List<CodeunitRange>>{
'A': [CodeunitRange(0, 5), CodeunitRange(7, 10)],
'B': [CodeunitRange(4, 6)],
};
var tree = IntervalTree<String>.createFromRanges(ranges);
expect(tree.intersections(1), ['A']);
expect(tree.intersections(4), ['A', 'B']);
expect(tree.intersections(6), ['B']);
expect(tree.intersections(7), ['A']);
expect(tree.intersections(11), <String>[]);
});
});
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册