未验证 提交 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();
......
此差异已折叠。
......@@ -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.
先完成此消息的编辑!
想要评论请 注册